1
0

12 Commity fb492128d2 ... 1966c98c95

Autor SHA1 Správa Dátum
  oscarleiva 1966c98c95 Ultimas modificaciones oct-2020 5 rokov pred
  oscarleiva 3d2a92f19a APPDATA Folder structure check and creation 5 rokov pred
  oscarleiva f6c0ad2d46 Add Splash on app start 5 rokov pred
  oscarleiva 0c860ba044 Structure Refactor 5 rokov pred
  oscarleiva 391f8f8f32 Implement solver check 5 rokov pred
  oscarleiva ddefd3edc7 implement check Excel file and clean log console 5 rokov pred
  oscarleiva 46ccda6bdc creat helper function for check excel file 5 rokov pred
  oscarleiva c13845c773 Add icons to img folder 5 rokov pred
  oscarleiva 6038197d18 Fix format and delete unused code 5 rokov pred
  oscarleiva e05495d31e Fix Format 5 rokov pred
  oscarleiva 6ec2471fe6 Fix format 5 rokov pred
  oscarleiva 2d3bf72a1e Arreglos en layout y logs 5 rokov pred
44 zmenil súbory, kde vykonal 850 pridanie a 557 odobranie
  1. 2 0
      .gitignore
  2. 26 0
      common/data.py
  3. 27 0
      common/exceptions.py
  4. 13 0
      common/log.py
  5. BIN
      img/icon.ico
  6. BIN
      img/icons/icons8-abrir-carpeta-16.png
  7. BIN
      img/icons/icons8-apoyo-16.png
  8. BIN
      img/icons/icons8-archivo-16.png
  9. BIN
      img/icons/icons8-bloquear-16.png
  10. BIN
      img/icons/icons8-cancelar-16.png
  11. BIN
      img/icons/icons8-cancelar-2-16.png
  12. BIN
      img/icons/icons8-carpeta-16.png
  13. BIN
      img/icons/icons8-casa-16.png
  14. BIN
      img/icons/icons8-comprobado-16.png
  15. BIN
      img/icons/icons8-desbloquear-16.png
  16. BIN
      img/icons/icons8-documento-16.png
  17. BIN
      img/icons/icons8-eliminar-16-2.png
  18. BIN
      img/icons/icons8-eliminar-16.png
  19. BIN
      img/icons/icons8-información-16.png
  20. BIN
      img/icons/icons8-play-16.png
  21. BIN
      img/icons/icons8-prismáticos-16.png
  22. BIN
      img/icons/icons8-servicios-16.png
  23. BIN
      img/splash.png
  24. 5 282
      main.py
  25. 7 7
      simsdt/mct/makeMCT.py
  26. 49 66
      simsdt/model.py
  27. 84 83
      simsdt/ofertas/readBids.py
  28. 7 3
      simsdt/red/create.py
  29. 20 14
      simsdt/red/makeBdc.py
  30. 1 0
      simsdt/red/makePTDF.py
  31. 3 9
      simsdt/red/read.py
  32. 9 42
      simsdt/runsdt.py
  33. 30 0
      simsdt/runtrhead.py
  34. 1 1
      simsdt/utils/arr2dict.py
  35. 27 0
      simsdt/utils/excel_checker.py
  36. 7 7
      simsdt/utils/idx_brch.py
  37. 49 43
      simsdt/utils/map.py
  38. 11 0
      simsdt/utils/solver_checker.py
  39. 129 0
      ui/app.py
  40. 69 0
      ui/console.py
  41. 86 0
      ui/forms.py
  42. 107 0
      ui/results.py
  43. 44 0
      ui/splash.py
  44. 37 0
      ui/status.py

+ 2 - 0
.gitignore

@@ -53,6 +53,7 @@ coverage.xml
 
 # Django stuff:
 *.log
+*.log.*
 local_settings.py
 db.sqlite3
 
@@ -138,5 +139,6 @@ venv.bak/
 
 # User
 *.xls
+*.xlsx
 *.csv
 results.json

+ 26 - 0
common/data.py

@@ -0,0 +1,26 @@
+'''
+    Application Data functions
+'''
+
+import os
+
+__APPFOLDER = os.path.expandvars(r'%LOCALAPPDATA%\Merelec\Simsdt')
+__CACHE = f'{__APPFOLDER}\cache'
+__DATA = f'{__APPFOLDER}\data'
+__CONFIG = f'{__APPFOLDER}\config'
+
+APPDIRS = {
+    'APPFOLDER': __APPFOLDER,
+    'CACHE': __CACHE,
+    'DATA': __DATA,
+    'CONFIG': __CONFIG
+}
+
+
+def check_appdata_folders():
+    [create_dir(v) for (k, v) in APPDIRS.items()]
+
+
+def create_dir(directory):
+    if not os.path.exists(directory):
+        os.makedirs(directory)

+ 27 - 0
common/exceptions.py

@@ -0,0 +1,27 @@
+class ExcelFileError(Exception):
+    """Exception raised for error in the input Excel File.
+
+    Attributes:
+        file -- input Excel file
+    """
+
+    def __init__(self, file, message="El archivo no es valido"):
+        self.file = file
+        self.message = message
+        super().__init__(self.message)
+
+    def __str__(self):
+        return f'{self.file} -> {self.message}'
+
+
+class SolverNotFoundError(Exception):
+    """Exception raised if solver not found.
+
+    """
+
+    def __init__(self, message="No se encontró el solver en el PATH"):
+        self.message = message
+        super().__init__(self.message)
+
+    def __str__(self):
+        return self.message

+ 13 - 0
common/log.py

@@ -1,3 +1,16 @@
 #
 # Utility classes for working with the logger
 #
