GIGAParviz commited on
Commit
b2f81c0
·
verified ·
1 Parent(s): b6ffbe0

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +312 -312
main.py CHANGED
@@ -1,313 +1,313 @@
1
- import pandas as pd
2
- import numpy as np
3
- import random
4
- import copy
5
- import optuna
6
- import matplotlib.pyplot as plt
7
- import seaborn as sns
8
- from skopt import gp_minimize
9
- from skopt.space import Real, Categorical
10
- from skopt.utils import use_named_args
11
- from statsmodels.tsa.arima.model import ARIMA
12
- import numpy_financial as npf
13
-
14
- optuna.logging.set_verbosity(optuna.logging.WARNING)
15
-
16
- PROJECT_YEARS = 15
17
- BASE_CAPACITY_KTA = 300
18
- INFLATION_RATE = 0.015
19
- TAX_RATE = 0.10
20
- DEPRECIATION_YEARS = 15
21
-
22
- TECHNOLOGY_DATA = {
23
- "JNC": {"capex_base_M": 180.0, "opex_base_cents_kg": 150.0},
24
- "Hoechst_AG": {"capex_base_M": 230.0, "opex_base_cents_kg": 155.0},
25
- "BF_Goodrich": {"capex_base_M": 280.0, "opex_base_cents_kg": 160.0},
26
- "Shin_Etsu_1991": {"capex_base_M": 260.0, "opex_base_cents_kg": 155.0},
27
- "Shin_Etsu_2004": {"capex_base_M": 1500.0, "opex_base_cents_kg": 150.0},
28
- "Vinnolit": {"capex_base_M": 240.0, "opex_base_cents_kg": 155.0},
29
- "QVC_Qatar": {"capex_base_M": 200.0, "opex_base_cents_kg": 145.0},
30
- "SP_Chemicals": {"capex_base_M": 250.0, "opex_base_cents_kg": 145.0},
31
- "Engro_Pakistan": {"capex_base_M": 1400.0, "opex_base_cents_kg": 155.0},
32
- "Formosa_BR_USA": {"capex_base_M": 380.0, "opex_base_cents_kg": 150.0},
33
- "Shintech_USA_Exp": {"capex_base_M": 1700.0, "opex_base_cents_kg": 160.0},
34
- "Zhongtai_China": {"capex_base_M": 1100.0, "opex_base_cents_kg": 155.0},
35
- "Shintech_USA_2021": {"capex_base_M": 1700.0, "opex_base_cents_kg": 160.0},
36
- "Reliance_India_2024": {"capex_base_M": 2200.0, "opex_base_cents_kg": 155.0},
37
- "Orbia_Germany_2023": {"capex_base_M": 180.0, "opex_base_cents_kg": 155.0},
38
- "Westlake_USA_2022": {"capex_base_M": 850.0, "opex_base_cents_kg": 160.0}
39
- }
40
-
41
- STRATEGY_DATA = {
42
- 'Integrated_Production': {'sourcing_cost_per_ton_pvc': 450.0, 'byproducts': {'caustic_soda_ton': 1.1, 'surplus_edc_ton': 0.523}},
43
- 'Purchase_VCM': {'sourcing_cost_per_ton_pvc': 650.0, 'byproducts': {'caustic_soda_ton': 0, 'surplus_edc_ton': 0}}
44
- }
45
-
46
- PRODUCT_PRICES_USD_PER_TON = {
47
- 'pvc_s65_export': 1100, 'pvc_s70_domestic': 950,
48
- 'byproduct_caustic_soda': 450, 'byproduct_surplus_edc': 170
49
- }
50
-
51
- OPTIMIZATION_SPACE = {
52
- 'capacity_kta': (500, 600),
53
- 'technology': ["Engro_Pakistan"], #, "Shin_Etsu_2004"],
54
- 'sourcing_strategy': ['Integrated_Production'],
55
- 'export_market_mix': (0.6, 0.8),
56
- 'sell_byproducts': [True]
57
- }
58
-
59
- def forecast_prices(base_price, years=PROJECT_YEARS, cagr=0.04):
60
- historical = [base_price * (1 + cagr + random.uniform(-0.03, 0.03)) ** i for i in range(-5, 0)]
61
- model = ARIMA(historical, order=(1, 1, 0))
62
- fit = model.fit()
63
- forecast = fit.forecast(steps=years)
64
- return [p * (1 + INFLATION_RATE) ** i for i, p in enumerate(forecast)]
65
-
66
- def calculate_project_kpis(**params):
67
- tech_data = TECHNOLOGY_DATA[params['technology']]
68
- scaling_factor = (params['capacity_kta'] / BASE_CAPACITY_KTA) ** 0.65
69
- total_capex = tech_data['capex_base_M'] * scaling_factor * 1_000_000
70
- capacity_tons = params['capacity_kta'] * 1000
71
-
72
- price_s65_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['pvc_s65_export'])
73
- price_s70_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['pvc_s70_domestic'])
74
- byproduct_caustic_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['byproduct_caustic_soda'])
75
- byproduct_edc_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['byproduct_surplus_edc'])
76
-
77
- cash_flows = [-total_capex]
78
- depreciation_annual = total_capex / DEPRECIATION_YEARS
79
-
80
- for year in range(1, PROJECT_YEARS + 1):
81
- infl_factor = (1 + INFLATION_RATE) ** (year - 1)
82
- price_s65 = price_s65_forecast[year - 1]
83
- price_s70 = price_s70_forecast[year - 1] * 0.95 if params['capacity_kta'] >= 550 else price_s70_forecast[year - 1]
84
- revenue_export = (capacity_tons * params['export_market_mix']) * price_s65
85
- revenue_domestic = (capacity_tons * (1 - params['export_market_mix'])) * price_s70
86
- pvc_revenue = revenue_export + revenue_domestic
87
-
88
- byproduct_revenue = 0
89
- if params['sourcing_strategy'] == 'Integrated_Production' and params['sell_byproducts']:
90
- byproducts = STRATEGY_DATA['Integrated_Production']['byproducts']
91
- byproduct_revenue += (byproducts['caustic_soda_ton'] * capacity_tons * byproduct_caustic_forecast[year - 1])
92
- byproduct_revenue += (byproducts['surplus_edc_ton'] * capacity_tons * byproduct_edc_forecast[year - 1])
93
-
94
- total_revenue = pvc_revenue + byproduct_revenue
95
-
96
- opex_sourcing = STRATEGY_DATA['Integrated_Production']['sourcing_cost_per_ton_pvc'] * capacity_tons * infl_factor
97
- opex_base = (tech_data['opex_base_cents_kg'] / 100) * capacity_tons * infl_factor
98
- total_opex = opex_sourcing + opex_base
99
-
100
- ebitda = total_revenue - total_opex
101
- taxable_income = ebitda - depreciation_annual
102
- taxes = max(taxable_income * TAX_RATE, 0)
103
- net_income = taxable_income - taxes
104
- free_cash_flow = net_income + depreciation_annual
105
- cash_flows.append(free_cash_flow)
106
-
107
- irr = npf.irr(cash_flows) * 100 if npf.irr(cash_flows) > 0 else -1.0
108
- cumulative_cash_flow = np.cumsum(cash_flows)
109
- payback_period_years = np.where(cumulative_cash_flow > 0)[0]
110
- payback_period = payback_period_years[0] + 1 if len(payback_period_years) > 0 else float('inf')
111
- annual_profit = np.mean([cf for cf in cash_flows[1:] if cf > 0])
112
-
113
- return {"irr": irr, "annual_profit": annual_profit, "total_capex": total_capex, "payback_period": payback_period}
114
-
115
- def run_optimizations_without_ml():
116
- print("\n--- Running Optimization Algorithms ---")
117
- results = []
118
- results.append(run_bayesian_optimization())
119
- results.append(run_genetic_algorithm())
120
- results.append(run_optuna_direct())
121
- return results
122
-
123
- def run_genetic_algorithm():
124
- print("Running Genetic Algorithm (GA)...")
125
- population = [{k: random.choice(v) if isinstance(v, list) else random.uniform(*v) for k,v in OPTIMIZATION_SPACE.items()} for _ in range(50)]
126
- best_overall_individual = None
127
- best_overall_fitness = -float('inf')
128
- for _ in range(100):
129
- fitnesses = [calculate_project_kpis(**ind)['irr'] for ind in population]
130
- if max(fitnesses) > best_overall_fitness:
131
- best_overall_fitness = max(fitnesses)
132
- best_overall_individual = population[np.argmax(fitnesses)]
133
- selected = [max(random.sample(list(zip(population, fitnesses)), 5), key=lambda i: i[1])[0] for _ in range(50)]
134
- next_gen = []
135
- for i in range(0, 50, 2):
136
- p1, p2 = selected[i], selected[i+1]
137
- c1, c2 = copy.deepcopy(p1), copy.deepcopy(p2)
138
- if random.random() < 0.9: c1['technology'], c2['technology'] = p2['technology'], c1['technology']
139
- if random.random() < 0.2: c1['export_market_mix'] = random.uniform(*OPTIMIZATION_SPACE['export_market_mix'])
140
- next_gen.extend([c1, c2])
141
- population = next_gen
142
- kpis = calculate_project_kpis(**best_overall_individual)
143
- return {"Method": "Genetic Algorithm", **kpis, "Params": best_overall_individual}
144
-
145
- def run_bayesian_optimization():
146
- print("Running Bayesian Optimization...")
147
- skopt_space = [
148
- Real(OPTIMIZATION_SPACE['capacity_kta'][0], OPTIMIZATION_SPACE['capacity_kta'][1], name='capacity_kta'),
149
- Categorical(OPTIMIZATION_SPACE['technology'], name='technology'),
150
- Categorical(OPTIMIZATION_SPACE['sourcing_strategy'], name='sourcing_strategy'),
151
- Real(OPTIMIZATION_SPACE['export_market_mix'][0], OPTIMIZATION_SPACE['export_market_mix'][1], name='export_market_mix'),
152
- Categorical(OPTIMIZATION_SPACE['sell_byproducts'], name='sell_byproducts')
153
- ]
154
- @use_named_args(skopt_space)
155
- def objective(**params):
156
- return -calculate_project_kpis(**params)['irr']
157
- res = gp_minimize(objective, skopt_space, n_calls=100, random_state=42, n_initial_points=20)
158
- best_params = {space.name: val for space, val in zip(skopt_space, res.x)}
159
- kpis = calculate_project_kpis(**best_params)
160
- return {"Method": "Bayesian Opt", **kpis, "Params": best_params}
161
-
162
- def run_optuna_direct():
163
- print("Running Optuna (TPE)...")
164
- def objective(trial):
165
- params = {
166
- "capacity_kta": trial.suggest_float("capacity_kta", *OPTIMIZATION_SPACE['capacity_kta']),
167
- "technology": trial.suggest_categorical("technology", OPTIMIZATION_SPACE['technology']),
168
- "sourcing_strategy": trial.suggest_categorical("sourcing_strategy", OPTIMIZATION_SPACE['sourcing_strategy']),
169
- "export_market_mix": trial.suggest_float("export_market_mix", *OPTIMIZATION_SPACE['export_market_mix']),
170
- "sell_byproducts": trial.suggest_categorical("sell_byproducts", OPTIMIZATION_SPACE['sell_byproducts'])
171
- }
172
- return calculate_project_kpis(**params)['irr']
173
- study = optuna.create_study(direction="maximize")
174
- study.optimize(objective, n_trials=200, n_jobs=-1)
175
- kpis = calculate_project_kpis(**study.best_params)
176
- return {"Method": "Optuna (TPE - Direct)", **kpis, "Params": study.best_params}
177
-
178
- def display_and_save_results(df_results):
179
- print("\n--- Final Results and Comparison ---")
180
- df_display = pd.DataFrame()
181
- df_display['Method'] = df_results['Method']
182
- df_display['Optimal IRR (%)'] = df_results['irr'].map('{:,.2f}%'.format)
183
- df_display['Annual Profit ($M)'] = (df_results['annual_profit'] / 1_000_000).map('{:,.1f}'.format)
184
- df_display['CAPEX ($M)'] = (df_results['total_capex'] / 1_000_000).map('{:,.1f}'.format)
185
- df_display['Payback (Yrs)'] = df_results['payback_period'].map('{:,.1f}'.format)
186
- param_df = pd.DataFrame(df_results['Params'].tolist())
187
- param_df['capacity_kta'] = param_df['capacity_kta'].round(1)
188
- param_df['export_market_mix'] = (param_df['export_market_mix'] * 100).round(1).astype(str) + '%'
189
- df_display = pd.concat([df_display, param_df.rename(columns={
190
- 'capacity_kta': 'Capacity (KTA)', 'technology': 'Technology', 'sourcing_strategy': 'Sourcing',
191
- 'export_market_mix': 'Export Mix', 'sell_byproducts': 'Sell Byproducts'
192
- })], axis=1)
193
-
194
- print("\n✅ **Final Comparison of Optimal Scenarios (Sorted by Best IRR)**")
195
- print("="*120)
196
- print(df_display.to_string(index=False))
197
- print("="*120)
198
- df_display.to_csv("results.csv", index=False, encoding='utf-8-sig')
199
-
200
- def create_kpi_comparison_dashboard(df_results):
201
- print("\n--- Generating KPI Comparison Dashboard ---")
202
- df_plot = df_results.sort_values(by='irr', ascending=False)
203
- df_plot['annual_profit_M'] = df_plot['annual_profit'] / 1_000_000
204
- df_plot['total_capex_M'] = df_plot['total_capex'] / 1_000_000
205
-
206
- fig, axes = plt.subplots(2, 2, figsize=(20, 14))
207
- fig.suptitle('Dashboard: Comprehensive Comparison of Optimization Methods', fontsize=22, weight='bold')
208
- palettes = ['viridis', 'plasma', 'magma', 'cividis']
209
- metrics = [
210
- ('irr', 'Optimal IRR (%)', axes[0, 0]),
211
- ('annual_profit_M', 'Annual Profit ($M)', axes[0, 1]),
212
- ('total_capex_M', 'Total CAPEX ($M)', axes[1, 0]),
213
- ('payback_period', 'Payback Period (Years)', axes[1, 1])
214
- ]
215
-
216
- for i, (metric, title, ax) in enumerate(metrics):
217
- sns.barplot(x=metric, y='Method', data=df_plot, ax=ax, palette=palettes[i])
218
- ax.set_title(title, fontsize=16, weight='bold')
219
- ax.set_xlabel('')
220
- ax.set_ylabel('')
221
- for p in ax.patches:
222
- width = p.get_width()
223
- ax.text(width * 1.01, p.get_y() + p.get_height() / 2,
224
- f'{width:,.2f}',
225
- va='center', fontsize=11)
226
-
227
- plt.tight_layout(rect=[0, 0.03, 1, 0.95])
228
- plt.savefig("static/images/kpi_dashboard.png", bbox_inches='tight')
229
- print("\n✅ A KPI dashboard graph has been saved as 'kpi_dashboard.png'")
230
-
231
- def run_sensitivity_analysis(best_params, base_irr):
232
- global PRODUCT_PRICES_USD_PER_TON, STRATEGY_DATA
233
- print("\n--- Sensitivity Analysis on Best Scenario ---")
234
- print(f"Analyzing sensitivity around the base IRR of {base_irr:,.2f}%")
235
-
236
- sensitivity_vars = {
237
- 'Byproduct Caustic Soda Price': ('price', 'byproduct_caustic_soda'),
238
- 'Sourcing Cost Integrated': ('strategy', 'Integrated_Production', 'sourcing_cost_per_ton_pvc'),
239
- 'Domestic PVC S70 Price': ('price', 'pvc_s70_domestic')
240
- }
241
- variations = [-0.20, -0.10, 0.10, 0.20]
242
- results = []
243
- original_prices = copy.deepcopy(PRODUCT_PRICES_USD_PER_TON)
244
- original_strategies = copy.deepcopy(STRATEGY_DATA)
245
-
246
- for key, path in sensitivity_vars.items():
247
- for var in variations:
248
- PRODUCT_PRICES_USD_PER_TON = copy.deepcopy(original_prices)
249
- STRATEGY_DATA = copy.deepcopy(original_strategies)
250
- if path[0] == 'price':
251
- base_value = PRODUCT_PRICES_USD_PER_TON[path[1]]
252
- PRODUCT_PRICES_USD_PER_TON[path[1]] = base_value * (1 + var)
253
- else:
254
- base_value = STRATEGY_DATA[path[1]][path[2]]
255
- STRATEGY_DATA[path[1]][path[2]] = base_value * (1 + var)
256
-
257
- kpis = calculate_project_kpis(**best_params)
258
- results.append({
259
- 'Variable': key, 'Change': f'{var:+.0%}',
260
- 'New IRR (%)': kpis['irr'], 'IRR Delta (%)': kpis['irr'] - base_irr
261
- })
262
-
263
- PRODUCT_PRICES_USD_PER_TON = original_prices
264
- STRATEGY_DATA = original_strategies
265
-
266
- df_sensitivity = pd.DataFrame(results)
267
- print("\n✅ **Sensitivity Analysis Results**")
268
- print("="*60)
269
- print(df_sensitivity.to_string(index=False))
270
- print("="*60)
271
-
272
- tornado_data = []
273
- for var_name in df_sensitivity['Variable'].unique():
274
- subset = df_sensitivity[df_sensitivity['Variable'] == var_name]
275
- min_delta = subset['IRR Delta (%)'].min()
276
- max_delta = subset['IRR Delta (%)'].max()
277
- tornado_data.append({
278
- 'Variable': var_name,
279
- 'Min_Delta': min_delta,
280
- 'Max_Delta': max_delta,
281
- 'Range': max_delta - min_delta
282
- })
283
- df_tornado = pd.DataFrame(tornado_data).sort_values('Range', ascending=True)
284
-
285
- fig, ax = plt.subplots(figsize=(12, 8))
286
- y = np.arange(len(df_tornado))
287
- ax.barh(y, df_tornado['Max_Delta'], color='mediumseagreen', label='Positive Impact')
288
- ax.barh(y, df_tornado['Min_Delta'], color='lightcoral', label='Negative Impact')
289
- ax.set_yticks(y)
290
- ax.set_yticklabels(df_tornado['Variable'], fontsize=12)
291
- ax.axvline(0, color='black', linewidth=0.8, linestyle='--')
292
- ax.set_title('Tornado Chart: IRR Sensitivity to Key Variables', fontsize=18, pad=20, weight='bold')
293
- ax.set_xlabel(f'Change in IRR (%) from Base IRR ({base_irr:.2f}%)', fontsize=14)
294
- ax.set_ylabel('Variable', fontsize=14)
295
- ax.legend()
296
- ax.grid(axis='x', linestyle='--', alpha=0.7)
297
- for i, (p, n) in enumerate(zip(df_tornado['Max_Delta'], df_tornado['Min_Delta'])):
298
- ax.text(p, i, f' +{p:.2f}%', va='center', ha='left', color='darkgreen')
299
- ax.text(n, i, f' {n:.2f}%', va='center', ha='right', color='darkred')
300
- plt.tight_layout()
301
- plt.savefig("static/images/sensitivity_analysis_tornado.png", bbox_inches='tight')
302
- print("\n✅ A sensitivity analysis Tornado chart has been saved as 'sensitivity_analysis_tornado.png'")
303
-
304
- if __name__ == "__main__":
305
- optimization_results = run_optimizations_without_ml()
306
- df_results = pd.DataFrame(optimization_results).sort_values(by="irr", ascending=False).reset_index(drop=True)
307
- df_results.round(2)
308
- display_and_save_results(df_results)
309
- create_kpi_comparison_dashboard(df_results)
310
-
311
- if not df_results.empty:
312
- best_scenario = df_results.iloc[0]
313
  run_sensitivity_analysis(best_scenario['Params'], best_scenario['irr'])
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import random
4
+ import copy
5
+ import optuna
6
+ import matplotlib.pyplot as plt
7
+ import seaborn as sns
8
+ from skopt import gp_minimize
9
+ from skopt.space import Real, Categorical
10
+ from skopt.utils import use_named_args
11
+ from statsmodels.tsa.arima.model import ARIMA
12
+ import numpy_financial as npf
13
+
14
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
15
+
16
+ PROJECT_YEARS = 15
17
+ BASE_CAPACITY_KTA = 300
18
+ INFLATION_RATE = 0.015
19
+ TAX_RATE = 0.10
20
+ DEPRECIATION_YEARS = 15
21
+
22
+ TECHNOLOGY_DATA = {
23
+ "JNC": {"capex_base_M": 180.0, "opex_base_cents_kg": 150.0},
24
+ # "Hoechst_AG": {"capex_base_M": 230.0, "opex_base_cents_kg": 155.0},
25
+ # "BF_Goodrich": {"capex_base_M": 280.0, "opex_base_cents_kg": 160.0},
26
+ # "Shin_Etsu_1991": {"capex_base_M": 260.0, "opex_base_cents_kg": 155.0},
27
+ # "Shin_Etsu_2004": {"capex_base_M": 1500.0, "opex_base_cents_kg": 150.0},
28
+ # "Vinnolit": {"capex_base_M": 240.0, "opex_base_cents_kg": 155.0},
29
+ # "QVC_Qatar": {"capex_base_M": 200.0, "opex_base_cents_kg": 145.0},
30
+ # "SP_Chemicals": {"capex_base_M": 250.0, "opex_base_cents_kg": 145.0},
31
+ # "Engro_Pakistan": {"capex_base_M": 1400.0, "opex_base_cents_kg": 155.0},
32
+ # "Formosa_BR_USA": {"capex_base_M": 380.0, "opex_base_cents_kg": 150.0},
33
+ # "Shintech_USA_Exp": {"capex_base_M": 1700.0, "opex_base_cents_kg": 160.0},
34
+ # "Zhongtai_China": {"capex_base_M": 1100.0, "opex_base_cents_kg": 155.0},
35
+ # "Shintech_USA_2021": {"capex_base_M": 1700.0, "opex_base_cents_kg": 160.0},
36
+ # "Reliance_India_2024": {"capex_base_M": 2200.0, "opex_base_cents_kg": 155.0},
37
+ # "Orbia_Germany_2023": {"capex_base_M": 180.0, "opex_base_cents_kg": 155.0},
38
+ # "Westlake_USA_2022": {"capex_base_M": 850.0, "opex_base_cents_kg": 160.0}
39
+ }
40
+
41
+ STRATEGY_DATA = {
42
+ 'Integrated_Production': {'sourcing_cost_per_ton_pvc': 450.0, 'byproducts': {'caustic_soda_ton': 1.1, 'surplus_edc_ton': 0.523}},
43
+ 'Purchase_VCM': {'sourcing_cost_per_ton_pvc': 650.0, 'byproducts': {'caustic_soda_ton': 0, 'surplus_edc_ton': 0}}
44
+ }
45
+
46
+ PRODUCT_PRICES_USD_PER_TON = {
47
+ 'pvc_s65_export': 1100, 'pvc_s70_domestic': 950,
48
+ 'byproduct_caustic_soda': 450, 'byproduct_surplus_edc': 170
49
+ }
50
+
51
+ OPTIMIZATION_SPACE = {
52
+ 'capacity_kta': (500, 600),
53
+ 'technology': ["Engro_Pakistan"], #, "Shin_Etsu_2004"],
54
+ 'sourcing_strategy': ['Integrated_Production'],
55
+ 'export_market_mix': (0.6, 0.8),
56
+ 'sell_byproducts': [True]
57
+ }
58
+
59
+ def forecast_prices(base_price, years=PROJECT_YEARS, cagr=0.04):
60
+ historical = [base_price * (1 + cagr + random.uniform(-0.03, 0.03)) ** i for i in range(-5, 0)]
61
+ model = ARIMA(historical, order=(1, 1, 0))
62
+ fit = model.fit()
63
+ forecast = fit.forecast(steps=years)
64
+ return [p * (1 + INFLATION_RATE) ** i for i, p in enumerate(forecast)]
65
+
66
+ def calculate_project_kpis(**params):
67
+ tech_data = TECHNOLOGY_DATA[params['technology']]
68
+ scaling_factor = (params['capacity_kta'] / BASE_CAPACITY_KTA) ** 0.65
69
+ total_capex = tech_data['capex_base_M'] * scaling_factor * 1_000_000
70
+ capacity_tons = params['capacity_kta'] * 1000
71
+
72
+ price_s65_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['pvc_s65_export'])
73
+ price_s70_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['pvc_s70_domestic'])
74
+ byproduct_caustic_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['byproduct_caustic_soda'])
75
+ byproduct_edc_forecast = forecast_prices(PRODUCT_PRICES_USD_PER_TON['byproduct_surplus_edc'])
76
+
77
+ cash_flows = [-total_capex]
78
+ depreciation_annual = total_capex / DEPRECIATION_YEARS
79
+
80
+ for year in range(1, PROJECT_YEARS + 1):
81
+ infl_factor = (1 + INFLATION_RATE) ** (year - 1)
82
+ price_s65 = price_s65_forecast[year - 1]
83
+ price_s70 = price_s70_forecast[year - 1] * 0.95 if params['capacity_kta'] >= 550 else price_s70_forecast[year - 1]
84
+ revenue_export = (capacity_tons * params['export_market_mix']) * price_s65
85
+ revenue_domestic = (capacity_tons * (1 - params['export_market_mix'])) * price_s70
86
+ pvc_revenue = revenue_export + revenue_domestic
87
+
88
+ byproduct_revenue = 0
89
+ if params['sourcing_strategy'] == 'Integrated_Production' and params['sell_byproducts']:
90
+ byproducts = STRATEGY_DATA['Integrated_Production']['byproducts']
91
+ byproduct_revenue += (byproducts['caustic_soda_ton'] * capacity_tons * byproduct_caustic_forecast[year - 1])
92
+ byproduct_revenue += (byproducts['surplus_edc_ton'] * capacity_tons * byproduct_edc_forecast[year - 1])
93
+
94
+ total_revenue = pvc_revenue + byproduct_revenue
95
+
96
+ opex_sourcing = STRATEGY_DATA['Integrated_Production']['sourcing_cost_per_ton_pvc'] * capacity_tons * infl_factor
97
+ opex_base = (tech_data['opex_base_cents_kg'] / 100) * capacity_tons * infl_factor
98
+ total_opex = opex_sourcing + opex_base
99
+
100
+ ebitda = total_revenue - total_opex
101
+ taxable_income = ebitda - depreciation_annual
102
+ taxes = max(taxable_income * TAX_RATE, 0)
103
+ net_income = taxable_income - taxes
104
+ free_cash_flow = net_income + depreciation_annual
105
+ cash_flows.append(free_cash_flow)
106
+
107
+ irr = npf.irr(cash_flows) * 100 if npf.irr(cash_flows) > 0 else -1.0
108
+ cumulative_cash_flow = np.cumsum(cash_flows)
109
+ payback_period_years = np.where(cumulative_cash_flow > 0)[0]
110
+ payback_period = payback_period_years[0] + 1 if len(payback_period_years) > 0 else float('inf')
111
+ annual_profit = np.mean([cf for cf in cash_flows[1:] if cf > 0])
112
+
113
+ return {"irr": irr, "annual_profit": annual_profit, "total_capex": total_capex, "payback_period": payback_period}
114
+
115
+ def run_optimizations_without_ml():
116
+ print("\n--- Running Optimization Algorithms ---")
117
+ results = []
118
+ results.append(run_bayesian_optimization())
119
+ results.append(run_genetic_algorithm())
120
+ results.append(run_optuna_direct())
121
+ return results
122
+
123
+ def run_genetic_algorithm():
124
+ print("Running Genetic Algorithm (GA)...")
125
+ population = [{k: random.choice(v) if isinstance(v, list) else random.uniform(*v) for k,v in OPTIMIZATION_SPACE.items()} for _ in range(50)]
126
+ best_overall_individual = None
127
+ best_overall_fitness = -float('inf')
128
+ for _ in range(10):
129
+ fitnesses = [calculate_project_kpis(**ind)['irr'] for ind in population]
130
+ if max(fitnesses) > best_overall_fitness:
131
+ best_overall_fitness = max(fitnesses)
132
+ best_overall_individual = population[np.argmax(fitnesses)]
133
+ selected = [max(random.sample(list(zip(population, fitnesses)), 5), key=lambda i: i[1])[0] for _ in range(50)]
134
+ next_gen = []
135
+ for i in range(0, 50, 2):
136
+ p1, p2 = selected[i], selected[i+1]
137
+ c1, c2 = copy.deepcopy(p1), copy.deepcopy(p2)
138
+ if random.random() < 0.9: c1['technology'], c2['technology'] = p2['technology'], c1['technology']
139
+ if random.random() < 0.2: c1['export_market_mix'] = random.uniform(*OPTIMIZATION_SPACE['export_market_mix'])
140
+ next_gen.extend([c1, c2])
141
+ population = next_gen
142
+ kpis = calculate_project_kpis(**best_overall_individual)
143
+ return {"Method": "Genetic Algorithm", **kpis, "Params": best_overall_individual}
144
+
145
+ def run_bayesian_optimization():
146
+ print("Running Bayesian Optimization...")
147
+ skopt_space = [
148
+ Real(OPTIMIZATION_SPACE['capacity_kta'][0], OPTIMIZATION_SPACE['capacity_kta'][1], name='capacity_kta'),
149
+ Categorical(OPTIMIZATION_SPACE['technology'], name='technology'),
150
+ Categorical(OPTIMIZATION_SPACE['sourcing_strategy'], name='sourcing_strategy'),
151
+ Real(OPTIMIZATION_SPACE['export_market_mix'][0], OPTIMIZATION_SPACE['export_market_mix'][1], name='export_market_mix'),
152
+ Categorical(OPTIMIZATION_SPACE['sell_byproducts'], name='sell_byproducts')
153
+ ]
154
+ @use_named_args(skopt_space)
155
+ def objective(**params):
156
+ return -calculate_project_kpis(**params)['irr']
157
+ res = gp_minimize(objective, skopt_space, n_calls=100, random_state=42, n_initial_points=20)
158
+ best_params = {space.name: val for space, val in zip(skopt_space, res.x)}
159
+ kpis = calculate_project_kpis(**best_params)
160
+ return {"Method": "Bayesian Opt", **kpis, "Params": best_params}
161
+
162
+ def run_optuna_direct():
163
+ print("Running Optuna (TPE)...")
164
+ def objective(trial):
165
+ params = {
166
+ "capacity_kta": trial.suggest_float("capacity_kta", *OPTIMIZATION_SPACE['capacity_kta']),
167
+ "technology": trial.suggest_categorical("technology", OPTIMIZATION_SPACE['technology']),
168
+ "sourcing_strategy": trial.suggest_categorical("sourcing_strategy", OPTIMIZATION_SPACE['sourcing_strategy']),
169
+ "export_market_mix": trial.suggest_float("export_market_mix", *OPTIMIZATION_SPACE['export_market_mix']),
170
+ "sell_byproducts": trial.suggest_categorical("sell_byproducts", OPTIMIZATION_SPACE['sell_byproducts'])
171
+ }
172
+ return calculate_project_kpis(**params)['irr']
173
+ study = optuna.create_study(direction="maximize")
174
+ study.optimize(objective, n_trials=2, n_jobs=-1)
175
+ kpis = calculate_project_kpis(**study.best_params)
176
+ return {"Method": "Optuna (TPE - Direct)", **kpis, "Params": study.best_params}
177
+
178
+ def display_and_save_results(df_results):
179
+ print("\n--- Final Results and Comparison ---")
180
+ df_display = pd.DataFrame()
181
+ df_display['Method'] = df_results['Method']
182
+ df_display['Optimal IRR (%)'] = df_results['irr'].map('{:,.2f}%'.format)
183
+ df_display['Annual Profit ($M)'] = (df_results['annual_profit'] / 1_000_000).map('{:,.1f}'.format)
184
+ df_display['CAPEX ($M)'] = (df_results['total_capex'] / 1_000_000).map('{:,.1f}'.format)
185
+ df_display['Payback (Yrs)'] = df_results['payback_period'].map('{:,.1f}'.format)
186
+ param_df = pd.DataFrame(df_results['Params'].tolist())
187
+ param_df['capacity_kta'] = param_df['capacity_kta'].round(1)
188
+ param_df['export_market_mix'] = (param_df['export_market_mix'] * 100).round(1).astype(str) + '%'
189
+ df_display = pd.concat([df_display, param_df.rename(columns={
190
+ 'capacity_kta': 'Capacity (KTA)', 'technology': 'Technology', 'sourcing_strategy': 'Sourcing',
191
+ 'export_market_mix': 'Export Mix', 'sell_byproducts': 'Sell Byproducts'
192
+ })], axis=1)
193
+
194
+ print("\n✅ **Final Comparison of Optimal Scenarios (Sorted by Best IRR)**")
195
+ print("="*120)
196
+ print(df_display.to_string(index=False))
197
+ print("="*120)
198
+ df_display.to_csv("results.csv", index=False, encoding='utf-8-sig')
199
+
200
+ def create_kpi_comparison_dashboard(df_results):
201
+ print("\n--- Generating KPI Comparison Dashboard ---")
202
+ df_plot = df_results.sort_values(by='irr', ascending=False)
203
+ df_plot['annual_profit_M'] = df_plot['annual_profit'] / 1_000_000
204
+ df_plot['total_capex_M'] = df_plot['total_capex'] / 1_000_000
205
+
206
+ fig, axes = plt.subplots(2, 2, figsize=(20, 14))
207
+ fig.suptitle('Dashboard: Comprehensive Comparison of Optimization Methods', fontsize=22, weight='bold')
208
+ palettes = ['viridis', 'plasma', 'magma', 'cividis']
209
+ metrics = [
210
+ ('irr', 'Optimal IRR (%)', axes[0, 0]),
211
+ ('annual_profit_M', 'Annual Profit ($M)', axes[0, 1]),
212
+ ('total_capex_M', 'Total CAPEX ($M)', axes[1, 0]),
213
+ ('payback_period', 'Payback Period (Years)', axes[1, 1])
214
+ ]
215
+
216
+ for i, (metric, title, ax) in enumerate(metrics):
217
+ sns.barplot(x=metric, y='Method', data=df_plot, ax=ax, palette=palettes[i])
218
+ ax.set_title(title, fontsize=16, weight='bold')
219
+ ax.set_xlabel('')
220
+ ax.set_ylabel('')
221
+ for p in ax.patches:
222
+ width = p.get_width()
223
+ ax.text(width * 1.01, p.get_y() + p.get_height() / 2,
224
+ f'{width:,.2f}',
225
+ va='center', fontsize=11)
226
+
227
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
228
+ plt.savefig("static/images/kpi_dashboard.png", bbox_inches='tight')
229
+ print("\n✅ A KPI dashboard graph has been saved as 'kpi_dashboard.png'")
230
+
231
+ def run_sensitivity_analysis(best_params, base_irr):
232
+ global PRODUCT_PRICES_USD_PER_TON, STRATEGY_DATA
233
+ print("\n--- Sensitivity Analysis on Best Scenario ---")
234
+ print(f"Analyzing sensitivity around the base IRR of {base_irr:,.2f}%")
235
+
236
+ sensitivity_vars = {
237
+ 'Byproduct Caustic Soda Price': ('price', 'byproduct_caustic_soda'),
238
+ 'Sourcing Cost Integrated': ('strategy', 'Integrated_Production', 'sourcing_cost_per_ton_pvc'),
239
+ 'Domestic PVC S70 Price': ('price', 'pvc_s70_domestic')
240
+ }
241
+ variations = [-0.20, -0.10, 0.10, 0.20]
242
+ results = []
243
+ original_prices = copy.deepcopy(PRODUCT_PRICES_USD_PER_TON)
244
+ original_strategies = copy.deepcopy(STRATEGY_DATA)
245
+
246
+ for key, path in sensitivity_vars.items():
247
+ for var in variations:
248
+ PRODUCT_PRICES_USD_PER_TON = copy.deepcopy(original_prices)
249
+ STRATEGY_DATA = copy.deepcopy(original_strategies)
250
+ if path[0] == 'price':
251
+ base_value = PRODUCT_PRICES_USD_PER_TON[path[1]]
252
+ PRODUCT_PRICES_USD_PER_TON[path[1]] = base_value * (1 + var)
253
+ else:
254
+ base_value = STRATEGY_DATA[path[1]][path[2]]
255
+ STRATEGY_DATA[path[1]][path[2]] = base_value * (1 + var)
256
+
257
+ kpis = calculate_project_kpis(**best_params)
258
+ results.append({
259
+ 'Variable': key, 'Change': f'{var:+.0%}',
260
+ 'New IRR (%)': kpis['irr'], 'IRR Delta (%)': kpis['irr'] - base_irr
261
+ })
262
+
263
+ PRODUCT_PRICES_USD_PER_TON = original_prices
264
+ STRATEGY_DATA = original_strategies
265
+
266
+ df_sensitivity = pd.DataFrame(results)
267
+ print("\n✅ **Sensitivity Analysis Results**")
268
+ print("="*60)
269
+ print(df_sensitivity.to_string(index=False))
270
+ print("="*60)
271
+
272
+ tornado_data = []
273
+ for var_name in df_sensitivity['Variable'].unique():
274
+ subset = df_sensitivity[df_sensitivity['Variable'] == var_name]
275
+ min_delta = subset['IRR Delta (%)'].min()
276
+ max_delta = subset['IRR Delta (%)'].max()
277
+ tornado_data.append({
278
+ 'Variable': var_name,
279
+ 'Min_Delta': min_delta,
280
+ 'Max_Delta': max_delta,
281
+ 'Range': max_delta - min_delta
282
+ })
283
+ df_tornado = pd.DataFrame(tornado_data).sort_values('Range', ascending=True)
284
+
285
+ fig, ax = plt.subplots(figsize=(12, 8))
286
+ y = np.arange(len(df_tornado))
287
+ ax.barh(y, df_tornado['Max_Delta'], color='mediumseagreen', label='Positive Impact')
288
+ ax.barh(y, df_tornado['Min_Delta'], color='lightcoral', label='Negative Impact')
289
+ ax.set_yticks(y)
290
+ ax.set_yticklabels(df_tornado['Variable'], fontsize=12)
291
+ ax.axvline(0, color='black', linewidth=0.8, linestyle='--')
292
+ ax.set_title('Tornado Chart: IRR Sensitivity to Key Variables', fontsize=18, pad=20, weight='bold')
293
+ ax.set_xlabel(f'Change in IRR (%) from Base IRR ({base_irr:.2f}%)', fontsize=14)
294
+ ax.set_ylabel('Variable', fontsize=14)
295
+ ax.legend()
296
+ ax.grid(axis='x', linestyle='--', alpha=0.7)
297
+ for i, (p, n) in enumerate(zip(df_tornado['Max_Delta'], df_tornado['Min_Delta'])):
298
+ ax.text(p, i, f' +{p:.2f}%', va='center', ha='left', color='darkgreen')
299
+ ax.text(n, i, f' {n:.2f}%', va='center', ha='right', color='darkred')
300
+ plt.tight_layout()
301
+ plt.savefig("static/images/sensitivity_analysis_tornado.png", bbox_inches='tight')
302
+ print("\n✅ A sensitivity analysis Tornado chart has been saved as 'sensitivity_analysis_tornado.png'")
303
+
304
+ if __name__ == "__main__":
305
+ optimization_results = run_optimizations_without_ml()
306
+ df_results = pd.DataFrame(optimization_results).sort_values(by="irr", ascending=False).reset_index(drop=True)
307
+ df_results.round(2)
308
+ display_and_save_results(df_results)
309
+ create_kpi_comparison_dashboard(df_results)
310
+
311
+ if not df_results.empty:
312
+ best_scenario = df_results.iloc[0]
313
  run_sensitivity_analysis(best_scenario['Params'], best_scenario['irr'])