Loading review_corrections.md +1 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ Faire retailer & Warehouse Forecast Accuracy -> En valeur "IA" et moins Supply Chain. Genre "ML Prediction Accuracy" Juste MAPE (sans "Forecast")   System Cost -> Logistics Costs ? Idée de tous les coûts associés à la supply chain Loading supply_chain_sim/customer_behavior.py +22 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ class CustomerBehavior: def sample_customers_for_timestep(self, n_customers: int = None) -> List[Customer]: """Sample customers who will shop in this timestep.""" if n_customers is None: # Sample 10-20% of customers per timestep (increased from 5-10%) # Sample 10-20% of customers per timestep n_customers = max(10, int(len(self.setup.customers) * np.random.uniform(0.1, 0.2))) sampled = np.random.choice(self.setup.customers, min(n_customers, len(self.setup.customers)), replace=False) Loading Loading @@ -48,11 +48,21 @@ class CustomerBehavior: if not watch: return 0 # Get retailer to calculate final price retailer = next((r for r in self.setup.retailers if r.region == customer.region), self.setup.retailers[0] if self.setup.retailers else None) if not retailer: return 0 # Calculate retail price (sell_price * retailer markup) retail_price = watch.sell_price * retailer.markup_policy # Simplified purchase probability calculation match_score = self.compute_preference_match(customer, watch_id) # Budget fit - much more lenient budget_fit = 1.0 if watch.base_price <= customer.budget else max(0.1, customer.budget / watch.base_price) budget_fit = 1.0 if retail_price <= customer.budget else max(0.1, customer.budget / retail_price) # Brand awareness factor brand_awareness = min(1.0, customer.brand_awareness + 0.3) # Base awareness boost Loading Loading @@ -95,7 +105,7 @@ class CustomerBehavior: # Choose a retailer to visit retailer = np.random.choice(region_retailers) # Get available watches at this retailer # Get available watches at this retailer (stock quantity > 0) available_watches = [(inv.watch_id, inv.quantity) for inv in retailer_inventories if inv.retailer_id == retailer.id and inv.quantity > 0] Loading @@ -104,8 +114,8 @@ class CustomerBehavior: continue # Try to buy something - check each available watch for watch_id, quantity in available_watches: if quantity <= 0: for watch_id, stock_quantity in available_watches: if stock_quantity <= 0: continue prob = self.evaluate_purchase_probability(customer, watch_id, event_modifiers) Loading @@ -115,6 +125,9 @@ class CustomerBehavior: watch = next((w for w in self.setup.watches if w.id == watch_id), None) if watch: # Calculate final retail price (customer pays this) retail_price = watch.sell_price * retailer.markup_policy # Create sale sale = Sale( id=self.sale_id_counter, Loading @@ -122,7 +135,7 @@ class CustomerBehavior: retailer_id=retailer.id, watch_id=watch_id, quantity=1, total_price=watch.base_price * retailer.markup_policy, total_price=retail_price, date=datetime.now() ) Loading @@ -130,12 +143,12 @@ class CustomerBehavior: self.sales.append(sale) self.sale_id_counter += 1 # Deduct from inventory # Deduct from stock key = (retailer.id, watch_id) if key in inv_map: inv_map[key].quantity -= 1 logger.debug(f"Customer {customer.id} purchased watch {watch_id} for ${sale.total_price:.0f}") logger.debug(f"Customer {customer.id} purchased watch {watch_id} for ${retail_price:.0f}") break # Customer only buys one watch per visit logger.info(f"Processed {len(new_sales)} sales") Loading @@ -152,7 +165,7 @@ class CustomerBehavior: for sale in recent_sales: # Use a simple 5% return rate if np.random.random() < 0.05: # Process return # Process return - add back to stock key = (sale.retailer_id, sale.watch_id) if key in inv_map: inv_map[key].quantity += sale.quantity Loading supply_chain_sim/distribution.py +47 −19 Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ class RetailDistribution: for watch_id, demand in region_demand.items(): allocated_qty = int(demand * retailer_share) # Respect capacity constraints # Respect capacity constraints (stock capacity) current_load = sum(allocation[retailer.id].values()) available_capacity = retailer.capacity - current_load Loading Loading @@ -91,13 +91,13 @@ class RetailDistribution: if watch_id not in warehouse_watches: continue # Find available stock available_qty = sum(inv.quantity for inv in warehouse_watches[watch_id]) # Find available stock in warehouse available_stock = sum(inv.quantity for inv in warehouse_watches[watch_id]) if available_qty > 0: to_ship = min(required_qty, available_qty) if available_stock > 0: to_ship = min(required_qty, available_stock) # Deduct from warehouses # Deduct from warehouse stock remaining = to_ship for w_inv in warehouse_watches[watch_id]: if remaining <= 0: Loading @@ -107,7 +107,7 @@ class RetailDistribution: w_inv.quantity -= taken remaining -= taken # Add to retailer # Add to retailer stock key = (retailer_id, watch_id) if key in retailer_inv_map: retailer_inv_map[key].quantity += to_ship Loading @@ -134,9 +134,9 @@ class RetailDistribution: return warehouse_inventories, retailer_inventories def calculate_distribution_costs(self) -> float: """Calculate total distribution costs.""" """Calculate total distribution/logistics costs.""" # Simplified cost model base_cost_per_unit = 5.0 base_cost_per_unit = 5.0 # $5 per unit shipped total_cost = 0 for dist in self.distribution_history: Loading @@ -144,16 +144,44 @@ class RetailDistribution: return total_cost def get_retailer_fill_rates(self, retailer_inventories: List[RetailerInventory]) -> Dict[int, float]: """Calculate fill rates for each retailer.""" fill_rates = {} def get_retailer_capacity_utilization(self, retailer_inventories: List[RetailerInventory]) -> Dict[int, float]: """Calculate capacity utilization for each retailer. Capacity utilization = Current Stock / Total Capacity """ capacity_utilization = {} # Group stock by retailer retailer_stock = {} for inv in retailer_inventories: if inv.retailer_id not in retailer_stock: retailer_stock[inv.retailer_id] = 0 retailer_stock[inv.retailer_id] += inv.quantity # Calculate utilization for each retailer for retailer in self.setup.retailers: retailer_inv = [inv for inv in retailer_inventories if inv.retailer_id == retailer.id] current_stock = retailer_stock.get(retailer.id, 0) utilization = current_stock / retailer.capacity if retailer.capacity > 0 else 0 capacity_utilization[retailer.id] = min(1.0, utilization) # Cap at 100% return capacity_utilization total_stock = sum(inv.quantity for inv in retailer_inv) fill_rate = min(1.0, total_stock / retailer.capacity) if retailer.capacity > 0 else 0 def get_warehouse_capacity_utilization(self, warehouse_inventories: List[WarehouseInventory]) -> Dict[int, float]: """Calculate capacity utilization for each warehouse. Capacity utilization = Current Stock / Total Capacity """ capacity_utilization = {} # Group stock by warehouse warehouse_stock = {} for inv in warehouse_inventories: if inv.warehouse_id not in warehouse_stock: warehouse_stock[inv.warehouse_id] = 0 warehouse_stock[inv.warehouse_id] += inv.quantity fill_rates[retailer.id] = fill_rate # Calculate utilization for each warehouse for warehouse in self.setup.warehouses: current_stock = warehouse_stock.get(warehouse.id, 0) utilization = current_stock / warehouse.capacity if warehouse.capacity > 0 else 0 capacity_utilization[warehouse.id] = min(1.0, utilization) # Cap at 100% return fill_rates No newline at end of file return capacity_utilization No newline at end of file supply_chain_sim/entities.py +7 −6 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ class ProductComponent: id: int name: str type: str # movement, case, strap, etc cost: float cost: float # Purchase cost from supplier storage_rate: float # cost per unit per day shelf_life: int # days quality_tier: int # 1-5 Loading @@ -48,7 +48,7 @@ class WarehouseInventory: warehouse_id: int item_id: int item_type: str # "component" or "watch" quantity: int quantity: int # Stock quantity age: int = 0 # days @dataclass Loading @@ -57,7 +57,8 @@ class WatchModel: brand_id: int name: str category: str # luxury, sport, casual base_price: float base_cost: float # Manufacturing cost (sum of components + labor) sell_price: float = 0 # Selling price to retailers (base_cost * markup) @dataclass class WatchBOM: Loading @@ -72,14 +73,14 @@ class Retailer: name: str region: str capacity: int markup_policy: float # multiplier markup_policy: float # multiplier for retail price @dataclass class RetailerInventory: id: int retailer_id: int watch_id: int quantity: int quantity: int # Stock quantity return_rate: float = 0.05 @dataclass Loading @@ -99,7 +100,7 @@ class Sale: retailer_id: int watch_id: int quantity: int total_price: float total_price: float # Price paid by customer date: datetime @dataclass Loading supply_chain_sim/forecasting.py +1 −1 Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ class Forecasting: base_demand = np.random.uniform(15, 30) # Casual has higher volume # Price sensitivity - higher prices reduce demand price_factor = max(0.3, 1.0 - (watch.base_price - 200) / 2000) price_factor = max(0.3, 1.0 - (watch.base_cost - 200) / 2000) base_demand *= price_factor # Regional variation Loading Loading
review_corrections.md +1 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ Faire retailer & Warehouse Forecast Accuracy -> En valeur "IA" et moins Supply Chain. Genre "ML Prediction Accuracy" Juste MAPE (sans "Forecast")   System Cost -> Logistics Costs ? Idée de tous les coûts associés à la supply chain Loading
supply_chain_sim/customer_behavior.py +22 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ class CustomerBehavior: def sample_customers_for_timestep(self, n_customers: int = None) -> List[Customer]: """Sample customers who will shop in this timestep.""" if n_customers is None: # Sample 10-20% of customers per timestep (increased from 5-10%) # Sample 10-20% of customers per timestep n_customers = max(10, int(len(self.setup.customers) * np.random.uniform(0.1, 0.2))) sampled = np.random.choice(self.setup.customers, min(n_customers, len(self.setup.customers)), replace=False) Loading Loading @@ -48,11 +48,21 @@ class CustomerBehavior: if not watch: return 0 # Get retailer to calculate final price retailer = next((r for r in self.setup.retailers if r.region == customer.region), self.setup.retailers[0] if self.setup.retailers else None) if not retailer: return 0 # Calculate retail price (sell_price * retailer markup) retail_price = watch.sell_price * retailer.markup_policy # Simplified purchase probability calculation match_score = self.compute_preference_match(customer, watch_id) # Budget fit - much more lenient budget_fit = 1.0 if watch.base_price <= customer.budget else max(0.1, customer.budget / watch.base_price) budget_fit = 1.0 if retail_price <= customer.budget else max(0.1, customer.budget / retail_price) # Brand awareness factor brand_awareness = min(1.0, customer.brand_awareness + 0.3) # Base awareness boost Loading Loading @@ -95,7 +105,7 @@ class CustomerBehavior: # Choose a retailer to visit retailer = np.random.choice(region_retailers) # Get available watches at this retailer # Get available watches at this retailer (stock quantity > 0) available_watches = [(inv.watch_id, inv.quantity) for inv in retailer_inventories if inv.retailer_id == retailer.id and inv.quantity > 0] Loading @@ -104,8 +114,8 @@ class CustomerBehavior: continue # Try to buy something - check each available watch for watch_id, quantity in available_watches: if quantity <= 0: for watch_id, stock_quantity in available_watches: if stock_quantity <= 0: continue prob = self.evaluate_purchase_probability(customer, watch_id, event_modifiers) Loading @@ -115,6 +125,9 @@ class CustomerBehavior: watch = next((w for w in self.setup.watches if w.id == watch_id), None) if watch: # Calculate final retail price (customer pays this) retail_price = watch.sell_price * retailer.markup_policy # Create sale sale = Sale( id=self.sale_id_counter, Loading @@ -122,7 +135,7 @@ class CustomerBehavior: retailer_id=retailer.id, watch_id=watch_id, quantity=1, total_price=watch.base_price * retailer.markup_policy, total_price=retail_price, date=datetime.now() ) Loading @@ -130,12 +143,12 @@ class CustomerBehavior: self.sales.append(sale) self.sale_id_counter += 1 # Deduct from inventory # Deduct from stock key = (retailer.id, watch_id) if key in inv_map: inv_map[key].quantity -= 1 logger.debug(f"Customer {customer.id} purchased watch {watch_id} for ${sale.total_price:.0f}") logger.debug(f"Customer {customer.id} purchased watch {watch_id} for ${retail_price:.0f}") break # Customer only buys one watch per visit logger.info(f"Processed {len(new_sales)} sales") Loading @@ -152,7 +165,7 @@ class CustomerBehavior: for sale in recent_sales: # Use a simple 5% return rate if np.random.random() < 0.05: # Process return # Process return - add back to stock key = (sale.retailer_id, sale.watch_id) if key in inv_map: inv_map[key].quantity += sale.quantity Loading
supply_chain_sim/distribution.py +47 −19 Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ class RetailDistribution: for watch_id, demand in region_demand.items(): allocated_qty = int(demand * retailer_share) # Respect capacity constraints # Respect capacity constraints (stock capacity) current_load = sum(allocation[retailer.id].values()) available_capacity = retailer.capacity - current_load Loading Loading @@ -91,13 +91,13 @@ class RetailDistribution: if watch_id not in warehouse_watches: continue # Find available stock available_qty = sum(inv.quantity for inv in warehouse_watches[watch_id]) # Find available stock in warehouse available_stock = sum(inv.quantity for inv in warehouse_watches[watch_id]) if available_qty > 0: to_ship = min(required_qty, available_qty) if available_stock > 0: to_ship = min(required_qty, available_stock) # Deduct from warehouses # Deduct from warehouse stock remaining = to_ship for w_inv in warehouse_watches[watch_id]: if remaining <= 0: Loading @@ -107,7 +107,7 @@ class RetailDistribution: w_inv.quantity -= taken remaining -= taken # Add to retailer # Add to retailer stock key = (retailer_id, watch_id) if key in retailer_inv_map: retailer_inv_map[key].quantity += to_ship Loading @@ -134,9 +134,9 @@ class RetailDistribution: return warehouse_inventories, retailer_inventories def calculate_distribution_costs(self) -> float: """Calculate total distribution costs.""" """Calculate total distribution/logistics costs.""" # Simplified cost model base_cost_per_unit = 5.0 base_cost_per_unit = 5.0 # $5 per unit shipped total_cost = 0 for dist in self.distribution_history: Loading @@ -144,16 +144,44 @@ class RetailDistribution: return total_cost def get_retailer_fill_rates(self, retailer_inventories: List[RetailerInventory]) -> Dict[int, float]: """Calculate fill rates for each retailer.""" fill_rates = {} def get_retailer_capacity_utilization(self, retailer_inventories: List[RetailerInventory]) -> Dict[int, float]: """Calculate capacity utilization for each retailer. Capacity utilization = Current Stock / Total Capacity """ capacity_utilization = {} # Group stock by retailer retailer_stock = {} for inv in retailer_inventories: if inv.retailer_id not in retailer_stock: retailer_stock[inv.retailer_id] = 0 retailer_stock[inv.retailer_id] += inv.quantity # Calculate utilization for each retailer for retailer in self.setup.retailers: retailer_inv = [inv for inv in retailer_inventories if inv.retailer_id == retailer.id] current_stock = retailer_stock.get(retailer.id, 0) utilization = current_stock / retailer.capacity if retailer.capacity > 0 else 0 capacity_utilization[retailer.id] = min(1.0, utilization) # Cap at 100% return capacity_utilization total_stock = sum(inv.quantity for inv in retailer_inv) fill_rate = min(1.0, total_stock / retailer.capacity) if retailer.capacity > 0 else 0 def get_warehouse_capacity_utilization(self, warehouse_inventories: List[WarehouseInventory]) -> Dict[int, float]: """Calculate capacity utilization for each warehouse. Capacity utilization = Current Stock / Total Capacity """ capacity_utilization = {} # Group stock by warehouse warehouse_stock = {} for inv in warehouse_inventories: if inv.warehouse_id not in warehouse_stock: warehouse_stock[inv.warehouse_id] = 0 warehouse_stock[inv.warehouse_id] += inv.quantity fill_rates[retailer.id] = fill_rate # Calculate utilization for each warehouse for warehouse in self.setup.warehouses: current_stock = warehouse_stock.get(warehouse.id, 0) utilization = current_stock / warehouse.capacity if warehouse.capacity > 0 else 0 capacity_utilization[warehouse.id] = min(1.0, utilization) # Cap at 100% return fill_rates No newline at end of file return capacity_utilization No newline at end of file
supply_chain_sim/entities.py +7 −6 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ class ProductComponent: id: int name: str type: str # movement, case, strap, etc cost: float cost: float # Purchase cost from supplier storage_rate: float # cost per unit per day shelf_life: int # days quality_tier: int # 1-5 Loading @@ -48,7 +48,7 @@ class WarehouseInventory: warehouse_id: int item_id: int item_type: str # "component" or "watch" quantity: int quantity: int # Stock quantity age: int = 0 # days @dataclass Loading @@ -57,7 +57,8 @@ class WatchModel: brand_id: int name: str category: str # luxury, sport, casual base_price: float base_cost: float # Manufacturing cost (sum of components + labor) sell_price: float = 0 # Selling price to retailers (base_cost * markup) @dataclass class WatchBOM: Loading @@ -72,14 +73,14 @@ class Retailer: name: str region: str capacity: int markup_policy: float # multiplier markup_policy: float # multiplier for retail price @dataclass class RetailerInventory: id: int retailer_id: int watch_id: int quantity: int quantity: int # Stock quantity return_rate: float = 0.05 @dataclass Loading @@ -99,7 +100,7 @@ class Sale: retailer_id: int watch_id: int quantity: int total_price: float total_price: float # Price paid by customer date: datetime @dataclass Loading
supply_chain_sim/forecasting.py +1 −1 Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ class Forecasting: base_demand = np.random.uniform(15, 30) # Casual has higher volume # Price sensitivity - higher prices reduce demand price_factor = max(0.3, 1.0 - (watch.base_price - 200) / 2000) price_factor = max(0.3, 1.0 - (watch.base_cost - 200) / 2000) base_demand *= price_factor # Regional variation Loading