+import logging
+from logging.handlers import RotatingFileHandler
+
+_fh = RotatingFileHandler(
+    'log/simsdt_app_monitor.log', maxBytes=1024*50, backupCount=10, encoding='utf-8')
+_fmt = logging.Formatter(
+    '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
+_fh.setFormatter(_fmt)
+_fh.setLevel(logging.INFO)
+
+logger = logging.getLogger('simsdt')
+logger.addHandler(_fh)
+logger.setLevel(logging.INFO)

BIN
img/icon.ico


BIN
img/icons/icons8-abrir-carpeta-16.png


BIN
img/icons/icons8-apoyo-16.png


BIN
img/icons/icons8-archivo-16.png


BIN
img/icons/icons8-bloquear-16.png


BIN
img/icons/icons8-cancelar-16.png


BIN
img/icons/icons8-cancelar-2-16.png


BIN
img/icons/icons8-carpeta-16.png


BIN
img/icons/icons8-casa-16.png


BIN
img/icons/icons8-comprobado-16.png


BIN
img/icons/icons8-desbloquear-16.png


BIN
img/icons/icons8-documento-16.png


BIN
img/icons/icons8-eliminar-16-2.png


BIN
img/icons/icons8-eliminar-16.png


BIN
img/icons/icons8-información-16.png


BIN
img/icons/icons8-play-16.png


BIN
img/icons/icons8-prismáticos-16.png


BIN
img/icons/icons8-servicios-16.png


BIN
img/splash.png


+ 5 - 282
main.py

@@ -1,290 +1,13 @@
-import datetime
-import logging
-import queue
-import signal
-import threading
-import time
-import tkinter as tk
-from logging.handlers import RotatingFileHandler
-from tkinter import (HORIZONTAL, VERTICAL, E, N, S, W, filedialog, messagebox,
-                     ttk)
-from tkinter.scrolledtext import ScrolledText
-
-from PIL import Image, ImageTk
-
-from qhandler import QueueHandler
-from simsdt import runsdt
-
-
-fh = RotatingFileHandler(
-    'log/simsdt_app_monitor.log', maxBytes=1024*50, backupCount=10)
-fmt = logging.Formatter(
-    '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-fh.setFormatter(fmt)
-fh.setLevel(logging.INFO)
-# logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
-#                     datefmt='%Y-%m-%dT%H:%M:%S',
-#                     handlers=[RotatingFileHandler(
-#                         'log/simsdt_app_monitor.log', maxBytes=10240, backupCount=10)],
-#                     level=logging.INFO)
-
-main_logger = logging.getLogger("simsdt")
-main_logger.addHandler(fh)
-main_logger.setLevel(logging.DEBUG)
-
-
-class RunsdtThread(threading.Thread):
-    def __init__(self, group=None, target=None, name=None,
-                 args=(), kwargs=None, *, daemon=None):
-        super(RunsdtThread, self).__init__(group=group,
-                                           target=target, name=name, daemon=daemon)
-        self._stop_event = threading.Event()
-        self.args = args
-        self.kwargs = kwargs
-        return
-
-    def run(self):
-        now = datetime.datetime.now()
-        runsdt.main(self.args)
-        # time.sleep(30)
-        main_logger.info('Fin de la función run')
-
-    def stop(self):
-        self._stop_event.set()
-
-
-class Clock(threading.Thread):
-    """Class to display the time every seconds
-
-    Every 5 seconds, the time is displayed using the logging.ERROR level
-    to show that different colors are associated to the log levels
-    """
-
-    def __init__(self):
-        super().__init__()
-        self._stop_event = threading.Event()
-
-    def run(self):
-        # main_logger.setLevel(logging.INFO)
-        main_logger.debug('Clock started')
-        previous = -1
-        while not self._stop_event.is_set():
-            now = datetime.datetime.now()
-            if previous != now.second:
-                previous = now.second
-                if now.second % 5 == 0:
-                    level = logging.ERROR
-                else:
-                    level = logging.INFO
-                main_logger.log(level, now)
-            time.sleep(0.2)
-
-    def stop(self):
-        self._stop_event.set()
-
-
-class ConsoleUi:
-    """Poll messages from a logging queue and display them in a scrolled text widget"""
-
-    def __init__(self, frame):
-        self.frame = frame
-        # Create a ScrolledText wdiget
-        self.scrolled_text = ScrolledText(frame, state='disabled', height=25)
-        self.scrolled_text.grid(row=0, column=0, sticky=(N, S, W, E))
-        # Font config
-        self.scrolled_text.configure(font='TkFixedFont')
-        self.scrolled_text.tag_config('INFO', foreground='black')
-        self.scrolled_text.tag_config('DEBUG', foreground='gray')
-        self.scrolled_text.tag_config('WARNING', foreground='orange')
-        self.scrolled_text.tag_config('ERROR', foreground='red')
-        self.scrolled_text.tag_config(
-            'CRITICAL', foreground='red', underline=1)
-        # Create a logging handler using a queue
-        self.log_queue = queue.Queue()
-        self.queue_handler = QueueHandler(self.log_queue)
-        formatter = logging.Formatter(
-            '%(asctime)s: %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-        self.queue_handler.setFormatter(formatter)
-        # self.queue_handler.setLevel(logging.INFO)
-        main_logger.addHandler(self.queue_handler)
-        # Start polling messages from the queue
-        self.frame.after(100, self.poll_log_queue)
-
-    def display(self, record):
-        msg = self.queue_handler.format(record)
-        self.scrolled_text.configure(state='normal')
-        self.scrolled_text.insert(tk.END, msg + '\n', record.levelname)
-        self.scrolled_text.configure(state='disabled')
-        # Autoscroll to the bottom
-        self.scrolled_text.yview(tk.END)
-
-    def poll_log_queue(self):
-        # Check every 100ms if there is a new message in the queue to display
-        while True:
-            try:
-                record = self.log_queue.get(block=False)
-            except queue.Empty:
-                break
-            else:
-                self.display(record)
-        self.frame.after(100, self.poll_log_queue)
-
-
-class FormUi:
 
-    runsdt_thread = None
-
-    def __init__(self, frame, q):
-        self.frame = frame
-        self.q = q
-        image = Image.open('img/play.png')
-        self.photo = ImageTk.PhotoImage(image)
-
-        # Add a text field for file path
-        self.file_path = tk.StringVar()
-        ttk.Label(self.frame, text='Archivo:').grid(column=0, row=0, sticky=W)
-        ttk.Entry(self.frame, textvariable=self.file_path,
-                  width=50).grid(column=1, row=0, sticky=(W, E))
-        self.button = ttk.Button(
-            self.frame, text='...', command=self.open_file_dialog, width=3)
-        self.button.grid(column=2, row=0, sticky=W)
-        self.button_run = ttk.Button(self.frame, image=self.photo, compound="right", text='Ejecutar',
-                                     command=self.run)
-        self.button_run.grid(column=1, row=1, sticky=W)
-
-    def open_file_dialog(self):
-        # Open file dialog
-        file = filedialog.askopenfilename(
-            filetypes=[('Excel', '.xlsx')], title='Seleccione un archivo de subasta...')
-        main_logger.info(f'Archivo seleccionado: {file}')
-        self.file_path.set(file)
-
-    def run(self):
-        if not self.file_path.get():
-            main_logger.error('Debe seleccionar un archivo para ejecutarlo')
-            return
-
-        self.button_run.configure(state='disabled')
-        main_logger.info('Ejecutando')
-        # self.runsdt_thread = threading.Thread(
-        #     target=runsdt.main, args=[self.file_path.get()])
-        # self.runsdt_thread.start()
-        self.runsdt_thread = RunsdtThread(args=(self.file_path.get()))
-        self.runsdt_thread.start()
-
-        self.check_thread()
-
-    def check_thread(self):
-
-        if not self.runsdt_thread.is_alive():
-            self.button_run.configure(state='normal')
-            self.q.put(False)
-        else:
-            self.q.put(True)
-            self.frame.after(100, self.check_thread)
-
-
-class ThirdUi:
-
-    def __init__(self, frame, q):
-        self.q = q
-        self.frame = frame
-        self.pb_max = 100
-        # ttk.Label(self.frame, text='This is just an example of a third frame').grid(
-        #     column=0, row=1, sticky=W)
-        self.progress_bar = ttk.Progressbar(
-            self.frame, mode='determinate')
-        self.progress_bar.pack(expand=True, fill=tk.BOTH, side=tk.TOP)
-
-        self.frame.after(100, self.check_q)
-
-    def check_q(self):
-        # Check every 100ms if there is a new message in the queue to display
-        while True:
-            try:
-                record = self.q.get(block=False)
-            except queue.Empty:
-                break
-            else:
-                if record:
-                    step = 1
-                    self.progress_bar.step(step)
-                    if step >= self.pb_max:
-                        step = 1
-                    else:
-                        step += 1
-                else:
-                    self.progress_bar.config(value=0)
-        self.frame.after(100, self.check_q)
-
-        # max_size = 100
-        # print(self.q.qsize())
-        # while self.q.qsize():
-        #     try:
-        #         active_tread = self.q.get(0)
-        #         print(active_tread)
-        #         main_logger.info(active_tread)
-        #         if active_tread:
-        #             step = 1
-        #             self.progress_bar.step(step)
-        #             if step == max_size:
-        #                 step = 1
-        #             else:
-        #                 step += 1
-        #     except queue.Empty:
-        #         pass
-
-        #     self.frame.after(100, self.check_q)
-
-
-class App:
-
-    def __init__(self, root):
-        self.root = root
-        pbqueue = queue.Queue()
-
-        root.title('SimSDT')
-        root.iconbitmap("app.ico")
-        root.columnconfigure(0, weight=1)
-        root.rowconfigure(0, weight=1)
-        # Create the panes and frames
-        vertical_pane = ttk.PanedWindow(self.root, orient=VERTICAL)
-        vertical_pane.grid(row=0, column=0, sticky="nsew")
-        horizontal_pane = ttk.PanedWindow(vertical_pane, orient=HORIZONTAL)
-        vertical_pane.add(horizontal_pane)
-        form_frame = ttk.Labelframe(horizontal_pane, text="Configuracion")
-        form_frame.columnconfigure(1, weight=1)
-        horizontal_pane.add(form_frame, weight=1)
-        console_frame = ttk.Labelframe(horizontal_pane, text="Console")
-        console_frame.columnconfigure(0, weight=1)
-        console_frame.rowconfigure(0, weight=1)
-        horizontal_pane.add(console_frame, weight=1)
-        third_frame = ttk.Labelframe(vertical_pane, text="Estado")
-        vertical_pane.add(third_frame, weight=1)
-        # Initialize all frames
-        self.form = FormUi(form_frame, pbqueue)
-        self.console = ConsoleUi(console_frame)
-        self.third = ThirdUi(third_frame, pbqueue)
-        self.root.protocol('WM_DELETE_WINDOW', self.quit)
-        self.root.bind('<Control-q>', self.quit)
-        signal.signal(signal.SIGINT, self.quit)
-
-    def quit(self, *args):
-
-        msg = 'Se esta ejecutando un proceso de optimización\n\n¿Desea salir de la aplicación?'
+import tkinter as tk
 
-        if self.form.runsdt_thread and self.form.runsdt_thread.is_alive():
-            if messagebox.askokcancel("Salir de SimSDT", msg):
-                main_logger.info('Cerrando aplicacion SimSDT')
-                self.form.runsdt_thread.stop()
-                self.root.destroy()
-        else:
-            main_logger.info('Saliendo de la aplicacion')
-            self.root.destroy()
+from common import data, log
+from ui.app import App
 
 
 def main():
-    main_logger.info('SimSDT ha iniciado')
+    log.logger.info('SimSDT ha iniciado')
+    data.check_appdata_folders()
     root = tk.Tk()
     app = App(root)
     app.root.mainloop()

+ 7 - 7
simsdt/mct/makeMCT.py

@@ -3,7 +3,7 @@
 #  Copyright (C) 2018 Mercados Electricos de Centroamérica. All rights reserved
 # =============================================================================
 
-"""Crea limites de trasnferencia entre área de Control 
+"""Crea limites de trasnferencia entre área de Control
 """
 from numpy import argwhere
 from pandas import read_excel
@@ -31,13 +31,13 @@ def linmct(brnames):
     circuito o linea que copone la interconexión entre las áreas de control
     """
     lmct = {}
-    
+
     for item in __INTER:
         lmct[item] = []
         for i in __INTER[item]:
             l = int(argwhere(brnames == i))
             lmct[item].append(l)
-    
+
     return lmct
 
 
@@ -46,24 +46,24 @@ def readmct(file):
     Excel del caso a resolver.
     """
     mct = read_excel(file, 'mct', index_col=[0, 1])
-    
+
     return mct
 
 
 def set_dir_flujo():
     """Consruye un diccionario en el cual se define la dirección del circuito
     que compone la interconexión.
-    
+
     En el caso de  la interconexión entre Costa Rica y Panamá, las líneas se
     encuentran definidas en el sentido Sur-Norte, por lo que su dirección de
     flujo corresponde a -1
     """
     d = {}
-    
+
     for key in __INTER.keys():
         if key == 'CRCPAN':
             d[key] = -1
         else:
             d[key] = 1
-            
+
     return d

+ 49 - 66
simsdt/model.py

@@ -4,20 +4,19 @@ Modulo principal para ejecutar el modelo de subasta
 """
 
 import logging
-from logging.handlers import RotatingFileHandler
 from os import path
-import queue
 
 import pandas as pd
 import pyomo.environ as pe
 import pyutilib.subprocess.GlobalData
 from numpy import array, zeros
-from openpyxl import load_workbook
 from pyomo.environ import SolverFactory
-from pyomo.kernel import value
 
+from common.data import APPDIRS
 from simsdt.mct.makeMCT import linmct, readmct, set_dir_flujo
-from simsdt.ofertas.readBids import *
+from simsdt.ofertas.readBids import (readexistentes, readofertas, setT, setTE,
+                                     setVIT, setVITE, setVITEX, setVITX,
+                                     setVRT, setVRTE)
 from simsdt.red.create import branchnames, setbranch, setbus
 from simsdt.red.makeBdc import makeBdc
 from simsdt.red.makePTDF import makePTDF
@@ -25,36 +24,17 @@ from simsdt.red.read import excel2net
 from simsdt.utils.arr2dict import arr2dict
 from simsdt.utils.idx_brch import RT_MAX, RT_MIN, R
 
-from qhandler import QueueHandler
-
 pyutilib.subprocess.GlobalData.DEFINE_SIGNAL_HANDLERS_DEFAULT = False
 
 
-# log_queue = queue.Queue()
-# qh = QueueHandler(log_queue)
-# formatter = logging.Formatter(
-#     '%(asctime)s: %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-# qh.setFormatter(formatter)
-
-fh = RotatingFileHandler(
-    'log/simsdt_model_monitor.log', maxBytes=10240, backupCount=10)
-fmt = logging.Formatter(
-    '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-fh.setFormatter(fmt)
-# ch = logging.StreamHandler()
-# ch.setFormatter(fmt)
-
-# logging.basicConfig(handlers=[fh, ch, qh], level=logging.INFO)
 model_logger = logging.getLogger('simsdt.model')
-# model_logger.addHandler(qh)
-model_logger.addHandler(fh)
 
 
 class ModeloSubasta:
 
-    def __init__(self, file, solver_path):
+    def __init__(self, file, tee=True):
         self.file = file
-        self.solver_path = solver_path
+        self.tee = tee
 
     def setmodel(self):
         model_logger.info('Modelo de Subasta de Derechos Firmes')
@@ -198,13 +178,17 @@ class ModeloSubasta:
 
         # Lim. Superior flujos asociados a linea c
         def bfu_param(model, c):
-            return model.Bu[c] - sum(max(0, sum(model.He[c, n]*model.TE[n, o] for n in model.n)) for o in model.o)
+            return model.Bu[c]
+            - sum(max(0, sum(model.He[c, n]*model.TE[n, o]
+                             for n in model.n)) for o in model.o)
 
         model.BFu = pe.Param(model.c, initialize=bfu_param)
 
         # LIm. Inferior flujos asociados a linea c
         def bfl_param(model, c):
-            return model.Bl[c] - sum(max(0, -sum(model.He[c, n]*model.TE[n, o] for n in model.n)) for o in model.o)
+            return model.Bl[c]
+            - sum(max(0, -sum(model.He[c, n]*model.TE[n, o]
+                              for n in model.n)) for o in model.o)
 
         model.BFl = pe.Param(model.c, initialize=bfl_param)
 
@@ -241,7 +225,8 @@ class ModeloSubasta:
         model_logger.info("Ecuación de Función Objetivo Max Z")
 
         def eq_Z(model):
-            return sum(model.alpha_k[k]*model.C[k] - model.gamma_k[k]*model.Cper[k] for k in model.k)
+            return sum(model.alpha_k[k]*model.C[k]
+                       - model.gamma_k[k]*model.Cper[k] for k in model.k)
 
         model.Z = pe.Objective(rule=eq_Z, sense=pe.maximize)
 
@@ -261,7 +246,9 @@ class ModeloSubasta:
         model_logger.info("Factibilidad de derechos Firmes")
 
         def eq_F_k_rule(model, c, k):
-            return model.F_k[c, k] == model.alpha_k[k]*sum(model.He[c, n]*model.T[n, k] for n in model.n)
+            return model.F_k[c, k] == (
+                model.alpha_k[k] *
+                sum(model.He[c, n] * model.T[n, k] for n in model.n))
 
         model.eq_F_k = pe.Constraint(model.c, model.k, rule=eq_F_k_rule)
 
@@ -327,7 +314,8 @@ class ModeloSubasta:
         model.eq_PL = pe.Constraint(model.c, rule=eq_PL_rule)
 
         def eq_Per_rule(model, n):
-            return model.Per[n] == sum(model.Inc[c, n]*(model.PL[c]/2) for c in model.c)
+            return model.Per[n] == (
+                sum(model.Inc[c, n]*(model.PL[c]/2) for c in model.c))
 
         model.eq_Per = pe.Constraint(model.n, rule=eq_Per_rule)
 
@@ -377,27 +365,29 @@ class ModeloSubasta:
 
         model_logger.info("Construcción del modelo terminada.")
 
-        filename = path.basename(self.file).split('.')
+        filename = path.basename(self.file).split('.')[0]
         filepath = path.dirname(self.file)
 
         try:
             model_logger.info("Enviando modelo algebraico al solver")
             opt = SolverFactory(
-                'ipopt',
-                executable=self.solver_path
+                'ipopt'
             )
 
             model_logger.info("Buscando una solución optima al problema")
             result = opt.solve(
-                model, tee=True, logfile="log/solver_{}.log".format(filename[0]))
+                model, tee=self.tee,
+                logfile=f"log/solver_{filename}.log")
 
             model.solutions.store_to(result)
-            result.write(filename='results.json', format='json')
+            result.write(
+                filename=f"{APPDIRS['DATA']}/results_{filename}.json",
+                format='json')
             model_logger.info("Solucion encontrada")
 
         except Exception as e:
-            model_logger.info("No se ejecutó el problema.")
-            model_logger.error("Error: {}".format(e))
+            model_logger.error("No se ejecutó el problema.")
+            model_logger.critical("Error: {}".format(e))
             return
 
         # Cálculo de Precios Nodales
@@ -439,8 +429,8 @@ class ModeloSubasta:
 
         # Construcción de array para grabar
         # =============================================================================
-        filename = path.basename(self.file).split('.')
-        filepath = path.dirname(self.file)
+        # filename = path.basename(self.file).split('.')
+        # filepath = path.dirname(self.file)
 
         model_logger.info(
             "Escribiendo resultados en carpeta {0}".format(filepath))
@@ -457,6 +447,9 @@ class ModeloSubasta:
         resultado['precio_mw'] = resultado['monto_asign'] / \
             (resultado['MW_asign'] * 24 * 31)  # Se toman meses de 31 días
 
+        resultado.to_pickle(
+            path=f'{APPDIRS["DATA"]}/asignaciones_{filename}.dat')
+
         # Flujos de Potencia
 
         flujos = net.copy()
@@ -467,50 +460,40 @@ class ModeloSubasta:
         flujos['perdidas'] = array(list(model.PL.get_values().values()))
         flujos['sigma'] = Sigma
 
+        flujos.to_pickle(
+            path=f'{APPDIRS["DATA"]}/flujos_{filename}.dat')
+
         # Precios por Nodo
 
         pon = bus.copy()
         pon['PON'] = PON
 
-        # Grabar en excel
-        # ================================================================================
-
-        # new_filename = filename[0]+'_results.'+filename[1]
-        #
-        # new_file = path.join(filepath, new_filename)
-        #
-        # book = load_workbook(new_file)
-        # writer = ExcelWriter(new_file, engine='openpyxl')
-        # writer.book = book
-        # writer.sheets = dict((ws.title, ws) for ws in book.worksheets)
-        #
-        # resultado.to_excel(writer, sheet_name='asignacion', index=False, header=True)
-        #
-        # flujos.to_excel(writer, sheet_name='flujos', index=False, header=True)
-        #
-        # pon.to_excel(writer, sheet_name='pon', index=False, header=True)
-        #
-        # writer.save()
-        # writer.close()
+        pon.to_pickle(
+            path=f'{APPDIRS["DATA"]}/pon_{filename}.dat')
+
+        flujos_inter.to_pickle(
+            path=f'{APPDIRS["DATA"]}/flujos_inter_{filename}.dat')
 
         # Grabar en csv
         # =================================================================================
         csv_path = filepath
         model_logger.info("Escribiendo archivo de asignaciones")
         resultado.to_csv(
-            path.join(csv_path, filename[0] + '_asignacion.csv'), sep=',', index=False)
+            path.join(csv_path,
+                      filename + '_asignacion.csv'), sep=',', index=False)
 
         model_logger.info("Escribiendo archivo de flujos de potencia")
         flujos.to_csv(
-            path.join(csv_path, filename[0] + '_flujos.csv'), sep=',', index=False)
+            path.join(csv_path,
+                      filename + '_flujos.csv'), sep=',', index=False)
 
         model_logger.info("Escribiendo archivo de flujos de precios")
         pon.to_csv(
-            path.join(csv_path, filename[0] + '_pon.csv'), sep=',', index=False)
+            path.join(csv_path,
+                      filename + '_pon.csv'), sep=',', index=False)
 
         model_logger.info(
             "Escribiendo archivo de flujos de potencia entre areas de control")
         flujos_inter.to_csv(
-            path.join(csv_path, filename[0] + '_flujos_inter.csv'), sep=',', index=False)
-
-        model_logger.info("Script Finalizado ")
+            path.join(csv_path,
+                      filename + '_flujos_inter.csv'), sep=',', index=False)

