GIGAParviz commited on
Commit
3fde923
·
verified ·
1 Parent(s): a10aed2

Upload 9 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/images/sensitivity_analysis_tornado.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, redirect, url_for, send_file, flash, jsonify
2
+ import os
3
+ import traceback
4
+ from pathlib import Path
5
+ import json
6
+ import pandas as pd
7
+
8
+ app = Flask(__name__)
9
+ app.secret_key = os.urandom(24)
10
+
11
+ try:
12
+ import main as project_main
13
+ except Exception as e:
14
+ project_main = None
15
+ import_error = traceback.format_exc()
16
+ else:
17
+ import_error = None
18
+
19
+ RESULTS_CSV = Path("results_flask.csv")
20
+
21
+ def safe_update_dict_from_json(orig_dict, json_text):
22
+ if not json_text or not json_text.strip():
23
+ return orig_dict
24
+ try:
25
+ new = json.loads(json_text)
26
+ if not isinstance(new, dict):
27
+ return orig_dict
28
+ d = orig_dict.copy()
29
+ d.update(new)
30
+ return d
31
+ except Exception:
32
+ return orig_dict
33
+
34
+ @app.route("/", methods=["GET"])
35
+ def index():
36
+ if project_main is None:
37
+ return render_template("index.html", import_error=import_error, project=None)
38
+ techs = list(project_main.TECHNOLOGY_DATA.keys())
39
+ defaults = {
40
+ "inflation": project_main.INFLATION_RATE,
41
+ "tax": project_main.TAX_RATE,
42
+ "years": project_main.PROJECT_YEARS,
43
+ "cap_min": project_main.OPTIMIZATION_SPACE['capacity_kta'][0],
44
+ "cap_max": project_main.OPTIMIZATION_SPACE['capacity_kta'][1],
45
+ "export_mix": project_main.OPTIMIZATION_SPACE['export_market_mix'][0],
46
+ }
47
+ return render_template("index.html", import_error=None, project=project_main, techs=techs, defaults=defaults)
48
+
49
+ @app.route("/run", methods=["POST"])
50
+ def run():
51
+ if project_main is None:
52
+ flash("خطا: فایل main.py بارگزاری نشد. لطفاً لاگ را بررسی کنید.", "danger")
53
+ return redirect(url_for("index"))
54
+ try:
55
+ inflation_rate = float(request.form.get("inflation", project_main.INFLATION_RATE))
56
+ tax_rate = float(request.form.get("tax", project_main.TAX_RATE))
57
+ project_years = int(request.form.get("years", project_main.PROJECT_YEARS))
58
+ capacity_min = float(request.form.get("cap_min", project_main.OPTIMIZATION_SPACE['capacity_kta'][0]))
59
+ capacity_max = float(request.form.get("cap_max", project_main.OPTIMIZATION_SPACE['capacity_kta'][1]))
60
+ technology = request.form.get("technology", list(project_main.TECHNOLOGY_DATA.keys())[0])
61
+ export_mix = float(request.form.get("export_mix", project_main.OPTIMIZATION_SPACE['export_market_mix'][0]))
62
+ sell_byproducts = request.form.get("sell_byproducts") == "on"
63
+ tech_json = request.form.get("tech_json", "")
64
+ prices_json = request.form.get("prices_json", "")
65
+ except Exception as e:
66
+ flash("خطا در خواندن ورودی‌ها: " + str(e), "danger")
67
+ return redirect(url_for("index"))
68
+
69
+ try:
70
+ project_main.INFLATION_RATE = inflation_rate
71
+ project_main.TAX_RATE = tax_rate
72
+ project_main.PROJECT_YEARS = project_years
73
+
74
+ project_main.OPTIMIZATION_SPACE['capacity_kta'] = (capacity_min, capacity_max)
75
+ if technology not in project_main.OPTIMIZATION_SPACE.get('technology', []):
76
+ project_main.OPTIMIZATION_SPACE['technology'] = [technology]
77
+ low = max(0.0, export_mix - 0.05)
78
+ high = min(1.0, export_mix + 0.05)
79
+ if low == high:
80
+ high = min(1.0, high + 0.01)
81
+ project_main.OPTIMIZATION_SPACE['export_market_mix'] = (low, high)
82
+ project_main.OPTIMIZATION_SPACE['sell_byproducts'] = [bool(sell_byproducts)]
83
+
84
+ project_main.TECHNOLOGY_DATA = safe_update_dict_from_json(project_main.TECHNOLOGY_DATA, tech_json)
85
+ project_main.PRODUCT_PRICES_USD_PER_TON = safe_update_dict_from_json(project_main.PRODUCT_PRICES_USD_PER_TON, prices_json)
86
+ except Exception as e:
87
+ flash("خطا در اعمال پارامترها: " + str(e), "danger")
88
+ return redirect(url_for("index"))
89
+
90
+ try:
91
+ flash("محاسبات شروع شد — صبر کنید تا عملیات به پایان برسد...", "info")
92
+ results = project_main.run_optimizations_without_ml()
93
+ df_results = pd.DataFrame(results).sort_values(by="irr", ascending=False).reset_index(drop=True)
94
+ df_results = df_results.round(2)
95
+
96
+ df_results.to_csv(RESULTS_CSV, index=False, encoding='utf-8-sig')
97
+
98
+ try:
99
+ project_main.display_and_save_results(df_results)
100
+ project_main.create_kpi_comparison_dashboard(df_results)
101
+ except Exception:
102
+ pass
103
+
104
+ if not df_results.empty:
105
+ best = df_results.iloc[0]
106
+ top_kpis = {
107
+ "irr": round(float(best['irr']), 2),
108
+ "annual_profit_M": round(float(best['annual_profit'])/1_000_000, 2),
109
+ "capex_M": round(float(best['total_capex'])/1_000_000, 2),
110
+ "payback": round(float(best['payback_period']), 2)
111
+ }
112
+ else:
113
+ top_kpis = None
114
+
115
+ charts_data = df_results.to_dict(orient="records")
116
+
117
+ flash("محاسبات با موفقیت تکمیل شد.", "success")
118
+
119
+ kpi_img = Path("static/images/kpi_dashboard.png") if Path("static/images/kpi_dashboard.png").exists() else None
120
+ tornado_img = Path("static/images/sensitivity_analysis_tornado.png") if Path("static/images/sensitivity_analysis_tornado.png").exists() else None
121
+
122
+ return render_template(
123
+ "index.html",
124
+ project=project_main,
125
+ techs=list(project_main.TECHNOLOGY_DATA.keys()),
126
+ defaults={"inflation": project_main.INFLATION_RATE, "tax": project_main.TAX_RATE},
127
+ table_html = df_results.to_html(
128
+ classes='table table-striped table-dark',
129
+ index=False,
130
+ justify='center',
131
+ float_format=lambda x: f"{x:.2f}"
132
+ ),
133
+ top_kpis=top_kpis,
134
+ charts_json=json.dumps(charts_data, default=str),
135
+ kpi_img="images/kpi_dashboard.png" if kpi_img else None,
136
+ tornado_img="images/sensitivity_analysis_tornado.png" if tornado_img else None
137
+ )
138
+
139
+ except Exception as e:
140
+ tb = traceback.format_exc()
141
+ flash("خطا در اجرای الگوریتم‌ها. لاگ: " + str(e), "danger")
142
+ return render_template("index.html", import_error=tb, project=project_main)
143
+
144
+ @app.route("/download")
145
+ def download_results():
146
+ if RESULTS_CSV.exists():
147
+ return send_file(str(RESULTS_CSV), as_attachment=True)
148
+ flash("فایل نتایج آماده نیست.", "warning")
149
+ return redirect(url_for("index"))
150
+
151
+ if __name__ == "__main__":
152
+ app.run(debug=True)
153
+
154
+
155
+
main.py ADDED
@@ -0,0 +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'])
static/css/style.css ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-main: #121212;
3
+ --bg-surface: #1E1E1E;
4
+ --border: #2C2C2C;
5
+ --text-primary: #E0E0E0;
6
+ --text-secondary: #A0A0A0;
7
+ --accent: #2DECBF;
8
+ --accent-hover: #58FFCF;
9
+ }
10
+
11
+ html, body {
12
+ background-color: var(--bg-main);
13
+ color: var(--text-primary);
14
+ margin: 0;
15
+ padding: 0;
16
+ font-family: "Vazirmatn", sans-serif;
17
+ height: 100%;
18
+ }
19
+
20
+ body, .container, .container-fluid, .row, .col, main {
21
+ background-color: var(--bg-main) !important;
22
+ color: var(--text-primary) !important;
23
+ }
24
+
25
+ .navbar {
26
+ background-color: var(--bg-surface) !important;
27
+ border-bottom: 1px solid var(--border);
28
+ }
29
+ .navbar-brand {
30
+ color: var(--accent) !important;
31
+ font-weight: bold;
32
+ }
33
+
34
+ .card {
35
+ background-color: var(--bg-surface) !important;
36
+ border: 1px solid var(--border);
37
+ border-radius: 10px;
38
+ box-shadow: 0 4px 12px rgba(0,0,0,0.6);
39
+ color: var(--text-primary);
40
+ }
41
+ .card-header {
42
+ background-color: var(--bg-surface) !important;
43
+ border-bottom: 1px solid var(--border);
44
+ font-weight: 600;
45
+ color: var(--text-primary);
46
+ }
47
+
48
+ .btn-primary {
49
+ background-color: var(--accent);
50
+ border: none;
51
+ color: #000000;
52
+ font-weight: 600;
53
+ border-radius: 6px;
54
+ transition: background-color 0.2s, transform 0.1s;
55
+ }
56
+ .btn-primary:hover {
57
+ background-color: var(--accent-hover);
58
+ transform: translateY(-1px);
59
+ }
60
+
61
+ .btn-outline-light {
62
+ background-color: transparent;
63
+ border: 1px solid var(--accent);
64
+ color: var(--accent);
65
+ }
66
+ .btn-outline-light:hover {
67
+ background-color: var(--accent);
68
+ color: #000000;
69
+ }
70
+
71
+
72
+ .form-control, .form-select, textarea {
73
+ background-color: var(--bg-main) !important;
74
+ color: var(--text-primary) !important;
75
+ border: 1px solid var(--border);
76
+ border-radius: 6px;
77
+ }
78
+ .form-control:focus, .form-select:focus {
79
+ border-color: var(--accent);
80
+ box-shadow: 0 0 0 0.2rem rgba(45, 236, 191, 0.25);
81
+ background-color: var(--bg-surface);
82
+ }
83
+
84
+
85
+ .table {
86
+ background-color: var(--bg-surface) !important;
87
+ color: var(--text-primary);
88
+ border-color: var(--border);
89
+ }
90
+ .table-striped>tbody>tr:nth-of-type(odd)>* {
91
+ background-color: var(--bg-main);
92
+ }
93
+ .table-striped>tbody>tr:hover>* {
94
+ background-color: #2A2A2A;
95
+ }
96
+
97
+ .table td, .table th {
98
+ color: #FFFFFF !important;
99
+ }
100
+
101
+ .card-body, .alert, .form-label, .form-check-label, .card-header {
102
+ color: #FFFFFF !important;
103
+ }
104
+
105
+ .alert, .custom-alert {
106
+ background-color: #1E1E1E !important;
107
+ color: #FFFFFF !important;
108
+ }
109
+
110
+ .custom-alert, .alert {
111
+ background-color: var(--bg-surface) !important;
112
+ border-left: 4px solid var(--accent);
113
+ color: var(--text-primary);
114
+ }
115
+
116
+ canvas, img {
117
+ background-color: var(--bg-surface);
118
+ border: 1px solid var(--border);
119
+ border-radius: 8px;
120
+ padding: 8px;
121
+ }
122
+
123
+ footer {
124
+ background-color: var(--bg-surface);
125
+ border-top: 1px solid var(--border);
126
+ color: var(--text-secondary);
127
+ padding-top: 10px;
128
+ padding-bottom: 10px;
129
+ }
130
+
131
+ ::-webkit-scrollbar {
132
+ width: 10px;
133
+ }
134
+ ::-webkit-scrollbar-track {
135
+ background: var(--bg-main);
136
+ }
137
+ ::-webkit-scrollbar-thumb {
138
+ background: var(--border);
139
+ border-radius: 8px;
140
+ }
141
+ ::-webkit-scrollbar-thumb:hover {
142
+ background: var(--accent);
143
+ }
144
+
145
+ .table>:not(caption)>*>* {
146
+ background-color: transparent !important;
147
+ }
148
+ .modal-content, .dropdown-menu, .popover, .tooltip {
149
+ background-color: var(--bg-surface) !important;
150
+ color: var(--text-primary) !important;
151
+ border: 1px solid var(--border);
152
+ }
153
+
154
+ ::placeholder {
155
+ color: var(--text-secondary);
156
+ opacity: 1;
157
+ }
158
+ ::-webkit-input-placeholder {
159
+ color: var(--text-secondary);
160
+ opacity: 1;
161
+ }
162
+ :-ms-input-placeholder {
163
+ color: var(--text-secondary);
164
+ opacity: 1;
165
+ }
166
+
167
+ #loading-overlay {
168
+ position: fixed;
169
+ top: 0;
170
+ left: 0;
171
+ width: 100%;
172
+ height: 100%;
173
+ background-color: rgba(18,18,18,0.95);
174
+ display: flex;
175
+ flex-direction: column;
176
+ align-items: center;
177
+ justify-content: center;
178
+ z-index: 9999;
179
+ }
180
+
181
+ .loading-spinner {
182
+ width: 60px;
183
+ height: 60px;
184
+ border: 6px solid #2C2C2C;
185
+ border-top-color: #2DECBF;
186
+ border-radius: 50%;
187
+ animation: spin 1s linear infinite;
188
+ }
189
+
190
+ .text-accent {
191
+ color: var(--accent);
192
+ }
193
+
194
+ .loading-text {
195
+ margin-top: 15px;
196
+ font-size: 1.1rem;
197
+ color: #E0E0E0;
198
+ text-align: center;
199
+ letter-spacing: 0.5px;
200
+ }
201
+
202
+ @keyframes spin {
203
+ to { transform: rotate(360deg); }
204
+ }
static/images/kpi_dashboard.png ADDED
static/images/logo.png ADDED
static/images/sensitivity_analysis_tornado.png ADDED

