Spaces:
Runtime error
Runtime error
ademcodesproducts commited on
Commit ·
b13fa7f
1
Parent(s): 7d21b32
First Commit New REpo
Browse files- .idea/.gitignore +8 -0
- .idea/Inventory-Simulations.iml +8 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +4 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +7 -0
- agent_environment.py +58 -0
- config.py +22 -0
- demand_generator.py +77 -0
- inventory_manager.py +30 -0
- main.py +36 -0
- montecarlo_simulator.py +71 -0
- order_processor.py +25 -0
- performance_tracker.py +33 -0
.idea/.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Default ignored files
|
| 2 |
+
/shelf/
|
| 3 |
+
/workspace.xml
|
| 4 |
+
# Editor-based HTTP Client requests
|
| 5 |
+
/httpRequests/
|
| 6 |
+
# Datasource local storage ignored files
|
| 7 |
+
/dataSources/
|
| 8 |
+
/dataSources.local.xml
|
.idea/Inventory-Simulations.iml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<module type="PYTHON_MODULE" version="4">
|
| 3 |
+
<component name="NewModuleRootManager">
|
| 4 |
+
<content url="file://$MODULE_DIR$" />
|
| 5 |
+
<orderEntry type="jdk" jdkName="Python 3.9 (SafetyStockSimulations)" jdkType="Python SDK" />
|
| 6 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
| 7 |
+
</component>
|
| 8 |
+
</module>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<component name="InspectionProjectProfileManager">
|
| 2 |
+
<settings>
|
| 3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
| 4 |
+
<version value="1.0" />
|
| 5 |
+
</settings>
|
| 6 |
+
</component>
|
.idea/misc.xml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (SafetyStockSimulations)" project-jdk-type="Python SDK" />
|
| 4 |
+
</project>
|
.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="ProjectModuleManager">
|
| 4 |
+
<modules>
|
| 5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/Inventory-Simulations.iml" filepath="$PROJECT_DIR$/.idea/Inventory-Simulations.iml" />
|
| 6 |
+
</modules>
|
| 7 |
+
</component>
|
| 8 |
+
</project>
|
.idea/vcs.xml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="VcsDirectoryMappings">
|
| 4 |
+
<mapping directory="" vcs="Git" />
|
| 5 |
+
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
| 6 |
+
</component>
|
| 7 |
+
</project>
|
agent_environment.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import abstractmethod, ABC
|
| 2 |
+
import numpy as np
|
| 3 |
+
from scipy.stats import norm
|
| 4 |
+
from config import LEAD_TIME, DEFAULT_SERVICE_LEVEL, MC_SIMS, HISTO_DAYS
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Agent(ABC):
|
| 8 |
+
def __init__(self, daily_demand_distribution):
|
| 9 |
+
self.daily_demand_distribution = daily_demand_distribution
|
| 10 |
+
|
| 11 |
+
@abstractmethod
|
| 12 |
+
def compute_reorder_point(self, time_period) -> float:
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
def get_historical_demand(self, time_period):
|
| 16 |
+
start_time = time_period - HISTO_DAYS
|
| 17 |
+
demand_list = self.daily_demand_distribution.daily_demand_distribution
|
| 18 |
+
return [d.actual_demand for d in demand_list[start_time:time_period]]
|
| 19 |
+
|
| 20 |
+
class BaseAgent(Agent):
|
| 21 |
+
|
| 22 |
+
def compute_reorder_point(self, time_period):
|
| 23 |
+
historical_demand = self.get_historical_demand(time_period)
|
| 24 |
+
demand_mean = np.mean(historical_demand)
|
| 25 |
+
return demand_mean * LEAD_TIME
|
| 26 |
+
|
| 27 |
+
class SafetyStockAgent(Agent):
|
| 28 |
+
|
| 29 |
+
def compute_reorder_point(self, time_period) -> float:
|
| 30 |
+
historical_demand = self.get_historical_demand(time_period)
|
| 31 |
+
|
| 32 |
+
demand_mean = np.mean(historical_demand)
|
| 33 |
+
demand_std = np.std(historical_demand, ddof=1) # sample standard deviation
|
| 34 |
+
|
| 35 |
+
safety_stock = norm.ppf(DEFAULT_SERVICE_LEVEL) * demand_std * np.sqrt(LEAD_TIME)
|
| 36 |
+
return demand_mean * LEAD_TIME + safety_stock
|
| 37 |
+
|
| 38 |
+
class ForecastAgent(Agent):
|
| 39 |
+
def __init__(self, daily_demand_distribution, demand_mean, demand_std):
|
| 40 |
+
super().__init__(daily_demand_distribution)
|
| 41 |
+
self.demand_mean = demand_mean
|
| 42 |
+
self.demand_std = demand_std
|
| 43 |
+
|
| 44 |
+
def compute_reorder_point(self, time_period) -> float:
|
| 45 |
+
safety_stock = norm.ppf(DEFAULT_SERVICE_LEVEL) * self.demand_std[time_period] * np.sqrt(LEAD_TIME)
|
| 46 |
+
return self.demand_mean[time_period] * LEAD_TIME + safety_stock
|
| 47 |
+
|
| 48 |
+
class MonteCarloAgent(Agent):
|
| 49 |
+
def __init__(self, daily_demand_distribution):
|
| 50 |
+
super().__init__(daily_demand_distribution)
|
| 51 |
+
self.q = DEFAULT_SERVICE_LEVEL
|
| 52 |
+
|
| 53 |
+
def compute_reorder_point(self, time_period) -> float:
|
| 54 |
+
samples = self.daily_demand_distribution.sample_lead_time_demand(
|
| 55 |
+
time_period=time_period,
|
| 56 |
+
mc_sims=MC_SIMS
|
| 57 |
+
)
|
| 58 |
+
return float(np.quantile(samples, self.q))
|
config.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simulation constraints
|
| 2 |
+
import numpy as np
|
| 3 |
+
np.random.seed(42)
|
| 4 |
+
|
| 5 |
+
SIM_DAYS = 730
|
| 6 |
+
HISTO_DAYS = 365
|
| 7 |
+
N_SIMULATIONS = 100
|
| 8 |
+
MC_SIMS = 1000
|
| 9 |
+
|
| 10 |
+
# Replenishment constraints & constants
|
| 11 |
+
WRITE_OFF_RATE = 0.01
|
| 12 |
+
WRITE_OFF_FREQUENCY = 7
|
| 13 |
+
|
| 14 |
+
# Stock constraints
|
| 15 |
+
LEAD_TIME = 3
|
| 16 |
+
BASE_STOCK = 0
|
| 17 |
+
DEFAULT_SERVICE_LEVEL = 0.95
|
| 18 |
+
|
| 19 |
+
# Demand constraints
|
| 20 |
+
GAMMA_SHAPE = np.random.uniform(6, 8) # 7
|
| 21 |
+
GAMMA_SCALE = np.random.uniform(14, 18) # 16
|
| 22 |
+
POISSON_LAMBDA = np.random.uniform(75, 85) # 80
|
demand_generator.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import ciw.dists
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import List
|
| 5 |
+
from config import POISSON_LAMBDA, GAMMA_SHAPE, GAMMA_SCALE, MC_SIMS, LEAD_TIME
|
| 6 |
+
|
| 7 |
+
np.random.seed(42)
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class DailyDemandDistribution:
|
| 11 |
+
day: int
|
| 12 |
+
actual_demand: int
|
| 13 |
+
demand_mean: float
|
| 14 |
+
demand_std: float
|
| 15 |
+
forecast_distribution: ciw.dists.Distribution
|
| 16 |
+
|
| 17 |
+
class DemandGenerator:
|
| 18 |
+
def __init__(self, SIM_DAYS):
|
| 19 |
+
self.SIM_DAYS = SIM_DAYS
|
| 20 |
+
self.daily_demand_distribution: List[DailyDemandDistribution] = self.generate_distribution()
|
| 21 |
+
|
| 22 |
+
def generate_distribution(self) -> List[DailyDemandDistribution]:
|
| 23 |
+
|
| 24 |
+
daily_demand_distribution = []
|
| 25 |
+
|
| 26 |
+
for day in range(self.SIM_DAYS):
|
| 27 |
+
demand_distribution = ciw.dists.MixtureDistribution(
|
| 28 |
+
[
|
| 29 |
+
ciw.dists.Gamma(shape=GAMMA_SHAPE, scale=GAMMA_SCALE),
|
| 30 |
+
ciw.dists.Poisson(rate=POISSON_LAMBDA)
|
| 31 |
+
],
|
| 32 |
+
[0.9, 0.1]
|
| 33 |
+
)
|
| 34 |
+
demand_mean, demand_std = self.get_demand_stats()
|
| 35 |
+
actual_demand = int(demand_distribution.sample())
|
| 36 |
+
|
| 37 |
+
daily_demand_distribution.append(
|
| 38 |
+
DailyDemandDistribution(
|
| 39 |
+
day=day,
|
| 40 |
+
actual_demand=actual_demand,
|
| 41 |
+
demand_mean=demand_mean,
|
| 42 |
+
demand_std=demand_std,
|
| 43 |
+
forecast_distribution=demand_distribution
|
| 44 |
+
)
|
| 45 |
+
)
|
| 46 |
+
return daily_demand_distribution
|
| 47 |
+
|
| 48 |
+
def get_daily_demand(self, time_period: int) -> int: # CHECK UTILITY OF THIS FUNCTION
|
| 49 |
+
return self.daily_demand_distribution[time_period].actual_demand
|
| 50 |
+
|
| 51 |
+
def sample_lead_time_demand(self, time_period, mc_sims=MC_SIMS): # CHECK TO REDUCE TIME COMPLEXITY
|
| 52 |
+
samples = []
|
| 53 |
+
for i in range(mc_sims):
|
| 54 |
+
total_demand = 0
|
| 55 |
+
for j in range(1, LEAD_TIME + 1):
|
| 56 |
+
total_demand += self.daily_demand_distribution[time_period + j].forecast_distribution.sample()
|
| 57 |
+
samples.append(total_demand)
|
| 58 |
+
return samples
|
| 59 |
+
|
| 60 |
+
# ciw.dists doesn't have a method to get mean and std, so we calculate it manually
|
| 61 |
+
def get_demand_stats(self) -> (float, float):
|
| 62 |
+
weights = [0.9, 0.1]
|
| 63 |
+
mean_gamma = GAMMA_SHAPE * GAMMA_SCALE
|
| 64 |
+
mean_poisson = POISSON_LAMBDA
|
| 65 |
+
|
| 66 |
+
var_gamma = GAMMA_SHAPE * GAMMA_SCALE ** 2
|
| 67 |
+
var_poisson = POISSON_LAMBDA
|
| 68 |
+
|
| 69 |
+
mixture_mean = weights[0] * mean_gamma + weights[1] * mean_poisson
|
| 70 |
+
|
| 71 |
+
mixture_var = (
|
| 72 |
+
weights[0] * (var_gamma + (mean_gamma - mixture_mean) ** 2) +
|
| 73 |
+
weights[1] * (var_poisson + (mean_poisson - mixture_mean) ** 2)
|
| 74 |
+
)
|
| 75 |
+
mixture_std = np.sqrt(mixture_var)
|
| 76 |
+
|
| 77 |
+
return mixture_mean, mixture_std
|
inventory_manager.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from config import WRITE_OFF_RATE, BASE_STOCK, DEFAULT_SERVICE_LEVEL, MC_SIMS
|
| 2 |
+
from demand_generator import DemandGenerator
|
| 3 |
+
|
| 4 |
+
class InventoryManager:
|
| 5 |
+
def __init__(self, order_processor, agent):
|
| 6 |
+
self.inventory = BASE_STOCK
|
| 7 |
+
self.order_processor = order_processor
|
| 8 |
+
self.total_write_off_quantity = 0
|
| 9 |
+
self.agent = agent
|
| 10 |
+
|
| 11 |
+
def reorder(self, time_period):
|
| 12 |
+
reorder_point = self.agent.compute_reorder_point(time_period)
|
| 13 |
+
if self.inventory <= reorder_point:
|
| 14 |
+
self.order_processor.place_order(time_period, reorder_point)
|
| 15 |
+
|
| 16 |
+
def inventory_update(self, demand_quantity):
|
| 17 |
+
if self.inventory >= demand_quantity:
|
| 18 |
+
self.inventory -= demand_quantity
|
| 19 |
+
else:
|
| 20 |
+
self.inventory = 0
|
| 21 |
+
|
| 22 |
+
def apply_writeoff(self, time_period):
|
| 23 |
+
write_off_quantity = int(self.inventory * WRITE_OFF_RATE)
|
| 24 |
+
self.inventory -= write_off_quantity
|
| 25 |
+
self.total_write_off_quantity += write_off_quantity
|
| 26 |
+
return write_off_quantity
|
| 27 |
+
|
| 28 |
+
def process_deliveries(self, time_period):
|
| 29 |
+
processed_delivery = self.order_processor.manage_order(time_period)
|
| 30 |
+
self.inventory += processed_delivery
|
main.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import montecarlo_simulator
|
| 2 |
+
from agent_environment import MonteCarloAgent, SafetyStockAgent, ForecastAgent, Agent, BaseAgent
|
| 3 |
+
from demand_generator import DemandGenerator
|
| 4 |
+
from config import SIM_DAYS, N_SIMULATIONS
|
| 5 |
+
import ciw
|
| 6 |
+
ciw.seed(11)
|
| 7 |
+
|
| 8 |
+
simulation_version = input("Choose a simulation version (0-3):\n"
|
| 9 |
+
"0: Historical Demand Agent\n"
|
| 10 |
+
"1: Safety Stock Agent\n"
|
| 11 |
+
"2: Forecast Agent\n"
|
| 12 |
+
"3: Monte Carlo Agent\n")
|
| 13 |
+
|
| 14 |
+
print(f"Running simulation version: {simulation_version}")
|
| 15 |
+
version = int(simulation_version)
|
| 16 |
+
|
| 17 |
+
# THIS SHIT SUPPOSED TO BE HAPPENING DURING SIMULATION
|
| 18 |
+
demand_generator = DemandGenerator(SIM_DAYS)
|
| 19 |
+
daily_demand_distribution = demand_generator.daily_demand_distribution
|
| 20 |
+
|
| 21 |
+
demand_mean = [d.demand_mean for d in daily_demand_distribution]
|
| 22 |
+
demand_std = [d.demand_std for d in daily_demand_distribution]
|
| 23 |
+
|
| 24 |
+
agent_versions = {
|
| 25 |
+
0: BaseAgent(daily_demand_distribution),
|
| 26 |
+
1: SafetyStockAgent(daily_demand_distribution),
|
| 27 |
+
2: ForecastAgent(daily_demand_distribution, demand_mean, demand_std),
|
| 28 |
+
3: MonteCarloAgent(daily_demand_distribution)
|
| 29 |
+
}
|
| 30 |
+
try:
|
| 31 |
+
selected_agent = agent_versions[version]
|
| 32 |
+
except:
|
| 33 |
+
raise ValueError("Invalid simulation version")
|
| 34 |
+
|
| 35 |
+
sim = montecarlo_simulator.MonteCarloSimulator(selected_agent)
|
| 36 |
+
sim.run_simulation(N_SIMULATIONS, SIM_DAYS)
|
montecarlo_simulator.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from config import LEAD_TIME, HISTO_DAYS
|
| 3 |
+
from demand_generator import DemandGenerator
|
| 4 |
+
from order_processor import OrderProcessor
|
| 5 |
+
from inventory_manager import InventoryManager
|
| 6 |
+
from performance_tracker import PerformanceTracker
|
| 7 |
+
|
| 8 |
+
class MonteCarloSimulator():
|
| 9 |
+
def __init__(self, agent):
|
| 10 |
+
self.agent = agent
|
| 11 |
+
|
| 12 |
+
def run_simulation(self, N_SIMULATIONS, SIM_DAYS):
|
| 13 |
+
|
| 14 |
+
all_simulation_results = []
|
| 15 |
+
|
| 16 |
+
for sim in range(N_SIMULATIONS):
|
| 17 |
+
time_period = 0
|
| 18 |
+
order_processor = OrderProcessor()
|
| 19 |
+
performance_tracker = PerformanceTracker()
|
| 20 |
+
demand_generator = DemandGenerator(SIM_DAYS)
|
| 21 |
+
self.agent.daily_demand_distribution = demand_generator
|
| 22 |
+
|
| 23 |
+
inventory_manager = InventoryManager(
|
| 24 |
+
order_processor=order_processor,
|
| 25 |
+
agent=self.agent
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Generate historical demand data -> consistency with algo 1 & 2
|
| 29 |
+
for day in range(HISTO_DAYS):
|
| 30 |
+
_ = demand_generator.get_daily_demand(day)
|
| 31 |
+
|
| 32 |
+
for day in range(HISTO_DAYS, SIM_DAYS):
|
| 33 |
+
demand_quantity = demand_generator.get_daily_demand(day)
|
| 34 |
+
base_inventory = inventory_manager.inventory
|
| 35 |
+
|
| 36 |
+
inventory_manager.inventory_update(demand_quantity)
|
| 37 |
+
|
| 38 |
+
if day < SIM_DAYS - LEAD_TIME: # no more orders after the last lead time
|
| 39 |
+
inventory_manager.reorder(day)
|
| 40 |
+
|
| 41 |
+
inventory_manager.process_deliveries(day)
|
| 42 |
+
|
| 43 |
+
fulfilled_demand = min(demand_quantity, base_inventory)
|
| 44 |
+
daily_writeoff = inventory_manager.apply_writeoff(day)
|
| 45 |
+
|
| 46 |
+
performance_tracker.daily_performance(
|
| 47 |
+
demand_quantity=demand_quantity,
|
| 48 |
+
fulfilled_demand=fulfilled_demand,
|
| 49 |
+
daily_writeoff=daily_writeoff
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
time_period += 1
|
| 53 |
+
|
| 54 |
+
print(f"\n Simulation {sim + 1}")
|
| 55 |
+
print("-" * 30)
|
| 56 |
+
sim_summary = performance_tracker.performance_summary()
|
| 57 |
+
|
| 58 |
+
for key, value in sim_summary.items():
|
| 59 |
+
print(f"{key.title()}: {value}")
|
| 60 |
+
|
| 61 |
+
all_simulation_results.append(performance_tracker.performance_summary())
|
| 62 |
+
|
| 63 |
+
self.generate_overall_report(all_simulation_results)
|
| 64 |
+
|
| 65 |
+
def generate_overall_report(self, results):
|
| 66 |
+
df = pd.DataFrame(results)
|
| 67 |
+
summary_stats = df.agg(['mean', 'std']).transpose()
|
| 68 |
+
summary_stats.columns = ['Mean', 'Standard Deviation']
|
| 69 |
+
print("Cummulatige Simulation Results")
|
| 70 |
+
print("---------------------------------------------")
|
| 71 |
+
print(summary_stats.to_string(float_format="%.6f"))
|
order_processor.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List
|
| 3 |
+
from config import LEAD_TIME
|
| 4 |
+
|
| 5 |
+
@dataclass
|
| 6 |
+
class Order:
|
| 7 |
+
arrival_day: int
|
| 8 |
+
quantity: int
|
| 9 |
+
|
| 10 |
+
class OrderProcessor:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.order_queue: List[Order] = [] # self.order_queue stores Order objects
|
| 13 |
+
|
| 14 |
+
def place_order(self, time_period: int, quantity: int):
|
| 15 |
+
arrival_day = time_period + LEAD_TIME # time_period = current_day
|
| 16 |
+
self.order_queue.append(Order(arrival_day=arrival_day, quantity=quantity))
|
| 17 |
+
|
| 18 |
+
def manage_order(self, time_period: int) -> int:
|
| 19 |
+
arrived_orders = [order for order in self.order_queue if order.arrival_day == time_period]
|
| 20 |
+
# if order.arrival_day < current_day order excluded after each call of process_order
|
| 21 |
+
self.order_queue = [order for order in self.order_queue if order.arrival_day > time_period]
|
| 22 |
+
return sum(order.quantity for order in arrived_orders)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
performance_tracker.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class PerformanceTracker:
|
| 2 |
+
|
| 3 |
+
def __init__(self):
|
| 4 |
+
self.total_demand = 0
|
| 5 |
+
self.total_fulfilled_demand = 0
|
| 6 |
+
self.fill_rate = 0
|
| 7 |
+
self.write_offs = 0
|
| 8 |
+
self.stock_out_count = 0
|
| 9 |
+
self.total_lost_sales = 0
|
| 10 |
+
|
| 11 |
+
def daily_performance(self, demand_quantity, fulfilled_demand, daily_writeoff):
|
| 12 |
+
self.total_demand += demand_quantity
|
| 13 |
+
self.total_fulfilled_demand += fulfilled_demand
|
| 14 |
+
self.fill_rate = 1 - (self.total_demand - self.total_fulfilled_demand ) / self.total_demand if self.total_demand > 0 else 0
|
| 15 |
+
self.write_offs += daily_writeoff
|
| 16 |
+
self.total_lost_sales += (demand_quantity - fulfilled_demand) if demand_quantity > fulfilled_demand else 0
|
| 17 |
+
|
| 18 |
+
if fulfilled_demand < demand_quantity:
|
| 19 |
+
self.stock_out_counter()
|
| 20 |
+
|
| 21 |
+
def stock_out_counter(self) -> int:
|
| 22 |
+
self.stock_out_count += 1
|
| 23 |
+
return self.stock_out_count
|
| 24 |
+
|
| 25 |
+
def performance_summary(self):
|
| 26 |
+
return {
|
| 27 |
+
"total_demand": self.total_demand,
|
| 28 |
+
"fulfilled_demand": self.total_fulfilled_demand,
|
| 29 |
+
"fill_rate": self.fill_rate,
|
| 30 |
+
"write_offs": self.write_offs,
|
| 31 |
+
"stock_out_count": self.stock_out_count,
|
| 32 |
+
"total_lost_sales": self.total_lost_sales
|
| 33 |
+
}
|