+ 84 - 83
simsdt/ofertas/readBids.py

@@ -6,32 +6,32 @@
 """Importa la información de inyecciones y retiros de archivo de Excel 
 """
 
-from pandas import read_excel, concat
 from numpy import zeros
+from pandas import concat, read_excel
 
 
 def readexistentes(subasta_file):
     """Lee del archivo Excel la información de Derechos Firmes existente.
-    
+
     Retorna un DataFrame con las columnas siguientes:
-        
+
         1. cod_agente    : Código del Agente propietario del DF existente.
         2. tke           : identificador del DF existente
         3. bus_ie        : Nodo de inyección del DF existente
         4. bus_je        : Nodo de retiro del DF existente
         5. MWe           : Potencia asignada al DF existente
     """
-    exist = read_excel(subasta_file,sheet_name='existentes')
-    exist.columns = ['cod_agente','tke','bus_ie','bus_je','MWe','per']
-    
+    exist = read_excel(subasta_file, sheet_name='existentes')
+    exist.columns = ['cod_agente', 'tke', 'bus_ie', 'bus_je', 'MWe', 'per']
+
     return exist
 
 
 def readofertas(subasta_file):
     """Lee del archivo Excel la información de Ofertad de Derechos Firmes.
-    
+
     Retorna un DataFrame con las columnas siguientes:
-        
+
         1. cod_agente    : Código del Agente que oferta.
         2. tko           : identificador de la oferta
         3. bus_io        : Nodo de inyección de la oferta
@@ -42,204 +42,205 @@ def readofertas(subasta_file):
         8. per           : Perdidas asignadas a la oferta
         9. cper          : Oferta en dolares por las pérdidas
     """
