ademcodesproducts commited on
Commit
b13fa7f
·
1 Parent(s): 7d21b32

First Commit New REpo

Browse files
.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
+ }