inventory_env / server /inventory_env.py
saketh1201's picture
Upload folder using huggingface_hub
3764c76 verified
Raw
History Blame Contribute Delete
10.3 kB
from openenv.core.env_server.interfaces import Environment
import copy
import random
from uuid import uuid4
from models import InventoryAction, InventoryObservation, InventoryState
from .constants import (
INITIAL_CASH, BASE_PRICES, COST_PRICES, SHELF_LIFE, INITIAL_STOCK,
EVENTS, SHIPPING_COST, SHIPPING_DAYS, INVENTORY_CAPACITY,
EXTRA_INVENTORY_COST, BASE_DEMAND, WEEKEND_MULTIPLIER, EVENT_EFFECTS,
EVENT_DURATION, MAX_DAYS, UPGRADE_DELIVERY_COST, TASKS, PRICE_ELASTICITY
)
def _build_inventory(stock):
"""Convert stock dict to batch format: {product: [[qty, days_left], ...]}"""
inv = {}
for product, qty in stock.items():
shelf = SHELF_LIFE[product]
inv[product] = [[qty, shelf]]
return inv
class InventoryEnvironment(Environment):
def __init__(self, task_name="medium"):
self.task_name = task_name
self.task = TASKS[task_name]
self.cash = self.task["initial_cash"]
self.inventory = _build_inventory(self.task["initial_stock"])
self.events = copy.deepcopy(self.task["events"])
self.deliveries = []
self.current_day = 0
self.total_profit = 0.0
self.seed = self.task["seed"]
self.reward = 0.0
self.max_days = self.task["max_days"]
self.inventory_capacity = self.task["inventory_capacity"]
self.base_demand = self.task["base_demand"]
self.reset()
def reset(self, seed: int = None) -> InventoryObservation:
if seed is not None:
self.seed = seed
else:
self.seed = self.task["seed"]
self.cash = self.task["initial_cash"]
self.inventory = _build_inventory(self.task["initial_stock"])
self.events = copy.deepcopy(self.task["events"])
self.deliveries = []
self.current_day = 0
self.total_profit = 0.0
self.reward = 0.0
self._state = InventoryState(
episode_id = str(uuid4()),
current_day = 0,
cash = self.task["initial_cash"],
inventory = dict(self.task["initial_stock"])
)
return InventoryObservation(
current_day = 0,
total_cash = self.cash,
day_profit = 0.0,
total_profit = 0.0,
demand_today = {},
updated_inventory = copy.deepcopy(self.inventory),
remaining_capacity = {p: max(0, self.inventory_capacity[p] - sum(b[0] for b in self.inventory[p])) for p in self.inventory},
updated_events = copy.deepcopy(self.events),
updated_deliveries = [],
reward = 0.0,
done = False,
)
def step(self, action: InventoryAction) -> InventoryObservation:
self.current_day += 1
self.reward = 0.0 # reset reward each step
day_cost = 0.0
day_revenue = 0.0
# 1. tick event countdowns (keep ticking into negative to track active duration)
for event_name in self.events:
self.events[event_name] -= 1
# 2. remove expired groceries
new_batches = []
expired_groceries_count = 0
for batch in self.inventory["groceries"]:
if batch[1] == 0:
expired_groceries_count += batch[0]
continue
else:
new_batches.append([batch[0], batch[1] - 1])
self.inventory["groceries"] = new_batches
self.reward -= 0.05 * expired_groceries_count
# 3. Handle incoming deliveries
remaining_deliveries = []
for delivery in self.deliveries:
for product, shipment in delivery.items():
qty, arrival_day = shipment
if arrival_day <= self.current_day:
self.inventory[product].append([qty, SHELF_LIFE[product]])
else:
remaining_deliveries.append(delivery)
self.deliveries = remaining_deliveries
# 4. process purchases
for product, qty in action.buy_quantities.items():
unit_cost = COST_PRICES[product] + SHIPPING_COST[action.delivery_method]
total_cost = qty * unit_cost
# capacity overage cost
current_qty = sum(b[0] for b in self.inventory[product])
overage = max(0, (current_qty + qty) - self.inventory_capacity[product])
extra_cost = overage * EXTRA_INVENTORY_COST[product]
total_cost += extra_cost
if total_cost > self.cash:
self.reward -= 0.5 # penalize for ordering what you can't afford
continue
self.cash -= total_cost
day_cost += total_cost
arrival_day = self.current_day + SHIPPING_DAYS[action.delivery_method]
# add jitter: slow ±2 days, medium ±1 day, fast is reliable
jitter_rng = random.Random(self.seed * 2000 + self.current_day * 100 + hash(product))
if action.delivery_method == "slow":
arrival_day += jitter_rng.randint(-2, 2)
elif action.delivery_method == "medium":
arrival_day += jitter_rng.randint(-1, 1)
# ensure arrival is at least next day
arrival_day = max(self.current_day + 1, arrival_day)
self.deliveries.append({product: [qty, arrival_day]})
# 5. generate demand
demand = self._generate_demand()
# apply price elasticity: demand scales with price^(-elasticity)
price_mults = {}
for product in demand:
pm = max(0.5, min(1.5, action.price_multipliers.get(product, 1.0)))
price_mults[product] = pm
e = PRICE_ELASTICITY[product]
demand[product] = max(0, int(demand[product] * pm ** -e))
# 6. sell products (fifo)
for product, demand_today in demand.items():
sell_price = BASE_PRICES[product] * price_mults[product]
product_availability = sum(batch[0] for batch in self.inventory[product])
if demand_today > product_availability:
missed_sales = demand_today - product_availability
sold = product_availability
day_revenue += sold * sell_price
self.inventory[product] = []
self.reward -= missed_sales * sell_price * 0.001
self.reward += sold * sell_price * 0.001
else:
day_revenue += demand_today * sell_price
self.reward += demand_today * sell_price * 0.001
new_batches = []
for batch in self.inventory[product]:
if batch[0] < demand_today:
demand_today = demand_today - batch[0]
elif demand_today == 0:
new_batches.append(batch)
else:
remaining = batch[0] - demand_today
if remaining > 0:
new_batches.append([remaining, batch[1]])
demand_today = 0
self.inventory[product] = new_batches
# 7. Liquidate some stock (FIFO, no revenue)
total_liquidation_loss = 0.0
for product, count in action.liquidate.items():
if product not in self.inventory or count <= 0:
continue
actually_removed = min(count, sum(b[0] for b in self.inventory[product]))
total_liquidation_loss += actually_removed * COST_PRICES[product]
remaining = count
new_batches = []
for batch in self.inventory[product]:
if remaining <= 0:
new_batches.append(batch)
elif batch[0] <= remaining:
remaining -= batch[0]
else:
new_batches.append([batch[0] - remaining, batch[1]])
remaining = 0
self.inventory[product] = new_batches
self.reward -= total_liquidation_loss * 0.001
# compute day profit
day_profit = day_revenue - day_cost
self.cash += day_revenue
self.total_profit += day_profit
# check done
done = self.current_day >= self.max_days
# update state
self._state = InventoryState(
episode_id = self._state.episode_id,
current_day = self.current_day,
cash = self.cash,
inventory = {p: sum(b[0] for b in self.inventory[p]) for p in self.inventory},
)
return InventoryObservation(
current_day = self.current_day,
total_cash = self.cash,
day_profit = day_profit,
total_profit = self.total_profit,
demand_today = demand,
updated_inventory = copy.deepcopy(self.inventory),
remaining_capacity = {p: max(0, self.inventory_capacity[p] - sum(b[0] for b in self.inventory[p])) for p in self.inventory},
updated_events = copy.deepcopy(self.events),
updated_deliveries = copy.deepcopy(self.deliveries),
reward = self.reward,
done = done,
)
def _generate_demand(self):
rng = random.Random(self.seed * 1000 + self.current_day)
demand = {}
for product, (lo, hi) in self.base_demand.items():
demand[product] = rng.randint(lo, hi)
# weekend boost
if self.current_day % 7 in (5, 6):
for product in demand:
demand[product] = int(demand[product] * WEEKEND_MULTIPLIER)
# active event multipliers (only for EVENT_DURATION days after triggering)
for event_name, days in self.events.items():
if -EVENT_DURATION < days <= 0 and event_name in EVENT_EFFECTS:
for product, mult in EVENT_EFFECTS[event_name].items():
demand[product] = int(demand[product] * mult)
return demand
@property
def state(self) -> InventoryState:
return self._state