-    ofer = read_excel(subasta_file,sheet_name='oferta')
-    ofer.columns = ['cod_agente','tko','bus_io','bus_jo','MWo','oferta','precio','per','cper']
-    
+    ofer = read_excel(subasta_file, sheet_name='oferta')
+    ofer.columns = ['cod_agente', 'tko', 'bus_io',
+                    'bus_jo', 'MWo', 'oferta', 'precio', 'per', 'cper']
+
     return ofer
 
 
 def setVITE(bus, ex):
     """Construye el vector VITE de inyecciones de Derechos Firmes existentes
-    
+
     Vector cuya dimensión es C{nb}, en donde c{nb} es el número de nodos, y 
     cuyas componentes son todas nulas, salvo la correspondiente al nodo de 
     inyección del derecho firme donde tiene el valor correspondiene a la 
     inyección asociada al derecho
-    
+
     La función regresa una matriz de dimensión C{nb x ne}, en donde C{nb} es el
     número de nodos y C{ne} es el número de derechos firmes existentes. Cada
     columna de la matriz es el vector VITEe de cada derecho firme e.
-    
+
     """
-    
-    _vite = zeros((bus.shape[0],ex.shape[0]))
-    
+
+    _vite = zeros((bus.shape[0], ex.shape[0]))
+
     for i in range(0, ex.shape[0]):
-        _vite[bus[bus == ex.iloc[i].bus_ie].index[0],i] = ex.iloc[i].MWe
-        
+        _vite[bus[bus == ex.iloc[i].bus_ie].index[0], i] = ex.iloc[i].MWe
+
     return _vite
 
 
 def setVRTE(bus, ex):
     """Construye el vector VRTE de retiros de Derechos Firmes existentes
-    
+
     Vector cuya dimensión es C{nb}, en donde nb es el número de nodos, y cuyas
     componentes son todas nulas, salvo la correspondiente al nodo de retiro
     del derecho firme donde tiene el valor correspondiene a la inyección
     asociada al derecho
-    
+
     La función regresa una matriz de dimensión C{nb x ne}, en donde C{nb} es el
     número de nodos y C{ne} es el número de derechos firmes existentes. Cada
     columna de la matriz es el vector VRTEe de cada derecho firme e.
-    
+
     """
-    
+
     _vrte = zeros((bus.shape[0], ex.shape[0]))
-    
+
     for i in range(0, ex.shape[0]):
-        _vrte[bus[bus == ex.iloc[i].bus_je].index[0],i] = ex.iloc[i].MWe
-        
+        _vrte[bus[bus == ex.iloc[i].bus_je].index[0], i] = ex.iloc[i].MWe
+
     return _vrte
 
 
-def setTE(vite,vrte):
+def setTE(vite, vrte):
     """Construye el vector TE resta VITE - VRTE
-    
+
     La suma de su componentes es nula.
-    
+
     La función regresa una matriz de dimensión C{nb x ne}, en donde C{nb} es el
     número de nodos y C{ne} es el número de derechos firmes existentes. Cada
     columna de la matriz es el vector TEe de cada derecho firme e.
     """
     _te = vite - vrte
-    
+
     return _te
 
 
 def setVITEX(bus, ex):
     """Construye el vector VITEX de perdidas de Derechos Firmes existentes
-    
+
     Vector cuya dimensión es C{nb}, en donde c{nb} es el número de nodos, y 
     cuyas componentes son todas nulas, salvo la correspondiente al nodo de 
     inyección del derecho firme donde tiene el valor correspondiene a la 
     inyección asociada al derecho
-    
+
     La función regresa una matriz de dimensión C{nb x ne}, en donde C{nb} es el
     número de nodos y C{ne} es el número de derechos firmes existentes. Cada
     columna de la matriz es el vector VITEXe de cada derecho firme e.
-    
+
     """
-    
-    _vitex = zeros((bus.shape[0],ex.shape[0]))
-    
+
+    _vitex = zeros((bus.shape[0], ex.shape[0]))
+
     for i in range(0, ex.shape[0]):
-        _vitex[bus[bus == ex.iloc[i].bus_ie].index[0],i] = ex.iloc[i].per
-        
+        _vitex[bus[bus == ex.iloc[i].bus_ie].index[0], i] = ex.iloc[i].per
+
     return _vitex
 
 
 def setVIT(bus, of):
     """Construye el vector VIT de inyecciones de Ofertas de Derechos Firmes
-    
+
     Vector cuya dimensión es C{nb}, en donde nb es el número de nodos, y cuyas
     componentes son todas nulas, salvo la correspondiente al nodo de inyeccion
     del derecho firme donde tiene el valor correspondiene a la inyección
     ofertada en el derecho k
-    
+
     La función regresa una matriz de dimensión C{nb x nk}, en donde C{nb} es el
     número de nodos y C{nk} es el número de ofertas de derechos firmes. Cada
     columna de la matriz es el vector VITk de cada oferta k.
-    
+
     """
-    
+
     _vit = zeros((bus.shape[0], of.shape[0]))
-    
+
     for i in range(0, of.shape[0]):
-        _vit[bus[bus == of.iloc[i].bus_io].index[0],i] = of.iloc[i].MWo
-        
+        _vit[bus[bus == of.iloc[i].bus_io].index[0], i] = of.iloc[i].MWo
+
     return _vit
 
 
 def setVRT(bus, of):
     """Construye el vector VRT de retiros de Ofertas de Derechos Firmes
-    
+
     Vector cuya dimensión es C{nb}, en donde nb es el número de nodos, y cuyas
     componentes son todas nulas, salvo la correspondiente al nodo de retiro
     del derecho firme donde tiene el valor correspondiene a la inyección
     ofertada en el derecho k
-    
+
     La función regresa una matriz de dimensión C{nb x nk}, en donde C{nb} es el
     número de nodos y C{nk} es el número de ofertas de derechos firmes. Cada
     columna de la matriz es el vector VRTk de cada oferta k.
-    
+
     """
