File size: 29,067 Bytes
79d3012
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
import gradio as gr
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import io 
from scipy.stats import norm # Using scipy.stats, but it's a common numpy-adjacent lib for stats. If not allowed, can be replaced.
# Let's stick to numpy. We can use norm.ppf or just ask for Z-score.
# User said NO new concepts. Z-score is simple. I will just ask for the Z-score directly.

# --- Calculation Functions ---

def calculate_basic_eoq(annual_demand, order_cost, holding_cost_per_unit):
    """Calculates EOQ and related metrics for the basic model."""
    if annual_demand <= 0 or order_cost <= 0 or holding_cost_per_unit <= 0:
        return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan

    eoq = np.sqrt((2 * annual_demand * order_cost) / holding_cost_per_unit)
    num_orders_per_year = annual_demand / eoq if eoq > 0 else np.inf
    avg_inventory = eoq / 2
    annual_ordering_cost = num_orders_per_year * order_cost
    annual_holding_cost = avg_inventory * holding_cost_per_unit
    total_annual_cost = annual_ordering_cost + annual_holding_cost
    
    demand_per_day = annual_demand / 365
    
    return eoq, num_orders_per_year, avg_inventory, annual_ordering_cost, annual_holding_cost, total_annual_cost, demand_per_day

def calculate_eoq_with_discount(annual_demand, order_cost, holding_cost_rate, unit_cost, discount_tiers):
    """Calculates EOQ with quantity discounts."""
    if annual_demand <= 0 or order_cost <= 0 or holding_cost_rate <= 0 or unit_cost <= 0:
        return pd.DataFrame(), np.nan, np.nan, np.nan, np.nan, "Invalid inputs for discount model."

    results = []
    best_total_cost = np.inf
    best_eoq = np.nan
    best_unit_cost = np.nan
    best_tier = ""

    discount_tiers = sorted(discount_tiers, key=lambda x: x[0])

    for i, (min_qty, max_qty, tier_unit_cost) in enumerate(discount_tiers):
        holding_cost_per_unit = holding_cost_rate * tier_unit_cost
        
        tier_eoq, _, _, _, _, _, _ = calculate_basic_eoq(annual_demand, order_cost, holding_cost_per_unit)
        
        if tier_eoq < min_qty:
            relevant_qty = min_qty
        elif tier_eoq > max_qty and max_qty != np.inf:
            relevant_qty = max_qty
        else: 
            relevant_qty = tier_eoq
        
        if relevant_qty <= 0:
            num_orders = np.inf
            ordering_cost = np.inf
            holding_cost = 0 
            purchase_cost = annual_demand * tier_unit_cost
            total_cost = np.inf
        else:
            num_orders = annual_demand / relevant_qty
            ordering_cost = num_orders * order_cost
            holding_cost = (relevant_qty / 2) * holding_cost_per_unit
            purchase_cost = annual_demand * tier_unit_cost
            total_cost = ordering_cost + holding_cost + purchase_cost
            
        results.append({
            "Tier": f"Tier {i+1} ({min_qty}-{max_qty if max_qty != np.inf else '∞'})",
            "Unit Cost ($)": tier_unit_cost,
            "Holding Cost/Unit/Year ($)": f"{holding_cost_per_unit:.2f}",
            "Theoretical EOQ (Units)": f"{tier_eoq:.0f}",
            "Relevant Q (Units)": f"{relevant_qty:.0f}",
            "Annual Ordering Cost ($)": f"{ordering_cost:.2f}",
            "Annual Holding Cost ($)": f"{holding_cost:.2f}",
            "Annual Purchase Cost ($)": f"{purchase_cost:.2f}",
            "Total Annual Cost ($)": f"{total_cost:.2f}"
        })

        if total_cost < best_total_cost:
            best_total_cost = total_cost
            best_eoq = relevant_qty
            best_unit_cost = tier_unit_cost
            best_tier = f"Tier {i+1}"

    df = pd.DataFrame(results)
    
    return df, best_eoq, best_total_cost, best_unit_cost, best_tier, "Analysis complete."


