pypsa-cl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypsa_cl/GUI.py +848 -0
- pypsa_cl/__init__.py +14 -0
- pypsa_cl/__main__.py +5 -0
- pypsa_cl/_data.py +186 -0
- pypsa_cl/_paths.py +34 -0
- pypsa_cl/assets/logo_PELP.png +0 -0
- pypsa_cl/assets/sen_network.png +0 -0
- pypsa_cl/cli.py +58 -0
- pypsa_cl/main.py +2243 -0
- pypsa_cl/tools.py +4254 -0
- pypsa_cl-0.1.0.dist-info/METADATA +213 -0
- pypsa_cl-0.1.0.dist-info/RECORD +16 -0
- pypsa_cl-0.1.0.dist-info/WHEEL +5 -0
- pypsa_cl-0.1.0.dist-info/entry_points.txt +2 -0
- pypsa_cl-0.1.0.dist-info/licenses/LICENSE +21 -0
- pypsa_cl-0.1.0.dist-info/top_level.txt +1 -0
pypsa_cl/GUI.py
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
"""Graphical configuration interface for the PyPSA-CL expansion model.
|
|
2
|
+
|
|
3
|
+
Builds the scenario/epoch configuration returned to ``main.py``. A run may
|
|
4
|
+
queue several global scenarios, each with one or more investment epochs, and
|
|
5
|
+
execute them sequentially. Preset scenario packages (E1-N3) auto-fill and lock
|
|
6
|
+
the six component selectors; the "Otro" option unlocks free configuration.
|
|
7
|
+
|
|
8
|
+
``run_gui`` returns a dictionary with a ``scenarios`` list and a flattened
|
|
9
|
+
``epochs`` list, where each epoch carries the ``global_scenario`` label and the
|
|
10
|
+
``_scenario_index`` / ``_epoch_in_scenario`` / ``_global_epoch`` markers used by
|
|
11
|
+
the export routines.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import tkinter as tk
|
|
15
|
+
from tkinter import ttk, messagebox
|
|
16
|
+
from tkcalendar import DateEntry
|
|
17
|
+
from PIL import Image, ImageTk
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
import os
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
from pypsa_cl._data import get_data_file
|
|
23
|
+
from pypsa_cl._paths import ASSETS_DIR, get_project_root
|
|
24
|
+
|
|
25
|
+
ordinal_english = [
|
|
26
|
+
"first", "second", "third", "fourth", "fifth", "sixth", "seventh",
|
|
27
|
+
"eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth",
|
|
28
|
+
"fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth",
|
|
29
|
+
"nineteenth", "twentieth"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
class Epoch:
|
|
33
|
+
def __init__(self, name, start_date, end_date, rep_years, rep_months, rep_days, settings=None):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.start_date = start_date
|
|
36
|
+
self.end_date = end_date
|
|
37
|
+
self.rep_years = rep_years
|
|
38
|
+
self.rep_months = rep_months
|
|
39
|
+
self.rep_days = rep_days
|
|
40
|
+
self.settings = settings or {}
|
|
41
|
+
|
|
42
|
+
def run_gui():
|
|
43
|
+
def get_scenarios_names():
|
|
44
|
+
current_dir = os.path.dirname(__file__)
|
|
45
|
+
parent_dir = get_project_root()
|
|
46
|
+
scenario_types = ["demand", "climate", "hydrology", "fuel_price",
|
|
47
|
+
"generation_investment_cost", "storage_investment_cost"]
|
|
48
|
+
scenario_names_path = {
|
|
49
|
+
"demand": [get_data_file("Loads.xlsx"), "loads"],
|
|
50
|
+
"climate": [get_data_file("Profiles.xlsx"), "profiles"],
|
|
51
|
+
"hydrology": [get_data_file("Water time series.xlsx"), "Inflow"],
|
|
52
|
+
"fuel_price": [get_data_file("Costs.xlsx"), "Fuel price"],
|
|
53
|
+
"generation_investment_cost": [get_data_file("Costs.xlsx"), "Generation investment cost"],
|
|
54
|
+
"storage_investment_cost": [get_data_file("Costs.xlsx"), "Storage investment cost"],
|
|
55
|
+
}
|
|
56
|
+
scenario_names = {}
|
|
57
|
+
for s in scenario_types:
|
|
58
|
+
file_path, sheet_name = scenario_names_path[s]
|
|
59
|
+
df = pd.read_excel(file_path, sheet_name=sheet_name)
|
|
60
|
+
scenario_names[s] = df["scenario"].unique().tolist()
|
|
61
|
+
return scenario_names
|
|
62
|
+
|
|
63
|
+
scenario_map = {}
|
|
64
|
+
scenario_map['E1 - Exploratorio desfavorable'] = {
|
|
65
|
+
'demand': 'Baja - E1', 'climate': 'current_projections',
|
|
66
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Alto',
|
|
67
|
+
'generation_investment_cost': 'Alto', 'storage_investment_cost': 'Alto'}
|
|
68
|
+
scenario_map['E2 - Exploratorio BAU'] = {
|
|
69
|
+
'demand': 'Media - E2', 'climate': 'current_projections',
|
|
70
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Medio',
|
|
71
|
+
'generation_investment_cost': 'Medio', 'storage_investment_cost': 'Medio'}
|
|
72
|
+
scenario_map['E3 - Exploratorio favorable'] = {
|
|
73
|
+
'demand': 'Alta - E3', 'climate': 'current_projections',
|
|
74
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Bajo',
|
|
75
|
+
'generation_investment_cost': 'Bajo', 'storage_investment_cost': 'Bajo'}
|
|
76
|
+
scenario_map['N1 - Normativo desfavorable'] = {
|
|
77
|
+
'demand': 'Baja - N1', 'climate': 'current_projections',
|
|
78
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Alto',
|
|
79
|
+
'generation_investment_cost': 'Alto', 'storage_investment_cost': 'Alto'}
|
|
80
|
+
scenario_map['N2 - Normativo BAU'] = {
|
|
81
|
+
'demand': 'Media - N2', 'climate': 'current_projections',
|
|
82
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Medio',
|
|
83
|
+
'generation_investment_cost': 'Medio', 'storage_investment_cost': 'Medio'}
|
|
84
|
+
scenario_map['N3 - Normativo favorable'] = {
|
|
85
|
+
'demand': 'Alta - N3', 'climate': 'current_projections',
|
|
86
|
+
'hydrology': 'MIROC-ESM-CHEM r1i1p1', 'fuel_price': 'Bajo',
|
|
87
|
+
'generation_investment_cost': 'Bajo', 'storage_investment_cost': 'Bajo'}
|
|
88
|
+
scenario_map['Otro'] = {}
|
|
89
|
+
|
|
90
|
+
PRESET_NAMES = [k for k in scenario_map if k != 'Otro']
|
|
91
|
+
ALL_GLOBAL_NAMES = PRESET_NAMES + ['Otro']
|
|
92
|
+
|
|
93
|
+
scenario_names = {
|
|
94
|
+
'demand': ['Alta - E3', 'Media - E2', 'Baja - E1', 'Alta - N3', 'Media - N2', 'Baja - N1'],
|
|
95
|
+
'climate': ['current_projections'],
|
|
96
|
+
'hydrology': ['H_1982', 'H_1986', 'H_1998', 'MIROC-ESM-CHEM r1i1p1', 'NorESM1-M r1i1p1'],
|
|
97
|
+
'fuel_price': ['Alto', 'Medio', 'Bajo'],
|
|
98
|
+
'generation_investment_cost': ['Alto', 'Medio', 'Bajo'],
|
|
99
|
+
'storage_investment_cost': ['Alto', 'Medio', 'Bajo'],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
BASE_COLORS = {
|
|
103
|
+
"fondo_azul_oscuro": "#081889",
|
|
104
|
+
"cyan": "#18ADE5",
|
|
105
|
+
"gray": "#8093AD",
|
|
106
|
+
"orange": "#F7A823",
|
|
107
|
+
"green": "#7DB928",
|
|
108
|
+
"mint": "#99D0C1",
|
|
109
|
+
"dark_green": "#397831",
|
|
110
|
+
"white": "#FFFFFF",
|
|
111
|
+
"black": "#000000",
|
|
112
|
+
"section_bg": "#0a20a8",
|
|
113
|
+
"section_border": "#2a40c8",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
THEME = {
|
|
117
|
+
"fondo_principal": "fondo_azul_oscuro",
|
|
118
|
+
"titulo": "cyan",
|
|
119
|
+
"texto_principal": "white",
|
|
120
|
+
"campo_activo": "mint",
|
|
121
|
+
"boton_principal": "orange",
|
|
122
|
+
"boton_hover": "green",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def color(role):
|
|
126
|
+
return BASE_COLORS[THEME[role]]
|
|
127
|
+
|
|
128
|
+
SBG = BASE_COLORS["section_bg"]
|
|
129
|
+
WHITE = BASE_COLORS["white"]
|
|
130
|
+
CYAN = BASE_COLORS["cyan"]
|
|
131
|
+
MINT = BASE_COLORS["mint"]
|
|
132
|
+
DARK = BASE_COLORS["fondo_azul_oscuro"]
|
|
133
|
+
ORANGE = BASE_COLORS["orange"]
|
|
134
|
+
|
|
135
|
+
result = {}
|
|
136
|
+
epochs = []
|
|
137
|
+
scenarios = []
|
|
138
|
+
|
|
139
|
+
def get_current_epoch_settings():
|
|
140
|
+
gs = global_scenario_var.get()
|
|
141
|
+
if gs == 'Otro':
|
|
142
|
+
global_label = custom_scenario_name_var.get().strip() or 'Otro'
|
|
143
|
+
else:
|
|
144
|
+
global_label = gs
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"global_scenario": global_label,
|
|
148
|
+
"demand_scenario": demand_scenario.get(),
|
|
149
|
+
"climate_scenario": climate_scenario.get(),
|
|
150
|
+
"hydrology_scenario": hydrology_scenario.get(),
|
|
151
|
+
"generation_investment_cost_scenario": generation_investment_cost_scenario.get(),
|
|
152
|
+
"storage_investment_cost_scenario": storage_investment_cost_scenario.get(),
|
|
153
|
+
"fuel_price_scenario": fuel_price_scenario.get(),
|
|
154
|
+
"solver_name": solver_name.get(),
|
|
155
|
+
"solver_threads": int(entry_threads.get()),
|
|
156
|
+
"solver_mip_gap": float(entry_mip_gap.get()),
|
|
157
|
+
"linearized_unit_commitment": linearized_uc_var.get(),
|
|
158
|
+
# Restriccion de emisiones maximas deshabilitada.
|
|
159
|
+
# "constraint_max_emissions": cb_emissions.get(),
|
|
160
|
+
# "max_emissions_MtCO2_year": float(entry_emissions.get()),
|
|
161
|
+
"constraint_max_emissions": False,
|
|
162
|
+
"max_emissions_MtCO2_year": 0.0,
|
|
163
|
+
"constraint_max_load_lost": cb_load_lost.get(),
|
|
164
|
+
"max_load_lost_GWh_year": float(entry_load_lost.get()),
|
|
165
|
+
"constraint_discrete_transmission_expansion": cb_discrete_transmission.get(),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def build_epoch_dicts():
|
|
169
|
+
"""Serializa las épocas actualmente añadidas a dicts exportables."""
|
|
170
|
+
return [
|
|
171
|
+
{
|
|
172
|
+
"name": ep.name,
|
|
173
|
+
"start_date": ep.start_date.strftime("%Y-%m-%d"),
|
|
174
|
+
"end_date": ep.end_date.strftime("%Y-%m-%d"),
|
|
175
|
+
"representative_years": ep.rep_years,
|
|
176
|
+
"representative_months": ep.rep_months,
|
|
177
|
+
"representative_days": ep.rep_days,
|
|
178
|
+
**ep.settings,
|
|
179
|
+
}
|
|
180
|
+
for ep in epochs
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
def add_scenario_to_queue():
|
|
184
|
+
"""Guarda el escenario global actual (con sus épocas) en la cola y deja
|
|
185
|
+
la GUI lista para configurar el siguiente escenario global."""
|
|
186
|
+
if not epochs:
|
|
187
|
+
messagebox.showerror("Error", "Debe añadir al menos una época antes de agregar el escenario a la cola.")
|
|
188
|
+
return False
|
|
189
|
+
label = epochs[0].settings.get("global_scenario", "")
|
|
190
|
+
if label in [s["global_scenario"] for s in scenarios]:
|
|
191
|
+
messagebox.showerror(
|
|
192
|
+
"Error",
|
|
193
|
+
f'Ya hay un escenario "{label}" en la cola.\n'
|
|
194
|
+
'Para agregar una variante use "Otro" con un nombre distinto.'
|
|
195
|
+
)
|
|
196
|
+
return False
|
|
197
|
+
scenarios.append({"global_scenario": label, "epochs": build_epoch_dicts()})
|
|
198
|
+
epochs.clear()
|
|
199
|
+
update_epoch_listbox()
|
|
200
|
+
update_scenario_listbox()
|
|
201
|
+
global_scenario_combo.config(state="readonly")
|
|
202
|
+
_update_global_lock_label(locked=False)
|
|
203
|
+
custom_scenario_name_var.set("")
|
|
204
|
+
cal_inicio.set_date("2026-01-01")
|
|
205
|
+
years_var.set("2")
|
|
206
|
+
months_var.set("6")
|
|
207
|
+
days_var.set("1")
|
|
208
|
+
try:
|
|
209
|
+
sync_end_date_with_years()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
suggest_epoch_name()
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
def confirmar():
|
|
216
|
+
if epochs:
|
|
217
|
+
if not add_scenario_to_queue():
|
|
218
|
+
return
|
|
219
|
+
if not scenarios:
|
|
220
|
+
messagebox.showerror("Error", "Debe agregar al menos un escenario (con al menos una época) antes de ejecutar.")
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
_max_workers = max(1, int(entry_max_workers.get()))
|
|
224
|
+
_ram_limit = float(entry_ram_limit.get())
|
|
225
|
+
if not (10 <= _ram_limit <= 100):
|
|
226
|
+
raise ValueError
|
|
227
|
+
except ValueError:
|
|
228
|
+
messagebox.showerror("Error", "Revise la configuracion de ejecucion:\n"
|
|
229
|
+
"- Max parallel scenarios debe ser un entero >= 1\n"
|
|
230
|
+
"- RAM limit debe ser un porcentaje entre 10 y 100")
|
|
231
|
+
return
|
|
232
|
+
result["run_config"] = {
|
|
233
|
+
"execution_mode": "parallel" if execution_mode_var.get() == "Paralelo" else "sequential",
|
|
234
|
+
"max_parallel_scenarios": _max_workers,
|
|
235
|
+
"ram_limit_pct": _ram_limit,
|
|
236
|
+
}
|
|
237
|
+
flat = []
|
|
238
|
+
for s_idx, sc in enumerate(scenarios):
|
|
239
|
+
for e_idx, ep in enumerate(sc["epochs"]):
|
|
240
|
+
d = dict(ep)
|
|
241
|
+
d["_scenario_index"] = s_idx
|
|
242
|
+
d["_epoch_in_scenario"] = e_idx
|
|
243
|
+
d["_global_epoch"] = len(flat)
|
|
244
|
+
flat.append(d)
|
|
245
|
+
result["scenarios"] = scenarios
|
|
246
|
+
result["epochs"] = flat
|
|
247
|
+
result["n_scenarios"] = len(scenarios)
|
|
248
|
+
root.destroy()
|
|
249
|
+
|
|
250
|
+
root = tk.Tk()
|
|
251
|
+
root.title("PyPSA-CL - a tool for Long-term Power Systems Planning in Chile")
|
|
252
|
+
root.configure(bg=color("fondo_principal"))
|
|
253
|
+
root.geometry("1350x900")
|
|
254
|
+
root.resizable(True, True)
|
|
255
|
+
|
|
256
|
+
def _on_callback_error(exc, val, tb):
|
|
257
|
+
import traceback
|
|
258
|
+
traceback.print_exception(exc, val, tb)
|
|
259
|
+
try:
|
|
260
|
+
root.destroy()
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
root.report_callback_exception = _on_callback_error
|
|
265
|
+
|
|
266
|
+
outer_frame = tk.Frame(root, bg=color("fondo_principal"))
|
|
267
|
+
outer_frame.pack(fill="both", expand=True)
|
|
268
|
+
|
|
269
|
+
canvas = tk.Canvas(outer_frame, bg=color("fondo_principal"), highlightthickness=0)
|
|
270
|
+
vscrollbar = ttk.Scrollbar(outer_frame, orient="vertical", command=canvas.yview)
|
|
271
|
+
hscrollbar = ttk.Scrollbar(outer_frame, orient="horizontal", command=canvas.xview)
|
|
272
|
+
canvas.configure(yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set)
|
|
273
|
+
|
|
274
|
+
canvas.grid(row=0, column=0, sticky="nsew")
|
|
275
|
+
vscrollbar.grid(row=0, column=1, sticky="ns")
|
|
276
|
+
hscrollbar.grid(row=1, column=0, sticky="ew")
|
|
277
|
+
outer_frame.rowconfigure(0, weight=1)
|
|
278
|
+
outer_frame.columnconfigure(0, weight=1)
|
|
279
|
+
|
|
280
|
+
container = tk.Frame(canvas, bg=color("fondo_principal"))
|
|
281
|
+
canvas_window = canvas.create_window((0, 0), window=container, anchor="nw")
|
|
282
|
+
|
|
283
|
+
def on_frame_configure(event):
|
|
284
|
+
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
285
|
+
|
|
286
|
+
def on_canvas_configure(event):
|
|
287
|
+
content_width = container.winfo_reqwidth()
|
|
288
|
+
canvas.itemconfig(canvas_window, width=max(event.width, content_width))
|
|
289
|
+
|
|
290
|
+
container.bind("<Configure>", on_frame_configure)
|
|
291
|
+
canvas.bind("<Configure>", on_canvas_configure)
|
|
292
|
+
|
|
293
|
+
def _on_mousewheel(event):
|
|
294
|
+
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
295
|
+
|
|
296
|
+
def _on_shift_mousewheel(event):
|
|
297
|
+
canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
298
|
+
|
|
299
|
+
canvas.bind_all("<MouseWheel>", _on_mousewheel)
|
|
300
|
+
canvas.bind_all("<Shift-MouseWheel>", _on_shift_mousewheel)
|
|
301
|
+
|
|
302
|
+
inner = tk.Frame(container, bg=color("fondo_principal"))
|
|
303
|
+
inner.pack(padx=20, pady=10)
|
|
304
|
+
|
|
305
|
+
style = ttk.Style(root)
|
|
306
|
+
style.theme_use("default")
|
|
307
|
+
style.configure("TLabel", background=color("fondo_principal"), foreground=color("texto_principal"), font=("Verdana", 10))
|
|
308
|
+
style.configure("TCombobox", fieldbackground=MINT, background=MINT, foreground=BASE_COLORS["black"], font=("Verdana", 9))
|
|
309
|
+
style.configure("TButton", background=color("boton_principal"), foreground=BASE_COLORS["black"], font=("Verdana", 10, "bold"), padding=6)
|
|
310
|
+
style.map("TButton", background=[("active", color("boton_hover"))])
|
|
311
|
+
style.configure("Section.TLabelframe", background=SBG, foreground=CYAN, font=("Verdana", 9, "bold"), relief="groove", bordercolor=BASE_COLORS["section_border"])
|
|
312
|
+
style.configure("Section.TLabelframe.Label", background=SBG, foreground=CYAN, font=("Verdana", 9, "bold"))
|
|
313
|
+
|
|
314
|
+
# Imágenes de la GUI: primero data/ de la carpeta de trabajo (permite reemplazarlas),
|
|
315
|
+
# luego las incluidas en el paquete.
|
|
316
|
+
_src_dir = ASSETS_DIR
|
|
317
|
+
_data_dir = os.path.join(get_project_root(), "data")
|
|
318
|
+
|
|
319
|
+
header_frame = tk.Frame(inner, bg=color("fondo_principal"))
|
|
320
|
+
header_frame.grid(row=0, column=0, columnspan=4, sticky="ew", pady=(0, 4))
|
|
321
|
+
header_frame.columnconfigure(0, weight=1)
|
|
322
|
+
|
|
323
|
+
header_inner = tk.Frame(header_frame, bg=color("fondo_principal"))
|
|
324
|
+
header_inner.grid(row=0, column=0)
|
|
325
|
+
|
|
326
|
+
for _logo_dir in (_data_dir, _src_dir):
|
|
327
|
+
_logo_path = os.path.join(_logo_dir, "logo_PELP.png")
|
|
328
|
+
if os.path.exists(_logo_path):
|
|
329
|
+
try:
|
|
330
|
+
_logo_raw = Image.open(_logo_path)
|
|
331
|
+
_logo_w = 200
|
|
332
|
+
_logo_h = int(_logo_w * _logo_raw.height / _logo_raw.width)
|
|
333
|
+
_logo_res = _logo_raw.resize((_logo_w, _logo_h), Image.LANCZOS)
|
|
334
|
+
logo_photo = ImageTk.PhotoImage(_logo_res)
|
|
335
|
+
logo_label = tk.Label(header_inner, image=logo_photo,
|
|
336
|
+
bg=color("fondo_principal"))
|
|
337
|
+
logo_label.image = logo_photo
|
|
338
|
+
logo_label.grid(row=0, column=0, padx=(0, 24), sticky="w")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print(f"No se pudo cargar el logo: {e}")
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
title = tk.Label(
|
|
344
|
+
header_inner,
|
|
345
|
+
text="PyPSA-CL - a tool for Long-term Power Systems Planning in Chile",
|
|
346
|
+
bg=color("fondo_principal"), fg=color("titulo"),
|
|
347
|
+
font=("Verdana", 15, "bold"),
|
|
348
|
+
)
|
|
349
|
+
title.grid(row=0, column=1, sticky="w")
|
|
350
|
+
|
|
351
|
+
header_sep = tk.Frame(header_frame, bg=CYAN, height=2)
|
|
352
|
+
header_sep.grid(row=1, column=0, sticky="ew", pady=(8, 10))
|
|
353
|
+
|
|
354
|
+
def add_row_in(frame, label_text, widget, row, bg):
|
|
355
|
+
lbl = tk.Label(frame, text=label_text, bg=bg, fg=WHITE, font=("Verdana", 9))
|
|
356
|
+
lbl.grid(row=row, column=0, padx=(6, 8), pady=4, sticky="e")
|
|
357
|
+
widget.grid(row=row, column=1, padx=(0, 6), pady=4, sticky="w")
|
|
358
|
+
|
|
359
|
+
def add_entry_row(frame, label_text, default, row, width=8):
|
|
360
|
+
lbl = tk.Label(frame, text=label_text, bg=SBG, fg=MINT, font=("Verdana", 9))
|
|
361
|
+
lbl.grid(row=row, column=0, padx=(6, 8), pady=4, sticky="e")
|
|
362
|
+
var = tk.StringVar(value=str(default))
|
|
363
|
+
ent = tk.Entry(frame, textvariable=var, bg=MINT, fg=DARK, font=("Verdana", 9),
|
|
364
|
+
width=width, relief="flat", bd=2)
|
|
365
|
+
ent.grid(row=row, column=1, padx=(0, 6), pady=4, sticky="w")
|
|
366
|
+
return var
|
|
367
|
+
|
|
368
|
+
left_col = tk.Frame(inner, bg=color("fondo_principal"))
|
|
369
|
+
left_col.grid(row=1, column=0, padx=(0, 10), sticky="n")
|
|
370
|
+
|
|
371
|
+
mid_col = tk.Frame(inner, bg=color("fondo_principal"))
|
|
372
|
+
mid_col.grid(row=1, column=1, padx=(0, 10), sticky="n")
|
|
373
|
+
|
|
374
|
+
frame_temporal = ttk.LabelFrame(left_col, text="Configuracion temporal",
|
|
375
|
+
style="Section.TLabelframe", padding=(12, 8))
|
|
376
|
+
frame_temporal.pack(fill="x", pady=(0, 10))
|
|
377
|
+
|
|
378
|
+
epoch_name_var = tk.StringVar()
|
|
379
|
+
|
|
380
|
+
def suggest_epoch_name():
|
|
381
|
+
idx = len(epochs)
|
|
382
|
+
name = ordinal_english[idx] + " epoch" if idx < len(ordinal_english) else f"{idx+1}th epoch"
|
|
383
|
+
epoch_name_var.set(name)
|
|
384
|
+
|
|
385
|
+
suggest_epoch_name()
|
|
386
|
+
|
|
387
|
+
lbl_epoch = tk.Label(frame_temporal, text="Epoch name:", bg=SBG, fg=WHITE, font=("Verdana", 9))
|
|
388
|
+
lbl_epoch.grid(row=0, column=0, padx=(6, 8), pady=4, sticky="e")
|
|
389
|
+
entry_epoch_name = tk.Entry(frame_temporal, textvariable=epoch_name_var, bg=MINT, fg=DARK,
|
|
390
|
+
font=("Verdana", 9), width=22, relief="flat", bd=2)
|
|
391
|
+
entry_epoch_name.grid(row=0, column=1, padx=(0, 6), pady=4, sticky="w")
|
|
392
|
+
|
|
393
|
+
cal_inicio = DateEntry(frame_temporal, date_pattern="yyyy-mm-dd")
|
|
394
|
+
cal_inicio.set_date("2026-01-01")
|
|
395
|
+
add_row_in(frame_temporal, "Start date", cal_inicio, 1, SBG)
|
|
396
|
+
|
|
397
|
+
cal_final = DateEntry(frame_temporal, date_pattern="yyyy-mm-dd")
|
|
398
|
+
cal_final.set_date("2027-12-31")
|
|
399
|
+
add_row_in(frame_temporal, "End date", cal_final, 2, SBG)
|
|
400
|
+
|
|
401
|
+
years_var = tk.StringVar(value="2")
|
|
402
|
+
years_combo = ttk.Combobox(frame_temporal, textvariable=years_var, values=list(range(1, 6)), state="readonly", width=12)
|
|
403
|
+
add_row_in(frame_temporal, "Representative years", years_combo, 3, SBG)
|
|
404
|
+
|
|
405
|
+
months_var = tk.StringVar(value="6")
|
|
406
|
+
months_combo = ttk.Combobox(frame_temporal, textvariable=months_var, values=[1, 2, 3, 4, 6, 12], state="readonly", width=12)
|
|
407
|
+
add_row_in(frame_temporal, "Representative months", months_combo, 4, SBG)
|
|
408
|
+
|
|
409
|
+
days_var = tk.StringVar(value="1")
|
|
410
|
+
days_combo = ttk.Combobox(frame_temporal, textvariable=days_var, values=[1, 4], state="readonly", width=12)
|
|
411
|
+
add_row_in(frame_temporal, "Representative days", days_combo, 5, SBG)
|
|
412
|
+
|
|
413
|
+
def suggested_end_date(start_date, rep_years):
|
|
414
|
+
return datetime(start_date.year + rep_years - 1, 12, 31).date()
|
|
415
|
+
|
|
416
|
+
def sync_end_date_with_years(event=None):
|
|
417
|
+
try:
|
|
418
|
+
start_date = cal_inicio.get_date()
|
|
419
|
+
rep_years = int(years_var.get())
|
|
420
|
+
cal_final.set_date(suggested_end_date(start_date, rep_years))
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
years_combo.bind("<<ComboboxSelected>>", sync_end_date_with_years)
|
|
425
|
+
cal_inicio.bind("<<DateEntrySelected>>", sync_end_date_with_years)
|
|
426
|
+
try:
|
|
427
|
+
sync_end_date_with_years()
|
|
428
|
+
except Exception as e:
|
|
429
|
+
print(f"Warning syncing end date: {e}")
|
|
430
|
+
|
|
431
|
+
def add_epoch():
|
|
432
|
+
name = epoch_name_var.get().strip()
|
|
433
|
+
if not name:
|
|
434
|
+
messagebox.showerror("Error", "Por favor ingrese un nombre para la época.")
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
if global_scenario_var.get() == 'Otro':
|
|
438
|
+
if not custom_scenario_name_var.get().strip():
|
|
439
|
+
messagebox.showerror("Error", 'Debe ingresar un nombre para el escenario "Otro".')
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
if len(epochs) == 0:
|
|
443
|
+
start_date = cal_inicio.get_date()
|
|
444
|
+
else:
|
|
445
|
+
start_date = epochs[-1].end_date + timedelta(days=1)
|
|
446
|
+
cal_inicio.set_date(start_date)
|
|
447
|
+
|
|
448
|
+
rep_years = int(years_var.get())
|
|
449
|
+
end_date = suggested_end_date(start_date, rep_years)
|
|
450
|
+
cal_final.set_date(end_date)
|
|
451
|
+
|
|
452
|
+
if start_date > end_date:
|
|
453
|
+
messagebox.showerror("Error", "La fecha de inicio no puede ser posterior a la fecha de fin.")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
ep = Epoch(
|
|
457
|
+
name=name,
|
|
458
|
+
start_date=start_date,
|
|
459
|
+
end_date=end_date,
|
|
460
|
+
rep_years=rep_years,
|
|
461
|
+
rep_months=int(months_var.get()),
|
|
462
|
+
rep_days=int(days_var.get()),
|
|
463
|
+
settings=get_current_epoch_settings(),
|
|
464
|
+
)
|
|
465
|
+
epochs.append(ep)
|
|
466
|
+
|
|
467
|
+
if len(epochs) == 1:
|
|
468
|
+
global_scenario_combo.config(state="disabled")
|
|
469
|
+
_update_global_lock_label(locked=True)
|
|
470
|
+
|
|
471
|
+
next_date = end_date + timedelta(days=1)
|
|
472
|
+
cal_inicio.set_date(next_date)
|
|
473
|
+
_next_yrs = 5 if len(epochs) >= 1 else 1
|
|
474
|
+
_next_mos = 6 if len(epochs) >= 1 else 1
|
|
475
|
+
cal_final.set_date(suggested_end_date(next_date, _next_yrs))
|
|
476
|
+
years_var.set(str(_next_yrs))
|
|
477
|
+
months_var.set(str(_next_mos))
|
|
478
|
+
days_var.set("1")
|
|
479
|
+
suggest_epoch_name()
|
|
480
|
+
update_epoch_listbox()
|
|
481
|
+
|
|
482
|
+
lbl_epochs_list = tk.Label(frame_temporal, text="Added epochs:", bg=SBG, fg=CYAN,
|
|
483
|
+
font=("Verdana", 9, "bold"))
|
|
484
|
+
lbl_epochs_list.grid(row=6, column=0, columnspan=2, padx=6, pady=(6, 2), sticky="w")
|
|
485
|
+
|
|
486
|
+
epoch_listbox = tk.Listbox(
|
|
487
|
+
frame_temporal, width=52, height=4,
|
|
488
|
+
bg="#061260", fg=WHITE, font=("Verdana", 8),
|
|
489
|
+
selectbackground=BASE_COLORS["cyan"], selectforeground=DARK,
|
|
490
|
+
relief="flat", bd=2
|
|
491
|
+
)
|
|
492
|
+
epoch_listbox.grid(row=7, column=0, columnspan=2, padx=6, pady=(0, 6), sticky="ew")
|
|
493
|
+
|
|
494
|
+
def update_epoch_listbox():
|
|
495
|
+
epoch_listbox.delete(0, tk.END)
|
|
496
|
+
for i, ep in enumerate(epochs, 1):
|
|
497
|
+
epoch_listbox.insert(
|
|
498
|
+
tk.END,
|
|
499
|
+
f"{i}. {ep.name} | {ep.start_date.strftime('%Y-%m-%d')} - "
|
|
500
|
+
f"{ep.end_date.strftime('%Y-%m-%d')} | "
|
|
501
|
+
f"{ep.rep_years} rep.yrs | {ep.rep_months} rep.months | {ep.rep_days} rep.days"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def remove_epoch():
|
|
505
|
+
sel = epoch_listbox.curselection()
|
|
506
|
+
if not sel:
|
|
507
|
+
messagebox.showwarning("Aviso", "Seleccione una época para eliminar.")
|
|
508
|
+
return
|
|
509
|
+
idx = sel[0]
|
|
510
|
+
epochs.pop(idx)
|
|
511
|
+
update_epoch_listbox()
|
|
512
|
+
suggest_epoch_name()
|
|
513
|
+
if epochs:
|
|
514
|
+
next_date = epochs[-1].end_date + timedelta(days=1)
|
|
515
|
+
cal_inicio.set_date(next_date)
|
|
516
|
+
cal_final.set_date(next_date)
|
|
517
|
+
else:
|
|
518
|
+
global_scenario_combo.config(state="readonly")
|
|
519
|
+
_update_global_lock_label(locked=False)
|
|
520
|
+
cal_inicio.set_date("2025-01-01")
|
|
521
|
+
cal_final.set_date(suggested_end_date(cal_inicio.get_date(), int(years_var.get())))
|
|
522
|
+
|
|
523
|
+
btns_epoch = tk.Frame(frame_temporal, bg=SBG)
|
|
524
|
+
btns_epoch.grid(row=8, column=0, columnspan=2, pady=(4, 4))
|
|
525
|
+
|
|
526
|
+
add_epoch_btn = tk.Button(
|
|
527
|
+
btns_epoch, text="Add Epoch", command=add_epoch,
|
|
528
|
+
bg=ORANGE, fg=DARK, font=("Verdana", 9, "bold"),
|
|
529
|
+
relief="flat", padx=8, pady=4,
|
|
530
|
+
activebackground=BASE_COLORS["green"], activeforeground=DARK,
|
|
531
|
+
cursor="hand2"
|
|
532
|
+
)
|
|
533
|
+
add_epoch_btn.grid(row=0, column=0, padx=(0, 6))
|
|
534
|
+
|
|
535
|
+
remove_epoch_btn = tk.Button(
|
|
536
|
+
btns_epoch, text="Remove Selected", command=remove_epoch,
|
|
537
|
+
bg=BASE_COLORS["green"], fg=DARK, font=("Verdana", 8, "bold"),
|
|
538
|
+
relief="flat", padx=6, pady=3,
|
|
539
|
+
activebackground=BASE_COLORS["mint"], activeforeground=DARK,
|
|
540
|
+
cursor="hand2"
|
|
541
|
+
)
|
|
542
|
+
remove_epoch_btn.grid(row=0, column=1, padx=(6, 0))
|
|
543
|
+
|
|
544
|
+
frame_scenario = ttk.LabelFrame(mid_col, text="Configuracion de escenario",
|
|
545
|
+
style="Section.TLabelframe", padding=(12, 8))
|
|
546
|
+
frame_scenario.pack(fill="x", pady=(0, 10))
|
|
547
|
+
|
|
548
|
+
global_scenario_var = tk.StringVar(value=PRESET_NAMES[0])
|
|
549
|
+
|
|
550
|
+
lbl_global = tk.Label(frame_scenario, text="Global scenario:", bg=SBG, fg=CYAN,
|
|
551
|
+
font=("Verdana", 9, "bold"))
|
|
552
|
+
lbl_global.grid(row=0, column=0, padx=(6, 8), pady=(6, 2), sticky="e")
|
|
553
|
+
|
|
554
|
+
global_scenario_combo = ttk.Combobox(
|
|
555
|
+
frame_scenario, textvariable=global_scenario_var,
|
|
556
|
+
values=ALL_GLOBAL_NAMES, state="readonly", width=32
|
|
557
|
+
)
|
|
558
|
+
global_scenario_combo.grid(row=0, column=1, padx=(0, 6), pady=(6, 2), sticky="w")
|
|
559
|
+
|
|
560
|
+
lbl_global_lock = tk.Label(frame_scenario, text="", bg=SBG, fg=BASE_COLORS["orange"],
|
|
561
|
+
font=("Verdana", 7, "italic"))
|
|
562
|
+
lbl_global_lock.grid(row=1, column=0, columnspan=2, padx=(6, 6), pady=(0, 2), sticky="e")
|
|
563
|
+
|
|
564
|
+
def _update_global_lock_label(locked: bool):
|
|
565
|
+
if locked:
|
|
566
|
+
lbl_global_lock.config(text="⚠ Locked after first epoch – add scenario to queue or remove all epochs to change")
|
|
567
|
+
else:
|
|
568
|
+
lbl_global_lock.config(text="")
|
|
569
|
+
|
|
570
|
+
custom_scenario_name_var = tk.StringVar(value="")
|
|
571
|
+
|
|
572
|
+
lbl_custom_name = tk.Label(frame_scenario, text="Custom name:", bg=SBG, fg=ORANGE,
|
|
573
|
+
font=("Verdana", 9, "italic"))
|
|
574
|
+
entry_custom_name = tk.Entry(frame_scenario, textvariable=custom_scenario_name_var,
|
|
575
|
+
bg=MINT, fg=DARK, font=("Verdana", 9), width=30,
|
|
576
|
+
relief="flat", bd=2)
|
|
577
|
+
|
|
578
|
+
sep = tk.Frame(frame_scenario, bg=BASE_COLORS["section_border"], height=1)
|
|
579
|
+
sep.grid(row=3, column=0, columnspan=2, sticky="ew", padx=6, pady=(4, 4))
|
|
580
|
+
|
|
581
|
+
demand_scenario = tk.StringVar()
|
|
582
|
+
hydrology_scenario = tk.StringVar()
|
|
583
|
+
climate_scenario = tk.StringVar()
|
|
584
|
+
generation_investment_cost_scenario = tk.StringVar()
|
|
585
|
+
storage_investment_cost_scenario = tk.StringVar()
|
|
586
|
+
fuel_price_scenario = tk.StringVar()
|
|
587
|
+
|
|
588
|
+
demand_combo = ttk.Combobox(frame_scenario, textvariable=demand_scenario,
|
|
589
|
+
values=scenario_names["demand"], state="disabled", width=28)
|
|
590
|
+
add_row_in(frame_scenario, "Demand scenario", demand_combo, 4, SBG)
|
|
591
|
+
|
|
592
|
+
hydrology_combo = ttk.Combobox(frame_scenario, textvariable=hydrology_scenario,
|
|
593
|
+
values=scenario_names["hydrology"], state="disabled", width=28)
|
|
594
|
+
add_row_in(frame_scenario, "Hydrology scenario", hydrology_combo, 5, SBG)
|
|
595
|
+
|
|
596
|
+
climate_combo = ttk.Combobox(frame_scenario, textvariable=climate_scenario,
|
|
597
|
+
values=scenario_names["climate"], state="disabled", width=28)
|
|
598
|
+
add_row_in(frame_scenario, "Climate scenario", climate_combo, 6, SBG)
|
|
599
|
+
|
|
600
|
+
gen_cost_combo = ttk.Combobox(frame_scenario, textvariable=generation_investment_cost_scenario,
|
|
601
|
+
values=scenario_names["generation_investment_cost"], state="disabled", width=28)
|
|
602
|
+
add_row_in(frame_scenario, "Generation investment cost", gen_cost_combo, 7, SBG)
|
|
603
|
+
|
|
604
|
+
sto_cost_combo = ttk.Combobox(frame_scenario, textvariable=storage_investment_cost_scenario,
|
|
605
|
+
values=scenario_names["storage_investment_cost"], state="disabled", width=28)
|
|
606
|
+
add_row_in(frame_scenario, "Storage investment cost", sto_cost_combo, 8, SBG)
|
|
607
|
+
|
|
608
|
+
fuel_combo = ttk.Combobox(frame_scenario, textvariable=fuel_price_scenario,
|
|
609
|
+
values=scenario_names["fuel_price"], state="disabled", width=28)
|
|
610
|
+
add_row_in(frame_scenario, "Fuel price scenario", fuel_combo, 9, SBG)
|
|
611
|
+
|
|
612
|
+
_component_combos = [demand_combo, hydrology_combo, climate_combo,
|
|
613
|
+
gen_cost_combo, sto_cost_combo, fuel_combo]
|
|
614
|
+
|
|
615
|
+
def on_global_scenario_change(event=None):
|
|
616
|
+
sel = global_scenario_var.get()
|
|
617
|
+
if sel == 'Otro':
|
|
618
|
+
lbl_custom_name.grid(row=2, column=0, padx=(6, 8), pady=(2, 2), sticky="e")
|
|
619
|
+
entry_custom_name.grid(row=2, column=1, padx=(0, 6), pady=(2, 2), sticky="w")
|
|
620
|
+
for cb in _component_combos:
|
|
621
|
+
cb.config(state="readonly")
|
|
622
|
+
else:
|
|
623
|
+
lbl_custom_name.grid_remove()
|
|
624
|
+
entry_custom_name.grid_remove()
|
|
625
|
+
vals = scenario_map[sel]
|
|
626
|
+
demand_scenario.set(vals['demand'])
|
|
627
|
+
climate_scenario.set(vals['climate'])
|
|
628
|
+
hydrology_scenario.set(vals['hydrology'])
|
|
629
|
+
generation_investment_cost_scenario.set(vals['generation_investment_cost'])
|
|
630
|
+
storage_investment_cost_scenario.set(vals['storage_investment_cost'])
|
|
631
|
+
fuel_price_scenario.set(vals['fuel_price'])
|
|
632
|
+
for cb in _component_combos:
|
|
633
|
+
cb.config(state="disabled")
|
|
634
|
+
|
|
635
|
+
global_scenario_combo.bind("<<ComboboxSelected>>", on_global_scenario_change)
|
|
636
|
+
|
|
637
|
+
on_global_scenario_change()
|
|
638
|
+
|
|
639
|
+
frame_queue = ttk.LabelFrame(mid_col, text="Cola de escenarios a ejecutar",
|
|
640
|
+
style="Section.TLabelframe", padding=(12, 8))
|
|
641
|
+
frame_queue.pack(fill="x", pady=(0, 10))
|
|
642
|
+
|
|
643
|
+
lbl_scenarios_list = tk.Label(frame_queue, text="Queued scenarios:", bg=SBG, fg=CYAN,
|
|
644
|
+
font=("Verdana", 9, "bold"))
|
|
645
|
+
lbl_scenarios_list.grid(row=0, column=0, columnspan=2, padx=6, pady=(4, 2), sticky="w")
|
|
646
|
+
|
|
647
|
+
scenario_listbox = tk.Listbox(
|
|
648
|
+
frame_queue, width=52, height=4,
|
|
649
|
+
bg="#061260", fg=WHITE, font=("Verdana", 8),
|
|
650
|
+
selectbackground=BASE_COLORS["cyan"], selectforeground=DARK,
|
|
651
|
+
relief="flat", bd=2
|
|
652
|
+
)
|
|
653
|
+
scenario_listbox.grid(row=1, column=0, columnspan=2, padx=6, pady=(0, 6), sticky="ew")
|
|
654
|
+
|
|
655
|
+
def update_scenario_listbox():
|
|
656
|
+
scenario_listbox.delete(0, tk.END)
|
|
657
|
+
for i, sc in enumerate(scenarios, 1):
|
|
658
|
+
n_ep = len(sc["epochs"])
|
|
659
|
+
first = sc["epochs"][0]["start_date"]
|
|
660
|
+
last = sc["epochs"][-1]["end_date"]
|
|
661
|
+
scenario_listbox.insert(
|
|
662
|
+
tk.END,
|
|
663
|
+
f"{i}. {sc['global_scenario']} | {n_ep} epoch(s) | {first} - {last}"
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def remove_scenario():
|
|
667
|
+
sel = scenario_listbox.curselection()
|
|
668
|
+
if not sel:
|
|
669
|
+
messagebox.showwarning("Aviso", "Seleccione un escenario para eliminar de la cola.")
|
|
670
|
+
return
|
|
671
|
+
scenarios.pop(sel[0])
|
|
672
|
+
update_scenario_listbox()
|
|
673
|
+
|
|
674
|
+
btns_queue = tk.Frame(frame_queue, bg=SBG)
|
|
675
|
+
btns_queue.grid(row=2, column=0, columnspan=2, pady=(4, 4))
|
|
676
|
+
|
|
677
|
+
add_scenario_btn = tk.Button(
|
|
678
|
+
btns_queue, text="Add scenario to queue", command=add_scenario_to_queue,
|
|
679
|
+
bg=ORANGE, fg=DARK, font=("Verdana", 9, "bold"),
|
|
680
|
+
relief="flat", padx=8, pady=4,
|
|
681
|
+
activebackground=BASE_COLORS["green"], activeforeground=DARK,
|
|
682
|
+
cursor="hand2"
|
|
683
|
+
)
|
|
684
|
+
add_scenario_btn.grid(row=0, column=0, padx=(0, 6))
|
|
685
|
+
|
|
686
|
+
remove_scenario_btn = tk.Button(
|
|
687
|
+
btns_queue, text="Remove Selected", command=remove_scenario,
|
|
688
|
+
bg=BASE_COLORS["green"], fg=DARK, font=("Verdana", 8, "bold"),
|
|
689
|
+
relief="flat", padx=6, pady=3,
|
|
690
|
+
activebackground=BASE_COLORS["mint"], activeforeground=DARK,
|
|
691
|
+
cursor="hand2"
|
|
692
|
+
)
|
|
693
|
+
remove_scenario_btn.grid(row=0, column=1, padx=(6, 0))
|
|
694
|
+
|
|
695
|
+
right_col = tk.Frame(inner, bg=color("fondo_principal"))
|
|
696
|
+
right_col.grid(row=1, column=2, padx=(10, 0), sticky="n")
|
|
697
|
+
|
|
698
|
+
frame_restricciones = ttk.LabelFrame(left_col, text="Restricciones adicionales",
|
|
699
|
+
style="Section.TLabelframe", padding=(14, 10))
|
|
700
|
+
frame_restricciones.pack(fill="x", pady=(0, 10))
|
|
701
|
+
|
|
702
|
+
def make_compact_constraint(parent, text, var, row, fields):
|
|
703
|
+
cb = tk.Checkbutton(
|
|
704
|
+
parent, text=text, variable=var,
|
|
705
|
+
bg=SBG, fg=WHITE, selectcolor=DARK,
|
|
706
|
+
activebackground=SBG, activeforeground=CYAN,
|
|
707
|
+
font=("Verdana", 10, "bold"), anchor="w",
|
|
708
|
+
wraplength=185, justify="left",
|
|
709
|
+
)
|
|
710
|
+
cb.grid(row=row, column=0, padx=(8, 8), pady=(10, 6), sticky="w")
|
|
711
|
+
|
|
712
|
+
fields_frame = tk.Frame(parent, bg=SBG)
|
|
713
|
+
fields_frame.grid(row=row, column=1, padx=(0, 8), pady=(10, 6), sticky="e")
|
|
714
|
+
|
|
715
|
+
vars_out = []
|
|
716
|
+
for i, (label, default) in enumerate(fields):
|
|
717
|
+
lbl = tk.Label(fields_frame, text=label, bg=SBG, fg=MINT, font=("Verdana", 9))
|
|
718
|
+
lbl.grid(row=i, column=0, padx=(0, 4), pady=2, sticky="e")
|
|
719
|
+
var_field = tk.StringVar(value=str(default))
|
|
720
|
+
ent = tk.Entry(fields_frame, textvariable=var_field, bg=MINT, fg=DARK,
|
|
721
|
+
font=("Verdana", 9), width=9, relief="flat", bd=2)
|
|
722
|
+
ent.grid(row=i, column=1, padx=(0, 0), pady=2, sticky="w")
|
|
723
|
+
vars_out.append(var_field)
|
|
724
|
+
return vars_out
|
|
725
|
+
|
|
726
|
+
def make_separator(parent, row):
|
|
727
|
+
sep2 = tk.Frame(parent, bg=BASE_COLORS["section_border"], height=1)
|
|
728
|
+
sep2.grid(row=row, column=0, columnspan=2, sticky="ew", padx=8, pady=4)
|
|
729
|
+
|
|
730
|
+
frame_restricciones.grid_columnconfigure(0, weight=1)
|
|
731
|
+
frame_restricciones.grid_columnconfigure(1, weight=0)
|
|
732
|
+
|
|
733
|
+
r = 0
|
|
734
|
+
# Restriccion de emisiones maximas deshabilitada.
|
|
735
|
+
# cb_emissions = tk.BooleanVar(value=False)
|
|
736
|
+
# (entry_emissions,) = make_compact_constraint(
|
|
737
|
+
# frame_restricciones,
|
|
738
|
+
# "Maximum emissions [MtCO2/year]",
|
|
739
|
+
# cb_emissions, r, [("MtCO2/year:", 30)]
|
|
740
|
+
# )
|
|
741
|
+
# r += 1
|
|
742
|
+
# make_separator(frame_restricciones, r); r += 1
|
|
743
|
+
|
|
744
|
+
cb_load_lost = tk.BooleanVar(value=False)
|
|
745
|
+
(entry_load_lost,) = make_compact_constraint(
|
|
746
|
+
frame_restricciones,
|
|
747
|
+
"Maximum load lost [MWh/year]",
|
|
748
|
+
cb_load_lost, r, [("MWh/year:", 2000)]
|
|
749
|
+
)
|
|
750
|
+
r += 1
|
|
751
|
+
make_separator(frame_restricciones, r); r += 1
|
|
752
|
+
|
|
753
|
+
cb_discrete_transmission = tk.BooleanVar(value=False)
|
|
754
|
+
cb_dt = tk.Checkbutton(
|
|
755
|
+
frame_restricciones,
|
|
756
|
+
text="Discrete transmission expansion",
|
|
757
|
+
variable=cb_discrete_transmission,
|
|
758
|
+
bg=SBG, fg=WHITE, selectcolor=DARK,
|
|
759
|
+
activebackground=SBG, activeforeground=CYAN,
|
|
760
|
+
font=("Verdana", 10, "bold"),
|
|
761
|
+
anchor="w", wraplength=185, justify="left",
|
|
762
|
+
)
|
|
763
|
+
cb_dt.grid(row=r, column=0, columnspan=2, padx=8, pady=(10, 6), sticky="w")
|
|
764
|
+
|
|
765
|
+
frame_optimizer = ttk.LabelFrame(left_col, text="Configuracion de optimizacion",
|
|
766
|
+
style="Section.TLabelframe", padding=(12, 8))
|
|
767
|
+
frame_optimizer.pack(fill="x", pady=(0, 10))
|
|
768
|
+
|
|
769
|
+
solver_name = tk.StringVar(value="highs")
|
|
770
|
+
solver_name_combo = ttk.Combobox(frame_optimizer, textvariable=solver_name,
|
|
771
|
+
values=["cbc", "highs", "gurobi", "cplex"],
|
|
772
|
+
state="readonly", width=12)
|
|
773
|
+
add_row_in(frame_optimizer, "Solver name", solver_name_combo, 0, SBG)
|
|
774
|
+
|
|
775
|
+
entry_threads = add_entry_row(frame_optimizer, "Threads", os.cpu_count(), 1, width=8)
|
|
776
|
+
entry_mip_gap = add_entry_row(frame_optimizer, "MIP gap", 0.1, 2, width=8)
|
|
777
|
+
|
|
778
|
+
linearized_uc_var = tk.BooleanVar(value=True)
|
|
779
|
+
cb_lin_uc = tk.Checkbutton(
|
|
780
|
+
frame_optimizer, text="Linearized unit commitment",
|
|
781
|
+
variable=linearized_uc_var,
|
|
782
|
+
bg=SBG, fg=WHITE, selectcolor=DARK,
|
|
783
|
+
activebackground=SBG, activeforeground=CYAN,
|
|
784
|
+
font=("Verdana", 9), anchor="w",
|
|
785
|
+
)
|
|
786
|
+
cb_lin_uc.grid(row=3, column=0, columnspan=2, padx=(6, 6), pady=(4, 6), sticky="w")
|
|
787
|
+
|
|
788
|
+
frame_exec = ttk.LabelFrame(mid_col, text="Configuracion de ejecucion",
|
|
789
|
+
style="Section.TLabelframe", padding=(12, 8))
|
|
790
|
+
frame_exec.pack(fill="x", pady=(0, 10))
|
|
791
|
+
|
|
792
|
+
execution_mode_var = tk.StringVar(value="Secuencial")
|
|
793
|
+
execution_mode_combo = ttk.Combobox(frame_exec, textvariable=execution_mode_var,
|
|
794
|
+
values=["Secuencial", "Paralelo"],
|
|
795
|
+
state="readonly", width=12)
|
|
796
|
+
add_row_in(frame_exec, "Execution mode", execution_mode_combo, 0, SBG)
|
|
797
|
+
|
|
798
|
+
entry_max_workers = add_entry_row(frame_exec, "Max parallel scenarios", 2, 1, width=8)
|
|
799
|
+
entry_ram_limit = add_entry_row(frame_exec, "RAM limit [% of total]", 85, 2, width=8)
|
|
800
|
+
|
|
801
|
+
lbl_exec_note = tk.Label(
|
|
802
|
+
frame_exec,
|
|
803
|
+
text=("Paralelo: cada escenario corre en un proceso propio y los CSVs\n"
|
|
804
|
+
"se fusionan en orden al final. El siguiente escenario no se lanza\n"
|
|
805
|
+
"si la RAM supera el límite. Ajuste Threads del solver para no\n"
|
|
806
|
+
"sobresuscribir la CPU (ej: nucleos / escenarios simultaneos)."),
|
|
807
|
+
bg=SBG, fg=MINT, font=("Verdana", 7, "italic"), justify="left",
|
|
808
|
+
)
|
|
809
|
+
lbl_exec_note.grid(row=3, column=0, columnspan=2, padx=6, pady=(4, 2), sticky="w")
|
|
810
|
+
|
|
811
|
+
confirm_btn = tk.Button(
|
|
812
|
+
mid_col,
|
|
813
|
+
text="Run all scenarios",
|
|
814
|
+
command=confirmar,
|
|
815
|
+
bg=ORANGE, fg=DARK,
|
|
816
|
+
font=("Verdana", 10, "bold"),
|
|
817
|
+
relief="flat", bd=0,
|
|
818
|
+
padx=20, pady=10,
|
|
819
|
+
activebackground=BASE_COLORS["green"],
|
|
820
|
+
activeforeground=DARK,
|
|
821
|
+
cursor="hand2",
|
|
822
|
+
width=30
|
|
823
|
+
)
|
|
824
|
+
confirm_btn.pack(fill="x", pady=(10, 0))
|
|
825
|
+
|
|
826
|
+
_net_path = os.path.join(_data_dir, "sen_network.png")
|
|
827
|
+
if not os.path.exists(_net_path):
|
|
828
|
+
_net_path = os.path.join(_src_dir, "sen_network.png")
|
|
829
|
+
if os.path.exists(_net_path):
|
|
830
|
+
try:
|
|
831
|
+
_net_raw = Image.open(_net_path).convert("RGBA").rotate(-90, expand=True)
|
|
832
|
+
_net_h = 650
|
|
833
|
+
_net_w = int(_net_h * _net_raw.width / _net_raw.height)
|
|
834
|
+
_net_res = _net_raw.resize((_net_w, _net_h), Image.LANCZOS)
|
|
835
|
+
net_photo = ImageTk.PhotoImage(_net_res)
|
|
836
|
+
net_label = tk.Label(right_col, image=net_photo, bg=color("fondo_principal"))
|
|
837
|
+
net_label.image = net_photo
|
|
838
|
+
net_label.pack(fill="both", expand=True)
|
|
839
|
+
except Exception as e:
|
|
840
|
+
print(f"No se pudo cargar el mapa del SEN: {e}")
|
|
841
|
+
|
|
842
|
+
root.after(0, lambda: root.state("zoomed"))
|
|
843
|
+
root.mainloop()
|
|
844
|
+
return result
|
|
845
|
+
|
|
846
|
+
if __name__ == "__main__":
|
|
847
|
+
r = run_gui()
|
|
848
|
+
print(r)
|