-   
+
     _vrt = zeros((bus.shape[0], of.shape[0]))
-    
+
     for i in range(0, of.shape[0]):
-        _vrt[bus[bus == of.iloc[i].bus_jo].index[0],i] = of.iloc[i].MWo
-        
+        _vrt[bus[bus == of.iloc[i].bus_jo].index[0], i] = of.iloc[i].MWo
+
     return _vrt
 
 
-def setT(vit,vrt):
+def setT(vit, vrt):
     """Construye el vector T resta VIT - VRT
-    
+
     La suma de sus componentes es nula.
-    
+
     La función regresa una matriz de dimensión C{nb x nk}, en donde C{nb} es el
     número de nodos y C{nk} es el número de ofertas de derechos firmes. Cada
     columna de la matriz es el vector Tk de cada oferta k.
     """
     _t = vit - vrt
-    
+
     return _t
 
 
-def setVITX(bus,of):
+def setVITX(bus, of):
     """Construye el vector VITX de perdidas de Ofertas de Derechos Firmes
-    
+
     Vector cuya dimensión es C{nb}, en donde nb es el número de nodos, y cuyas
     componentes son todas nulas, salvo la correspondiente al nodo de inyección
     de la oferta de derecho firme donde tiene el valor correspondiene a la 
     perdida máxima asociada a la oferta k
-    
+
     La función regresa una matriz de dimensión C{nb x nk}, en donde C{nb} es el
     número de nodos y C{nk} es el número de ofertas de derechos firmes. Cada
     columna de la matriz es el vector VITXk de cada oferta k.
-    
+
     """
     _vitx = zeros((bus.shape[0], of.shape[0]))
-    
+
     for i in range(0, of.shape[0]):
-        _vitx[bus[bus == of.iloc[i].bus_io].index[0],i] = of.iloc[i].per
-        
+        _vitx[bus[bus == of.iloc[i].bus_io].index[0], i] = of.iloc[i].per
+
     return _vitx
 
 
 def setIny(bus, exist, ofer):
     """Construye el vector de inyecciones para todos los nodos de la red
     """
-    
+
     iny_e = exist.pivot_table(values='MWe', aggfunc='sum', columns='bus_ie').T
-    iny_o = ofer.pivot_table(values = 'MWo', aggfunc='sum', columns='bus_io').T
-    
-    iny=concat([iny_e, iny_o], axis=1).fillna(0)
-    
-    iny['total'] = iny.iloc[:,0]+iny.iloc[:,1]
-    
+    iny_o = ofer.pivot_table(values='MWo', aggfunc='sum', columns='bus_io').T
+
+    iny = concat([iny_e, iny_o], axis=1).fillna(0)
+
+    iny['total'] = iny.iloc[:, 0]+iny.iloc[:, 1]
+
     _iny = zeros(bus.shape[0])
-    
-    for i in range(0,iny.shape[0]):
+
+    for i in range(0, iny.shape[0]):
         _iny[bus[bus == iny.iloc[i].name].index[0]] = iny.iloc[i].total
-    
+
     return _iny
 
 
 def setRet(bus, exist, ofer):
     """ Construye el vector de retiros para todos los nodos de la red
     """
-    
+
     ret_e = exist.pivot_table(values='MWe', aggfunc='sum', columns='bus_je').T
-    ret_o = ofer.pivot_table(values = 'MWo', aggfunc='sum', columns='bus_jo').T
-    
-    ret=concat([ret_e, ret_o], axis=1).fillna(0)
-    
-    ret['total'] = ret.iloc[:,0]+ret.iloc[:,1]
-    
+    ret_o = ofer.pivot_table(values='MWo', aggfunc='sum', columns='bus_jo').T
+
+    ret = concat([ret_e, ret_o], axis=1).fillna(0)
+
+    ret['total'] = ret.iloc[:, 0]+ret.iloc[:, 1]
+
     _ret = zeros(bus.shape[0])
-    
+
     for i in range(0, ret.shape[0]):
         _ret[bus[bus == ret.iloc[i].name].index[0]] = ret.iloc[i].total
-    
-    return _ret
+
+    return _ret

+ 7 - 3
simsdt/red/create.py

@@ -3,14 +3,17 @@
 #  Copyright (C) 2018 Mercados Electricos de Centroamérica. All rights reserved
 # =============================================================================
 
-"""Crea las estructuras con numeración secuencial de buses, circuitos y nombres de circuitos
+"""Crea las estructuras con numeración secuencial de buses,
+    circuitos y nombres de circuitos
 """
 from numpy import array
+
 from simsdt.utils.idx_brch import BUS_I, BUS_J, CKT
 
 
 def setbus(net):
-    """Lee la información de la red y crea una serie con los códigos de nodos y su numeración correlativa.
+    """Lee la información de la red y crea una serie con los códigos de nodos
+        y su numeración correlativa.
     """
     b = net['bus_i'].append(net['bus_j']).drop_duplicates().sort_values()
     b = b.reset_index(drop=True)
@@ -19,7 +22,8 @@ def setbus(net):
 
 
 def setbranch(net, b):
-    """Utiliza la información de la red, lod códigos de nodos y su numeración correlativa para generar un listado con
+    """Utiliza la información de la red, lod códigos de nodos y su numeración
+    correlativa para generar un listado con
     de los circuitos de acuerdo a la numeración correlativo del nodo.
     """
     br = net.copy()

+ 20 - 14
simsdt/red/makeBdc.py

@@ -7,39 +7,45 @@
 Construye las matrices B para el cálculo de flujos DC
 """
 
-from numpy import ones, r_, zeros
+from numpy import ones, r_
 from scipy.sparse import csr_matrix as sparse
+
 from simsdt.utils.idx_brch import BUS_I, BUS_J, X
 
 
 def makeBdc(bus, branch):
     """Construye las matrices B para el cálculo de flujos DC.
 
-    Se construye primer la matriz de incidencia Cft (o matriz A, como se    denomina en la formulación de substas de
+    Se construye primer la matriz de incidencia Cft (o matriz A, como se
+    denomina en la formulación de substas de
     Derechos Firmes) y se define como:
 
-    El valor será uno (1) para las líneas ij donde su nodo inicial sea igual al nodo i y menos uno (-1) para las lineas
-    ij cuyo nodo final sea igual al nodo j.
+    El valor será uno (1) para las líneas ij donde su nodo inicial sea igual
+    al nodo i y menos uno (-1) para las lineas ij cuyo nodo final sea igual
+    al nodo j.
 
-    La matriz de incidencias Cft tiene dimenciones C{nbr x nb}, en donde C{nbr}es el numero de líneas (branches) y
-    C{nb} el número de buses.
+    La matriz de incidencias Cft tiene dimenciones C{nbr x nb}, en donde C{nbr}
+    es el numero de líneas (branches) y C{nb} el número de buses.
 
-    Luego se calcula la matriz Bf (ZZ) de dimensiones C{nbr x nb}, en donde C{nbr} es el numero de líneas (branches) y
-    C{nb} el número de buses, igual que la Matriz de Incidencias.
+    Luego se calcula la matriz Bf (ZZ) de dimensiones C{nbr x nb}, en donde
+    C{nbr} es el numero de líneas (branches) y C{nb} el número de buses,
+    igual que la Matriz de Incidencias.
 
-    Contiene los mismos elementos nulos que la matriz Cft y cuyas componentes no nulas son:
+    Contiene los mismos elementos nulos que la matriz Cft y cuyas componentes
+    no nulas son:
 
-        1. Para la componente correspondiente a la fila de cada circuito br,y cuya columna corresponde al nodo de
-        llegada del circuito b
+        1. Para la componente correspondiente a la fila de cada circuito br,y
+        cuya columna corresponde al nodo de llegada del circuito b
 
             :math:`Bf[br,b] = 1/Xbr`
 
-        2. Para la componente correspondiente a la fila de cada circuito br, y cuya columna corresponde al nodo de
-        salida del circuito b
+        2. Para la componente correspondiente a la fila de cada circuito br, y
+        cuya columna corresponde al nodo de salida del circuito b
 
             :math:`Bf[br,b] = -1/Xbr`
 
-    La matriz Bbus se calcula como la transpuesta de la matriz de incidencias Cft por la matriz Bf
+    La matriz Bbus se calcula como la transpuesta de la matriz de incidencias
+    Cft por la matriz Bf
 
         :math:`Bbus = Cft.T * Bf`
 

+ 1 - 0
simsdt/red/makePTDF.py

@@ -8,6 +8,7 @@
 
 from numpy import zeros
 from numpy.linalg import solve
+
 from simsdt.red.makeBdc import makeBdc
 
 

+ 3 - 9
simsdt/red/read.py

@@ -3,18 +3,12 @@
 #  Copyright (C) 2018 Mercados Electricos de Centroamérica. All rights reserved
 # =============================================================================
 
-"""Importa el archivo de red RTR a utilizar para el cálculo 
+"""Importa el archivo de red RTR a utilizar para el cálculo
 """