def calculate_poq(annual_demand, order_cost, holding_cost_per_unit, daily_production_rate, daily_demand_rate):
    """Calculates Production Order Quantity (POQ) and related metrics."""
    if annual_demand <= 0 or order_cost <= 0 or holding_cost_per_unit <= 0 or \
       daily_production_rate <= 0 or daily_demand_rate <= 0:
        return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
    
    if daily_production_rate <= daily_demand_rate:
        return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, "Production rate must be greater than demand rate."

    poq = np.sqrt((2 * annual_demand * order_cost) / (holding_cost_per_unit * (1 - (daily_demand_rate / daily_production_rate))))
    
    num_setups_per_year = annual_demand / poq if poq > 0 else np.inf
    max_inventory_level = poq * (1 - (daily_demand_rate / daily_production_rate))
    avg_inventory = max_inventory_level / 2
    
    annual_setup_cost = num_setups_per_year * order_cost
    annual_holding_cost = avg_inventory * holding_cost_per_unit
    total_annual_cost = annual_setup_cost + annual_holding_cost
    
    production_run_days = poq / daily_production_rate
    inventory_cycle_days = poq / daily_demand_rate
    
    return poq, num_setups_per_year, max_inventory_level, avg_inventory, annual_setup_cost, \
           annual_holding_cost, total_annual_cost, production_run_days, inventory_cycle_days

def calculate_rop_and_ss(avg_daily_demand, lead_time_days, std_dev_daily_demand, service_level_z):
    """Calculates Reorder Point and Safety Stock."""
    if avg_daily_demand < 0 or lead_time_days < 0 or std_dev_daily_demand < 0 or service_level_z < 0:
        return 0, 0, 0, "Inputs must be non-negative."
        
    std_dev_lead_time = std_dev_daily_demand * np.sqrt(lead_time_days)
    safety_stock = std_dev_lead_time * service_level_z
    demand_during_lead_time = avg_daily_demand * lead_time_days
    reorder_point = demand_during_lead_time + safety_stock
    
    return safety_stock, demand_during_lead_time, reorder_point, "Calculation successful."

def calculate_sma_forecast(demand_data_str, window_size):
    """Calculates Simple Moving Average (SMA) forecast."""
    if not demand_data_str:
        return pd.DataFrame(), None, "Please enter demand data."
    
    try:
        demand_values = [float(d.strip()) for d in demand_data_str.split(',') if d.strip()]
        if len(demand_values) < window_size:
            return pd.DataFrame(), None, f"Not enough data for window size {window_size}. Need at least {window_size} data points."
        
        df = pd.DataFrame({'Demand': demand_values})
        df['Period'] = range(1, len(df) + 1)
        
        # Calculate SMA
        df[f'SMA (Window={window_size})'] = df['Demand'].rolling(window=window_size).mean()
        
        # Forecast next period
        forecast_next_period = df['Demand'].tail(window_size).mean()
        
        # Create plot
        fig, ax = plt.subplots(figsize=(8, 4))
        ax.plot(df['Period'], df['Demand'], label='Actual Demand', marker='o', linestyle='-')
        ax.plot(df['Period'], df[f'SMA (Window={window_size})'], label='SMA', marker='x', linestyle='--')
        
        ax.set_title('Simple Moving Average (SMA) Forecast', fontsize=14)
        ax.set_xlabel('Period', fontsize=10)
        ax.set_ylabel('Demand', fontsize=10)
        ax.legend()
        ax.grid(True, linestyle=':', alpha=0.7)
        plt.tight_layout()
        plt.close(fig)
        
        return df, fig, f"Forecast for next period: {forecast_next_period:.2f}"

    except Exception as e:
        return pd.DataFrame(), None, f"Error: {e}. Ensure data is comma-separated numbers."

# --- Plotting Function ---