Git LFS Details

  • SHA256: 20a9ad651ccaf76c74226ff350e37b6f6f76c0a6e2aaf1d7596cd471e907ad40
  • Pointer size: 131 Bytes
  • Size of remote file: 206 kB
static/js/charts.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function renderKPIChart(labels, data) {
2
+ const ctx = document.getElementById('kpiChart').getContext('2d');
3
+ if (window.kpiChart) window.kpiChart.destroy();
4
+
5
+ window.kpiChart = new Chart(ctx, {
6
+ type: 'bar',
7
+ data: {
8
+ labels: labels,
9
+ datasets: [{
10
+ label: 'IRR (%)',
11
+ data: data,
12
+ backgroundColor: '#00C3A3',
13
+ borderColor: '#2DECBF',
14
+ borderWidth: 1,
15
+ borderRadius: 6,
16
+ }]
17
+ },
18
+ options: {
19
+ responsive: true,
20
+ plugins: {
21
+ legend: { display: false },
22
+ tooltip: { mode: 'index', intersect: false, bodyColor: '#FFFFFF', titleColor: '#FFFFFF' }
23
+ },
24
+ scales: {
25
+ x: {
26
+ ticks: { color: '#FFFFFF' },
27
+ grid: { color: 'rgba(255,255,255,0.1)' }
28
+ },
29
+ y: {
30
+ ticks: { color: '#FFFFFF' },
31
+ grid: { color: 'rgba(255,255,255,0.1)' }
32
+ }
33
+ }
34
+ }
35
+ });
36
+ }
37
+
38
+ function renderTornadoChart(labels, data) {
39
+ const ctx = document.getElementById('tornadoChart').getContext('2d');
40
+ if (window.tornadoChart) window.tornadoChart.destroy();
41
+
42
+ window.tornadoChart = new Chart(ctx, {
43
+ type: 'bar',
44
+ data: {
45
+ labels: labels,
46
+ datasets: [{
47
+ label: 'ΔIRR',
48
+ data: data,
49
+ backgroundColor: '#2DECBF',
50
+ borderRadius: 5
51
+ }]
52
+ },
53
+ options: {
54
+ indexAxis: 'y',
55
+ responsive: true,
56
+ plugins: {
57
+ legend: { display: false },
58
+ tooltip: { bodyColor: '#FFFFFF', titleColor: '#FFFFFF' }
59
+ },
60
+ scales: {
61
+ x: {
62
+ ticks: { color: '#FFFFFF' },
63
+ grid: { color: 'rgba(255,255,255,0.1)' }
64
+ },
65
+ y: {
66
+ ticks: { color: '#FFFFFF' },
67
+ grid: { color: 'rgba(255,255,255,0.1)' }
68
+ }
69
+ }
70
+ }
71
+ });
72
+ }
templates/index.html ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+ {% block content %}
3
+
4
+ <div class="row">
5
+ <div class="col-lg-4">
6
+ <div class="card bg-secondary mb-3">
7
+ <div class="card-header">پارامترهای مدل</div>
8
+ <div class="card-body">
9
+ {% if import_error %}
10
+ <div class="alert alert-danger">خطا در بارگذاری main.py — لطفاً فایل و وابستگی‌ها را بررسی کنید.<pre style="white-space:pre-wrap">{{ import_error }}</pre></div>
11
+ {% endif %}
12
+ <form method="post" action="{{ url_for('run') }}">
13
+ <div class="mb-2">
14
+ <label class="form-label">نرخ تورم (مثلاً 0.015)</label>
15
+ <input name="inflation" type="text" class="form-control" value="{{ defaults.inflation if defaults else '0.015' }}">
16
+ </div>
17
+ <div class="mb-2">
18
+ <label class="form-label">نرخ مالیات (مثلاً 0.10)</label>
19
+ <input name="tax" type="text" class="form-control" value="{{ defaults.tax if defaults else '0.10' }}">
20
+ </div>
21
+ <div class="mb-2">
22
+ <label class="form-label">طول پروژه (سال)</label>
23
+ <input name="years" type="number" class="form-control" value="{{ defaults.years if defaults else 15 }}">
24
+ </div>
25
+ <div class="mb-2">
26
+ <label class="form-label">ظرفیت (KTA) - حداقل</label>
27
+ <input name="cap_min" type="number" step="1" class="form-control" value="{{ defaults.cap_min if defaults else 500 }}">
28
+ </div>
29
+ <div class="mb-2">
30
+ <label class="form-label">ظرفیت (KTA) - حداکثر</label>
31
+ <input name="cap_max" type="number" step="1" class="form-control" value="{{ defaults.cap_max if defaults else 600 }}">
32
+ </div>
33
+ <div class="mb-2">
34
+ <label class="form-label">Technology</label>
35
+ <select name="technology" class="form-select">
36
+ {% for t in techs %}
37
+ <option {{ 'selected' if loop.first }}>{{ t }}</option>
38
+ {% endfor %}
39
+ </select>
40
+ </div>
41
+ <div class="mb-2">
42
+ <label class="form-label">Export market mix (0-1)</label>
43
+ <input name="export_mix" type="number" step="0.01" class="form-control" value="{{ defaults.export_mix if defaults else 0.7 }}">
44
+ </div>
45
+ <div class="form-check mb-2">
46
+ <input class="form-check-input" type="checkbox" id="sell_byproducts" name="sell_byproducts" checked>
47
+ <label class="form-check-label" for="sell_byproducts">فروش محصولات جانبی (Sell byproducts)</label>
48
+ </div>
49
+
50
+ <hr class="my-3">
51
+ <label class="form-label">اعمال تغییرات پیشرفته (JSON)</label>
52
+ <small class="text-muted d-block mb-2">برای ویرایش / اضافه کردن تکنولوژی یا قیمت‌ها از JSON استفاده کنید (اختیاری)</small>
53
+ <div class="mb-2">
54
+ <textarea name="tech_json" rows="6" class="form-control" placeholder='مثال: {"MyTech": {"capex_base_M": 200, "opex_base_cents_kg": 150}}'></textarea>
55
+ </div>
56
+ <div class="mb-2">
57
+ <textarea name="prices_json" rows="4" class="form-control" placeholder='مثال: {"pvc_s65_export": 1200}'></textarea>
58
+ </div>
59
+
60
+ <button class="btn btn-primary w-100" type="submit">اجرا و محاسبه</button>
61
+ </form>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="card bg-secondary">
66
+ <div class="card-header">فایل خروجی</div>
67
+ <div class="card-body">
68
+ <a class="btn btn-outline-light w-100" href="{{ url_for('download_results') }}">دانلود CSV نتایج</a>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="col-lg-8">
74
+ {% if top_kpis %}
75
+ <div class="row mb-3">
76
+ <div class="col-md-3">
77
+ <div class="card text-dark bg-light mb-2">
78
+ <div class="card-body text-center">
79
+ <h6>بهترین IRR (%)</h6>
80
+ <h4>{{ top_kpis.irr }}</h4>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div class="col-md-3">
85
+ <div class="card text-dark bg-light mb-2">
86
+ <div class="card-body text-center">
87
+ <h6>سود سالانه ($M)</h6>
88
+ <h4>{{ top_kpis.annual_profit_M }}</h4>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ <div class="col-md-3">
93
+ <div class="card text-dark bg-light mb-2">
94
+ <div class="card-body text-center">
95
+ <h6>CAPEX ($M)</h6>
96
+ <h4>{{ top_kpis.capex_M }}</h4>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="col-md-3">
101
+ <div class="card text-dark bg-light mb-2">
102
+ <div class="card-body text-center">
103
+ <h6>Payback (yrs)</h6>
104
+ <h4>{{ top_kpis.payback }}</h4>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ {% endif %}
110
+
111
+ <!-- Charts -->
112
+ <div class="card bg-secondary mb-3">
113
+ <div class="card-header">نمودارها و تحلیل حساسیت</div>
114
+ <div class="card-body text-center">
115
+
116
+ {% if kpi_img %}
117
+ <h6 class="text-light mb-2">KPI Dashboard</h6>
118
+ <img src="{{ url_for('static', filename=kpi_img) }}" alt="KPI Dashboard" class="img-fluid rounded shadow border border-secondary mb-3">
119
+ {% else %}
120
+ <div class="row">
121
+ <div class="col-md-6">
122
+ <canvas id="kpiChart" height="200"></canvas>
123
+ </div>
124
+ <div class="col-md-6">
125
+ <canvas id="tornadoChart" height="200"></canvas>
126
+ </div>
127
+ </div>
128
+ {% endif %}
129
+
130
+ {% if tornado_img %}
131
+ <h6 class="text-light mt-4 mb-2">Sensitivity Analysis</h6>
132
+ <img src="{{ url_for('static', filename=tornado_img) }}" alt="Tornado Chart" class="img-fluid rounded shadow border border-secondary">
133
+ {% endif %}
134
+ </div>
135
+ </div>
136
+
137
+
138
+ </div>
139
+ </div>
140
+
141
+ <script>
142
+ const chartsPayload = {{ charts_json|safe if charts_json else 'null' }};
143
+ if (chartsPayload) {
144
+ const labels = chartsPayload.map((r, i) => r.Method ? r.Method : `M${i+1}`);
145
+ const irrs = chartsPayload.map(r => Number(r.irr) || 0);
146
+
147
+ renderKPIChart(labels, irrs);
148
+
149
+ const ranges = chartsPayload.map(r => {
150
+ const low = r.irr - 5; const high = r.irr + 5; return high - low;
151
+ });
152
+ renderTornadoChart(labels, ranges);
153
+ }
154
+ </script>
155
+
156
+ {% endblock %}
templates/layout.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>داشبورد بهینه‌سازی پروژه PVC</title>
7
+
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
9
+
10
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
+
12
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
13
+ </head>
14
+
15
+ <body>
16
+ <nav class="navbar navbar-dark bg-dark border-bottom border-secondary shadow-sm">
17
+ <div class="container-fluid d-flex align-items-center">
18
+ <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" height="120" class="me-2 rounded">
19
+ <span class="navbar-brand fw-bold text-accent">🏭 Petro Optima</span>
20
+ </div>
21
+ </nav>
22
+
23
+ <main class="px-4 py-3">
24
+ {% with messages = get_flashed_messages(with_categories=true) %}
25
+ {% if messages %}
26
+ {% for category, msg in messages %}
27
+ <div class="alert alert-{{ 'info' if category=='info' else ('danger' if category=='danger' else ('success' if category=='success' else 'warning')) }} alert-dismissible fade show custom-alert" role="alert">
28
+ {{ msg }}
29
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
30
+ </div>
31
+ {% endfor %}
32
+ {% endif %}
33
+ {% endwith %}
34
+ {% block content %}{% endblock %}
35
+ </main>
36
+
37
+ <footer class="text-center text-muted py-3 border-top border-secondary">
38
+ </footer>
39
+
40
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
41
+ <script src="{{ url_for('static', filename='js/charts.js') }}"></script>
42
+ <div id="loading-overlay" class="d-none">
43
+ <div class="loading-spinner"></div>
44
+ <div class="loading-text">در حال انجام محاسبات، لطفاً صبر کنید...</div>
45
+ </div>
46
+
47
+ <script>
48
+ document.addEventListener("DOMContentLoaded", function() {
49
+ const forms = document.querySelectorAll("form");
50
+ forms.forEach(form => {
51
+ form.addEventListener("submit", function() {
52
+ const overlay = document.getElementById("loading-overlay");
53
+ overlay.classList.remove("d-none");
54
+ });
55
+ });
56
+ });
57
+ </script>
58
+ </body>
59
+ </html>