-from pandas import read_sql, read_csv, read_excel
-
-# SERVER = "192.168.98.134"
-# DB = "MEIntegrado"
-# conn = pyodbc.connect('DRIVER={SQL Server};SERVER=' + SERVER + ';DATABASE=' + DB)
-# query = "SELECT BUS_I, BUS_J, ID_CKT, X, R, CTOIJ, CTOJI  FROM MATRIZ_RTR WHERE FECHA = '{f}'"
-
-# logger = logging.getLogger('simsdt.red')
+from pandas import read_csv, read_excel, read_sql
 
 
+# TODO: Implementar lectura desde SQL
 def sql2net(fecha):
     """Lee la información de  la red desde una base de datos
 

+ 9 - 42
simsdt/runsdt.py

@@ -3,65 +3,32 @@
 Modulo principal para ejecutar el modelo de subasta
 """
 import logging
-from logging.handlers import RotatingFileHandler
-import queue
-import shutil
 import sys
 
-
 from simsdt.model import ModeloSubasta
-from qhandler import QueueHandler
-
-# log_queue = queue.Queue()
-# qh = QueueHandler(log_queue)
-# formatter = logging.Formatter(
-#     '%(asctime)s: %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-# qh.setFormatter(formatter)
-
-fh = RotatingFileHandler(
-    'log/simsdt_run_monitor.log', maxBytes=1024*50, backupCount=10)
-fmt = logging.Formatter(
-    '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
-fh.setFormatter(fmt)
-ch = logging.StreamHandler()
-ch.setFormatter(fmt)
 
 run_logger = logging.getLogger('simsdt.run')
-run_logger.setLevel(logging.INFO)
-run_logger.addHandler(fh)
-# run_logger.addHandler(qh)
-# run_logger.addHandler(ch)
 
 
-def main(file, solver_path='', q=None):
+def main(file, tee=False):
     """Ejecuta la Simulación de Subastas de Derechos de Transmisión."""
+    try:
+        # Check if solver 'ipopt' is on path
+        run_logger.info("Inicio de la ejecucion del modelo de subasta")
 
-    if not file:
-        run_logger.error('No se incluyo un archivo para la ejecucion')
-        return
-
-    # Check if solver 'ipopt' is on path
-    run_logger.info('Verificando que exista el solver ipopt')
-
-    solver_path = shutil.which('ipopt')
-
-    if(solver_path):
-        run_logger.info('Ubicación del solver: {}'.format(solver_path))
-    else:
-        run_logger.error('El solver no se encuentra en el PATH')
-        return
-
-    run_logger.info("Inicio de la ejecucion del modelo de subasta")
+        modelo_subasta = ModeloSubasta(file, tee=tee)
 
-    modelo_subasta = ModeloSubasta(file, solver_path)
-    try:
         modelo_subasta.setmodel()
+        # time.sleep(10)
     except Exception as e:
         run_logger.error(e)
         return
 
 
 if __name__ == "__main__":
+    ch = logging.StreamHandler()
+
+    run_logger.addHandler(ch)
     if len(sys.argv) < 2:
         print("Se debe incluir un archivo")
 

+ 30 - 0
simsdt/runtrhead.py

@@ -0,0 +1,30 @@
+import logging
+import threading
+import time
+
+import simsdt.runsdt as runsdt
+
+logger = logging.getLogger("simsdt.thread")
+
+
+class RunsdtThread(threading.Thread):
+    def __init__(self, group=None, target=None, name=None,
+                 args=(), kwargs=None, *, daemon=None):
+        super(RunsdtThread, self).__init__(group=group,
+                                           target=target,
+                                           name=name,
+                                           daemon=daemon)
+        self._stop_event = threading.Event()
+        self.args = args
+        self.kwargs = kwargs
+        return
+
+    def run(self):
+        t = time.time()
+        runsdt.main(self.args)
+        elapsed_time = time.time() - t
+        et = time.strftime("%H:%M:%S", time.gmtime(elapsed_time))
+        logger.info('Tiempo de ejecución %s' % et)
+
+    def stop(self):
+        self._stop_event.set()

+ 1 - 1
simsdt/utils/arr2dict.py

@@ -12,5 +12,5 @@ def arr2dict(x):
     for i in range(0, x.shape[0]):
         for j in range(0, x.shape[1]):
             d[i, j] = x[i, j]
-            
+
     return d

+ 27 - 0
simsdt/utils/excel_checker.py

@@ -0,0 +1,27 @@
+import xlrd
+
+from common.exceptions import ExcelFileError
+
+
+def check_excel_file(file):
+
+    sheets_names = ['oferta', 'existentes', 'mct', 'rtr']
+
+    try:
+        xls = xlrd.open_workbook(file, on_demand=True)
+        sheets = xls.sheet_names()
+    except Exception:
+        raise ExcelFileError(file)
+    else:
+        not_found = []
+        for sheet in sheets_names:
+            sheet_exists = sheet in sheets
+            if not sheet_exists:
+                not_found.append(sheet)
+
+        if not_found:
+            msg = 'El archivo no contiene la(s) hoja(s) '
+            msg += f'<{", ".join(sheet for sheet in not_found)}>'
+            raise ExcelFileError(
+                file,
+                message=msg)

+ 7 - 7
simsdt/utils/idx_brch.py

@@ -17,10 +17,10 @@ Columnas 0-5 incluidas en la definición de la red
 """
 
 # definición de indices
-BUS_I    = 0
-BUS_J    = 1
-CKT      = 2
-X        = 3
-R        = 4
-RT_MAX   = 5
-RT_MIN   = 6
+BUS_I = 0
+BUS_J = 1
+CKT = 2
+X = 3
+R = 4
+RT_MAX = 5
+RT_MIN = 6

+ 49 - 43
simsdt/utils/map.py

@@ -5,59 +5,65 @@ Created on Wed Jul 11 09:28:46 2018
 @author: BI4
 """
 
-from mpl_toolkits.basemap import Basemap
-import matplotlib.pyplot as plt
 import matplotlib.patches as mpatches
+import matplotlib.pyplot as plt
+from mpl_toolkits.basemap import Basemap
 
-def plotMCT(MCT):
-    
-    plt.figure(figsize=(24,12))
+
+def plotMCT(flujos_inter):
+
+    plt.figure(figsize=(24, 12))
     _map = Basemap(projection='merc',
-                  resolution='l',
-                  llcrnrlon=-93, llcrnrlat=7,
-                  urcrnrlon=-75, urcrnrlat=19)
-    
+                   resolution='l',
+                   llcrnrlon=-93, llcrnrlat=7,
+                   urcrnrlon=-75, urcrnrlat=19)
+
     _map.drawcoastlines()
     _map.drawcountries(linewidth=1)
     _map.drawmapboundary(fill_color='#99ffff')
-    _map.fillcontinents(color='#cc9966',lake_color='#99ffff')
-    
-    
-    _lons = [-90.25,-88.9167,-86.5,-85,-84,-81]
-    _lats = [15.5,13.7,15,13,10,8.2]
-    _inter = {'GUAESA':[0,1,'{0} MW'],
-              'GUAHON':[0,2,'{0} MW'],
-              'ESAHON':[1,2,'{0} MW'],
-              'HONNIC':[2,3,'{0} MW'],
-              'NICCRC':[3,4,'{0} MW'],
-              'CRCPAN':[4,5,'{0} MW']}
-    
-    _x,_y = _map(_lons,_lats)
-    
+    _map.fillcontinents(color='#cc9966', lake_color='#99ffff')
+
+    _lons = [-90.25, -88.9167, -86.5, -85, -84, -81]
+    _lats = [15.5, 13.7, 15, 13, 10, 8.2]
+    _inter = {'GUAESA': [0, 1, '{0} MW'],
+              'GUAHON': [0, 2, '{0} MW'],
+              'ESAHON': [1, 2, '{0} MW'],
+              'HONNIC': [2, 3, '{0} MW'],
+              'NICCRC': [3, 4, '{0} MW'],
+              'CRCPAN': [4, 5, '{0} MW']}
+
+    _x, _y = _map(_lons, _lats)
+
     for i in _inter:
-        a = [_x[_inter[i][0]],_x[_inter[i][1]]]
-        b = [_y[_inter[i][0]],_y[_inter[i][1]]]
-        
-        if MCT[i][1] >= MCT[i][2] or MCT[i][1] <= MCT[i][0]:
+        a = [_x[_inter[i][0]], _x[_inter[i][1]]]
+        b = [_y[_inter[i][0]], _y[_inter[i][1]]]
+
+        if flujos_inter[i][1] >= flujos_inter[i][2] or flujos_inter[i][1] <= flujos_inter[i][0]:
             color = 'orangered'
-        elif MCT[i][1] >= 0.8*MCT[i][2] or MCT[i][1] <= 0.8*MCT[i][0]:
+        elif flujos_inter[i][1] >= 0.8*flujos_inter[i][2] or flujos_inter[i][1] <= 0.8*flujos_inter[i][0]:
             color = 'yellow'
         else:
             color = 'green'