def create_eoq_plot(annual_demand, order_cost, holding_cost_per_unit, min_q=1, max_q_multiplier=2.5, current_eoq=None):
    """Generates the EOQ cost curves plot."""
    if annual_demand <= 0 or order_cost <= 0 or holding_cost_per_unit <= 0:
        fig, ax = plt.subplots(figsize=(6, 4))
        ax.text(0.5, 0.5, "Invalid input for plot.", horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
        ax.axis('off')
        plt.close(fig)
        return fig

    if current_eoq is None or np.isnan(current_eoq) or current_eoq <= 0:
        current_eoq = np.sqrt((2 * annual_demand * order_cost) / holding_cost_per_unit)
        if np.isnan(current_eoq) or current_eoq <= 0:
            current_eoq = 100 

    quantity_range = np.linspace(min_q, current_eoq * max_q_multiplier, 300)
    
    quantity_range = quantity_range[quantity_range > 0] 
    
    ordering_costs = (annual_demand / quantity_range) * order_cost
    holding_costs = (quantity_range / 2) * holding_cost_per_unit
    total_costs = ordering_costs + holding_costs

    fig, ax = plt.subplots(figsize=(6, 4))

    ax.plot(quantity_range, holding_costs, label='Annual Holding Cost', color='orange')
    ax.plot(quantity_range, ordering_costs, label='Annual Ordering Cost', color='blue')
    ax.plot(quantity_range, total_costs, label='Total Cost', color='green', linewidth=2.5)
    
    if not np.isnan(current_eoq) and current_eoq > 0:
        ax.axvline(x=current_eoq, color='red', linestyle='--', label=f'EOQ: {current_eoq:.0f} units')
        total_cost_at_eoq_idx = np.argmin(np.abs(quantity_range - current_eoq))
        total_cost_at_eoq = total_costs[total_cost_at_eoq_idx]
        ax.plot(current_eoq, total_cost_at_eoq, 'ro')
    
    ax.set_title('EOQ Cost Analysis', fontsize=12)
    ax.set_xlabel('Order Quantity (Units)', fontsize=10)
    ax.set_ylabel('Annual Cost ($)', fontsize=10)
    ax.legend(fontsize=8, loc='upper right')
    ax.grid(True, linestyle=':', alpha=0.7)
    ax.set_ylim(bottom=0)
    ax.set_xlim(left=0)
    ax.tick_params(axis='both', which='major', labelsize=8)
    
    plt.tight_layout()
    plt.close(fig)
    return fig

# --- Combined Interface Function for Basic EOQ ---

def update_basic_eoq(annual_demand, order_cost, holding_cost_per_unit, lead_time_days):
    """Updates all outputs for the Basic EOQ tab."""
    if any(x <= 0 for x in [annual_demand, order_cost, holding_cost_per_unit]):
        return None, 0, 0, 0, 0, 0, 0, 0, "Please enter positive values for all basic EOQ parameters."

    eoq, num_orders, avg_inventory, annual_ordering_cost, annual_holding_cost, total_annual_cost, demand_per_day = \
        calculate_basic_eoq(annual_demand, order_cost, holding_cost_per_unit)

    # Simple Reorder Point (no safety stock)
    reorder_point = demand_per_day * lead_time_days
    
    plot_fig = create_eoq_plot(annual_demand, order_cost, holding_cost_per_unit, current_eoq=eoq)

    return plot_fig, eoq, num_orders, avg_inventory, annual_ordering_cost, annual_holding_cost, \
           total_annual_cost, reorder_point, "Calculation successful."

# --- Combined Interface Function for POQ ---
def update_poq_model(annual_demand, setup_cost, holding_cost_per_unit, daily_production_rate, daily_demand_rate):
    """Updates all outputs for the POQ tab."""
    if any(x <= 0 for x in [annual_demand, setup_cost, holding_cost_per_unit, daily_production_rate, daily_demand_rate]):
        return None, 0, 0, 0, 0, 0, 0, 0, 0, "Please enter positive values for all POQ parameters."
    
    if daily_production_rate <= daily_demand_rate:
         return None, 0, 0, 0, 0, 0, 0, 0, 0, "Production rate must be greater than demand rate."

    poq, num_setups, max_inv, avg_inv, annual_setup_cost, annual_holding_cost, total_cost, prod_days, cycle_days = \
        calculate_poq(annual_demand, setup_cost, holding_cost_per_unit, daily_production_rate, daily_demand_rate)
    
    plot_fig = create_eoq_plot(annual_demand, setup_cost, holding_cost_per_unit, current_eoq=poq, max_q_multiplier=2.0)

    return plot_fig, poq, num_setups, max_inv, avg_inv, annual_setup_cost, annual_holding_cost, total_cost, \
           prod_days, cycle_days, "Calculation successful."

# --- Helper for Discount Tiers ---
def create_discount_tiers_df(tier1_min, tier1_max, tier1_uc,
                             tier2_min, tier2_max, tier2_uc,
                             tier3_min, tier3_max, tier3_uc):
    """Helper to create the discount_tiers list from Gradio inputs."""
    tiers = []
    if tier1_min is not None and tier1_uc is not None and tier1_min >= 0 and tier1_uc >= 0:
        tiers.append((tier1_min, tier1_max if tier1_max is not None else np.inf, tier1_uc))
    if tier2_min is not None and tier2_uc is not None and tier2_min >= 0 and tier2_uc >= 0:
        tiers.append((tier2_min, tier2_max if tier2_max is not None else np.inf, tier2_uc))
    if tier3_min is not None and tier3_uc is not None and tier3_min >= 0 and tier3_uc >= 0:
        tiers.append((tier3_min, tier3_max if tier3_max is not None else np.inf, tier3_uc))
    
    valid_tiers = []
    for t in tiers:
        if t[1] != np.inf and t[0] >= t[1]:
            print(f"Warning: Invalid tier range {t}. Skipping.")
        else:
            valid_tiers.append(t)

    valid_tiers = sorted(valid_tiers, key=lambda x: x[0])
    
    return valid_tiers

# --- Combined Interface Function for Discount Model ---
def update_discount_model(annual_demand, order_cost, holding_cost_rate_percent, 
                          tier1_min, tier1_max, tier1_uc,
                          tier2_min, tier2_max, tier2_uc,
                          tier3_min, tier3_max, tier3_uc):
    """Updates all outputs for the Quantity Discount tab."""
    holding_cost_rate = holding_cost_rate_percent / 100.0

    if any(x <= 0 for x in [annual_demand, order_cost, holding_cost_rate_percent]):
        return pd.DataFrame(), 0, 0, 0, "", "Please enter positive values for core discount parameters."
    
    discount_tiers = create_discount_tiers_df(
        tier1_min, tier1_max, tier1_uc,
        tier2_min, tier2_max, tier2_uc,
        tier3_min, tier3_max, tier3_uc
    )
    
    if not discount_tiers:
        return pd.DataFrame(), 0, 0, 0, "", "No valid discount tiers defined. Please define at least one tier with positive min quantity and unit cost."

    for i in range(len(discount_tiers) - 1):
        if discount_tiers[i][2] < discount_tiers[i+1][2]:
            return pd.DataFrame(), 0, 0, 0, "", "Error: Unit costs must be non-increasing with quantity."
            
    df_results, best_eoq, best_total_cost, best_unit_cost, best_tier, message = \
        calculate_eoq_with_discount(annual_demand, order_cost, holding_cost_rate, discount_tiers[0][2], discount_tiers)

    return df_results, best_eoq, best_total_cost, best_unit_cost, best_tier, message

# --- Gradio Interface ---

with gr.Blocks(theme=gr.themes.Soft(), title="Advanced Operations Management Dashboard") as demo:
    gr.Markdown(
        """
        # πŸ“Š Advanced Operations Management Dashboard (Xyphor Advisors)
        Welcome to your comprehensive tool for optimizing operations and inventory decisions.
        """
    )
    
    with gr.Tabs():
        # --- Tab 1: Basic EOQ Model ---
        with gr.TabItem("Basic EOQ Model"):
            gr.Markdown("## Economic Order Quantity (EOQ) Calculation")
            gr.Markdown("Find the optimal order quantity that minimizes the sum of ordering and holding costs.")
            with gr.Row():
                with gr.Column():
                    gr.Markdown("### πŸ› οΈ Input Parameters")
                    basic_demand = gr.Slider(100, 50000, value=12000, step=100, label="Annual Demand (D) [units/year]")
                    basic_order_cost = gr.Slider(5, 500, value=100, step=5, label="Ordering Cost (S) [$/order]")
                    basic_holding_cost = gr.Slider(0.1, 50, value=5, step=0.1, label="Holding Cost (H) [$/unit/year]")
                    basic_lead_time = gr.Slider(0, 30, value=7, step=1, label="Lead Time [days]", info="Time from order placement to receipt.")
                    
                    basic_status_message = gr.Textbox(label="Status", interactive=False, value="Enter parameters and run.")

                with gr.Column():
                    gr.Markdown("### πŸ“ˆ Cost Analysis & Optimal Q")
                    basic_plot_output = gr.Plot(label="EOQ Cost Curves", scale=2)

                    with gr.Accordion("Detailed Results", open=True):
                        gr.Markdown("#### Key Metrics")
                        with gr.Row():
                            basic_eoq_out = gr.Number(label="Optimal Order Quantity (EOQ) [units]", precision=0)
                            basic_num_orders_out = gr.Number(label="Annual Orders [count]", precision=2)
                            basic_avg_inventory_out = gr.Number(label="Average Inventory [units]", precision=2)
                        gr.Markdown("#### Annual Costs")
                        with gr.Row():
                            basic_ordering_cost_out = gr.Number(label="Annual Ordering Cost [$]", precision=2)
                            basic_holding_cost_out = gr.Number(label="Annual Holding Cost [$]", precision=2)
                            basic_total_cost_out = gr.Number(label="Total Annual Cost [$]", precision=2)
                        gr.Markdown("#### Reorder Point (Simple)")
                        with gr.Row():
                            basic_reorder_point_out = gr.Number(label="Reorder Point (without Safety Stock) [units]", precision=0)

            basic_inputs = [basic_demand, basic_order_cost, basic_holding_cost, basic_lead_time]
            basic_outputs = [basic_plot_output, basic_eoq_out, basic_num_orders_out, basic_avg_inventory_out,
                             basic_ordering_cost_out, basic_holding_cost_out, basic_total_cost_out,
                             basic_reorder_point_out, basic_status_message]
            
            for inp in basic_inputs:
                inp.change(
                    fn=update_basic_eoq, 
                    inputs=basic_inputs, 
                    outputs=basic_outputs
                )
            
            demo.load(
                fn=update_basic_eoq, 
                inputs=basic_inputs, 
                outputs=basic_outputs
            )

        # --- Tab 2: Reorder Point & Safety Stock ---
        with gr.TabItem("Reorder Point (ROP) & Safety Stock"):
            gr.Markdown("## Reorder Point & Safety Stock Calculator")
            gr.Markdown("Determine the precise inventory level at which to place a new order to avoid stockouts.")
            with gr.Row():
                with gr.Column():
                    gr.Markdown("### πŸ› οΈ Input Parameters")
                    rop_avg_demand = gr.Number(label="Average Daily Demand [units/day]", value=50)
                    rop_lead_time = gr.Number(label="Lead Time [days]", value=10)
                    rop_std_dev = gr.Number(label="Standard Deviation of Daily Demand", value=5)
                    rop_z_score = gr.Slider(minimum=0.0, maximum=3.0, value=1.65, step=0.01, 
                                            label="Z-Score (for Service Level)", 
                                            info="e.g., 1.65 for 95% service level, 2.33 for 99%")
                    rop_status = gr.Textbox(label="Status", interactive=False)
                
                with gr.Column():
                    gr.Markdown("### πŸ”‘ Calculated Metrics")
                    rop_ss_out = gr.Number(label="Safety Stock (SS) [units]", precision=0, 
                                           info="The buffer stock held to prevent stockouts.")
                    rop_dlt_out = gr.Number(label="Demand During Lead Time [units]", precision=0,
                                            info="Total expected demand while waiting for the order.")
                    rop_out = gr.Number(label="Reorder Point (ROP) [units]", precision=0, 
                                        info="Place an order when inventory hits this level. (ROP = SS + Demand During Lead Time)")

            rop_inputs = [rop_avg_demand, rop_lead_time, rop_std_dev, rop_z_score]
            rop_outputs = [rop_ss_out, rop_dlt_out, rop_out, rop_status]

            for inp in rop_inputs:
                inp.change(fn=calculate_rop_and_ss, inputs=rop_inputs, outputs=rop_outputs)
            demo.load(fn=calculate_rop_and_ss, inputs=rop_inputs, outputs=rop_outputs)

        # --- Tab 3: Quantity Discount Model ---
        with gr.TabItem("Quantity Discount Model"):
            gr.Markdown("## EOQ with Quantity Discounts")
            gr.Markdown("Evaluate the impact of price breaks on the optimal order quantity and total cost.")
            with gr.Row():
                with gr.Column():
                    gr.Markdown("### πŸ› οΈ Core Parameters")
                    discount_demand = gr.Slider(100, 50000, value=15000, step=100, label="Annual Demand (D) [units/year]")
                    discount_order_cost = gr.Slider(5, 500, value=75, step=5, label="Ordering Cost (S) [$/order]")
                    discount_holding_rate = gr.Slider(0.01, 0.5, value=0.2, step=0.01, label="Holding Cost Rate [% of Unit Cost]", info="e.g., 20% = 0.2")

                    gr.Markdown("### 🏷️ Discount Tiers")
                    gr.Markdown("Define up to three discount tiers. Unit costs must be non-increasing.")
                    with gr.Accordion("Tier 1", open=True):
                        with gr.Row():
                            tier1_min_qty = gr.Number(label="Min Qty", value=0, precision=0)
                            tier1_max_qty = gr.Number(label="Max Qty", value=999, precision=0)
                            tier1_unit_cost = gr.Number(label="Unit Cost ($)", value=10.00, precision=2)
                    with gr.Accordion("Tier 2"):
                        with gr.Row():
                            tier2_min_qty = gr.Number(label="Min Qty", value=1000, precision=0)
                            tier2_max_qty = gr.Number(label="Max Qty", value=4999, precision=0)
                            tier2_unit_cost = gr.Number(label="Unit Cost ($)", value=9.50, precision=2)
                    with gr.Accordion("Tier 3"):
                        with gr.Row():
                            tier3_min_qty = gr.Number(label="Min Qty", value=5000, precision=0)
                            tier3_max_qty = gr.Number(label="Max Qty", value=None, precision=0, info="Leave blank for infinity") 
                            tier3_unit_cost = gr.Number(label="Unit Cost ($)", value=9.00, precision=2)
                    
                    discount_status_message = gr.Textbox(label="Status", interactive=False, value="Enter parameters and run.")

                with gr.Column():
                    gr.Markdown("### πŸ“Š Tier Analysis & Optimal Selection")
                    discount_output_df = gr.DataFrame(label="Discount Tier Analysis", interactive=False)
                    gr.Markdown("#### Optimal Discount Solution")
                    with gr.Row():
                        best_discount_eoq_out = gr.Number(label="Optimal Order Quantity [units]", precision=0)
                        best_discount_cost_out = gr.Number(label="Minimum Total Annual Cost [$]", precision=2)
                    with gr.Row():
                        best_discount_unit_cost_out = gr.Number(label="Unit Cost at Optimal Q [$]", precision=2)
                        best_discount_tier_out = gr.Textbox(label="Optimal Tier", interactive=False)
            
            discount_inputs = [discount_demand, discount_order_cost, discount_holding_rate,
                               tier1_min_qty, tier1_max_qty, tier1_unit_cost,
                               tier2_min_qty, tier2_max_qty, tier2_unit_cost,
                               tier3_min_qty, tier3_max_qty, tier3_unit_cost]
            discount_outputs = [discount_output_df, best_discount_eoq_out, best_discount_cost_out, 
                                best_discount_unit_cost_out, best_discount_tier_out, discount_status_message]

            for inp in discount_inputs:
                inp.change(
                    fn=update_discount_model, 
                    inputs=discount_inputs, 
                    outputs=discount_outputs
                )
            
            demo.load(
                fn=update_discount_model, 
                inputs=discount_inputs, 
                outputs=discount_outputs
            )

        # --- Tab 4: Production Order Quantity (POQ) Model ---
        with gr.TabItem("Production Order Quantity (POQ) Model"):
            gr.Markdown("## Production Order Quantity (POQ) Calculation")
            gr.Markdown("Determine the optimal batch size when production is internal and gradual.")
            with gr.Row():
                with gr.Column():
                    gr.Markdown("### πŸ› οΈ Input Parameters")
                    poq_demand = gr.Slider(100, 50000, value=20000, step=100, label="Annual Demand (D) [units/year]")
                    poq_setup_cost = gr.Slider(5, 500, value=200, step=5, label="Setup Cost (S) [$/setup]")
                    poq_holding_cost = gr.Slider(0.1, 50, value=8, step=0.1, label="Holding Cost (H) [$/unit/year]")
                    gr.Markdown("### 🏭 Production Specifics")
                    poq_prod_rate = gr.Slider(10, 2000, value=500, step=10, label="Daily Production Rate (P) [units/day]")
                    poq_demand_rate = gr.Slider(1, 1000, value=80, step=1, label="Daily Demand Rate (d) [units/day]", info="Must be less than Daily Production Rate.")
                    
                    poq_status_message = gr.Textbox(label="Status", interactive=False, value="Enter parameters and run.")

                with gr.Column():
                    gr.Markdown("### πŸ“ˆ Cost Analysis & Optimal Batch Size")
                    poq_plot_output = gr.Plot(label="POQ Cost Curves", scale=2)

                    with gr.Accordion("Detailed Results", open=True):
                        gr.Markdown("#### Key Metrics")
                        with gr.Row():
                            poq_out = gr.Number(label="Optimal Production Quantity (POQ) [units]", precision=0)
                            poq_num_setups_out = gr.Number(label="Annual Setups [count]", precision=2)
                            poq_max_inv_out = gr.Number(label="Maximum Inventory Level [units]", precision=2)
                            poq_avg_inv_out = gr.Number(label="Average Inventory [units]", precision=2)
                        gr.Markdown("#### Annual Costs")
                        with gr.Row():
                            poq_setup_cost_out = gr.Number(label="Annual Setup Cost [$]", precision=2)
                            poq_holding_cost_out = gr.Number(label="Annual Holding Cost [$]", precision=2)
                            poq_total_cost_out = gr.Number(label="Total Annual Cost [$]", precision=2)
                        gr.Markdown("#### Production Cycle Details")
                        with gr.Row():
                            poq_prod_days_out = gr.Number(label="Production Run Duration [days]", precision=2)
                            poq_cycle_days_out = gr.Number(label="Inventory Cycle Duration [days]", precision=2)

            poq_inputs = [poq_demand, poq_setup_cost, poq_holding_cost, poq_prod_rate, poq_demand_rate]
            poq_outputs = [poq_plot_output, poq_out, poq_num_setups_out, poq_max_inv_out, poq_avg_inv_out,
                           poq_setup_cost_out, poq_holding_cost_out, poq_total_cost_out,
                           poq_prod_days_out, poq_cycle_days_out, poq_status_message]

            for inp in poq_inputs:
                inp.change(
                    fn=update_poq_model, 
                    inputs=poq_inputs, 
                    outputs=poq_outputs
                )
            
            demo.load(
                fn=update_poq_model, 
                inputs=poq_inputs, 
                outputs=poq_outputs
            )

        # --- Tab 5: Demand Forecasting (SMA) ---
        with gr.TabItem("Demand Forecasting (SMA)"):
            gr.Markdown("## Simple Moving Average (SMA) Forecasting")
            gr.Markdown("Forecast future demand based on the average of past demand data.")
            with gr.Row():
                with gr.Column(scale=1):
                    gr.Markdown("### πŸ› οΈ Input Parameters")
                    sma_data = gr.Textbox(label="Past Demand Data (comma-separated)", 
                                          value="100, 110, 105, 120, 115, 125, 130, 122, 135, 140")
                    sma_window = gr.Slider(minimum=2, maximum=10, value=3, step=1, 
                                           label="SMA Window Size (Periods)")
                    sma_status = gr.Textbox(label="Status & Forecast", interactive=False)
                
                with gr.Column(scale=2):
                    gr.Markdown("### πŸ“ˆ Forecast Plot")
                    sma_plot = gr.Plot()
                    sma_df = gr.DataFrame(label="Data and SMA")

            sma_inputs = [sma_data, sma_window]
            sma_outputs = [sma_df, sma_plot, sma_status]

            for inp in sma_inputs:
                inp.change(fn=calculate_sma_forecast, inputs=sma_inputs, outputs=sma_outputs)
            demo.load(fn=calculate_sma_forecast, inputs=sma_inputs, outputs=sma_outputs)

if __name__ == "__main__":
    demo.launch(share=True, inbrowser=True, debug=True)