-        
-        plt.plot(a,b,'-',color=color,linewidth=3, label=_inter[i][2])
-    
-        plt.text(sum(a)/2,sum(b)/2,'{:.2f} MW'.format(MCT[i][1]*100), 
-                 fontdict={'fontsize': 12, 'backgroundcolor':'#cc9966', 'fontweight':'bold'},
+
+        plt.plot(a, b, '-', color=color, linewidth=3, label=_inter[i][2])
+
+        plt.text(sum(a)/2, sum(b)/2, '{:.2f} MW'.format(flujos_inter[i][1]*100),
+                 fontdict={'fontsize': 12,
+                           'backgroundcolor': '#cc9966', 'fontweight': 'bold'},
                  horizontalalignment='center',
                  verticalalignment='center',)
-    
-    _map.plot(_x,_y,'o', color='navy',markersize=18)
-    
-    red_patch = mpatches.Patch(color='red', label='Interconexión al 100% de capacidad')
-    yellow_patch = mpatches.Patch(color='yellow', label='Interconexión a mas del 80% de capacidad')
-    green_patch = mpatches.Patch(color='green',label='Interconexión a menos del 80% de capacidad')
-    plt.legend(handles=[red_patch,yellow_patch,green_patch], loc=3, fontsize=16)
-    
-    plt.suptitle('Flujos entre Áreas de Control', fontsize =18, fontweight='bold')
-    plt.show()
+
+    _map.plot(_x, _y, 'o', color='navy', markersize=18)
+
+    red_patch = mpatches.Patch(
+        color='red', label='Interconexión al 100% de capacidad')
+    yellow_patch = mpatches.Patch(
+        color='yellow', label='Interconexión a mas del 80% de capacidad')
+    green_patch = mpatches.Patch(
+        color='green', label='Interconexión a menos del 80% de capacidad')
+    plt.legend(handles=[red_patch, yellow_patch,
+                        green_patch], loc=3, fontsize=16)
+
+    plt.suptitle('Flujos entre Áreas de Control',
+                 fontsize=18, fontweight='bold')
+    plt.show()

+ 11 - 0
simsdt/utils/solver_checker.py

@@ -0,0 +1,11 @@
+import shutil
+
+from common.exceptions import SolverNotFoundError
+
+
+def check_solver():
+    # Check if solver 'ipopt' is on path
+    solver_path = shutil.which('ipopt')
+
+    if not solver_path:
+        raise SolverNotFoundError

+ 129 - 0
ui/app.py

@@ -0,0 +1,129 @@
+import logging
+import queue
+import signal
+import time
+from tkinter import HORIZONTAL, VERTICAL, messagebox, ttk
+
+from qhandler import QueueHandler
+from ui.console import ConsoleUi
+from ui.forms import FormUi
+from ui.results import ResultUi
+from ui.splash import Splash
+from ui.status import StatusUi
+
+logger = logging.getLogger('simsdt')
+
+
+def center(win):
+    """
+    centers a tkinter window
+    :param win: the root or Toplevel window to center
+    """
+    win.update_idletasks()
+    width = win.winfo_width()
+    frm_width = win.winfo_rootx() - win.winfo_x()
+    win_width = width + 2 * frm_width
+    height = win.winfo_height()
+    titlebar_height = win.winfo_rooty() - win.winfo_y()
+    win_height = height + titlebar_height + frm_width
+    x = win.winfo_screenwidth() // 2 - win_width // 2
+    y = win.winfo_screenheight() // 2 - win_height // 2
+    win.geometry('{}x{}+{}+{}'.format(width, height, x, y))
+    win.deiconify()
+
+
+class App:
+
+    def __init__(self, root):
+        self.root = root
+
+        # Display Splash
+        # Splash(self.root)
+
+        # Queue a nivel de App para manejar el progress bar
+        pbqueue = queue.Queue()
+        log_queue = queue.Queue()
+
+        # Setup QueueHandler for app
+
+        self.queue_handler = QueueHandler(log_queue)
+        formatter = logging.Formatter(
+            '%(asctime)s: %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
+        self.queue_handler.setFormatter(formatter)
+        # self.queue_handler.setLevel(logging.INFO)
+        logger.addHandler(self.queue_handler)
+
+        root.title('SimSDT')
+        root.iconbitmap("app.ico")
+        root.columnconfigure(0, weight=1)
+        root.rowconfigure(0, weight=1)
+
+        w = ttk.Notebook(self.root)
+        w.grid(row=1, column=0, sticky="nsew",
+               padx=10, pady=(0, 10))
+
+        tab_exec = ttk.Frame(w)
+        tab_result = ttk.Frame(w)
+
+        # Create the panes and frames
+        vertical_pane = ttk.PanedWindow(
+            tab_exec, orient=VERTICAL)
+        vertical_pane.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
+
+        main_title = ttk.Label(
+            self.root,
+            text='Simulación de Subastas de Derechos de Transmisión',
+            font=('', '20', 'bold'))
+        main_title.grid(row=0, column=0, pady=10)
+        # self.root.add(main_title, weight=0)
+
+        horizontal_pane = ttk.PanedWindow(vertical_pane, orient=HORIZONTAL)
+        vertical_pane.add(horizontal_pane, weight=1)
+
+        form_frame = ttk.Labelframe(
+            horizontal_pane, text="Configuración", padding=(5, 5))
+        form_frame.columnconfigure(1, weight=1)
+        horizontal_pane.add(form_frame, weight=1)
+
+        console_frame = ttk.Labelframe(
+            horizontal_pane, text="Consola", padding=(5, 5))
+        console_frame.columnconfigure(0, weight=1)
+        console_frame.rowconfigure(0, weight=1)
+
+        horizontal_pane.add(console_frame, weight=1)
+        status_frame = ttk.Labelframe(
+            vertical_pane, text="Estado", padding=(5, 5))
+        vertical_pane.add(status_frame, weight=0)
+        # Initialize all frames
+        self.form = FormUi(form_frame, pbqueue)
+        self.console = ConsoleUi(console_frame, log_queue, self.queue_handler)
+        self.status = StatusUi(status_frame, pbqueue)
+
+        self.result = ResultUi(tab_result)
+
+        # setup tabs
+
+        w.add(tab_exec, text='Ejecución')
+        w.add(tab_result, text='Resultados')
+
+        # bindings
+
+        self.root.protocol('WM_DELETE_WINDOW', self.quit)
+        self.root.bind('<Control-q>', self.quit)
+        signal.signal(signal.SIGINT, self.quit)
+
+        center(self.root)
+
+    def quit(self, *args):
+
+        msg = 'Se esta ejecutando un proceso de optimización\n\n'
+        msg += '¿Desea salir de la aplicación?'
+
+        if self.form.runsdt_thread and self.form.runsdt_thread.is_alive():
+            if messagebox.askokcancel("Salir de SimSDT", msg):
+                logger.info('Cerrando aplicacion SimSDT')
+                self.form.runsdt_thread.stop()
+                self.root.destroy()
+        else:
+            logger.info('Saliendo de la aplicacion')
+            self.root.destroy()

+ 69 - 0
ui/console.py

@@ -0,0 +1,69 @@
+import queue
+import tkinter as tk
+from tkinter import E, N, S, W, ttk
+from tkinter.scrolledtext import ScrolledText
+
+from PIL import Image, ImageTk
+
+
+class ConsoleUi:
+    """Poll messages from a logging queue and display them in a scrolled text
+    widget"""
+
+    def __init__(self, frame, q, qh):
+        self.frame = frame
+        self.log_queue = q
+        self.queue_handler = qh
+
+        trash_icon = Image.open('img/icons/icons8-eliminar-16.png')
+        self.trash_icon = ImageTk.PhotoImage(trash_icon)
+        # Create a ScrolledText wdiget
+        self.scrolled_text = ScrolledText(frame, state='disabled', height=25)
+        self.scrolled_text.grid(row=0, column=0, sticky=(N, S, W, E))
+        self.clear = ttk.Button(frame, text='Limpiar Log',
+                                command=self.clear_console,
+                                image=self.trash_icon,
+                                compoun='right')
+        self.clear.grid(row=1, column=0, sticky=E, pady=(5, 5))
+        # Font config
+        self.scrolled_text.configure(font='TkFixedFont')
+        self.scrolled_text.tag_config('INFO', foreground='black')
+        self.scrolled_text.tag_config('DEBUG', foreground='gray')
+        self.scrolled_text.tag_config('WARNING', foreground='orange')
+        self.scrolled_text.tag_config('ERROR', foreground='red')
+        self.scrolled_text.tag_config(
+            'CRITICAL', foreground='red', underline=1)
+        # Create a logging handler using a queue
+
+        # self.queue_handler = QueueHandler(self.log_queue)
+        # formatter = logging.Formatter(
+        #     '%(asctime)s: %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
+        # self.queue_handler.setFormatter(formatter)
+        # # self.queue_handler.setLevel(logging.INFO)
+        # logger.addHandler(self.queue_handler)
+        # Start polling messages from the queue
+        self.frame.after(100, self.poll_log_queue)
+
+    def display(self, record):
+        msg = self.queue_handler.format(record)
+        self.scrolled_text.configure(state='normal')
+        self.scrolled_text.insert(tk.END, msg + '\n', record.levelname)
+        self.scrolled_text.configure(state='disabled')
+        # Autoscroll to the bottom
+        self.scrolled_text.yview(tk.END)
+
+    def poll_log_queue(self):
+        # Check every 100ms if there is a new message in the queue to display
+        while True:
+            try:
+                record = self.log_queue.get(block=False)
+            except queue.Empty:
+                break
+            else:
+                self.display(record)
+        self.frame.after(100, self.poll_log_queue)
+
+    def clear_console(self):
+        self.scrolled_text.configure(state='normal')
+        self.scrolled_text.delete(1.0, tk.END)
+        self.scrolled_text.configure(state='disabled')

+ 86 - 0
ui/forms.py

@@ -0,0 +1,86 @@
+import logging
+import tkinter as tk
+from tkinter import E, W, filedialog, ttk
+
+from PIL import Image, ImageTk
+
+from simsdt.runtrhead import RunsdtThread
+from simsdt.utils.excel_checker import check_excel_file
+from simsdt.utils.solver_checker import check_solver
+
+main_logger = logging.getLogger('simsdt')
+
+
+class FormUi:
+
+    runsdt_thread = None
+
+    def __init__(self, frame, q):
+        self.frame = frame
+        self.q = q
+        image = Image.open('img/icons/icons8-play-16.png')
+        self.photo = ImageTk.PhotoImage(image)
+
+        opfile = Image.open('img/icons/icons8-abrir-carpeta-16.png')
+        self.opfile = ImageTk.PhotoImage(opfile)
+
+        # Add a text field for file path
+        self.file_path = tk.StringVar()
+        ttk.Label(self.frame, text='Archivo:').grid(
+            column=0, row=0, sticky=W, pady=(5, 5), padx=(0, 10))
+        self.entry = ttk.Entry(self.frame, textvariable=self.file_path,
+                               width=50)
+        self.entry.grid(column=1, row=0, sticky=(W, E), pady=(5, 5))
+        self.button = ttk.Button(
+            self.frame, image=self.opfile, compound='center',
+            command=self.open_file_dialog, width=3)
+        self.button.grid(column=2, row=0, sticky=W)
+        self.button_run = ttk.Button(self.frame, image=self.photo,
+                                     compound="right", text='Ejecutar',
+                                     command=self.run)
+        self.button_run.grid(column=1, row=1, sticky=W)
+
+    def open_file_dialog(self):
+        # Open file dialog
+        try:
+            file = filedialog.askopenfilename(
+                filetypes=[('Excel', '.xlsx')],
+                title='Seleccione un archivo de subasta...')
+            check_excel_file(file)
+        except Exception as e:
+            main_logger.error(e)
+            self.file_path.set('')
+        else:
+            main_logger.info(f'Archivo seleccionado: {file}')
+            self.file_path.set(file)
+
+    def run(self):
+        try:
+
+            if not self.file_path.get():
+                main_logger.error(
+                    'Debe seleccionar un archivo para ejecutarlo')
+                return
+
+            check_solver()
+
+            self.button_run.configure(state='disabled')
+            main_logger.info('Ejecutando')
+
+            self.runsdt_thread = RunsdtThread(args=(self.file_path.get()))
+            self.runsdt_thread.start()
+
+            self.check_thread()
+
+        except Exception as e:
+            main_logger.error(e)
+            return
+
+    def check_thread(self):
+
+        if not self.runsdt_thread.is_alive():
+            self.button_run.configure(state='normal')
+            self.q.put(False)
+        else:
+            self.q.put(True)
+            self.frame.after(100, self.check_thread)

+ 107 - 0
ui/results.py

@@ -0,0 +1,107 @@
+
+import tkinter as tk
+from tkinter import ttk
+
+import pandas as pd
+
+
+class ResultUi:
+
+    def __init__(self, frame):
+        self._frame = frame
+
+        ttk.Label(self._frame, text="Flujos entre Áreas de Control", font=('', 12, 'bold')).grid(
+            row=0, column=0, columnspan=3, padx=10, pady=(10, 0))
+
+        headers = ['Interconexión', 'Flujo [MW]',
+                   'Max. Norte - Sur [MW]', 'Max. Sur - Norte [MW]']
+
+        for i in range(4):
+            ttk.Label(self._frame, text=headers[i]).grid(
+                row=1, column=i, padx=5, pady=5)
+
+        self._inter = {
+            'GUAESA': {
+                'label': 'Guatemala - El Salvador',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            },
+            'GUAHON': {
+                'label': 'Guatemala - Honduras',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            },
+            'ESAHON': {
+                'label': 'El Salvador - Honduras',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            },
+            'HONNIC': {
+                'label': 'Honduras - Nicaragua',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            },
+            'NICCRC': {
+                'label': 'Nicaragua - Costa Rica',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            },
+            'CRCPAN': {
+                'label': 'Costa Rica - Panamá',
+                'value': None,
+                'tklabel': None,
+                'max_ns': None,
+                'max_sn': None
+            }
+        }
+
+        self.flujos = []
+
+        r = 2
+        for (k, v) in self._inter.items():
+            for j in range(2):
+                if j == 0:
+                    ttk.Label(self._frame, text=v['label']).grid(
+                        row=r, column=j, padx=5, pady=5)
+                else:
+                    v['tklabel'] = ttk.Label(self._frame, font=('', 9, 'bold'),
+                                             text='-')
+                    v['tklabel'].grid(
+                        row=r, column=j, padx=5, pady=5)
+                    v['max_ns'] = ttk.Label(self._frame, text='-')
+                    v['max_ns'].grid(row=r, column=j+1, padx=5, pady=5)
+                    v['max_sn'] = ttk.Label(self._frame, text='-')
+                    v['max_sn'].grid(row=r, column=j+2, padx=5, pady=5)
+            r += 1
+
+        ttk.Button(self._frame, text='Load', command=self.load_results).grid(
+            column=10, row=0
+        )
+
+    def load_results(self):
+        fi = pd.read_pickle(
+            r"C:\Users\oleiva\AppData\Local\Merelec\Simsdt\data\flujos_inter_subasta_A1901_s1_c1.dat")
+
+        for (k, v) in self._inter.items():
+            f = fi[k][1]
+            ns = fi[k][0]
+            sn = fi[k][2]
+
+            if f >= sn or f <= ns:
+                color = 'red'
+            else:
+                color = 'black'
+
+            v['tklabel'].configure(text=f'{f:.2f}', foreground=color)
+            v['max_ns'].configure(text=f'{ns:.2f}')
+            v['max_sn'].configure(text=f'{sn:.2f}')

+ 44 - 0
ui/splash.py

@@ -0,0 +1,44 @@
+import time
+import tkinter as tk
+
+from PIL import Image, ImageTk
+
+
+class Splash(tk.Toplevel):
+    def __init__(self, master=None, **kw):
+        tk.Toplevel.__init__(self, master=master, **kw)
+        self._root = master
+        _image = Image.open('img/splash.png')
+        self._image = ImageTk.PhotoImage(_image)
+
+        self._root.withdraw()
+
+        tk.Label(self, image=self._image, relief='flat').pack()
+
+        self.center()
+
+        self.overrideredirect(1)
+        self.update()
+
+        time.sleep(3)
+
+        self.destroy()
+
+        self._root.deiconify()
+
+    def center(self):
+        """
+        centers a tkinter window
+        :param self: the root or Toplevel window to center
+        """
+        self.update_idletasks()
+        width = self.winfo_width()
+        frm_width = self.winfo_rootx() - self.winfo_x()
+        win_width = width + 2 * frm_width
+        height = self.winfo_height()
+        titlebar_height = self.winfo_rooty() - self.winfo_y()
+        win_height = height + titlebar_height + frm_width
+        x = self.winfo_screenwidth() // 2 - win_width // 2
+        y = self.winfo_screenheight() // 2 - win_height // 2
+        self.geometry('{}x{}+{}+{}'.format(width, height, x, y))
+        self.deiconify()

+ 37 - 0
ui/status.py

@@ -0,0 +1,37 @@
+import queue
+import tkinter as tk
+from tkinter import ttk
+
+
+class StatusUi:
+
+    def __init__(self, frame, q):
+        self.q = q
+        self.frame = frame
+        self.pb_max = 100
+        # ttk.Label(self.frame, text='This is just an example of a third frame').grid(
+        #     column=0, row=1, sticky=W)
+        self.progress_bar = ttk.Progressbar(
+            self.frame, mode='determinate')
+        self.progress_bar.pack(expand=True, fill=tk.X, side=tk.TOP)
+
+        self.frame.after(100, self.check_q)
+
+    def check_q(self):
+        # Check every 100ms if there is a new message in the queue to display
+        while True:
+            try:
+                record = self.q.get(block=False)
+            except queue.Empty:
+                break
+            else:
+                if record:
+                    step = 1
+                    self.progress_bar.step(step)
+                    if step >= self.pb_max:
+                        step = 1
+                    else:
+                        step += 1
+                else:
+                    self.progress_bar.config(value=0)
+        self.frame.after(100, self.check_q)