Full text
1 Dpto. Organización Industrial y Gestión de Empresas I Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2025 Trabajo Fin de Grado en Ingeniería de Organización Industrial Resolución del problema de gestión de quirófanos y camas de la Unidad de Recuperación Postanestésica mediante algoritmos aproximados. Autor: Sergio Álvarez Correa Tutor: José Manuel Molina Pariente
iii Trabajo Fin de Grado en Ingeniería de Organización Industrial Resolución del problema de gestión de quirófanos y camas de la Unidad de Recuperación Postanestésica mediante algoritmos aproximados. Autor: Sergio Álvarez Correa Tutor: José Manuel Molina Pariente Profesor Titular de la Universidad Dpto. de Organización Industrial y Gestión de Empresas I Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2025
v Trabajo Fin de Grado: Resolución del problema de gestión de quirófanos y camas de la Unidad de Recuperación Postanestésica mediante algoritmos aproximados. Autor: Sergio Álvarez Correa Tutor: José Manuel Molina Pariente El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros: Presidente: Vocales: Secretario: Acuerdan otorgarle la calificación de: Sevilla, 2025 El Secretario del Tribunal
vii Agradecimientos Quiero comenzar agradeciendo al pilar más importante de mi vida, mis familiares más cercanos. No quiero personalizar en ninguno, porque todos ellos son igual de importantes para mí. En los momentos de mayor presión siempre han sabido cómo tratarme y apoyarme. Sin su cariño y comprensión, no sería quien soy. Gracias a cada uno de ellos por saber estar siempre. Gracias también a quienes comenzaron siendo compañeros y hoy son amigos. La motivación para ir a las clases era verlos a ellos. Sin ellos la carrera no habría sido ni la mitad de enriquecedora de lo que ha sido. Me gustaría agradecer también a José Manuel Molina Pariente, mi tutor de TFG. Siempre que he necesitado ayuda ha estado disponible, respondiendo a mis correos con inmediatez y ofreciéndome la máxima flexibilidad posible para realizar tutorías. En ningún momento me he sentido presionado, ni siquiera en las etapas en las que no avanzaba como debía. Siempre he sentido que tenía su confianza y eso lo agradezco mucho. Por su dedicación, su cercanía y su profesionalidad, mi primer agradecimiento es para él. Por último, quiero recordar y agradecer a todos los profesores que me han acompañado a lo largo de estos años, especialmente aquellos que impartieron asignaturas relacionadas con la optimización y la programación. A todos ellos se les nota la pasión por lo que enseñan, y son las materias que más he disfrutado y en las que más me he implicado durante la carrera. Sergio Álvarez Correa Sevilla, 2025
ix Resumen En la actualidad, los servicios quirúrgicos hospitalarios enfrentan un elevado nivel de saturación, reflejado en listas de espera prolongadas que, en muchos casos, exceden el plazo máximo de garantías de la mayoría de las comunidades autónomas. Ante la imposibilidad de ampliar los recursos disponibles a corto plazo, la optimización del uso de los recursos existentes se convierte en una prioridad. En este trabajo se aborda la optimización de la programación integrada de quirófanos y de la Unidad de Recuperación Postanestésica (PACU), con el objetivo de generar un programa quirúrgico realista y eficiente para un horizonte temporal determinado. La indisponibilidad de camas en la PACU al finalizar una intervención implica que el paciente debe permanecer en el quirófano, lo que impide su uso para nuevas cirugías y reduce la eficiencia del sistema. Ignorar esta relación puede dar lugar a calendarios quirúrgicos inviables desde el punto de vista operativo, por lo que la incorporación explícita de la PACU en la programación resulta esencial. Los objetivos que se marcan a la hora de optimizar la programación son, en primer lugar, operar al máximo número de pacientes posibles, ponderados por el peso clínico de cada uno. En segundo lugar, se pretende disminuir al máximo posible el tiempo que pasan ocioso los quirófanos y los cirujanos, para así mejorar la eficiencia en la utilización de los recursos hospitalarios. Para llevar a cabo la optimización de la programación, se harán uso de distintas metaheurísticas. En concreto, se han implementado el algoritmo Iterated Greedy (IG) y Artificial Bee Colony (ABC), incluyendo, adicionalmente, una variante para cada uno. Además, se evaluará como afectan diferentes políticas de dimensionalidad de la PACU en la consecución de los objetivos marcados previamente.
Figura 4-9. Boxplot del RPD de la FO de las combinaciones de parámetros para ABC con elitismo ................ 48 Figura 4-10. Boxplot del RPD de la FO de las MH ................................................................................................ 50
1 1 INTRODUCCIÓN a gestión de las listas de espera quirúrgica representa uno de los mayores retos en la actualidad en el Sistema Nacional de Salud de España (SNS). El número de días que los pacientes permanecen en la lista de espera (waiting list, WL) es elevado, sobrepasando en muchos casos el plazo máximo garantizado para la intervención quirúrgica, establecido por las comunidades autónomas (CCAA). Pese a no existir un plazo común a nivel nacional, la mayoría de las CCAA establecen un máximo de seis meses para las operaciones de menor complejidad, como es el caso de Andalucía, donde dicho plazo se establece en [2]. Como se observa en la Figura 1-1, la tendencia es constante desde el fin de la pandemia y no hay atisbos de que vaya a cambiar. Figura 1-1. WL quirúrgica del Sistema Nacional de Salud [1] En particular, como se muestra en Tabla 1-1, Andalucía presenta el peor indicador a nivel nacional en cuanto al porcentaje de pacientes que sobrepasan el plazo máximo de garantía de seis meses sin ser intervenidos y es la segunda CCAA con el tiempo medio de espera más elevado, solo superada por Extremadura. L
Introducción 2 Tabla 1-1. Indicadores WL en las CCAA en el cierre de 2024 [1] Los datos mostrados no solo reflejan una alta carga asistencial, sino que ponen de manifiesto la urgencia en implementar medidas para aumentar la eficiencia en la gestión de los recursos disponibles, especialmente en sistemas sometidos a una fuerte presión, como el andaluz. A lo largo de la literatura, los problemas de gestión de los recursos quirúrgicos han sido abordados desde diferentes enfoques, siendo uno de los más extendidos, como se menciona en [3], la descomposición jerárquica de las decisiones en tres niveles bien diferenciados: 1. Nivel estratégico: corresponde a decisiones de largo plazo, como el número de cirujanos contratados, la distribución de especialistas por área quirúrgica, la dotación de salas quirúrgicas o la compra de equipamiento. Estas decisiones configuran el marco de recursos disponibles para realizar las cirugías en el hospital. Si se sobredimensionan los recursos se estará cayendo en ineficiencias, mientras si se infra dimensionan, los recursos no conseguirán abastecer a la demanda de servicios quirúrgicos. 2. Nivel táctico: con un horizonte temporal de varias semanas o meses, este nivel se centra en la planificación del uso de los recursos definidos a nivel estratégico. Las decisiones típicas incluyen la definición del número de quirófanos disponibles por día y turno, cuánto tiempo tendrá reservado cada especialidad para el uso de distintos quirófanos, o la planificación de turnos y vacaciones del personal.
3. Nivel operativo: una vez definidos los recursos y su distribución, el nivel operativo se ocupa de la asignación de fecha y hora de pacientes de la WL a quirófanos. Esta asignación se realiza en un horizonte de días o semanas y da lugar al denominado programa quirúrgico. El presente trabajo se centrará en el nivel operativo, es decir, en la programación quirúrgica. Dado que la mayoría de las intervenciones requieren una etapa postoperatoria inmediata tras su finalización, en la que el paciente se recupera de la anestesia en la Unidad de Recuperación Posanestésica (PACU), la programación de los quirófanos se llevará a cabo considerando las relaciones de dependencia existentes entre los quirófanos y la PACU. Se ha adoptado este enfoque integrado porque, en caso de que un paciente no disponga de cama en PACU tras su intervención, el quirófano permanecerá bloqueado, impidiendo el inicio de nuevas cirugías y reduciendo de forma drástica la eficiencia del sistema. Por ello, la integración de quirófano y PACU en el proceso de programación resulta fundamental para obtener programas quirúrgicos realistas y eficientes. La forma en la que se realiza la programación quirúrgica es mediante la codificación de los pacientes de la WL como una secuencia. Esta secuencia representa el orden que se tratarán de asignar al quirófano y a la PACU a cada uno de los pacientes. Las reglas que se utilizarán para asignar a los pacientes siguiendo el orden establecido por la secuencia, así como los algoritmos utilizados para la construcción de la propia secuencia que dará como resultado el programa quirúrgico, quedarán explicados paso a paso en la sección 3. Para lograr un programa quirúrgico eficiente, existen diversos enfoques de optimización ampliamente abordados en la literatura. Si bien lo ideal sería aplicar un modelo de Programación Lineal Entera Mixta (MILP), en el que se modela matemáticamente el problema al completo, permitiendo alcanzar la solución óptima, esto resulta inviable en tiempos razonables debido a la complejidad combinatoria del problema. MILP requiere explorar exhaustivamente el espacio de soluciones, lo que en problemas de secuenciación implica evaluar todas las posibles permutaciones, cuyo número crece de forma factorial con el número de elementos de la secuencia. En la Figura 1-2 se muestra claramente cómo el tiempo requerido para evaluar todas las permutaciones crece de forma factorial, mostrando el tiempo necesario para calcular n! permutaciones, donde n representa el número de elementos en la secuencia, y n! sería el número de secuencias posibles realizando permutaciones de los elementos (o, dicho de otro modo, el espacio de soluciones al completo). Este cálculo se hace bajo dos capacidades de cómputo diferentes: • Una CPU "slow" de 1.0 GHz, que representa una capacidad de un ordenador antiguo. • Una CPU "fast" de 5 PHz (5 millones de GHz), que representa una capacidad computacional teórica extremadamente elevada. A pesar de que una CPU de 5 PHz supera ampliamente las capacidades actuales, incluso con esa potencia, evaluar todo el espacio de soluciones para secuencias de más de 20 elementos se hace inabordable, como se puede observar en la Figura 1-2.
Introducción 4 En problemas de pequeña escala, con pocos elementos, el uso de MILP no solo sería adecuado, sino que sería la mejor opción. Sin embargo, en problemas como los que se trata en este trabajo, donde las secuencias poseerán más de veinte elementos, se ha optado por el uso de algoritmos aproximados, conocidos como metaheurísticas (MH), capaces de proporcionar soluciones de buena calidad en tiempos computacionalmente razonables. Las MH realizan la optimización descartando regiones poco prometedoras del espacio de soluciones y enfocando la búsqueda a regiones que mejoran el objetivo, es decir, no realizan una búsqueda exhaustiva como MILP, sino que priorizan ciertas regiones. Pese a que el uso de MH no garantiza alcanzar el óptimo global, al no explorar todas las regiones existentes, permite alcanzar soluciones de buena calidad en tiempos de cómputo muy reducidos. Estos algoritmos funcionan gracias dos pilares fundamentales, cuyo equilibrio es clave para la eficacia del proceso de búsqueda: • Diversificación. Permite explorar distintas regiones del espacio de búsqueda. Un uso excesivo de la diversificación puede impedir que el algoritmo profundice en soluciones prometedoras, reduciendo la calidad final. • Intensificación. Permite explorar de manera exhaustiva una región prometedora. Esto se consigue explorando la vecindad de una solución dada, es decir, soluciones cercanas a ella. Para intensificar se aceptan solo los movimientos de mejora, esto significa que la secuencia se actualizará solo si el vecino que se está evaluando mejora el objetivo buscado. Un uso excesivo de la intensificación puede hacer que el algoritmo se estanque en óptimos locales (OL), que serán mínimos relativos de la solución, sin explorar otras regiones potencialmente mejores. Figura 1-2. Tiempos computacionales para la evaluación de permutaciones en secuencias [4]
El objetivo principal del proyecto será, por tanto, optimizar la programación integrada de quirófanos y PACU a través del uso de MH. Además, se aprovecharán los resultados para comparar la influencia que tiene la política de camas en la PACU sobre distintos indicadores, como la utilización de los quirófanos o en la forma que afecta en la consecución de los objetivos de la programación, marcados en la sección 2.4. Para alcanzar dichos objetivos se definen los siguientes objetivos específicos: • Describir el problema a nivel general, exponiendo las hipótesis operativas, definiendo con precisión los conjuntos y parámetros implicados, enunciando las restricciones del sistema y cuantificando los objetivos mediante una función objetivo. • Desarrollar una metodología de resolución basada en MH, implementando para ello las MH de Iterated Greedy y Artificial Bee Colony, con una variante de cada una de ellas. Esta metodología incluirá: o El diseño de las reglas de asignación de pacientes de la WL a quirófano y PACU. o La definición de los métodos de generación de secuencias iniciales que actuarán como punto de partida de las MH. o El desarrollo de los operadores de vecindad empleados en las versiones del algoritmo ABC. • Generar un conjunto de instancias que permitan tanto la calibración de los métodos como la evaluación experimental de las MH bajo distintos escenarios hospitalarios. • Establecer herramientas de evaluación faciliten la calibración y selección de la MH más adecuada. • Calibrar cada una de las etapas de la metodología de resolución, con el objetivo de identificar las configuraciones que ofrecen mejores resultados en términos de calidad de solución y eficiencia computacional. • Definir y ajustar los parámetros internos de las MH. • Seleccionar la MH más eficaz a partir del análisis comparativo de los resultados experimentales. • Comparar los distintos indicadores obtenidos tras la experimentación con el fin de identificar que política de dimensionamiento de camas en la PACU es la más adecuada.
Introducción 6
2 DESCRIPCIÓN DEL PROBLEMA n esta sección se presenta de forma detallada el problema de programación quirúrgica integrado con PACU. Se definen las características operativas del entorno hospitalario, incluyendo las hipótesis asumidas, los conjuntos y parámetros utilizados para modelar el sistema, las restricciones clave a las que está sometido y la función objetivo (FO) propuesta. El objetivo es ofrecer una formulación estructurada y realista del problema que permita desarrollar e implementar las metodologías de resolución. 2.1 Hipótesis El problema se ha formulado bajo los siguientes supuestos, que delimitan el alcance: a) Cada paciente cuenta con un cirujano responsable asignado de antemano, por tanto, no forma parte del problema a optimizar. No se considera la participación de cirujanos acompañantes. b) Todas las cirugías forman parte de la misma especialidad y los quirófanos son idénticos entre sí, por lo que cualquier intervención puede realizarse en cualquier quirófano. c) Se considera un esquema de quirófanos del tipo open-scheduling, esto quiere decir que los cirujanos pueden operar en cualquiera de los quirófanos disponibles en un día del horizonte de planificación. Aunque existe la alternativa de limitar el número de quirófanos en los que puede operar cada cirujano (esquema block-scheduling), dada la homogeneidad de los quirófanos y a que esta es una opción mucho más restrictiva, se ha decidido descartar este esquema, ambos métodos quedan resumidos en [3]. d) El recurso limitante en la PACU es la disponibilidad de camas, que actúa como cuello de botella en el sistema. Se asume que el personal sanitario que atiende esta unidad no es un recurso escaso, por lo que no se considera su disponibilidad como una restricción en la programación quirúrgica. e) Por simplicidad, se abordará únicamente la programación del turno de mañana, aunque la implementación es adaptable al turno de tarde. Se definen las siguientes condiciones operativas: 1. El horario del quirófano y la disponibilidad de los cirujanos se extiende de 7:00 a 15:00. 2. La PACU permanece operativa de forma continua, y se asume que puede atender a pacientes del turno de mañana hasta 1 hora y 30 minutos después de la finalización de dicho turno (es decir, hasta las 16:30). A partir de ese momento, se considera que comienzan a llegar pacientes del turno de tarde. E
Descripción del Problema 8 2.2 Conjuntos y parámetros de control Siguiendo la notación propuesta en [3], se definen los siguientes conjuntos: • H = {1, 2, …, ∣H∣}: conjunto de días del horizonte de planificación. • I = {1, 2, …, ∣I∣}: conjunto de intervenciones quirúrgicas, o, dicho de otro modo, pacientes en la WL. • J = {1, 2, …, ∣J∣}: conjunto de quirófanos disponibles. • S = {1, 2, …, ∣S∣}: conjunto de cirujanos. • P = {1, 2, …, ∣P∣}: conjunto de camas disponibles en la PACU. Por otro lado, los parámetros de control principales son los siguientes: • β: parámetro de carga quirúrgica, define la capacidad requerida de operación de quirófano para atender toda la WL, es decir, todo el tiempo que los quirófanos deberían estar disponibles para poder realizar todas las intervenciones de la WL. • α: parámetro encargado de la generación de cirujanos. • 𝛾𝑝: es el parámetro de control encargado de la generación del número de camas en la PACU. • mds: número de días que un cirujano puede operar dentro del horizonte de planificación • 𝑝𝑗𝑖: tiempos de procesos de cada uno de los paciente, en cada etapa, concretamente el paciente i tendrá asociado un el tiempo que tardará en realizarse su intervención quirúrgica (𝑝𝑡𝑂𝑅) y un tiempo de recuperación de la anestesia (𝑝𝑡𝑃𝐴𝐶𝑈). A lo largo del trabajo se utilizará en más de una ocasión OR como abreviatura de quirófano. • 𝜔𝑖: corresponde al peso clínico del paciente i, que refleja su importancia relativa respecto al resto de la WL, teniendo en cuenta tanto su prioridad clínica como el tiempo que lleva esperando, usando de referencia el artículo [5]. 2.3 Restricciones En este apartado se presentan las restricciones a las que está sujeto el problema y que deben ser respetadas para garantizar la factibilidad de la solución.
2.3.1 Jornada laboral Las cirugías deben ser programadas exclusivamente dentro del horario operativo de quirófano. El resto del tiempo se considera ventana de indisponibilidad, y no está permitido realizar cirugías en ese tramo. Esta restricción aplica también al horario operativo de la PACU. En la Figura 2-1 se pueden distinguir dichas ventanas de indisponibilidad, pudiéndose ver que no hay ningún trabajo programado durante esos intervalos. 2.3.2 Bloqueo Cuando un paciente finaliza su intervención, debe recuperarse de la anestesia en la PACU. Si en ese momento no hay ninguna cama disponible, el paciente deberá permanecer en el quirófano, bloqueándolo temporalmente para nuevas cirugías. Durante este periodo de espera, el paciente continúa su recuperación postoperatoria, por lo que ese tiempo se descuenta del 𝑝𝑡𝑃𝐴𝐶𝑈, pudiéndose dar el caso de que complete su recuperación íntegramente en el quirófano, sin necesidad de ocupar una cama en la PACU. La Figura 2-2 muestra bloqueos de quirófanos (bloques grises) por falta de camas PACU (p.e. pacientes 4, 7, 9, 12), mientras que, la Figura 2-3 ilustra casos en que el bloqueo del quirófano actúa como sustituto completo de PACU (p.e. pacientes 3, 5, 6). Figura 2-1.Ventana de indisponibilidad Figura 2-2. Ejemplo de bloqueos
Metodología de Resolución 16 WORST FIT COMPLETE Lista de pacientes NP Valor FO [7, 8, 9, 11, 12] 0.697 Figura 3-4. Ejemplo de Worst Fit Complete BEST FIT COMPLETE Lista de pacientes NP Valor FO [8, 9, 11, 12] 0.441 Figura 3-5. Ejemplo de Best Fit Complete
3.2 Heurísticas Constructivas (HC) En el apartado anterior se explicó cómo se asignan los pacientes a las distintas etapas del problema mediante el uso de un operador BP. Sin embargo, este operador únicamente se encarga de decodificar una secuencia dada, es decir, no construye la secuencia por sí mismo. Para ello, se utilizarán las HC, que no son más que métodos sencillos que generan secuencias completas introduciendo los elementos uno a uno, bajo un cierto criterio, sin necesidad de explorar profundamente el espacio de soluciones. Aunque suelen ofrecer soluciones de calidad limitada, resultan útiles como punto de partida para las MH. Por otro lado, las dispatching rules (DR) son un tipo particular de HC muy simple. A diferencia de otras HC más elaboradas, las DR no evalúan múltiples ubicaciones posibles para cada elemento, sino que se limitan a establecer un criterio de ordenación basado en atributos como el pt o ω,[4]. En el presente proyecto, se han evaluado trece DR distintas, que serán adaptaciones de las versiones clásicas ampliamente utilizadas en la literatura como son: • Random Order Los pacientes se ordenan aleatoriamente, sin seguir ningún criterio específico. • Shortest PT (SPT) Esta DR ordena los pacientes de menor a mayor pt. Se han evaluado tres adaptaciones distintas en función de la etapa considerada, lo que da lugar a tres variantes de la SPT: a) Basada en el 𝑝𝑡𝑂𝑅 (SPT-OR). b) Basada en el 𝑝𝑡𝑃𝐴𝐶𝑈 (SPT-PACU). c) Basada en la suma de 𝑝𝑡𝑂𝑅 con 𝑝𝑡𝑃𝐴𝐶𝑈 (SPT-TOTAL). • Longest PT (LPT) Se ordenan los pacientes de mayor a menor pt, priorizando las cirugías más largas. Se han considerado, al igual que en SPT, las siguientes tres variantes: LPT-OR, LPT-PACU y LPT-TOTAL. • Weighted SPT (WSPT) WSPT prioriza a los pacientes según la relación entre su ω y su pt, ordenándolos de menor a mayor según el resultado de la siguiente ecuación, donde i representa al paciente. De la misma forma que en los anteriores se han considerado tres variantes: WSPT-OR, WSPT-PACU y WSPT-TOTAL. 𝐶𝑟𝑖𝑡𝑒𝑟𝑖𝑜 𝑑𝑒 𝑜𝑟𝑑𝑒𝑛𝑎𝑐𝑖ó𝑛 𝑊𝑆𝑃𝑇=𝑝𝑡𝑖𝜔𝑖 ⁄
Metodología de Resolución 18 • Weighted LPT (WLPT) En este caso, se ordena de menor a mayor la relación entre la ω y la pt de los pacientes, dicha relación se expresa mediante la siguiente ecuación. Se consideran las variantes: WSPT-OR, WSPT-PACU y WSPTTOTAL. 𝐶𝑟𝑖𝑡𝑒𝑟𝑖𝑜 𝑑𝑒 𝑜𝑟𝑑𝑒𝑛𝑎𝑐𝑖ó𝑛 𝑊𝐿𝑃𝑇=𝑝𝑡𝑖 𝑥 𝜔𝑖 Además de las DR, se evaluará tambien la regla de Johnson. Esta es una HC clásica diseñada para minimizar el makespan en problemas tipo Flow Shop (FS) con dos máquinas en serie, logrando alcanzar el óptimo, comportándose en estos contextos como un método de resolución exacto [6]. En el problema que se plantea en este trabajo, al no tratarse de un FS puro, la optimalidad no queda garantizada. Por ello, se usará la regla de Johnson como método para construir una secuencia inicial. Se adaptará esta regla al entorno quirúrgico considerando el quirófano como la primera etapa y PACU como la segunda. En la Figura 3-6 se puede observar la lógica de esta HC recogida en un pseudocódigo. Figura 3-6. Pseudocódigo de la Regla de Johnson
3.3 Operadores de vecindad (OV) Los OV se encargan de explorar las soluciones cercanas a una dada, conocidas como vecinas, para intentar mejorarla. Por tanto, estos operadores son claves en el proceso de intensificación de las MHs. La vecina de una secuencia se obtiene al aplicarle un ligero cambio a dicha secuencia, como podría ser cambiar de posición un elemento de esta, es decir, para obtener las vecinas de una secuencia no se deben modificar los elementos en sí, sino la posición de estos en la secuencia [4]. Se han considerado dos operadores clásicos ampliamente utilizados en la literatura Random Insertion (RI) y Random Swap (RS), así como cuatro operadores adicionales adaptados del artículo [7], que están diseñados precisamente para la resolución de la programación de cirugías: External Swap (ES), Internal Swap (IS), External Insertion (EI), Internal Insertion (II). A continuación, se explicará en que consiste cada uno de ellos y se ilustrará con un ejemplo la forma en la que estos alteran las secuencias. • RI: Extrae aleatoriamente un elemento de la secuencia y lo inserta en otra posición también aleatoria. • RS: Selecciona aleatoriamente dos elementos de la secuencia e intercambia sus posiciones (swap). • ES: Se seleccionan dos pacientes, uno que ya está planificado y otro que pertenece a la lista de NP. Una vez seleccionados, se realiza un swap en la secuencia. Figura 3-9. Ejemplo de ES Figura 3-8. Ejemplo de RS Figura 3-7. Ejemplo de RI
Metodología de Resolución 20 • IS: El operador realiza un swap de dos elementos de la secuencia, asegurándose que… 1. Ambos pacientes deben estar planificados. 2. No deben estar asignados al mismo quirófano el mismo día. • EI: Se extrae un paciente NP y se inserta aleatoriamente en la secuencia. • II: Selecciona un paciente planificado, lo extrae de su posición actual y lo inserta aleatoriamente en otra posición dentro de la secuencia. Figura 3-12. Ejemplo de II Figura 3-11. Ejemplo de EI Figura 3-10. Ejemplo de IS
3.4 Metaheurísticas En esta sección, se describen en detalle las MH utilizadas para la obtención de la secuencia de pacientes optimizada. En concreto, se han evaluado Iterated Greedy (IG) y Artificial Bee Colony (ABC). Para cada uno de estos algoritmos, se incluye también una variante adaptada, con el objetivo de explorar mejoras en la calidad de las soluciones obtenidas. 3.4.1 Iterated Greedy Básico (IG) En el presente apartado se explicará el funcionamiento del algoritmo IG en su versión básica [8]. IG es una MH basada en vecindad, esto significa que intenta mejorar a partir de una solución inicial explorando entre sus vecinas. La secuencia inicial utilizada como punto de partida será la generada a partir de la HC que se identifique como la más prometedora tras la evaluación que se realice en la sección 4.4.2. Esta elección garantiza un punto de partida con una calidad razonable. Aunque en el artículo original se propone la incorporación de una búsqueda local tras la etapa de reconstrucción, los resultados obtenidos al compararla con la versión básica no muestran mejoras significativas, por ello, en el presente trabajo se opta por emplear la versión básica del algoritmo, primando la simplicidad. La forma que IG utiliza para explorar la vecindad de la secuencia es la siguiente, es realizar de forma iterativa las siguientes etapas: 1. Etapa de destrucción. Se eliminan aleatoriamente ciertos pacientes de la secuencia actual. El número de pacientes extraídos está determinado por el parámetro (d). Como resultado de esta fase se obtiene una secuencia parcial a la que le faltan los trabajos extraídos (𝑝𝑑) y una secuencia con los d trabajos extraídos respetando el orden en el que fueron seleccionados (𝑝𝑟). 2. Etapa de reconstrucción. Los pacientes 𝑝𝑟 se reinsertan uno a uno en 𝑝𝑑, siguiendo el orden en que fueron extraídos. Para cada paciente, se evalúan todas las posibles posiciones de inserción dentro 𝑝𝑑, insertándolo en la posición de esta que genere un mejor valor de la FO. Esta HC se conoce como NEH. El proceso se repite hasta que todos los pacientes hayan sido reinsertados. En la Figura 3-13 queda explicado el funcionamiento de NEH en un pseudocódigo. En caso de que la nueva secuencia mejore la función objetivo respecto a la empleada para su generación, esta pasará a ser la nueva solución base en la siguiente iteración. Además, si dicha secuencia supera también a la mejor solución registrada en la memoria global del algoritmo, esta será actualizada también, garantizándose así que no se pueda perder la mejor solución encontrada.
Metodología de Resolución 22 Por el contrario, si la nueva secuencia no mejora a la actual, la actualización de la secuencia podrá ser aceptada con una determinada probabilidad (P), calculada mediante la siguiente fórmula: 𝑃= 𝑒−(𝐹𝑂 𝑐𝑢𝑟𝑟𝑒𝑛𝑡 − 𝐹𝑂 𝑇) donde: • FO current es el valor de la función objetivo de la solución actual. • FO es el valor correspondiente a la nueva secuencia generada tras la destrucción y reconstrucción. • T es un parámetro denominado temperatura, que en esta versión del algoritmo se mantiene constante. Este parámetro permitirá diversificar las soluciones. Si T tiende a infinito, P tenderá a uno. Esto quiere decir que se tenderá a aceptar todos los movimientos de empeoramiento, cayendo en el riesgo de explorar muy superficialmente el espacio de soluciones, explotando muy poco las soluciones prometedoras. Por otro lado, si T tiende a cero, P tiende a cero también. Esto provocara que se tiendan a no aceptar ningún movimiento de empeoramiento, lo que puede conducir a una exploración escasa y a un estancamiento prematuro en un OL. En la Figura 3-14 se explica el funcionamiento de IG básico mediante un psudocódigo. Figura 3-13. Pseudocódigo de la heurística NEH [8] NEH
3.4.2 Variante IG. Temperatura Adaptativa (IG_TA) La estructura del algoritmo es exactamente la misma que el IG original, la única diferencia de esta variante es que el parámetro T se irá adaptando a las condiciones de mejora del algoritmo. Se ha utilizado de inspiración [9], donde se introduce un control adaptativo de la temperatura para el algoritmo Simulated Annealing. Las adaptaciones que sufrirá T se darán bajo la siguiente ecuación: 𝑇𝑖𝑡𝑒𝑟 =𝑇𝑀𝑖𝑛+𝑛·ln (1+𝑟𝑖𝑡𝑒𝑟) Donde: • 𝑇𝑖𝑡𝑒𝑟: Temperatura en cada iteración. • 𝑇𝑀𝑖𝑛: Temperatura mínima que puede alcanzar el algoritmo. Figura 3-14. Pseudocódigo de IG básico basado en [8]
Metodología de Resolución 24 • 𝑛: Parámetro que controla el ritmo al que crece la temperatura, cuanto mayor sea el parámetro, mayores serán los incrementos de la T. • 𝑟𝑖𝑡𝑒𝑟: número de movimientos de empeoramiento consecutivos previos a la iteración actual. La actualización de esta variable en cada iteración se hace de la siguiente forma: 𝑟𝑖𝑡𝑒𝑟 = { 𝑟𝑖𝑡𝑒𝑟−1+1 𝑠𝑖 𝑠𝑒 ℎ𝑎 𝑟𝑒𝑎𝑙𝑖𝑧𝑎𝑑𝑜 𝑢𝑛 𝑚𝑜𝑣𝑖𝑚𝑖𝑒𝑛𝑡𝑜 𝑑𝑒 𝑒𝑚𝑝𝑒𝑜𝑟𝑎𝑚𝑖𝑒𝑛𝑡𝑜 ó 𝑟𝑖𝑡𝑒𝑟−1 𝑠𝑖 𝑒𝑙 𝑣𝑎𝑙𝑜𝑟 𝑑𝑒 𝑙𝑎 𝐹𝑂 𝑛𝑜 𝑐𝑎𝑚𝑏𝑖𝑎 𝑐𝑜𝑛 𝑒𝑙 𝑚𝑜𝑣𝑖𝑚𝑖𝑒𝑛𝑡𝑜 𝑟𝑒𝑎𝑙𝑖𝑧𝑎𝑑𝑜 ó 0 𝑠𝑖 𝑠𝑒 ℎ𝑎 𝑟𝑒𝑎𝑙𝑖𝑧𝑎𝑑𝑜 𝑢𝑛 𝑚𝑜𝑣𝑖𝑚𝑖𝑒𝑛𝑡𝑜 𝑞𝑢𝑒 𝑚𝑒𝑗𝑜𝑟𝑎 𝑙𝑎 𝑠𝑜𝑙𝑢𝑐𝑖ó𝑛 𝑎𝑐𝑡𝑢𝑎𝑙 El pseudocódigo del funcionamiento del funcionamiento de la MH viene recogido en la Figura 3-15. Al iniciarse el algoritmo, la temperatura será mínima, lo que quiere decir que se aceptarán pocos movimientos de empeoramiento. Se hace de esta forma porque hay más probabilidad de que al principio se produzcan movimientos de mejora, así que la solución difícilmente se verá estancada en esta fase. Conforme el algoritmo avanza, tendrá mayor dificultad para encontrar una mejor solución, este será el momento en el que la temperatura empiece aumentar, aceptándose de manera progresiva cada vez más movimientos de empeoramiento, permitiendo así diversificar mucho más la búsqueda y evitando quedar estancados en OL. La ventaja de esta variante es que no solo permite “calentar” la temperatura cuando la solución se estanca en un OL, es que también permite “enfriarla” cuando las iteraciones empiezan a generar mejoras, reduciéndose así los movimientos de empeoramiento e intensificando la búsqueda. Figura 3-15. Pseudocódigo de IG con TA basado en [9]
3.4.3 ABC básico En este apartado se explicará el funcionamiento del ABC básico propuesto por [10]. El ABC es un algoritmo poblacional, es decir, trabaja con un conjunto de secuencias e intenta evolucionar la población favoreciendo el aumento del fitness, que será la calidad de dicha población. El funcionamiento del algoritmo trata de imitar el comportamiento de una colmena de abejas melíferas, donde distintas abejas tratan de seleccionar y mejorar diferentes fuentes de néctar. Las mejores fuentes de néctar serán las que mayor cantidad de alimento proporcionen y para mejorarlas las abejas exploran distintas fuentes en los alrededores de una ya conocida. En ABC, las fuentes de néctar serán las secuencias pertenecientes a la población, donde la cantidad de néctar que tienen corresponde al fitness de la secuencia, que se calcula con la siguiente fórmula: 𝑓𝑖𝑡𝑛𝑒𝑠𝑠𝑖= 1 𝐹𝑂𝑖 Dado que el esquema de vecindad propuesto en el artículo original fue diseñado para variables continuas, se ha sustituido este por operadores adecuados a un entorno discreto. Para ello, se utilizará el mejor OV de los explicados en el apartado 3.3. El número de secuencias de la población está establecido por el parámetro SN, es decir, la población tendrá SN secuencias. El algoritmo consta de tres tipos de abejas, las cuales se explicarán en el orden en el aparecen en el algoritmo. Las fases del algoritmo son las siguientes: • Employed bees – Fase de exploración Hay SN abejas de este tipo y cada una tiene asociada una secuencia de la población. Cada abeja tratará de mejorar la secuencia a la que está asociada, utilizando para ello el OV una sola vez por cada secuencia en cada iteración del algoritmo. • Onlooker bees – Fase de explotación Hay también SN abejas de este tipo y, a diferencia de las employed bees, no tienen asociada una secuencia de partida. Su función principal es reforzar la exploración de las soluciones más prometedoras descubiertas hasta el momento. Para ello, se aplica un mecanismo de selección probabilístico conocido como Roulette Wheel Selection (RWS), en el que la probabilidad de elegir una secuencia está directamente relacionada con su fitness, pudiéndose repetir las secuencias seleccionadas. De este modo, las soluciones de mayor calidad tienen más probabilidad de ser seleccionadas, aunque no se descartan completamente las de menor calidad, lo que introduce un componente de diversidad. La probabilidad de ser seleccionado viene dada por la siguiente fórmula. 𝑃𝑖= 𝑓𝑖𝑡𝑛𝑒𝑠𝑠𝑖 𝑇𝑜𝑡𝑎𝑙 𝑓𝑖𝑡𝑛𝑒𝑠𝑠
Resultados Computacionales 32 4.2 Generación de instancias Para la fase de calibración de los distintos métodos (como selección del operador BP o la calibración de los parámetros de las MHs), se ha realizado una batería de diez instancia por cada tamaño del problema, mientras que, para la posterior fase de experimentación, se utilizará una batería distinta de treinta instancia por cada tamaño del problema. En la Tabla 4-1 se detallan los valores de los parámetros que se utilizarán para la generación de los distintos tamaños de instancias [3]. Tabla 4-1. Datos usados para la generación de instancias H J β α γ mds 5 3, 6 125% 2 1, 1.5, 2 5 En la generación de ambas baterías se han utilizado semillas deterministas, de esta forma se asegura que cada instancia pueda ser reconstruida exactamente igual, incluso en caso de pérdida o corrupción de los archivos, garantizando la reproducibilidad del experimento y se evita además la generación de instancias repetidas de forma no intencionada. La fórmula utilizada para definir la semilla asociada a cada instancia se ha definido: 𝑠𝑒𝑚𝑖𝑙𝑙𝑎=𝑏𝑎𝑠𝑒𝑠𝑒𝑒𝑑+1000·|𝐽|+100·⌊|𝛾|∗10⌋+𝑁º𝐼𝑛𝑠𝑡𝑎𝑛𝑐𝑖𝑎 Donde la semilla base variara en función si se realiza la generación de la batería de calibración o la de experimentación. 𝑏𝑎𝑠𝑒𝑠𝑒𝑒𝑑𝑐𝑎𝑙𝑖𝑏𝑟𝑎𝑐𝑖ó𝑛 =2000 𝑏𝑎𝑠𝑒𝑠𝑒𝑒𝑑𝑒𝑥𝑝𝑒𝑟𝑖𝑚𝑒𝑛𝑡𝑎𝑐𝑖ó𝑛 =5000 • Tiempo máximo de ejecución de las MH Para la calibración de los parámetros de las MH y la posterior experimentación con ellas, se debe limitar el tiempo máximo de búsqueda, tomando como referencia la metodología propuesta en [5]. Este límite temporal se establece de forma proporcional a la dimensión del problema, con el objetivo de no penalizar injustamente a las instancias de mayor tamaño. Para ello, se define un tiempo máximo 𝑇𝑀𝑎𝑥 proporcional a la dimensión del problema, según las siguientes expresiones adaptadas a la dimensión del quirófano: 𝑇𝑀𝑎𝑥3𝑂𝑅 = |𝐼|·|𝐽|·| 𝐻|·𝜂 𝑇𝑀𝑎𝑥6𝑂𝑅 = |𝐼|·|𝐽|·| 𝐻|·𝜂·2 Tras un análisis preliminar, se ha determinado que la variación del número de camas en la PACU no repercute significativamente en el tiempo de ejecución del decoding, por lo que dicho parámetro no se tendrá en cuenta al establecer el límite de tiempo.
Asimismo, se ha observado que el tiempo de ejecución del decoding no escala linealmente con el número de quirófanos. En concreto, al duplicar el número de quirófanos de tres a seis, el tiempo de ejecución del proceso de decodificación se incrementa aproximadamente en un factor de cuatro. Este patrón será tenido en cuenta para ajustar de forma proporcional el tiempo de ejecución permitido a cada instancia. η es un factor que regula la cantidad de tiempo, viene expresado en segundos. En el artículo mencionado se comparan los resultados obtenidos al utilizar los valores 0.0125 y 0.025, concluyéndose que no hay diferencias significativas entre ambos, por tanto, se adoptará el valor más reducido. No obstante, es importante destacar que en dicho artículo no se contempla la PACU, mientras que en este trabajo sí. En consecuencia, se ha decidido adoptar un valor intermedio de η=0,0185 que permita mantener la comparabilidad con la literatura, sin subestimar la complejidad adicional introducida por la PACU. • Generación del tiempo de cirugía Para simular de forma realista la duración de las intervenciones quirúrgicas, se ha seguido el procedimiento descrito en el artículo [3] que asume que dichas duraciones siguen una distribución log-normal cuando la cirugía es realizada exclusivamente por el cirujano responsable. La duración de estas intervenciones no solo incluye el tiempo quirúrgico, también incluyen el tiempo de preparación y el tiempo de limpieza. El tiempo medio (μ) que se espera que durare la cirugía para cada paciente se selecciona aleatoriamente de entre los siguientes valores {60, 120, 180, 240} mientras que el coeficiente de variación (CV) se escogerá aleatoriamente entre {0.1, 0.2, 0.3, 0.4, 0.5}. Para garantizar la factibilidad de las soluciones generadas, se impone una restricción adicional: ningún tiempo quirúrgico puede superar la duración máxima de una jornada de quirófano (480 minutos). En caso de que se obtenga un valor superior, se repite el proceso de generación aleatoria hasta obtener un valor admisible. • Generación del tiempo de recuperación en PACU Para la generación de los tiempos de recuperación de la anestesia, se ha adoptado un enfoque basado en el estudio, [12] que analiza el ratio entre 𝑝𝑡𝑃𝐴𝐶𝑈 y 𝑝𝑡𝑂𝑅 a partir de más de 19.000 casos reales. Este estudio concluye que la relación entre 𝑝𝑡𝑃𝐴𝐶𝑈 y 𝑝𝑡𝑂𝑅 no es lineal. El cociente empleado, que se utilizará en este trabajo para modelar dicha relación, se define como: δ=𝑝𝑡𝑃𝐴𝐶𝑈 𝑝𝑡𝑂𝑅 En concreto, se observa que: • En cirugías inferiores a 130 minutos, el ratio δ tiende a ser mayor que uno. • En cirugías superiores a 130 minutos, este ratio tiende a ser menor que uno.
Resultados Computacionales 34 A partir de estos hallazgos, se ha definido la Tabla 4-2 en la que se recogen los rangos probables para el valor de δ, con el objetivo de generar tiempos de recuperación anestésica realistas para cada paciente en función de su 𝑝𝑡𝑂𝑅: Tabla 4-2. Generación de tiempos de recuperación en PACU DOS (min) Intervalos para δ < 30 [2.00 , 3.00] (30, 60] [1.40 , 2.50] (60, 120] [0.90 , 1.30] (120, 150] [0.80 , 1.10] (150, 180] [0.70 , 1.00] (180, 210] [0.55 , 0.80] (210, 240] [0.50 , 0.70] (240, 300] [0.40 , 0.60] > 300 [0.30, 0.50] Así pues, para la generación 𝑝𝑡𝑃𝐴𝐶𝑈 de cada paciente, se selecciona aleatoriamente un valor dentro del intervalo δ correspondiente a su duración 𝑝𝑡𝑂𝑅, y se aplica la siguiente ecuación: 𝑝𝑡𝑃𝐴𝐶𝑈𝑖=𝑝𝑡𝑂𝑅𝑖·𝛿𝑖 Del mismo modo que en el apartado anterior se estableció una restricción sobre la duración máxima de las cirugías para evitar generar pacientes que no pudieran ser incluidos en la planificación, se ha aplicado un criterio similar al tiempo total que un paciente permanece en el sistema, es decir, la suma de 𝑝𝑡𝑃𝐴𝐶𝑈𝑖 y 𝑝𝑡𝑂𝑅𝑖 . Si el valor generado para la suma de 𝑝𝑡𝑃𝐴𝐶𝑈𝑖 y 𝑝𝑡𝑂𝑅𝑖 de un paciente da lugar a un tiempo total superior a la duración de la jornada laboral de la PACU, se descarta dicho valor de δ y se vuelve a seleccionar aleatoriamente otro valor dentro del intervalo correspondiente. Este proceso se repite hasta que se obtenga un paciente cuya permanencia total en el sistema sea admisible y planificable. Este mecanismo asegura la validez operativa de todos los pacientes generados y evita introducir en la instancia cirugías que, por su duración y recuperación asociada, no podrían ser planificadas dentro del horizonte temporal definido. • Generación de la WL Para obtener la WL, se generan pacientes con sus correspondientes tiempos hasta que el sumatorio del 𝑝𝑡𝑂𝑅, sobrepase o iguales la capacidad máxima de los quirófanos en el horizonte de planificación multiplicado por el parámetro 𝛽. Pese a que en artículo se utiliza un parámetro específico para definir la capacidad de cada quirófano en cada día, para este trabajo no tenía mucho sentido definir dicho parámetro, ya que la jornada laboral es fija y es de 480 minutos. Por tanto, se generarán pacientes hasta que se cumpla: 𝛽·|𝐽|·|𝐻|·480≤ ∑𝑝𝑡𝑂𝑅 𝑖 𝑖𝜖|𝐼|
Enfatizar el hecho de que β = 125% representando escenarios de alta saturación como los vividos actualmente, donde ni siquiera utilizando el 100% de la capacidad de quirófanos se consiguen atender a todos los pacientes de la WL. • Generación de las camas de PACU El número de camas en la PACU corresponde al entero inferior del producto del parámetro de control 𝛾 por el número de quirófanos de la instancia: |𝑃|=⌊ 𝛾· |𝐽|⌋ • Generación del número de cirujanos El número de cirujanos se generará dividendo la capacidad de los quirófanos entre la capacidad de los cirujanos y se multiplicará por el parámetro de control 𝛼: |𝑆|=𝛼·|𝐻|·|𝐽|·480 𝑚𝑑𝑠·480 • Generación del peso clínico de cada paciente Se ha tomado como referencia la propuesta descrita en el artículo [3]. En dicho trabajo se sugiere que el peso clínico debe reflejar una combinación de dos factores que afectan directamente a la prioridad con la que un paciente debe ser atendido, dándole la misma importancia a ambos. En concreto, se ha definido una combinación lineal de los siguientes dos factores: - Prioridad clínica (PC). Representa la urgencia médica de la intervención quirúrgica. Para cada paciente, este valor se genera como un número entero aleatorio entre 1 y 5, siendo 5 el valor de máxima urgencia. - Tiempo en la WL (TWL). Es importante considerar este factor para así tratar de reducir el número de pacientes que sobrepasan el límite legal de seis meses en la WL. Se generará como para cada paciente como un número aleatorio entero entre uno y trescientos sesenta y cinco. La normalización de ambos factores para cada paciente se hará de la siguiente forma: 𝑃𝐶𝑖∗=𝑃𝐶𝑖 ∑𝑃𝐶𝑖 𝑖∈|𝐼| 𝑇𝑊𝐿𝑖∗=𝑇𝑊𝐿𝑖 ∑𝑇𝑊𝐿𝑖 𝑖∈|𝐼| De modo que el peso clínico de cada paciente (ω) se calcula como: 𝜔𝑖=𝑃𝐶𝑖∗+ 𝑇𝑊𝐿𝑖∗ Finalmente, se realiza una segunda normalización sobre el conjunto de pesos para su correcta incorporación en la FO:
Resultados Computacionales 36 𝜔𝑖∗=𝜔𝑖 ∑𝜔𝑖𝑖∈|𝐼| • Tamaños promedio de los problemas En la Tabla 4-3. Tamaño promedio de los problemas vienen recogido el tamaño promedio de los problemas que surgen con la combinación de los distintos parámetros. Lo tiempos de paradas se refiere al momento en el que dejará de ejecutarse la MH. Tabla 4-3. Tamaño promedio de los problemas 4.3 Herramientas utilizadas para la evaluación de alternativas Para realizar la calibración de los elementos descritos en cada una de las etapas de la resolución del problema detalladas en la sección 3, y la posterior experimentación con las MH se han comparado distintos indicadores relevantes como el valor de la FO o el tiempo de ejecución. Dado que los valores absolutos de estos indicadores pueden verse sesgados por el tamaño de la instancia (las instancias de mayor dimensión tienden a generar peores resultados, como valores de la FO más altos o mayores tiempos de ejecución), se ha decidido utilizar el indicador Relative Percentage Desviation (RPD), ampliamente usado en la literatura de MH [3] o [8]. El RPD permite evaluar el rendimiento de un método con respecto al mejor resultado obtenido en una misma instancia. Es decir, proporciona una medida normalizada del rendimiento en cada instancia, que evita distorsiones derivadas del tamaño, de modo que, cuanto más cercano a cero sea el RPD, mejor será el rendimiento relativo del método evaluado respecto al resto en una misma instancia. El RPD se calculará de la siguiente forma: 𝑅𝑃𝐷= 𝑉𝑎𝑙𝑢𝑒−𝐵𝑒𝑠𝑡𝑉𝑎𝑙𝑢𝑒 𝐵𝑒𝑠𝑡𝑉𝑎𝑙𝑢𝑒 ·100 Posteriormente, se calcula el Average Relative Percentage Deviation (ARPD), que se define como el promedio de los RPD obtenidos para cada método a lo largo de todas las instancias. Por tanto, el ARPD, sirve como criterio comparativo global entre enfoques. Todos lo ARPD mostrados en el documento están expresados en porcentaje. J P S I Tiempo de parada (s) 3 3 6 60,83 11,61 3 4 6 61,53 11,73 3 6 6 61,03 11,63 6 6 12 123,23 47,27 6 9 12 122,33 46,95 612 12 121,37 46,44
Pese a que el ARPD es una métrica ampliamente reconocida por su capacidad para resumir el rendimiento medio de un método, su uso exclusivo puede ocultar aspectos relevantes del comportamiento del método, al no reflejar la variabilidad entre las instancias de los RPD obtenidos por cada método. Dos métodos pueden mostrar ARPD similares, pero comportamientos muy distintos, uno puede ofrecer resultados más consistentes, mientras que otro puede alternar entre RPD más altos y bajos, con mayor dispersión. Para analizar visualmente este tipo de diferencias, se utilizan diagramas de caja (boxplots), que permiten representar la dispersión de los resultados obtenidos e identificar posibles valores atípicos . 1. Caja central: Representa el rango intercuartílico (IQR), que es el 50% central de los datos. Por tanto, la caja esta delimitada por: • Q1 (primer cuartil): el 25% de los datos están por debajo de este valor. • Q3 (tercer cuartil): el 75% de los datos están por debajo de este valor. Cuanto mayor sea la caja, más dispersión mostraran los datos centrales. 2. Línea horizontal de dentro de la caja: indica la mediana (Q2). El 50 % de los datos están por debajo de ella y el 50 % por encima. Si la mediana no está centrada en la caja, indica asimetría en la distribución. 3. X de dentro de la caja: Representa la media aritmética. En este trabajo, cuando se muestran boxplots del RPD, esta “X” corresponde al ARPD del método. 4. Líneas unidas al extremo inferior y superior de la caja: A estas líneas se le denominan bigotes y muestran el rango de valores que están fuera de IQR y que son considerados valores normales. Los límites que se establecen normalmente para estos bigotes son los siguientes: 𝐿í𝑚𝑖𝑡𝑒 𝑖𝑛𝑓𝑒𝑟𝑖𝑜𝑟 = 𝑄1−1.5⋅𝐼𝑄𝑅 𝐿í𝑚𝑖𝑡𝑒 𝑠𝑢𝑝𝑒𝑟𝑖𝑜𝑟 = 𝑄3+1.5⋅𝐼𝑄𝑅 5. Puntos a los bordes de los bigotes: Estos puntos se conocen como outliers y son valores atípicos, es decir, sobrepasan los límites establecidos en los bigotes. En este trabajo se utilizarán boxplots para representar de forma visual los valores del RPD obtenidos por cada método a lo largo de todas las instancias. Dentro de cada caja, se incluye una “X” que indica el valor del ARPD correspondiente al método evaluado. Aunque los boxplots no permiten extraer conclusiones estadísticas formales, su uso complementa al análisis basado exclusivamente en el ARPD, ya que permite visualizar la variabilidad de los valores del RPD en cada técnica. Durante toda la calibración se mostrarán los boxplot y los resultados del ARPD para cada tamaño de problema.
Resultados Computacionales 38 4.4 Resultados de la calibración de las metaheurísticas En este apartado se seleccionarán las mejores alternativas de las variantes presentadas en cada una de las fases de la metodología de resolución, explicadas en la sección 3 y se realizará la calibración de los parámetros que utilizarán las MH. 4.4.1 Selección del operador BP Para elegir el operador BP, se evaluará tanto la calidad que es capaz de ofrecer cada uno, medida en términos de FO, como el tiempo que tardan en ejecutar una secuencia dada. Se busca seleccionar aquel operador que ofrezca un mejor equilibrio entre ambos criterios. La inclusión del tiempo como criterio adicional se justifica en que las MH modifican iterativamente secuencias para mejorar la solución, por lo que un operador BP más eficiente computacionalmente permite ejecutar más iteraciones en el mismo tiempo, lo que puede traducirse en una mayor capacidad de mejora y, por tanto, una mayor eficacia global del algoritmo. Tras el análisis de los resultados, se ha optado por utilizar WF_C como operador BP, al presentar un excelente equilibrio entre calidad de solución y eficiencia computacional. Tal y como muestra la Figura 4-2, WF_C no solo presenta el ARPD para la FO más bajo, sino que también muestra una mayor estabilidad en los valores de RPD a lo largo de todas la instancias. En cuanto al tiempo de computación, como se puede observar en la Figura 4-1, es el segundo operador más rápido y el segundo con menos variabilidad entre instancias. 0 50 100 150 200 250 300 350 400 RPD BP - FO FF WF_W BF_W WF_C BF_C 0 50 100 150 200 250 300 350 400 450 RPD BP - Tiempo ejecución FF WF_W BF_W WF_C BF_C Figura 4-1. Boxplot del RPD del tiempo de ejecución de los operadores BP Figura 4-2. Boxplot del RPD de la FO de los operadores BP
4.4.2 Selección de las HC Como se explicó en el apartado 3.2, las HC se emplearán tanto para generar la secuencia inicial de las variantes de IG como para definir la población inicial de las variantes de ABC. Tal y como se muestra en la Figura 4-3, WSPT_OR es la mejor DR, siendo la que mejor ARPD y menor variabilidad en los valores de RPD presenta. Además, se han seleccionado las siguientes HC que constituirán parte de la población inicial de los algoritmos ABC, dados sus rendimientos y estabilidad a lo largo de las instancias: WSPT_PACU, WSPT_OR__PACU, Regla de Johnson y SPT_PACU. Por otro lado, las HC que no se deben usar bajo ningún concepto son las LPT, nisiquiera para la generación de individuos de la población para ABC y su variante. Los valores del ARPD de estas HC distan en más de un 100% de la mejor alcanzada y la dispersión en los valores de RPD es muy alta. Tabla 4-4. ARPD de la FO de operadores BP para cada tamaño de problema J P FF WF_W BF_W WF_C BF_C 3 3 39,63 48,20 27,24 18,33 33,59 3 4 36,83 54,69 41,18 10,33 35,52 3 6 23,10 152,41 32,53 21,47 44,90 6 6 51,82 155,14 57,63 1,01 73,65 6 9 31,89 225,99 27,60 12,43 23,88 612 24,01 221,23 13,15 6,31 15,74 34,55 142,94 33,22 11,65 37,88 ARPD TOTAL Tabla 4-5. ARPD del tiempo de computación de operadores BP para cada tamaño de problema J P FF WF_W BF_W WF_C BF_C 3 3 109,85 2,82 139,85 46,65 117,90 3 4 94,33 4,99 120,68 39,96 123,72 3 6 86,29 1,40 101,75 35,60 113,95 6 6 272,76 0,00 305,82 122,65 309,99 6 9 241,68 0,00 293,92 123,64 262,01 612 241,51 0,00 273,39 120,77 256,94 174,40 1,54 205,90 81,55 197,42 ARPD TOTAL
Resultados Computacionales 40 0 50 100 150 200 250 300 350 400 450 500 550 RPD HC Random SPT_OR SPT_PACU SPT_OR_PACU LPT_OR LPT_PACU LPT_OR_PACU WSPT_OR WSPT_PACU WSPT_OR_PACU WLPT_OR WLPT_PACU WLPT_OR_PACU JOHONSON Figura 4-3. Boxplot del RPD de la FO de las HC
4.4.3 Selección del OV Para la selección del OV, no es solo relevante el valor alcanzado de la FO, sino también cuánto ha conseguido mejorar una solución inicial tras un número determinado de iteraciones y el tiempo necesario para llevar a cabo dichas iteraciones. Esta relación resulta clave, ya que los OV formarán parte del flujo principal de las variantes del ABC, por lo que conviene que sean lo más eficientes posible a la hora de encontrar nuevas soluciones, es decir, que obtengan la mayor mejora en el menor tiempo. Se evaluará cada OV con 25 iteraciones, para analizar su capacidad de generar mejoras rápidas, y con 100 iteraciones, para valorar su rendimiento sostenido a lo largo del tiempo y detectar posibles estancamientos. Para cuantificar esta eficiencia, en este TFG se propone un indicador específico descrito a continuación. La mejora relativa de la FO indica cuánto consigue mejorar el operador una solución inicial, y se calculará como: 𝑀𝑒𝑗𝑜𝑟𝑎 𝐹𝑂 (%)= 𝐹𝑂𝑖𝑛𝑖𝑐𝑖𝑎𝑙−𝐹𝑂 𝐹𝑂𝑖𝑛𝑖𝑐𝑖𝑎𝑙 A partir de la mejora y el tiempo de cómputo se define el indicador de eficiencia, siguiendo la siguiente fórmula: J P Random SPT_O SPT_P SPT_O_P LPT_O LPT_P LPT_O_P 3 3 54,57 37,05 40,40 49,04 273,24 142,34 247,68 3 4 63,71 57,26 55,96 54,38 290,23 107,78 324,08 3 6 60,42 66,68 52,89 52,22 268,40 143,22 278,45 6 6 53,98 54,82 25,49 47,92 175,80 126,32 198,01 6 9 29,61 58,23 26,60 45,51 297,00 124,08 297,42 612 24,89 55,66 27,34 38,20 210,90 102,38 221,22 47,86 54,95 38,11 47,88 252,60 124,36 261,14 ARPD TOTAL Tabla 4-6. ARPD de la FO de HC para cada tamaño de problema I J P WSPT_O WSPT_P WSPT_O_P WLPT_O WLPT_P WLPT_O_P JOHN. 3 3 38,46 33,03 14,46 130,32 50,86 113,25 35,97 3 4 13,56 40,14 38,69 98,38 69,49 100,83 43,26 3 6 14,65 43,04 29,89 79,65 87,45 107,89 43,51 6 6 4,50 21,87 19,87 132,46 50,37 92,35 29,45 6 9 14,14 43,26 26,12 110,66 51,88 81,14 29,33 612 12,41 19,24 6,70 106,37 57,43 104,33 26,92 16,29 33,43 22,62 109,64 61,25 99,96 34,74 ARPD TOTAL Tabla 4-7. ARPD de la FO de HC para cada tamaño de problema II
Resultados Computacionales 48 4.4.5.2 Parámetros ABC con elitismo Para la definición de los parámetros de este algoritmo se ha seguido el mismo procedimiento que con el ABC básico. Se ha utilizado de referencia el artículo original [7] y se han realizado pruebas preliminares para ver como afectaban los parámetros a las instancias. Además, en este algoritmo se utilizan dos OV, que serán los mejores seleccionados en 4.4.3, es decir, RI y RS. Tal y como se puede comprobar en la Figura 4-9, la mejor solución la ofrece la combinación de parámetros recogida en la Tabla 4-17. Tabla 4-17. Mejor combinación de parámetros de ABC con elitismo SN S Élite MIWOI 15 30 3 10 0 20 40 60 80 100 120 RPD parámetros ABC_ELITISM SN=15,S=15,Elite=3,MIWOI=10 SN=15,S=15,Elite=3,MIWOI=20 SN=15,S=30,Elite=3,MIWOI=10 SN=15,S=30,Elite=3,MIWOI=20 SN=30,S=15,Elite=3,MIWOI=10 SN=30,S=15,Elite=3,MIWOI=20 SN=30,S=30,Elite=3,MIWOI=10 SN=30,S=30,Elite=3,MIWOI=20 SN=45,S=15,Elite=3,MIWOI=10 SN=45,S=15,Elite=3,MIWOI=20 SN=45,S=30,Elite=3,MIWOI=10 SN=45,S=30,Elite=3,MIWOI=20 Figura 4-9. Boxplot del RPD de la FO de las combinaciones de parámetros para ABC con elitismo
4.5 Experimentación Tras la calibración, se puede dar paso a evaluar cuál de las MH ha generado un mejor resultado, para seleccionarla como el método que usará en la resolución de la programación integrada de quirófano y PACU. Como se puede observar en la Figura 4-10, tanto IG básico, como IG_TA y ABC_ELITISM presentan resultados bastante similares, por lo que, se seleccionará IG en su versión básica como MH para realizar la programación por su simplicidad en la implementación. Tabla 4-20. Experimentación MH - ARPD FO para todos los tamaños de problemas J P IG IG_TA ABC ABC_ELITISM 3 3 14,99 12,08 41,10 18,58 3 4 13,49 13,95 27,94 17,08 3 6 11,77 14,85 34,86 9,35 6 6 6,52 8,50 30,01 16,42 6 9 9,27 13,36 19,30 9,17 612 14,32 11,52 24,08 8,58 11,728 12,375 29,550 13,197 ARPD TOTAL J P SN=15,S=15, MIWOI=10 SN=15,S=15, MIWOI=20 SN=15,S=30, MIWOI=10 SN=15,S=30, MIWOI=20 SN=30,S=15, MIWOI=10 SN=30,S=15, MIWOI=20 3 3 31,89 31,46 13,29 22,00 37,00 41,92 3 4 20,28 27,70 16,21 16,28 34,81 31,27 3 6 44,85 32,93 15,57 20,62 46,05 36,81 6 6 22,33 25,58 8,10 11,92 32,29 28,11 6 9 16,58 16,10 18,29 12,93 20,82 21,44 612 21,36 20,03 17,43 13,46 22,84 25,44 26,22 25,64 14,82 16,20 32,30 30,83 ARPD TOTAL Tabla 4-18. ARPD de la FO de las combinaciones de parámetros de ABC con elitismo para todos los tamaños de problemas I J P SN=30,S=30, MIWOI=10 SN=30,S=30, MIWOI=20 SN=45,S=15, MIWOI=10 SN=45,S=15, MIWOI=20 SN=45,S=30, MIWOI=10 SN=45,S=30, MIWOI=20 3 3 35,29 30,30 43,36 50,13 38,23 24,57 3 4 25,46 23,79 41,34 32,93 31,99 28,04 3 6 30,87 41,35 38,50 46,76 42,99 29,30 6 6 18,35 17,09 25,09 32,83 18,99 18,66 6 9 13,09 10,17 25,77 26,93 20,32 20,97 612 14,06 18,69 31,02 32,18 21,56 17,83 22,85 23,56 34,18 36,96 29,01 23,23 ARPD TOTAL Tabla 4-19. ARPD de la FO de las combinaciones de parámetros de ABC con elitismo para todos los tamaños de problemas II
Resultados Computacionales 50 La selección final de los métodos que se utilizarán para realizar la programación integrada de quirófano y PACU vienen resumidos en la Tabla 4-21. Tabla 4-21. Resumen de la selección de métodos para la programación integrada de quirófanos y PACU Operador BP HC MH WF_C WSPT_OR IG básico 4.6 Políticas de camas de la PACU Una vez realizada la experimentación y seleccionada la MH, que ha sido IG básico, se evaluará como afectan las distintas políticas de cama en la PACU tanto en el valor de la FO como en los siguientes indicadores de gestión: • Utilización de los quirófanos. • Número de bloqueos en quirófanos. 0 20 40 60 80 100 120 Experimentación MH RPD - FO IG IG_TA ABC ABC_ELITISM Figura 4-10. Boxplot del RPD de la FO de las MH
Para la evaluación de los resultados obtenidos al aplicar diferentes configuraciones de la PACU, se ha optado por dividir el análisis según el número de quirófanos presentes en cada instancia, es decir, se analizan por separado los casos con tres y con seis quirófanos. En este apartado, el uso del RPD deja de ser pertinente, ya que no se pretende comparar distintos métodos aplicados a una misma instancia, como sería el caso de un análisis entre MH, sino estudiar cómo varían los valores promedios de los indicadores al modificar la dimensionalidad de la PACU. Al comparar instancias con distinta capacidad de camas en PACU, pero con idéntica configuración de quirófanos, es posible aislar el impacto que tiene la PACU en el rendimiento del sistema, y así valorar hasta qué punto limita o condiciona la calidad de la solución alcanzada. Como muestra la Tabla 4-22, para instancias con tres quirófanos, incrementar la relación de camas PACU de γ = 1 a γ = 1.5 mejora notablemente la FO, aumenta la utilización de quirófanos y reduce drásticamente el número de bloqueos. Sin embargo, el salto de γ = 1.5 a γ = 2 apenas aporta mejoras adicionales. Este impacto de la PACU es similar en instancias con seis quirófanos como se muestra en la Tabla 4-23. Estos resultados permiten concluir que la relación más equilibrada entre quirófanos y camas PACU se obtiene con γ = 1.5, es decir, 1.5 camas por cada quirófano disponible. Tabla 4-22. Política de PACU en tres quirófanos Promedio 3 PACU (γ=1) 4 PACU (γ=1.5) 6 PACU (γ=2) FO 0.4748 0.4068 0.3930 Nº de bloqueos 9.23 2.20 0.07 % U OR 87.46 89.06 89.42 Tabla 4-23. Política de PACU en seis quirófanos Promedio 6 PACU (γ=1) 9PACU (γ=1.5) 12PACU(γ=2) FO 1.1704 1.0961 1.1092 Nº de bloqueos 9.23 2.20 0.07 % U OR 85.16 85.33 85.78
Resultados Computacionales 52
5 CONCLUSIÓN En este trabajo se ha desarrollado con éxito una metodología para la optimización mediante el uso de MH de la programación integrada de quirófanos y PACU, empleando concretamente IG y ABC, y una variante cada una. La metodología de resolución aborda el problema con un enfoque realista, considerando restricciones operativas y recursos críticos, como quirófanos, cirujanos y camas PACU, siendo los objetivos que se pretenden alcanzar maximizar la planificación de los pacientes con más peso clínico y reducir los tiempos ociosos de cirujanos y quirófanos para garantizar que el sistema sea eficiente. Se han evaluado cinco operadores BP distintos para la correcta decodificación de la secuencia, donde el mejor ha resultado ser el operador WFC con una consistencia claramente superior al resto en cuanto a la relación de la calidad de la solución y los recursos computacionales requeridos para alcanzarla. Este operador trata de asignar a cada paciente, según el orden definido en la secuencia, al quirófano que ofrezca mayor disponibilidad en todo el horizonte de planificación. Pero como ha quedado explicado, estos operadores no construyen las distintas secuencias que servirán como secuencia inicial en las variantes de IG o como primera población en las variantes de ABC. Para la generación de estas secuencias, se han evaluado múltiples HC con el fin de obtener secuencias de buena calidad para que le sirvieran como punto de partida a los MH, erigiéndose como la mejor WSPT_OR, que ordena a los pacientes con menor tiempo de duración y más peso clínico al principio. Los OV son los encargados de generar secuencias vecinas a una dada, con el fin de explorar el espacio de soluciones para mejorarla. Concretamente, en el caso de este trabajo los OV se han utilizado para la mejora de secuencias dentro de las variantes del ABC. Se han comparado dos OV clásicos (RI y RS) y cuatro operadores específicos adaptados al problema quirúrgico (ES, IS, EI, II). Los resultados indican que los operadores clásicos obtienen un mejor equilibrio entre mejora alcanzada y coste computacional, superando a los operadores diseñados específicamente para quirófanos en términos de eficiencia. En cuanto a las MH, se han implementado dos enfoques distintos: IG y ABC junto con una variante de cada uno: IG_TA y ABC con elitismo. Para todas las variantes se ha realizado una calibración de sus parámetros y posteriormente se ha realizado una experimentación para compararlos entre ellos. Las tres variantes IG, IG_TA y ABC_ELITISM han mostrado rendimientos muy similares, sin diferencias significativas. Por tanto, se ha seleccionado IG en su versión básica por la simplicidad en su implementación. Para la evaluación de los resultados computacionales se han utilizado herramientas como el ARPD para evaluar le rendimiento general de cada método y boxplot para visualizar la dispersión en los valores de RPD obtenidos a lo largo de todas las instancias por cada método. Por último, se han analizado diferentes políticas de dimensionamiento de la PACU, comprobando que la política de camas que ofrece mejores resultados es en la que hay 1.5 camas por cada quirófano.
Conclusión 54 5.1 Futuras líneas de investigación A partir de los resultados obtenidos, se identifican varias oportunidades de mejora y ampliación del estudio: • Realizar una simulación de Montecarlo para evaluar la robustez del programa quirúrgico obtenido. Se debe contrastar como responde el calendario a la variabilidad real del sistema (duraciones quirúrgicas inciertas, retrasos, cancelaciones, etc.), para ver si el calendario obtenido es realmente práctico o debido a la incertidumbre derivada del comportamiento real del sistema es inviable aplicarlo. • Aplicar test estadísticos de mayor formalidad que los propuesto como ANOVA, con el fin de seleccionar con rigor estadístico entre las soluciones obtenidas. • Implementar la metodología completa con el operador WF_W, que, aunque genera soluciones de menor calidad, presenta un coste computacional muy reducido, lo que puede conducir a alcanzar excelentes soluciones integrandolo como operador de BP en las MH. • Elaborar la programación en la que se añada una restricción donde ciertos pacientes de la WL deben ser intervenidos obligatoriamente dentro del horizonte de planificación. Estos casos existen y permitiría generar soluciones más realistas. • Aumentar el tiempo de ejecución de los algoritmos para permitir una mayor exploración del espacio de soluciones. • Incluir una búsqueda local tras la fase de reconstrucción del IG, siendo esta una variante propuesta en el artículo original [8]. • Probar una calibración más agresiva de los parámetros de IG_TA. En el presente trabajo se ha observado que las combinaciones evaluadas ofrecen rendimientos similares, por lo que un ajuste más extremo de los parámetros podría ser útil para evaluar su impacto real en la calidad de las soluciones. .
55 REFERENCIAS [1] Ministerio de Sanidad. Sistema de información sobre listas de espera en el Sistema Nacional de Salud [Internet]. 2024. Report No.: RD 605/2003. Disponible en: https://www.sanidad.gob.es/estadEstudios/estadisticas/inforRecopilaciones/docs/LISTAS_PUBLICACIO N_jun2024.pdf [2] Junta de Andalucía. Decreto 209/2001, de 18 de septiembre, por el que se establece la garantía de plazo de respuesta quirúrgica en el Sistema Sanitario Público de Andalucía. Boletín Oficial de la Junta de Andalucía (BOJA), núm. 114, 2 oct 2001. Disponible en: https://www.sspa.juntadeandalucia.es/servicioandaluzdesalud/ciudadania/derechos-y-garantias/tiemposde-respuesta-asistencial-listas-de-espera/plazos-y-garantías [3] Molina-Pariente JM, Fernandez-Viagas V, Framinan JM. Integrated operating room planning and scheduling problem with assistant surgeon dependent surgery durations. Computers & Industrial Engineering. abril de 2015;82:8-20. [4] Perez Gonzalez P, Framinan Torres JM. Programación y Control de la Producción. Sevilla: Universidad de Sevilla, Escuela Técnica Superior de Ingeniería; 2023. Material docente nº 1065. [5] Molina-Pariente JM, Hans EW, Framinan JM, Gomez-Cia T. New heuristics for planning operating rooms. Computers & Industrial Engineering. diciembre de 2015;90:429-43 [6] Johnson SM. Optimal twoand three-stage production schedules with setup times included. Naval Research Logistics Quarterly. 1954;1(1):61-8. [7] Lin YK, Li MY. Solving Operating Room Scheduling Problem Using Artificial Bee Colony Algorithm. Healthcare. 2 de febrero de 2021;9(2):152. [8] Ruiz R, Stützle T. A simple and effective iterated greedy algorithm for the permutation flowshop scheduling problem. European Journal of Operational Research. marzo de 2007;177(3):2033-49. [9] Azizi N, Zolfaghari S. Adaptive temperature control for simulated annealing: a comparative study. Computers & Operations Research. diciembre de 2004;31(14):2439-51. [10] Karaboga D, Basturk B. A powerful and efficient algorithm for numerical function optimization: artificial bee colony (ABC) algorithm. J Glob Optim. 9 de octubre de 2007;39(3):459-71. [11] framinan. GitHub. [citado 7 de mayo de 2025]. Home. Disponible en: https://github.com/framinan/scheptk/wiki/Home
Referencias 56 56 [12] Weissman C, Scemama J, Weiss YG. The ratio of PACU length‐of‐stay to surgical duration: Practical observations. Acta Anaesthesiol Scand. octubre de 2019;63(9):1143-51.
ANEXO Código Generación de instancias import pandas as pd import numpy as np from scheptk.util import read_tag, print_tag def pesos(jobs): "Se generan PRIORIDADES QUIRURJICAS que van de 1 a 5 y luego se normaliza" p = np.random.randint(1, 6, jobs) P=0 for j1 in range(jobs): P+=p[j1] p_norm=[] for j2 in range(jobs): p_norm.append(p[j2]/P) "Se generan los días que el paciente lleva en la lista de espera como un número aleatorío entre 1 y 365" wl = np.random.randint(1, 365, jobs) #Calcula tiempo total que han estado esperando los pacientes TWL=0 for j3 in range(jobs): TWL+=wl[j3] #Se hace la lista de tiempos de espera de pacientes normalizada wl_norm=[] for j4 in range(jobs): wl_norm.append(wl[j4]/TWL) "Los pesos serán una combinación lineal de ambos" pesos=[round(0.5*p_norm[j5] + 0.5*wl_norm[j5],5) for j5 in range(jobs)] return pesos "Creación de batería usada para la calibración (semilla base 2000)(10 archivos por cada tamaño)" PH = 5 OR = [3, 6] PACU_FACTOR = [1, 1.5, 2, 2.5, 3]
Anexo 64 64 "No se considera idle hasta realizada la primera operación" fin_paciente_previo = instancia.completion_time[pacientes_operados[r][j]][0] else: inicio_paciente_actual = instancia.completion_time[pacientes_operados[r][j]][0] - instancia.pt[0][pacientes_operados[r][j]] idle_surgien[r] += inicio_paciente_actual - fin_paciente_previo fin_paciente_previo = instancia.completion_time[pacientes_operados[r][j]][0] c+=1 total_idle_surgien = 0 for r in range(instancia.resources): total_idle_surgien += idle_surgien[r] #Tiempo total disponible de quirófano total_time_OR = 480*instancia.ph*instancia.machines[0] #Tiempo total que pueden trabajar los cirujanos total_time_surgien = 480*instancia.ph*instancia.resources #Sumatorio de los pesos de los pacientes no planificados non_planified_prio = 0 for np in range(len(instancia.non_planified)): non_planified_prio += instancia.w[S.index(instancia.non_planified[np])] #Cálculo del valor de la FO FO = 0.25*((total_idle_surgien/total_time_surgien) + (total_idle_machine/total_time_OR)) + 0.75*non_planified_prio*len(instancia.non_planified)/instancia.jobs return FO #Realiza el reparto de cirujanos def reparto_cirujanos(nro_cirujanos,nro_pacientes): #Si division entre pacientes y cirujanos es par, todos los cirujanos tendran los mismos pacientes if nro_pacientes%nro_cirujanos==0: surgien=[[0 for j in range(nro_pacientes//nro_cirujanos)] for r in range(nro_cirujanos)] else: surgien=[[0 for j in range(nro_pacientes//nro_cirujanos + 1)] if r<nro_pacientes%nro_cirujanos \ else [0 for j in range(nro_pacientes//nro_cirujanos)]for r in range(nro_cirujanos)] c=0 if nro_pacientes%nro_cirujanos==0:
for j in range(nro_pacientes//nro_cirujanos): for r in range(nro_cirujanos): surgien[r][j]=c c+=1 else: for j in range(nro_pacientes//nro_cirujanos+1): for r in range(nro_cirujanos): if c<nro_pacientes: surgien[r][j]=c c+=1 return surgien def conflicto_recursos(reparto,current_job,OR,ct_machines,ct,pt, non_planified): # Comprobamos si comparten recursos for r in range(len(reparto)): if current_job in reparto[r]: cirujano=r break #Lista con trabajos secuenciados antes que el actual y que comparte cirujanos con el trabajos_previos = [z for z in reparto[cirujano] if z != current_job and z not in non_planified and ct[z][0] > 0] d = pt[0][current_job] # Duración del trabajo actual #El while asegurará evaluar todos los posibles casos de conflicto conflicto = True while conflicto: conflicto = False for z in trabajos_previos: a = ct[z][0] # Tiempo de fin del trabajo z b = pt[0][z] # Duración del trabajo z c = ct_machines[0][OR] # Tiempo de finalización del trabajo actual inicio_z = a - b fin_z = a inicio_current = c - d fin_current = c "Si hay un mismo recurso en distintos quirofanos al mismo tiempo, se lleva el trabjo una vez que finalice en la maquina" #Evalua: #a) El trabajo termina en un intervalo invalido #b) El trabajo empieza en un intervalo invalido #c) El trabajo termina después y empieza antes if (inicio_z < fin_current <= fin_z) or (inicio_z <= inicio_current < fin_z) or (fin_z <= fin_current and inicio_current <= inicio_z): ct_machines[0][OR]= a + d conflicto=True
Anexo 66 66 break return ct_machines def asignar_PACU_C(machines,ct_machines_day,ct,OR,current_job,pt,ph,machines_used ,lista_bloqueos,nuevos_pt_pacu,dia,delay_pacu): asignado=False #Guardamos los bloqueos que provocarían las PACU en ese día bloqueos = [max(0, ct_machines_day[1][u + machines[1]*(dia-1)] - ct_machines_day[0][OR]) for u in range(machines[1])] #Buscamos la maquina con más hueco, introducimos ahi el trabajo PACU = find_index_min(bloqueos) PACU_MIN = PACU + machines[1]*(dia-1) bloqueo_urpa = bloqueos[PACU] #La PACU no puede comenzar hasta que se quede libre una PACU #Si la OR se bloqueara, todo ese tiempo se le restará a la PACU, ya que el paciente estará recibiendo los cuidados necesarios en la OR #Si el tiempo de bloqueo excede al tiempo de trabajo de la PACU, significa que el paciente ha completado su recuperación en quirófano, no tendrá que pasar por la PACU bloqueo_urpa = min(bloqueo_urpa, pt[1][current_job]) start_PACU = ct_machines_day[1][PACU_MIN] ct_machines_day[1][PACU_MIN] = max(ct_machines_day[1][PACU_MIN], ct[current_job][0]) + pt[1][current_job] - bloqueo_urpa # Verificar si finaliza dentro de una ventana no válida inicio_ventana_PACU = (dia-1)*1440 + 480 + delay_pacu fin_ventana_PACU = dia*1440 if not (inicio_ventana_PACU < ct_machines_day[1][PACU_MIN] < fin_ventana_PACU): asignado=True ct[current_job][1]=ct_machines_day[1][PACU_MIN] #SI el trabajo no entra directamente en la PACU, la OR se queda bloqueada ct_machines_day[0][OR] += bloqueo_urpa #Si toda la recuperacion se hace quirofano, no se pasa por PACU if bloqueo_urpa < pt[1][current_job]: machines_used[1][current_job] = PACU else: machines_used[1][current_job] = 'none' registrar_bloqueos(bloqueo_urpa, nuevos_pt_pacu, lista_bloqueos, pt, current_job, machines_used, ct) else: ct_machines_day[1][PACU_MIN] = start_PACU
machines_used[1][current_job] = 'none' return asignado def registrar_bloqueos(bloqueo_urpa, nuevos_pt_pacu, lista_bloqueos, pt, current_job, machines_used, ct): #Guardamos el nuevo pt de la PACU para dibujarlo en el gantt (ya que en caso de haber existido bloqueo este será menor) nuevos_pt_pacu[current_job] = pt[1][current_job] - bloqueo_urpa #Registramos la duración del bloqueo del OR para dibujarlo en el gantt, para ello, se comprueba si el quirofano queda bloqueado if bloqueo_urpa > 0: lista_bloqueos.append({ 'job': current_job, 'or': machines_used[0][current_job], 'start': ct[current_job][0], 'dur': bloqueo_urpa }) def gantt(self, sequence): pio.renderers.default = 'browser' # Abrir en navegador fig = go.Figure() colors = {} reparto=reparto_cirujanos(self.resources, self.jobs) #Vector que guarda el cirujano que usa cada paciente cirujano_j=[0 for _ in range(self.jobs)] for j in range(self.jobs): for r in range(len(reparto)): if j in reparto[r]: cirujano_j[j]=r # Crear recursos OR y PACU completos (aunque no se usen) resources = [f'PACU {i}' for i in reversed(range(self.machines[1]))] + [f'OR {i}' for i in reversed(range(self.machines[0]))] # OR (etapa 0) for j in range(self.jobs): if sequence[j] not in self.non_planified: recurso_nombre = f"OR {self.machines_used[0][sequence[j]]}" y_index = resources.index(recurso_nombre) start = self.completion_time[sequence[j]][0] - self.pt[0][sequence[j]]
Anexo 68 68 if sequence[j] not in colors: hue = (sequence[j] * 137.5) % 360 sat = 60 + (sequence[j] * 13 % 30) # entre 60% y 90% lum = 50 + (sequence[j] * 7 % 20) # entre 50% y 70% colors[sequence[j]] = f"hsl({hue},{sat}%,{lum}%)" # colors[sequence[j]] = f"hsl({(sequence[j] * 45) % 360},70%,60%)" color = colors[sequence[j]] fig.add_trace(go.Bar( x=[self.pt[0][sequence[j]]], y=[resources[y_index]], base=start, orientation='h', marker=dict(color=color, line=dict(color='black', width=1)), name=f'J{sequence[j]}', hovertemplate=f'Paciente: J{sequence[j]}<br>Inicio: {start}<br>Fin: {start + self.pt[0][sequence[j]]}<br>Duración: {self.pt[0][sequence[j]]}<extra></extra>' )) # Anotación superior: paciente fig.add_annotation( x=start + self.pt[0][sequence[j]] / 2, y=resources[y_index], text=f"J{sequence[j]}", showarrow=False, font=dict(color='black', size=10), xanchor='center', yanchor='middle', yshift=10 # más cerca del centro ) # Anotación inferior: cirujano fig.add_annotation( x=start + self.pt[0][sequence[j]] / 2, y=resources[y_index], text=f"C{cirujano_j[sequence[j]]}", showarrow=False, font=dict(color='black', size=10), xanchor='center', yanchor='middle', yshift=-10 # más cerca del centro ) #Añadimos los bloqueos for b in self.bloqueos: recurso = f"OR {b['or']}" fig.add_trace(go.Bar( x=[b['dur']], y=[recurso], base=b['start'], orientation='h', marker=dict(color='lightgray', line=dict(color='black', width=1), pattern_shape='/'), showlegend=False,
hovertemplate=f'Bloqueo<br>Inicio: {b["start"]}<br>Fin: {b["start"] + b["dur"]}<br>Duración: {b["dur"]}<extra></extra>' )) # PACU (etapa 1) for j in range(self.jobs): if sequence[j] not in self.non_planified and self.nuevos_pt_pacu[sequence[j]]!=0: recurso_nombre = f"PACU {self.machines_used[1][sequence[j]]}" y_index = resources.index(recurso_nombre) start = self.completion_time[sequence[j]][1] - self.nuevos_pt_pacu[sequence[j]] color = colors[sequence[j]] fig.add_trace(go.Bar( x=[self.nuevos_pt_pacu[sequence[j]]], y=[resources[y_index]], base=start, orientation='h', marker=dict(color=color, line=dict(color='black', width=1)), name=f'J{sequence[j]}', hovertemplate=f'Paciente: J{sequence[j]}<br>Inicio: {start}<br>Fin: {start + self.nuevos_pt_pacu[sequence[j]]}<br>Duración: {self.nuevos_pt_pacu[sequence[j]]}<extra></extra>' )) fig.add_annotation( x=start + self.nuevos_pt_pacu[sequence[j]] / 2, y=resources[y_index], text=f"J{sequence[j]}", showarrow=False, font=dict(color='black', size=10), xanchor='center', yanchor='middle' ) # Añadir trazas invisibles para mostrar todas las máquinas aunque estén vacías for i in range(self.machines[0]): fig.add_trace(go.Bar( x=[0.001], y=[f'OR {i}'], base=[-10000], # fuera del rango visual marker=dict(color='rgba(0,0,0,0)'), showlegend=False, hoverinfo='skip' )) for i in range(self.machines[1]): fig.add_trace(go.Bar( x=[0.001],
Anexo 70 70 y=[f'PACU {i}'], base=[-10000], marker=dict(color='rgba(0,0,0,0)'), showlegend=False, hoverinfo='skip' )) # Configuración general fig.update_layout( title='Diagrama de Gantt OR-PACU', barmode='stack', xaxis=dict( title='Tiempo (min)', range=[0, (self.ph + 1) * 1440], fixedrange=False, constrain='domain', rangemode='nonnegative', rangeslider=dict(visible=True) ), yaxis=dict( title='Recurso (máquina)', categoryorder='array', categoryarray=resources, fixedrange=True, showgrid=False ), height=600, showlegend=False ) # Líneas rojas de referencia fig.add_shape(type='line', x0=0, x1=0, y0=0, y1=1, xref='x', yref='paper', line=dict(color='red', width=2)) limite = (self.ph - 1)* 1440 + 480 + self.delay_pacu fig.add_shape(type='line', x0=limite, x1=limite, y0=0, y1=1, xref='x', yref='paper', line=dict(color='red', width=2)) # Texto con el instante del fin fig.add_annotation( x=limite, y=1.05, xref='x', yref='paper', text=f"{limite}", showarrow=False, font=dict(color='red', size=7), align='center' ) # Texto con el instante del fin fig.add_annotation( x=0, y=1.05, xref='x', yref='paper', text=f"{0}",
showarrow=False, font=dict(color='red', size=7), align='center' ) # Líneas y etiquetas de inicio/fin de jornada quirúrgica for h in range(1, self.ph + 1): inicio_ventana_OR = (h - 1) * 1440 + 480 fin_ventana_OR = h * 1440 for instante, color in [(inicio_ventana_OR, 'gray'), (fin_ventana_OR, 'gray')]: if not (instante==fin_ventana_OR and h==self.ph): fig.add_shape(type='line', x0=instante, x1=instante, y0=0, y1=1, xref='x', yref='paper', line=dict(color=color, width=1, dash='dash')) fig.add_annotation(x=instante, y=1.05, xref='x', yref='paper', text=f"{instante}", showarrow=False, font=dict(color=color, size=7), align='center') for h in range(1, self.ph): inicio_ventana_PACU = (h - 1) * 1440 + 480 + self.delay_pacu fin_ventana_PACU = h * 1440 # Línea discontinua en el inicio de la jornada fig.add_shape( type='line', x0=inicio_ventana_PACU, x1=inicio_ventana_PACU, y0=0, y1=1, xref='x', yref='paper', line=dict(color='black', width=1, dash='dash') ) # Texto con el instante del inicio de la ventana de indisponibilidad fig.add_annotation( x=inicio_ventana_PACU, y=1.05, xref='x', yref='paper', text=f"{inicio_ventana_PACU}", showarrow=False, font=dict(color='black', size=7), align='center' ) # Línea discontinua en el fin de la jornada if h<self.ph-1: fig.add_shape( type='line', x0=fin_ventana_PACU, x1=fin_ventana_PACU, y0=0, y1=1, xref='x', yref='paper', line=dict(color='black', width=1, dash='dash')
Anexo 72 72 ) # Texto con el instante del fin fig.add_annotation( x=fin_ventana_PACU, y=1.05, xref='x', yref='paper', text=f"{fin_ventana_PACU}", showarrow=False, font=dict(color='black', size=7), align='center' ) fig.show() #Funcion de lectura de datos def load_instance_data(filename): print("----- Reading instance data from file", filename, "-------") jobs = read_tag(filename, "JOBS") if jobs == -1: print("No jobs specified. The program cannot continue.") sys.exit() print_tag("JOBS", jobs) resources = read_tag(filename, "NR") if resources == -1: print("No resources specified. The program cannot continue.") sys.exit() print_tag("NR", resources) machines = read_tag(filename, "MACHINES") if machines == -1: print("No machines specified. The program cannot continue.") sys.exit() if len(machines) != 2: print("Invalid dimension for MACHINES vector.") sys.exit() print_tag("MACHINES", machines) pt = read_tag(filename, "PT") if pt == -1: print("No processing times specified. The program cannot continue.") sys.exit() for i in range(len(pt)): if len(pt[i]) != jobs: print(f"Processing times length mismatch (JOBS={jobs}, len(PT)={len(pt[i])})") sys.exit() print_tag("PT", pt) ph = read_tag(filename, "PH") if ph == -1: print("No planning horizon specified. The program cannot continue.") sys.exit() print_tag("PH", ph)
times = read_tag(filename, "TIMES") if times == -1: print("No times specified. The program cannot continue.") sys.exit() for i in range(len(times)): if len(times[i]) != jobs: print(f"Processing times length mismatch (JOBS={jobs}, len(TIMES)={len(times[i])})") sys.exit() print_tag("T", times) delay_pacu = read_tag(filename, "DELAY_PACU") if delay_pacu == -1: print("No planning horizon specified. The program cannot continue.") sys.exit() print_tag("DELAY", delay_pacu) print("----- End of instance data reading -------") return jobs, resources, machines, pt, ph, times, delay_pacu
Anexo 80 80 # Inicialización de los tiempos de comienzo por día para las máquinas OR for z0 in range(self.machines[0]*self.ph): dia = ceil((z0+1)/self.machines[0]) ct_machines_day[0][z0] = (dia-1)*1440 # Inicialización de los tiempos de comienzo por día para las máquinas PACU for z1 in range(self.machines[1]*self.ph): dia = ceil((z1+1)/self.machines[1]) ct_machines_day[1][z1] = (dia-1)*1440 # Asignar todos los trabajos a OR for j in range(len(sequence)): dia = 1 orden = sorted_index_asc(huecos_day[dia - 1]) #Contador para comprobar los intentos de asignacion en un dia (como máximo puede haber machines intentos) k=0 planificable = True asignado = False while not asignado and planificable: OR_MAX = orden[k] + (dia - 1)*self.machines[0]#OR con menos hueco #Se estima el ct de la maquina start_or = deepcopy(ct_machines_day[0][OR_MAX]) ct_machines_day[0][OR_MAX] = ct_machines_day[0][OR_MAX] + self.pt[0][sequence[j]] # Comprobamos si comparten recursos conflicto_recursos(reparto, sequence[j], OR_MAX, ct_machines_day, ct, self.pt, non_planified) #Verificar si trabajo entra en ventana indispionible inicio_ventana_OR = (dia-1)*1440 + 480 fin_ventana_OR = dia*1440 if inicio_ventana_OR < ct_machines_day[0][OR_MAX] < fin_ventana_OR: #Si no es la ultima maquina que evaluamos en ese dia, podemos seguir evaluando otras if k < self.machines[0]-1: #Reseteamos el valor del completion time en la maquina ct_machines_day[0][OR_MAX] = start_or k+=1 else: if dia < self.ph:
#Si no es el ultimo dia, se pasa a probar al dia siguiente si hay ct_machines_day[0][OR_MAX]=start_or dia+=1 k=0 orden = sorted_index_asc(huecos_day[dia - 1]) #Se actualiza el orden else: #Si no hay mas dias no es planificable ct_machines_day[0][OR_MAX]=start_or planificable = False else: # Registrar el tiempo de finalización del trabajo actual ct[sequence[j]][0] = ct_machines_day[0][OR_MAX] #Registrar la maquina usada machines_used[0][sequence[j]] = OR_MAX - (dia - 1)*self.machines[0] # Intento de asignación a PACU asignado = asignar_PACU_C(self.machines, ct_machines_day, ct, OR_MAX, sequence[j],self.pt, self.ph, machines_used, lista_bloqueos, nuevos_pt_pacu, dia, self.delay_pacu) if asignado: #En caso de que se haya asignado a OR y PACU, actualizamos el valor del hueco #Se debe quietar el tiempo que el trabajo ha ocupado el la OR (su pt y su tiempo de bloqueo) bloqueos_trabajo = [b for b in lista_bloqueos if b['job'] == sequence[j]] # Acceder a la duración del bloqueo si existe duracion_bloqueo=0 if bloqueos_trabajo: duracion_bloqueo = bloqueos_trabajo[0]['dur'] #Se actualiza el espacio que queda disponible en la maquina actual huecos_day[dia - 1][OR_MAX-(dia - 1)*self.machines[0]] -= (self.pt[0][sequence[j]] + duracion_bloqueo) else: # En caso contrario, se restaura el estado previo de la máquina OR y se borra el ct provisional ct_machines_day[0][OR_MAX] = start_or ct[sequence[j]][0] = 0 if k < self.machines[0] - 1: k += 1 # Se prueba con la siguiente OR si hay else: if dia < self.ph: k = 0
Anexo 82 82 dia += 1 orden = sorted_index_asc(huecos_day[dia - 1]) #Se actualiza el orden else: planificable = False if not asignado: ct[sequence[j]][0] = 0 machines_used[0][sequence[j]] = 'none' machines_used[1][sequence[j]] = 'none' non_planified.append(sequence[j]) # Almacenamiento de los resultados como atributos del objeto para su posterior uso self.completion_time = ct self.machines_used = machines_used self.bloqueos = lista_bloqueos self.nuevos_pt_pacu = nuevos_pt_pacu self.non_planified = non_planified tiempo_real = (time.perf_counter() - start_time)*10000 return ct, sequence, non_planified, tiempo_real class OR_PACU_WF_C(OR_PACU_Base): def ct(self, sequence): start_time = time.perf_counter() # Se inicializan las variables ct_machines_day = [[0 for _ in range(self.machines[i]*self.ph)] for i in range(len(self.machines))] #Al principio los espacio disponibles sesran las jornadas completas huecos = [480 for _ in range(self.machines[0]*self.ph)] ct = [[0 for s in range(2)]for j in range(self.jobs)] machines_used = [[0 for _ in range(self.jobs)],[ 0 for _ in range(self.jobs)]] reparto=reparto_cirujanos(self.resources, self.jobs) lista_bloqueos=[] nuevos_pt_pacu=[0 for _ in range(self.jobs)] non_planified = [] #Incializamos los ct de las maquinas en cada dia for z0 in range(self.machines[0]*self.ph): dia=ceil((z0+1)/self.machines[0]) ct_machines_day[0][z0] = (dia-1)*1440
for z1 in range(self.machines[1]*self.ph): dia=ceil((z1+1)/self.machines[1]) ct_machines_day[1][z1] = (dia-1)*1440 # Asignar todos los trabajos a OR y a PACU for j in range(len(sequence)): #Ordenamos las maquinas de mas a menos hueco orden=sorted_index_desc(huecos) k=0 asignado=False while k<self.machines[0]*self.ph and not asignado: #Calculamos el dia en el que estemos dia = ceil((orden[k]+1)/self.machines[0]) #Introucimos el trabajo en la maquina que toque segun el orden OR_MIN = orden[k] start_or = deepcopy(ct_machines_day[0][OR_MIN]) ct_machines_day[0][OR_MIN] = ct_machines_day[0][OR_MIN] + self.pt[0][sequence[j]] # Comprobamos si comparten recursos conflicto_recursos(reparto, sequence[j], OR_MIN, ct_machines_day, ct, self.pt, non_planified) #Verificar si trabajo entra en ventana indispionible inicio_ventana_OR = (dia-1)*1440 + 480 fin_ventana_OR = dia*1440 if inicio_ventana_OR < ct_machines_day[0][OR_MIN] < fin_ventana_OR: # Si no entra en la ventana con mas hueco, pasamos a la siguiente en la lista (puede ser que no entre por conflictos de recursos) #Reseteamos el ct ct_machines_day[0][OR_MIN] = start_or k+=1 else: # Registrar el tiempo de finalización del trabajo actual ct[sequence[j]][0] = ct_machines_day[0][OR_MIN] machines_used[0][sequence[j]] = OR_MIN - self.machines[0]*(dia-1) # Asignación del trabajo en la PACU asignado = asignar_PACU_C(self.machines, ct_machines_day, ct, OR_MIN, sequence[j], self.pt, self.ph, machines_used, lista_bloqueos, nuevos_pt_pacu, dia, self.delay_pacu) if asignado:
Anexo 84 84 #En caso de que se haya asignado a OR y PACU, actualizamos el valor del hueco #Se debe quietar el tiempo que el trabajo ha ocupado el la OR (su pt y su tiempo de bloqueo) bloqueos_trabajo = [b for b in lista_bloqueos if b['job'] == sequence[j]] # Acceder a la duración del bloqueo si existe duracion_bloqueo=0 if bloqueos_trabajo: duracion_bloqueo = bloqueos_trabajo[0]['dur'] huecos[orden[k]] -= (self.pt[0][sequence[j]] + duracion_bloqueo) else: #Pasamos a buscar la siguiente OR #Reseteamos el ct_machines tanto de la OR como de la PACU ct_machines_day[0][OR_MIN] = start_or #Avanzamos a la siguiente OR en la lista k+=1 if not asignado: ct[sequence[j]][0]=0 machines_used[0][sequence[j]] = 'none' machines_used[1][sequence[j]] = 'none' non_planified.append(sequence[j]) #Almacenamos como atributos internos self.completion_time = ct self.machines_used = machines_used self.bloqueos=lista_bloqueos self.nuevos_pt_pacu=nuevos_pt_pacu self.non_planified = non_planified tiempo_real = (time.perf_counter() - start_time)*10000 return ct, sequence, non_planified, tiempo_real class OR_PACU_BF_C(OR_PACU_Base): def ct(self, sequence): start_time = time.perf_counter() # Inicialización de los tiempos de finalización (completion times) por máquina y por día ct_machines_day = [[0 for _ in range(self.machines[i]*self.ph)] for i in range(len(self.machines))] # Inicialización de los huecos disponibles por máquina en cada día (duración máxima de jornada OR: 480 minutos) huecos = [480 for _ in range(self.machines[0]*self.ph)]
# Matriz de tiempos de finalización para cada trabajo en cada etapa (OR y PACU) ct = [[0 for s in range(2)]for j in range(self.jobs)] # Matriz para registrar qué máquina (OR y PACU) se usó para cada trabajo machines_used = [[0 for _ in range(self.jobs)],[ 0 for _ in range(self.jobs)]] # Asignación de cirujanos a los trabajos reparto = reparto_cirujanos(self.resources, self.jobs) # Lista de bloqueos sufridos por los quirófanos cuando no hay PACU disponible lista_bloqueos = [] # Duración del bloqueo experimentado por cada trabajo al acceder a PACU nuevos_pt_pacu = [0 for _ in range(self.jobs)] # Lista de trabajos que no pudieron ser planificados non_planified = [] # Inicialización de los tiempos de comienzo por día para las máquinas OR for z0 in range(self.machines[0]*self.ph): dia = ceil((z0+1)/self.machines[0]) ct_machines_day[0][z0] = (dia-1)*1440 # Inicialización de los tiempos de comienzo por día para las máquinas PACU for z1 in range(self.machines[1]*self.ph): dia = ceil((z1+1)/self.machines[1]) ct_machines_day[1][z1] = (dia-1)*1440 # Asignación secuencial de todos los trabajos a quirófano (OR) y luego a recuperación (PACU) for j in range(self.jobs): # Se ordenan las máquinas OR según su hueco disponible (de menor a mayor ocupación) orden = sorted_index_asc(huecos) k = 0 asignado = False while k < self.machines[0]*self.ph and not asignado: # Cálculo del día en función del índice de la máquina dia = ceil((orden[k]+1)/self.machines[0]) # Se selecciona la máquina OR con mayor hueco disponible OR_MIN = orden[k] start_or = deepcopy(ct_machines_day[0][OR_MIN]) # copia de respaldo del estado original
Anexo 86 86 ct_machines_day[0][OR_MIN] += self.pt[0][sequence[j]] # tiempo estimado de finalización OR # Comprobación de conflictos de recursos (cirujanos) conflicto_recursos(reparto, sequence[j], OR_MIN, ct_machines_day, ct, self.pt, non_planified) # Verificación de que la operación entra en la ventana temporal válida del quirófano inicio_ventana_OR = (dia-1)*1440 + 480 fin_ventana_OR = dia*1440 if inicio_ventana_OR < ct_machines_day[0][OR_MIN] < fin_ventana_OR: # Si no entra en la ventana, se descarta esta máquina y se prueba la siguiente # Se restaura el tiempo original de la máquina OR ct_machines_day[0][OR_MIN] = start_or k+=1 else: # Si entra, se registra el tiempo de finalización del trabajo en OR ct[sequence[j]][0] = ct_machines_day[0][OR_MIN] machines_used[0][sequence[j]] = OR_MIN - self.machines[0]*(dia-1) # Intento de asignación a PACU asignado = asignar_PACU_C(self.machines, ct_machines_day, ct, OR_MIN, sequence[j],self.pt, self.ph, machines_used, lista_bloqueos, nuevos_pt_pacu, dia, self.delay_pacu) if asignado: #En caso de que se haya asignado a OR y PACU, actualizamos el valor del hueco #Se debe quietar el tiempo que el trabajo ha ocupado el la OR (su pt y su tiempo de bloqueo) bloqueos_trabajo = [b for b in lista_bloqueos if b['job'] == sequence[j]] # Acceder a la duración del bloqueo si existe duracion_bloqueo=0 if bloqueos_trabajo: duracion_bloqueo = bloqueos_trabajo[0]['dur'] huecos[orden[k]] -= (self.pt[0][sequence[j]] + duracion_bloqueo) else: # En caso contrario, se restaura el estado previo de la máquina OR y se borra el ct provisional ct_machines_day[0][OR_MIN] = start_or ct[sequence[j]][0] = 0 k += 1 # Se prueba con la siguiente OR
if not asignado: ct[sequence[j]][0] = 0 machines_used[0][sequence[j]] = 'none' machines_used[1][sequence[j]] = 'none' non_planified.append(sequence[j]) # Almacenamiento de los resultados como atributos del objeto para su posterior uso self.completion_time = ct self.machines_used = machines_used self.bloqueos = lista_bloqueos self.nuevos_pt_pacu = nuevos_pt_pacu self.non_planified = non_planified tiempo_real = (time.perf_counter() - start_time)*10000 return ct, sequence, non_planified, tiempo_real
Anexo 88 88 ABC y ABC_Elitismo import numpy as np import copy import time from biblioteca import objetivo from scheptk.util import random_sequence, sorted_index_asc, sorted_index_desc, find_index_max, find_index_min, sorted_value_desc from DR import SPT_OR, SPT_PACU, SPT_OR_PACU, LPT_OR, LPT_PACU, LPT_OR_PACU, WSPT_OR, WSPT_PACU, WSPT_OR_PACU, WLPT_OR, WLPT_PACU, WLPT_OR_PACU, JOHONSON "Random insertion" def RI(S,instancia): mejor_vecino = S.copy() sol_mejor_vecino = objetivo(instancia, mejor_vecino) vecino = mejor_vecino.copy() x = int(np.random.choice(vecino, 1)) pos = np.random.randint(len(vecino)) while pos == vecino.index(x): pos = np.random.randint(len(vecino)) vecino.remove(x) vecino.insert(pos, x) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino return mejor_vecino, sol_mejor_vecino "Random swap" def RS(S, instancia): mejor_vecino = S.copy() sol_mejor_vecino = objetivo(instancia, mejor_vecino) vecino = mejor_vecino.copy() x, y = np.random.choice(vecino, 2, replace=False) idx_x = vecino.index(x) idx_y = vecino.index(y) vecino[idx_x], vecino[idx_y] = vecino[idx_y], vecino[idx_x] sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino:
mejor_vecino = vecino sol_mejor_vecino = sol_vecino return mejor_vecino, sol_mejor_vecino "**************************************************************************** *****************" "Aproximacion al original" def ABC(instancia, Max_Time, SN, limit): Solutions_Set = [] trial = [] Dispatching_Rule = [WSPT_OR, WSPT_PACU, WSPT_OR_PACU, JOHONSON, SPT_PACU] #Se genera la población de soluciones for i in range(len(Dispatching_Rule)): S = Dispatching_Rule[i](instancia) Solutions_Set.append(S) trial.append(0) for i in range(len(Dispatching_Rule),SN): Solutions_Set.append(random_sequence(instancia.jobs)) trial.append(0) indices = np.arange(SN) #Se cálcula el fitness fitness = [0 for _ in range(SN)] for i in range(SN): pi_current = Solutions_Set[i] fitness[i] = 1/objetivo(instancia, pi_current) probabilidad = [0 for _ in range(SN)] c = 0 start_time = time.time() while time.time() - start_time < Max_Time: "EXPLORATION PROCESS (se intenta mejorar las soluciones iniciales)" for i in range(SN): pi_current = Solutions_Set[i].copy() vecino, obj_vecino = RI(pi_current, instancia) #Se aplica el operador de vecindada fitness_vecino = 1/obj_vecino #Se evalúa si el vecino mejora a la secuencia actual if fitness[i] < fitness_vecino: Solutions_Set[i] = vecino.copy() fitness[i] = fitness_vecino trial[i] = 0 else: trial[i]+=1
Anexo 96 96 pi_D, FO_current = best_insertion(pi_D, instancia, pi_R[k]) pi_current = pi_D #Se acepta movimiento si... #La Fo es mejor if FO_current<FO: pi = pi_current.copy() FO = FO_current if FO < FO_best: pi_best = pi.copy() FO_best= FO #La FO es peor pero se cumple cierta probabilidad else: Random = np.random.uniform(0,1) if Random <= exp(-((FO_current - FO)/T)): pi = pi_current.copy() FO = FO_current iteraciones += 1 tiempo_real = time.time() - start_time return pi_best, FO_best, tiempo_real #IG con temperatura adaptativa def IG_TA(S, instancia,delta,T_min,MaxTime,n): pi = S.copy() FO = objetivo(instancia, pi) pi_best = pi.copy() FO_best = FO r = 0 T = T_min #Inicialmente la temperatura es igual a su minimo start_time = time.time() while time.time() - start_time < MaxTime: pi_current = pi.copy() pi_R = [] #Trabajos que se deben reintroducir pi_D = pi_current.copy() #Secuencia destruida #Destrucción parcial de la secuencia for k in range(delta): pi_j = np.random.choice(pi_D) pi_D.remove(pi_j) pi_R.append(pi_j) #Reconstruccion con best insertion for k in range(delta): pi_D, FO_current = best_insertion(pi_D, instancia, pi_R[k]) pi_current = pi_D #Se acepta movimiento si...
#La Fo es mejor if FO_current<FO: r = 0 pi = pi_current.copy() FO = FO_current if FO < FO_best: pi_best = pi.copy() FO_best= FO #La FO es peor pero se cumple cierta probabilidad else: A = FO_current - FO Random = np.random.uniform(0,1) if Random <= exp(-((A)/T)): if A > 0: r += 1 #Se ha producido un movimiento de empeoramiento pi = pi_current.copy() FO = FO_current T = T_min + n*np.log(1 + r) #Se actualiza la temperatura tiempo_real = time.time() - start_time return pi_best, FO_best, tiempo_real
Anexo 98 98 Heuristicas constructivas from scheptk.util import sorted_index_asc, sorted_index_desc def SPT_OR(instancia): S = sorted_index_asc(instancia.pt[0]) return S def LPT_OR(instancia): S = sorted_index_desc(instancia.pt[0]) return S def SPT_PACU(instancia): S = sorted_index_asc(instancia.pt[1]) return S def LPT_PACU(instancia): S = sorted_index_desc(instancia.pt[1]) return S def SPT_OR_PACU(instancia): #Suma del pt en OR y PACU para cada trabajo PT = [instancia.pt[0][j]+instancia.pt[1][j] for j in range(instancia.jobs)] S = sorted_index_asc(PT) return S def LPT_OR_PACU(instancia): #Suma del pt en OR y PACU para cada trabajo PT = [instancia.pt[0][j]+instancia.pt[1][j] for j in range(instancia.jobs)] S = sorted_index_desc(PT) return S #Para WSPT divimos el peso por el tiempo def WSPT_OR(instancia): pt_weighted = [instancia.w[j]/instancia.pt[0][j] for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S def WSPT_PACU(instancia): pt_weighted = [instancia.w[j]/instancia.pt[1][j] for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S def WSPT_OR_PACU(instancia):
pt_weighted = [instancia.w[j]/(instancia.pt[0][j] + instancia.pt[1][j]) for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S #Para WLPT multiplicamos el peso por el pt def WLPT_OR(instancia): pt_weighted = [instancia.pt[0][j]*instancia.w[j] for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S def WLPT_PACU(instancia): pt_weighted = [instancia.pt[1][j]*instancia.w[j] for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S def WLPT_OR_PACU(instancia): pt_weighted = [(instancia.pt[0][j] + instancia.pt[1][j])*instancia.w[j] for j in range(instancia.jobs)] S = sorted_index_desc(pt_weighted) return S def JOHONSON(instancia): #Se crean dos secuencias dependiendo si su pt en PACU es mayor o menor que su pt en OR pi_1 = [j for j in range(instancia.jobs) if instancia.pt[0][j] <= instancia.pt[1][j]] pi_2 = [j for j in range(instancia.jobs) if instancia.pt[0][j] > instancia.pt[1][j]] pi_1_sorted = sorted(pi_1, key=lambda j: instancia.pt[0][j]) pi_2_sorted = sorted(pi_2, key=lambda j: instancia.pt[1][j]) pi_1_sorted.extend(pi_2_sorted) S = pi_1_sorted return S
Anexo 100 100 Operadores de vecindad import numpy as np import time from biblioteca import objetivo from scheptk.util import random_sequence, sorted_index_asc, sorted_index_desc, find_index_max, find_index_min, sorted_value_desc "OPERADORES RANODM" def RI_GS(S, instancia, iteraciones): start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino) sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras=0 for i in range(iteraciones): vecino = mejor_vecino.copy() x, y = np.random.choice(vecino, 2, replace=False) idx_x = vecino.index(x) idx_y = vecino.index(y) vecino[idx_x], vecino[idx_y] = vecino[idx_y], vecino[idx_x] instancia.ct(vecino) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino sol_mejor_vecino = sol_vecino mejoras +=1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras def RI_I(S,instancia, iteraciones): start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino) sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras = 0 for i in range(iteraciones): vecino = mejor_vecino.copy() x = int(np.random.choice(vecino, 1)) pos = np.random.randint(len(vecino)) while pos == vecino.index(x): pos = np.random.randint(len(vecino))
vecino.remove(x) vecino.insert(pos, x) instancia.ct(vecino) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino mejoras += 1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras #Se realiza un Swap entre un trabajo no planificado y un planificado def RI_ES(S,instancia, iteraciones): start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino) sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras = 0 for i in range(iteraciones): # Lista planificada preservando orden instancia.ct(mejor_vecino) planified = [x for x in mejor_vecino if x not in instancia.non_planified] vecino = mejor_vecino.copy() x = int(np.random.choice(planified, 1)) y = int(np.random.choice(instancia.non_planified, 1)) idx_x = vecino.index(x) idx_y = vecino.index(y) vecino[idx_x], vecino[idx_y] = vecino[idx_y], vecino[idx_x] instancia.ct(vecino) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino mejoras += 1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras #Se realiza un Swap entre un trabajo planificado y otro planificado def RI_IS(S,instancia, iteraciones):
Anexo 102 102 start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino) sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras = 0 for i in range(iteraciones): # Lista planificada preservando orden instancia.ct(mejor_vecino) planified = [x for x in mejor_vecino if x not in instancia.non_planified] vecino = mejor_vecino.copy() Insertion = False while not Insertion: Insertion = True # Elegir dos distintos trabajos planificados x0, x1 = np.random.choice(planified, 2, replace=False) instancia.ct(vecino) dia1 = instancia.completion_time[x0][0] // 1440 + 1 dia2 = instancia.completion_time[x1][0] // 1440 + 1 maq1 = instancia.machines_used[0][x0] maq2 = instancia.machines_used[0][x1] # Verificar condición: diferentes máquinas o días if (dia1 == dia2 and maq1 == maq2): Insertion = False continue # No se permite swap, saltar # Realizar el swap idx_x = vecino.index(x0) idx_y = vecino.index(x1) vecino[idx_x], vecino[idx_y] = vecino[idx_y], vecino[idx_x] instancia.ct(vecino) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino mejoras += 1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras #Se realiza una Insertion con un trabajo no planificado def RI_EI(S,instancia, iteraciones): start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino)
sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras = 0 for i in range(iteraciones): # Lista planificada preservando orden instancia.ct(mejor_vecino) planified = [x for x in mejor_vecino if x not in instancia.non_planified] vecino = mejor_vecino.copy() x = int(np.random.choice(instancia.non_planified, 1)) p = np.random.randint(len(planified)) pos = vecino.index(planified[p]) vecino.remove(x) vecino.insert(pos, x) instancia.ct(vecino) sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino mejoras += 1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras #Se realiza una Insertion con un trabajo planificado def RI_II(S,instancia, iteraciones): start_time = time.perf_counter() mejor_vecino = S.copy() instancia.ct(mejor_vecino) sol_mejor_vecino = objetivo(instancia, mejor_vecino) mejoras = 0 for i in range(iteraciones): # Lista planificada preservando orden instancia.ct(mejor_vecino) planified = [x for x in mejor_vecino if x not in instancia.non_planified] vecino = mejor_vecino.copy() # Selecciona dos posiciones distintas p1, p2 = np.random.choice(len(planified), 2, replace=False) x = planified[p1] pos = vecino.index(planified[p2]) vecino.remove(x) vecino.insert(pos, x) instancia.ct(vecino)
Anexo 104 104 sol_vecino = objetivo(instancia, vecino) if sol_vecino < sol_mejor_vecino: mejor_vecino = vecino.copy() sol_mejor_vecino = sol_vecino mejoras +=1 tiempo_real = (time.perf_counter() - start_time)*1000 return sol_mejor_vecino, tiempo_real, mejoras
Calibraciones import pandas as pd from scheptk.util import random_sequence from ClassTFG import OR_PACU_FF, OR_PACU_WF_W, OR_PACU_BF_W, OR_PACU_WF_C, OR_PACU_BF_C from biblioteca import objetivo from DR import SPT_OR, SPT_PACU, SPT_OR_PACU, LPT_OR, LPT_PACU, LPT_OR_PACU, WSPT_OR, WSPT_PACU, WSPT_OR_PACU, WLPT_OR, WLPT_PACU, WLPT_OR_PACU, JOHONSON from IG import IG, IG_TA from Operadores import RI_GS, RI_I, RI_ES, RI_IS, RI_EI, RI_II from ABC_ORIGINAL import ABC, ABC_ELITISM "Calibracion de DECODING con registro de tiempo" OR = [3,6] PACU_FACTOR = [1, 1.5, 2, 2.5, 3] DECODING = [OR_PACU_FF, OR_PACU_WF_W, OR_PACU_BF_W, OR_PACU_WF_C, OR_PACU_BF_C] resultados = [] for i1 in range(len(OR)): for i2 in range(len(PACU_FACTOR)): for n_archivo in range(1,11): c=0 for i0 in range(len(DECODING)): PACU = int(OR[i1]*PACU_FACTOR[i2]) instancia = DECODING[i0]("Instancias\instancia_"+str(OR[i1])+"OR_"+str(PACU)+"PACU_"+str( n_archivo)+".txt") if c==0: S = random_sequence(instancia.jobs) c+=1 time = instancia.ct(S)[3] Objetivo=objetivo(instancia, S) resultados.append({ "Instancia": "instancia_"+str(OR[i1])+"OR_"+str(PACU)+"PACU_"+str(n_archivo), "Decoding": DECODING[i0], "Time": time, "Objetivo": Objetivo }) df = pd.DataFrame(resultados) df.to_excel("Resultados\Vaciado_Decoding_Time.xlsx", index=False, engine='openpyxl') "Calibracion dispatching rules" OR = [3,6] PACU_FACTOR = [1, 1.5, 2]
Anexo 112 112 print(f"> Backup guardado tras {contador} instancias.") # Guardado final df = pd.DataFrame(resultados) df.to_excel("Resultados\\ABC\\Vaciado_ABC_ELite.xlsx", index=False, engine='openpyxl') print("> Resultados finales guardados.") fo_data = {} tiempo_data = {} iter_data = {} for r in resultados: instancia = r["Instancia"] SN = int(r["SN"]) S_val = int(r["S"]) Elite = int(r["Elite"]) MIWOI_val = int(r["MIWOI"]) clave = f"SN={SN},S={S_val},Elite={Elite},MIWOI={MIWOI_val}" if instancia not in fo_data: fo_data[instancia] = {} tiempo_data[instancia] = {} iter_data[instancia] = {} fo_data[instancia][clave] = r["Objetivo"] tiempo_data[instancia][clave] = r["Tiempo"] iter_data[instancia][clave] = r["Iteraciones"] # Convertimos a DataFrames fo_df = pd.DataFrame.from_dict(fo_data, orient="index").sort_index() tiempo_df = pd.DataFrame.from_dict(tiempo_data, orient="index").sort_index() iter_df = pd.DataFrame.from_dict(iter_data, orient="index").sort_index() # Guardamos en un Excel con 3 hojas with pd.ExcelWriter("Resultados\\ABC\\Resumen_ABC_Elite_por_parametros.xlsx", engine='openpyxl') as writer: fo_df.to_excel(writer, sheet_name="FO") tiempo_df.to_excel(writer, sheet_name="Tiempo") iter_df.to_excel(writer, sheet_name="Iteraciones") print("> Resumen estructurado por parámetros (ABC Elite) guardado.") "Calibracion IG con temperatura adaptiva" OR = [3, 6] PACU_FACTOR = [1, 1.5, 2] resultados = [] n = [1,2] T_min = [0.4, 1] delta = 2
backup_interval = 40 # cada 60 iteraciones contador = 1 # cuenta cuántas instancias se han procesado tiempo_experiment = 0 for i1 in range(len(OR)): for i2 in range(len(PACU_FACTOR)): for i in range(2): for t in range(2): PACU = int(OR[i1] * PACU_FACTOR[i2]) for n_archivo in range(1, 11): print(f"\n\n######PROCESANDO INSTANCIA_{OR[i1]}OR_{PACU}PACU_{n_archivo}#####") print(f"[T_min = {T_min[t]}; n = {n[i]}]\n\n") instancia = OR_PACU_WF_C(f"Instancias\\instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}.txt") MaxTime = OR[i1] * instancia.jobs * instancia.ph * 0.0125 S = WSPT_OR(instancia) # Mejor DR conocida S_best, Objetivo, Tiempo = IG_TA(S, instancia, delta, T_min[t], MaxTime, n[i]) tiempo_experiment += Tiempo resultados.append({ "Instancia": f"instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}", "Delta": delta, "Tempt": T_min[t], "rise_control": n[i], "Tiempo": Tiempo, "Tiempo Máximo": MaxTime, "Secuencia": S_best, "Objetivo": Objetivo }) # BACKUP CADA 40 INSTANCIAS if contador % backup_interval == 0: backup_df = pd.DataFrame(resultados) backup_df.to_excel(f"Resultados\\IG\\IG_BI\\backup_IG_TA_{contador}_instancia s.xlsx", index=False, engine='openpyxl') print(f"> Backup guardado tras {contador} instancias.") contador += 1 # Guardado final df = pd.DataFrame(resultados) df.to_excel("Resultados\\IG\\IG_BI\\Vaciado_IG_TA.xlsx", index=False, engine='openpyxl') print("> Resultados finales guardados.") print("Tiempo total de experimentación:", tiempo_experiment)
Anexo 114 114 fo_data = {} tiempo_data = {} for r in resultados: instancia = r["Instancia"] tmin = float(r["Tempt"]) rc = int(r["rise_control"]) clave = f"T_min={tmin},n={rc}" if instancia not in fo_data: fo_data[instancia] = {} tiempo_data[instancia] = {} fo_data[instancia][clave] = r["Objetivo"] tiempo_data[instancia][clave] = r["Tiempo"] # Convertimos a DataFrames fo_df = pd.DataFrame.from_dict(fo_data, orient="index").sort_index() tiempo_df = pd.DataFrame.from_dict(tiempo_data, orient="index").sort_index() # Guardamos en un Excel con dos hojas with pd.ExcelWriter("Resultados\\IG\\IG_BI\\Resumen_IG_TA_por_parametros.xlsx", engine='openpyxl') as writer: fo_df.to_excel(writer, sheet_name="FO") tiempo_df.to_excel(writer, sheet_name="Tiempo") print("> Resumen estructurado por parámetros (IG-TA) guardado.")
Experimentación import pandas as pd # Agrega la ruta manualmente from ClassTFG import OR_PACU_WF_C from DR import WSPT_OR from IG import IG, IG_TA from ABC_ORIGINAL import ABC, ABC_ELITISM OR = [3, 6] PACU_FACTOR = [1, 1.5, 2] resultados = [] MH = [IG, IG_TA, ABC, ABC_ELITISM] "Parámetros IG" delta = 2 #Parametro de destrucción de IG T = 0.4 #Temperatura constante en IG y T_min de IG_TA n = 1 #Parámetro que controla el incremento de T en IG_TA (un poco peor que dos pero soluciones más robustas) "Parámetros ABC básico" SN_B = 30 #Población limit = 60 #Intentos máximos de busqueda de vecindad sin mejora "Parámetros ABC " SN_E = 15 #Población S = 30 #Intentos máximos de busqueda de vecindad sin mejora Elite = 3 MIWOI = 10 #Nº maximo de iteraciones sin mejorar el conjunto élite backup_interval = 120 # cada 120 iteraciones contador = 0 # cuenta cuántas instancias se han procesado for i1 in range(len(OR)): for i2 in range(len(PACU_FACTOR)): PACU = int(OR[i1] * PACU_FACTOR[i2]) for n_archivo in range(1, 31): for m in range(len(MH)): print(f"\n\n######PROCESANDO INSTANCIA_{OR[i1]}OR_{PACU}PACU_{n_archivo}#####") instancia = OR_PACU_WF_C(f"Instancias\\instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}.txt") MaxTime = OR[i1] * instancia.jobs * instancia.ph * 0.0125 sequence = WSPT_OR(instancia) # Mejor DR conocida if MH[m]==IG: S_best, Objetivo, Tiempo = IG(sequence, instancia, delta, T, MaxTime) elif MH[m]==IG_TA: S_best, Objetivo, Tiempo = IG_TA(sequence, instancia, delta, T, MaxTime, n)
Anexo 116 116 elif MH[m]==ABC: S_best, Objetivo, Tiempo = ABC(instancia, MaxTime, SN_B, limit) elif MH[m]==ABC_ELITISM: S_best, Objetivo, Tiempo = ABC_ELITISM(instancia, MaxTime, SN_E, S, Elite, MIWOI) instancia.ct(S_best) nro_jobs_np = len(instancia.non_planified) nro_bloqueos = len(instancia.bloqueos) "Calculamos utilizacion del quirófano y PACU" total_time_or = OR[i1]*480*5 tiempo_uso_or = 0 total_time_pacu = PACU*(480+150)*5 tiempo_uso_pacu = 0 for j in range(instancia.jobs): if not S_best[j] in instancia.non_planified: tiempo_uso_or += instancia.pt[0][S_best[j]] tiempo_uso_pacu += instancia.nuevos_pt_pacu[S_best[j]] utilizacion_or = tiempo_uso_or/total_time_or utilizacion_pacu = tiempo_uso_pacu/total_time_pacu print("\n\n",utilizacion_or) print(utilizacion_pacu,"\n\n") resultados.append({ "Instancia": f"instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}", "MH": MH[m].__name__, "Tiempo": Tiempo, "Tiempo Máximo": MaxTime, "Secuencia": S_best, "Objetivo": Objetivo, "U_OR": utilizacion_or, "U_PACU": utilizacion_pacu, "nro np": nro_jobs_np, "nro bloq": nro_bloqueos, }) # BACKUP CADA 120 INSTANCIAS if contador % backup_interval == 0: backup_df = pd.DataFrame(resultados) backup_df.to_excel(f"Resultados\\backup_MH_{contador}_instancias.xlsx", index=False, engine='openpyxl') print(f"> Backup guardado tras {contador} instancias.") contador += 1
# Guardado final df = pd.DataFrame(resultados) df.to_excel("Resultados\\Vaciado_Experimentacion_MH.xlsx", index=False, engine='openpyxl') print("> Resultados finales guardados.") metricas = ["Objetivo", "Tiempo", "U_OR", "U_PACU", "nro np", "nro bloq", "Secuencia"] resumenes = {met: {} for met in metricas} for r in resultados: instancia = r["Instancia"] mh = r["MH"] for met in metricas: if instancia not in resumenes[met]: resumenes[met][instancia] = {} resumenes[met][instancia][mh] = r[met] # Convertimos a DataFrames resumen_dfs = {met: pd.DataFrame.from_dict(resumenes[met], orient="index").sort_index() for met in metricas} # Guardamos en un Excel con una hoja por métrica with pd.ExcelWriter("Resultados\\Resumen_Experimentacion_por_MH.xlsx", engine='openpyxl') as writer: for met, df_met in resumen_dfs.items(): df_met.to_excel(writer, sheet_name=met) print("> Resumen estructurado por metaheurística guardado.") resultados = [] for i1 in range(len(OR)): for i2 in range(len(PACU_FACTOR)): PACU = int(OR[i1] * PACU_FACTOR[i2]) for n_archivo in range(1, 31): instancia = OR_PACU_WF_C(f"Instancias\\instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}.txt") resultados.append({ "Instancia": f"instancia_{OR[i1]}OR_{PACU}PACU_{n_archivo}", "Jobs": instancia.jobs, }) df = pd.DataFrame(resultados) df.to_excel("Resultados\\Trabajos_Instancias.xlsx", index=False, engine='openpyxl')
118 Resultados de la calibración • RPD decoding RDP FF WF_W BF_W WF_C BF_C FF WF_W BF_W WF_C BF_C 3OR_3PACU_1 15 51 1 0 1 108 0142 44 124 3OR_3PACU_2 58 037 057 105 0127 31 79 3OR_3PACU_3 25 012 68 42 148 0182 45 113 3OR_3PACU_4 73 032 27 47 109 0188 89 199 3OR_3PACU_5 454 413 0137 098 51 106 3OR_3PACU_6 22 54 24 9 0 69 0108 16 86 3OR_3PACU_7 70 165 94 087 67 28 130 085 3OR_3PACU_8 077 21 65 60 91 0127 48 98 3OR_3PACU_9 78 60 36 032 109 0169 90 162 3OR_3PACU_10 50 21 10 010 155 0128 53 127 3OR_4PACU_1 100 71 101 081 108 0133 83 169 3OR_4PACU_2 0 3 0 29 2116 0130 49 132 3OR_4PACU_3 10 162 027 33 126 0136 55 177 3OR_4PACU_4 16 33 16 0 3 33 473 070 3OR_4PACU_5 36 787 086 75 11 114 050 3OR_4PACU_6 0141 48 5 1 120 0188 83 183 3OR_4PACU_7 34 17 0 0 0 53 35 80 029 3OR_4PACU_8 0 9 0 43 16 93 0113 76 135 3OR_4PACU_9 69 99 50 054 101 0123 24 177 3OR_4PACU_10 103 5109 080 118 0117 30 115 3OR_6PACU_1 41 144 38 6 0 70 071 37 105 3OR_6PACU_2 13 86 13 12 097 079 45 71 3OR_6PACU_3 0196 46 46 52 105 0113 82 190 3OR_6PACU_4 2205 053 10 86 0109 31 134 3OR_6PACU_5 31 132 31 024 62 12 112 025 3OR_6PACU_6 17 62 17 048 90 0122 30 145 3OR_6PACU_7 44 21 44 066 61 0125 32 125 3OR_6PACU_8 67 229 67 035 92 278 0107 3OR_6PACU_9 0169 53 97 135 80 084 48 89 3OR_6PACU_10 15 281 15 079 120 0126 49 149 FO TIME RDP FF WF_W BF_W WF_C BF_C FF WF_W BF_W WF_C BF_C 6OR_6PACU_1 50 178 17 040 331 0355 143 314 6OR_6PACU_2 60 140 72 054 220 0335 104 344 6OR_6PACU_3 53 63 73 066 302 0322 161 315 6OR_6PACU_4 64 122 76 0124 370 0330 134 372 6OR_6PACU_5 96 248 93 0130 227 0276 80 269 6OR_6PACU_6 0124 34 10 51 295 0288 126 248 6OR_6PACU_7 31 151 53 084 256 0301 118 284 6OR_6PACU_8 57 155 29 067 253 0291 140 377 6OR_6PACU_9 33 108 33 021 266 0318 152 363 6OR_6PACU_10 73 262 95 0101 208 0243 69 215 6OR_9PACU_1 52 212 52 018 231 0286 87 278 6OR_9PACU_2 34 190 40 052 294 0357 213 397 6OR_9PACU_3 56 190 13 011 364 0404 156 316 6OR_9PACU_4 0149 38 45 54 291 0287 144 255 6OR_9PACU_5 39 384 5 0 12 276 0257 96 246 6OR_9PACU_6 88 379 67 12 0212 0271 72 203 6OR_9PACU_7 16 253 36 039 139 0271 41 181 6OR_9PACU_8 25 137 013 22 194 0195 86 207 6OR_9PACU_9 0191 16 39 32 201 0283 171 238 6OR_9PACU_10 10 174 10 15 0216 0329 171 300 6OR_12PACU_1 29 247 012 24 238 0203 97 188 6OR_12PACU_2 30 206 44 24 0168 0280 63 199 6OR_12PACU_3 17 148 0 6 10 199 0278 118 233 6OR_12PACU_4 15 230 6 0 19 229 0266 138 333 6OR_12PACU_5 26 171 16 039 349 0331 228 436 6OR_12PACU_6 22 157 29 026 257 0255 103 259 6OR_12PACU_7 63 306 24 033 191 0332 128 269 6OR_12PACU_8 14 357 0 7 6 343 0307 142 255 6OR_12PACU_9 10 274 414 0179 0245 68 170 6OR_12PACU_10 13 115 7 0 0 262 0238 120 229 FO TIME RPD de FO y TIME de operadores BP - II RPD de FO y TIME de operadores BP - I
• RPD DR RPD - FO Random SPT_O SPT_P SPT_O_P LPT_O LPT_P LPT_O_P WSPT_O WSPT_P WSPT_O_P WLPT_O WLPT_P WLPT_O_P JOHN. 3OR_3PACU_1 60 75 115 120 375 233 334 107 16 0162 155 139 109 3OR_3PACU_2 26 26 52 5283 151 199 82 46 080 39 43 24 3OR_3PACU_3 42 010 28 407 183 429 10 64 4226 53 227 2 3OR_3PACU_4 168 52 44 100 520 376 540 021 77 261 131 125 3 3OR_3PACU_5 0 42 11 42 83 52 58 37 24 242 24 61 8 3OR_3PACU_6 103 52 72 53 297 84 235 18 45 0152 10 87 53 3OR_3PACU_7 12 31 31 74 141 44 143 40 1 0 181 37 148 30 3OR_3PACU_8 36 939 26 133 52 92 045 10 66 23 42 18 3OR_3PACU_9 34 37 537 206 163 190 21 37 52 39 0117 48 3OR_3PACU_10 65 46 27 7287 86 256 69 34 094 37 144 65 3OR_4PACU_1 79 56 58 76 274 26 428 049 56 46 155 130 15 3OR_4PACU_2 104 52 82 28 306 193 440 33 041 101 84 143 92 3OR_4PACU_3 59 361 19 136 31 194 061 43 30 43 132 5 3OR_4PACU_4 100 48 38 68 194 78 220 079 2108 68 69 30 3OR_4PACU_5 62 90 55 71 435 151 355 17 80 55 121 52 71 0 3OR_4PACU_6 0 74 44 36 207 69 228 27 45 30 78 14 43 69 3OR_4PACU_7 78 220 45 302 43 336 36 641 100 10 112 0 3OR_4PACU_8 34 108 72 25 376 32 343 058 62 81 25 66 74 3OR_4PACU_9 106 91 65 93 376 270 457 024 11 131 127 65 35 3OR_4PACU_10 15 48 64 82 298 185 239 23 045 186 116 176 112 3OR_6PACU_1 90 45 14 23 263 323 377 058 21 82 62 74 19 3OR_6PACU_2 73 17 44 42 222 126 324 038 49 63 33 108 9 3OR_6PACU_3 87 85 66 73 266 165 248 19 064 114 118 33 65 3OR_6PACU_4 65 52 0105 398 55 313 49 39 48 47 137 140 43 3OR_6PACU_5 23 78 47 0171 71 278 10 39 760 106 160 45 3OR_6PACU_6 39 76 56 26 217 101 165 049 4126 130 152 16 3OR_6PACU_7 53 28 54 106 314 72 169 071 631 88 224 31 3OR_6PACU_8 120 162 132 91 391 261 339 44 42 72 111 111 0114 3OR_6PACU_9 11 97 47 29 116 201 258 24 53 0141 62 184 63 3OR_6PACU_10 43 28 68 27 326 57 314 040 28 21 27 328 RPD de FO de DR - I
Anexo 120 120 RPD - FO Random SPT_O SPT_P SPT_O_P LPT_O LPT_P LPT_O_P WSPT_O WSPT_P WSPT_O_P WLPT_O WLPT_P WLPT_O_P JOHN. 6OR_6PACU_1 0 27 27 13 145 83 132 019 23 77 56 61 33 6OR_6PACU_2 44 56 32 42 126 157 149 0 5 12 84 15 39 53 6OR_6PACU_3 44 78 053 164 196 168 520 13 160 90 87 5 6OR_6PACU_4 57 51 61 38 135 77 221 8 0 44 113 98 53 25 6OR_6PACU_5 19 62 25 56 163 130 198 015 17 116 44 81 34 6OR_6PACU_6 58 33 52 24 137 132 155 022 27 82 23 102 41 6OR_6PACU_7 76 77 378 319 178 275 045 58 314 44 145 58 6OR_6PACU_8 88 39 39 53 192 137 241 25 20 0148 69 161 13 6OR_6PACU_9 45 46 151 195 82 214 721 3117 26 105 0 6OR_6PACU_10 108 79 15 71 183 91 228 052 0112 39 90 33 6OR_9PACU_1 20 71 31 45 290 244 315 094 10 319 71 183 56 6OR_9PACU_2 35 60 34 53 442 178 318 026 23 75 71 103 49 6OR_9PACU_3 0 102 83 74 513 242 521 43 40 100 114 83 66 42 6OR_9PACU_4 23 25 30 24 119 70 228 12 83 17 69 042 24 6OR_9PACU_5 49 29 035 330 18 302 35 31 886 51 68 9 6OR_9PACU_6 9 28 27 42 210 53 233 537 053 43 46 17 6OR_9PACU_7 26 81 29 54 189 84 236 054 43 68 80 55 23 6OR_9PACU_8 3 33 034 365 150 217 10 23 61 150 55 134 32 6OR_9PACU_9 76 66 29 56 189 93 264 13 22 0120 40 57 3 6OR_9PACU_10 55 88 238 324 109 340 24 24 051 24 59 39 6OR_12PACU_1 14 68 020 133 65 143 812 849 50 31 4 6OR_12PACU_2 0 62 19 62 235 66 206 28 25 14 130 53 86 23 6OR_12PACU_3 107 120 60 100 161 49 266 47 80 0147 135 257 46 6OR_12PACU_4 19 56 50 20 306 216 367 14 17 0133 29 89 32 6OR_12PACU_5 0 33 19 24 120 45 95 8 3 7 53 947 2 6OR_12PACU_6 4 34 29 13 117 54 171 1 0 6 56 15 80 27 6OR_12PACU_7 23 62 40 35 238 181 259 9 0 1 179 44 158 48 6OR_12PACU_8 46 37 13 27 409 224 345 040 9214 105 139 16 6OR_12PACU_9 24 32 823 225 77 204 016 367 107 95 33 6OR_12PACU_10 13 51 35 57 165 48 157 10 019 37 25 63 39 RPD de FO de DR - II
• Calibración OV RPD - Eficiencia RS RI ES IS EI II RS RI ES IS EI II 3OR_3PACU_1 0 33 55 61 45 36 0 1 47 35 64 39 3OR_3PACU_2 0 15 23 50 53 38 021 26 61 15 24 3OR_3PACU_3 17 049 62 35 33 016 71 67 45 81 3OR_3PACU_4 21 050 90 53 89 29 51 0100 63 14 3OR_3PACU_5 0 17 51 49 44 39 0 2 35 50 27 50 3OR_3PACU_6 27 28 070 29 60 0 0 64 86 12 78 3OR_3PACU_7 16 036 65 43 34 42 056 58 42 42 3OR_3PACU_8 0 37 40 85 58 65 14 26 084 34 27 3OR_3PACU_9 0 25 38 67 52 49 3 0 27 40 24 28 3OR_3PACU_10 1 0 15 55 19 53 017 64 78 40 33 3OR_4PACU_1 0 29 77 96 65 90 0100 48 77 57 97 3OR_4PACU_2 29 025 63 45 31 0 0 23 79 44 60 3OR_4PACU_3 21 48 079 55 70 99 49 099 33 90 3OR_4PACU_4 77 018 79 76 17 96 038 99 72 72 3OR_4PACU_5 44 032 68 33 31 45 027 76 68 43 3OR_4PACU_6 0 45 48 74 39 61 29 8 0 64 22 53 3OR_4PACU_7 0 30 49 83 55 77 39 061 76 48 49 3OR_4PACU_8 0 7 32 71 24 59 059 28 64 68 41 3OR_4PACU_9 32 040 59 45 35 89 047 92 50 87 3OR_4PACU_10 19 038 62 58 59 17 043 58 62 33 3OR_6PACU_1 42 51 081 26 71 33 43 21 70 075 3OR_6PACU_2 0 13 37 31 57 48 020 31 33 76 24 3OR_6PACU_3 22 011 58 41 48 025 14 70 41 28 3OR_6PACU_4 4 0 33 61 26 54 23 026 50 39 35 3OR_6PACU_5 2 0 15 59 32 58 332 19 73 56 0 3OR_6PACU_6 0 30 28 67 59 64 023 720 25 66 3OR_6PACU_7 42 27 096 80 95 100 39 84 96 0100 3OR_6PACU_8 7 62 096 36 71 082 39 97 59 61 3OR_6PACU_9 0 25 42 50 71 40 0 0 27 20 68 13 3OR_6PACU_10 14 050 84 77 53 16 934 56 12 0 100 iteraciones 25 iteraciones RPD de eficiencia de OV para 25 y 100 iteraciones - II RPD de eficiencia de OV para 25 y 100 iteraciones - I
Anexo 128 128 Resultados de la experimentación RPD - FO IG IG_TA ABC ABC_ELIT 3OR_3PACU_1 48,54 0,00 77,82 5,30 3OR_3PACU_2 3,01 0,00 33,12 28,97 3OR_3PACU_3 0,00 9,60 29,13 0,70 3OR_3PACU_4 29,99 0,00 44,83 22,16 3OR_3PACU_5 30,05 18,46 51,68 0,00 3OR_3PACU_6 0,00 16,89 104,39 66,61 3OR_3PACU_7 16,50 14,22 28,46 0,00 3OR_3PACU_8 50,84 0,00 47,20 99,96 3OR_3PACU_9 0,94 0,00 33,51 2,43 3OR_3PACU_10 0,00 3,13 22,26 3,60 3OR_3PACU_11 0,00 6,04 18,34 13,57 3OR_3PACU_12 2,46 0,00 1,52 1,00 3OR_3PACU_13 12,27 0,00 53,59 32,65 3OR_3PACU_14 0,00 5,09 15,28 28,52 3OR_3PACU_15 39,51 0,00 66,32 44,37 3OR_3PACU_16 16,28 0,00 18,54 12,95 3OR_3PACU_17 0,00 9,51 46,19 34,68 3OR_3PACU_18 0,00 11,08 15,93 5,58 3OR_3PACU_19 15,18 15,90 11,70 0,00 3OR_3PACU_20 19,90 0,00 73,11 36,57 3OR_3PACU_21 26,03 23,10 10,25 0,00 3OR_3PACU_22 17,18 30,93 7,77 0,00 3OR_3PACU_23 1,56 0,00 35,17 43,17 3OR_3PACU_24 0,00 24,26 39,51 19,55 3OR_3PACU_25 21,78 26,17 24,30 0,00 3OR_3PACU_26 3,50 0,00 55,80 29,79 3OR_3PACU_27 2,22 31,62 13,52 0,00 3OR_3PACU_28 28,67 0,00 76,31 25,35 3OR_3PACU_29 43,44 45,87 90,53 0,00 3OR_3PACU_30 19,75 70,39 86,91 0,00 RPD - FO IG IG_TA ABC ABC_ELIT 3OR_4PACU_1 0,00 38,60 31,30 21,94 3OR_4PACU_2 3,39 23,43 0,00 17,30 3OR_4PACU_3 5,97 17,61 0,00 22,10 3OR_4PACU_4 0,00 7,04 31,16 6,06 3OR_4PACU_5 1,66 0,00 58,78 21,33 3OR_4PACU_6 9,31 0,00 46,12 58,96 3OR_4PACU_7 48,87 32,06 0,00 50,59 3OR_4PACU_8 0,00 7,94 36,79 9,12 3OR_4PACU_9 1,33 0,00 28,22 2,20 3OR_4PACU_10 20,53 23,21 33,81 0,00 3OR_4PACU_11 6,19 0,00 65,89 23,64 3OR_4PACU_12 15,12 0,00 15,43 16,87 3OR_4PACU_13 34,29 22,56 9,67 0,00 3OR_4PACU_14 0,00 34,46 25,08 23,31 3OR_4PACU_15 0,00 36,89 48,19 17,45 3OR_4PACU_16 0,00 10,58 23,44 12,54 3OR_4PACU_17 0,00 22,85 20,79 7,08 3OR_4PACU_18 0,00 19,09 37,66 22,23 3OR_4PACU_19 40,54 0,00 25,05 6,88 3OR_4PACU_20 0,00 13,48 30,33 19,93 3OR_4PACU_21 24,92 0,00 4,49 15,98 3OR_4PACU_22 3,65 7,91 37,48 0,00 3OR_4PACU_23 0,00 18,75 26,90 33,65 3OR_4PACU_24 19,02 23,93 3,56 0,00 3OR_4PACU_25 28,55 0,00 11,82 8,71 3OR_4PACU_26 1,81 44,17 31,13 0,00 3OR_4PACU_27 25,02 13,86 30,41 0,00 3OR_4PACU_28 102,58 0,00 102,19 43,97 3OR_4PACU_29 10,70 0,00 22,65 28,55 3OR_4PACU_30 1,25 0,03 0,00 21,98 RPD - FO IG IG_TA ABC ABC_ELIT 3OR_6PACU_1 11,89 19,66 35,99 0,00 3OR_6PACU_2 0,00 23,08 17,24 8,67 3OR_6PACU_3 0,00 66,58 51,74 13,42 3OR_6PACU_4 15,82 22,52 40,05 0,00 3OR_6PACU_5 0,00 0,31 50,71 21,03 3OR_6PACU_6 28,29 0,00 13,33 7,64 3OR_6PACU_7 1,18 16,62 0,00 3,57 3OR_6PACU_8 2,59 0,00 45,75 23,98 3OR_6PACU_9 21,81 0,00 16,28 18,59 3OR_6PACU_10 0,00 1,02 19,10 2,92 3OR_6PACU_11 0,00 11,42 33,17 6,79 3OR_6PACU_12 17,91 13,81 46,63 0,00 3OR_6PACU_13 18,49 0,00 25,58 40,63 3OR_6PACU_14 9,27 12,16 36,63 0,00 3OR_6PACU_15 52,91 56,08 49,24 0,00 3OR_6PACU_16 4,58 14,87 27,56 0,00 3OR_6PACU_17 3,79 0,00 68,56 28,99 3OR_6PACU_18 22,11 11,48 0,00 0,93 3OR_6PACU_19 15,49 0,00 12,09 3,71 3OR_6PACU_20 0,00 1,61 27,85 17,33 3OR_6PACU_21 11,14 2,85 28,07 0,00 3OR_6PACU_22 7,87 26,09 29,25 0,00 3OR_6PACU_23 30,34 1,16 54,79 0,00 3OR_6PACU_24 0,00 4,09 31,91 20,13 3OR_6PACU_25 4,79 0,00 50,92 40,64 3OR_6PACU_26 36,98 15,28 38,69 0,00 3OR_6PACU_27 7,14 59,40 43,19 0,00 3OR_6PACU_28 6,72 21,90 41,06 0,00 3OR_6PACU_29 0,00 28,78 36,78 21,40 3OR_6PACU_30 22,05 14,66 73,69 0,00 RPD de FO de experimentación - III RPD de FO de experimentación - I RPD de FO de experimentación - II
RPD - FO IG IG_TA ABC ABC_ELIT 6OR_6PACU_1 0,00 4,78 2,50 2,26 6OR_6PACU_2 6,94 0,00 48,35 38,59 6OR_6PACU_3 23,25 0,00 37,72 7,05 6OR_6PACU_4 0,00 11,44 23,18 11,49 6OR_6PACU_5 0,00 7,65 55,98 56,40 6OR_6PACU_6 0,00 14,69 45,16 3,87 6OR_6PACU_7 4,06 0,00 29,07 26,04 6OR_6PACU_8 0,66 0,00 19,42 9,54 6OR_6PACU_9 0,00 17,80 25,32 27,14 6OR_6PACU_10 0,00 7,03 36,51 9,90 6OR_6PACU_11 0,00 43,52 39,55 33,67 6OR_6PACU_12 4,18 8,57 0,00 2,95 6OR_6PACU_13 0,00 16,94 47,23 24,75 6OR_6PACU_14 4,62 0,00 28,06 5,62 6OR_6PACU_15 0,00 23,44 45,35 27,65 6OR_6PACU_16 0,00 17,85 75,26 45,18 6OR_6PACU_17 14,88 0,00 20,02 25,23 6OR_6PACU_18 12,76 0,00 23,01 26,16 6OR_6PACU_19 23,31 4,19 17,18 0,00 6OR_6PACU_20 0,00 26,34 24,60 20,85 6OR_6PACU_21 11,53 0,00 23,39 2,82 6OR_6PACU_22 25,61 0,00 36,01 11,64 6OR_6PACU_23 0,00 18,71 12,58 11,72 6OR_6PACU_24 5,48 0,00 40,20 12,60 6OR_6PACU_25 14,89 6,78 39,93 0,00 6OR_6PACU_26 5,96 0,00 27,46 16,67 6OR_6PACU_27 2,34 0,00 43,47 16,11 6OR_6PACU_28 0,00 1,06 5,46 11,05 6OR_6PACU_29 0,00 0,61 2,11 5,73 6OR_6PACU_30 35,26 23,51 26,23 0,00 RPD - FO IG IG_TA ABC ABC_ELIT 6OR_9PACU_1 9,36 0,00 19,89 4,96 6OR_9PACU_2 0,00 17,90 28,15 23,45 6OR_9PACU_3 0,00 3,17 19,41 15,05 6OR_9PACU_4 39,30 31,55 40,16 0,00 6OR_9PACU_5 33,18 60,09 0,00 5,05 6OR_9PACU_6 13,69 26,93 8,81 0,00 6OR_9PACU_7 0,00 15,72 9,79 0,40 6OR_9PACU_8 5,44 5,93 12,09 0,00 6OR_9PACU_9 4,93 13,08 17,72 0,00 6OR_9PACU_10 8,48 0,00 19,65 26,29 6OR_9PACU_11 10,49 27,15 0,00 17,37 6OR_9PACU_12 0,00 17,53 13,03 7,16 6OR_9PACU_13 12,20 0,00 25,32 18,34 6OR_9PACU_14 23,13 5,52 31,30 0,00 6OR_9PACU_15 0,00 10,10 45,00 11,05 6OR_9PACU_16 0,00 21,33 25,26 24,69 6OR_9PACU_17 0,00 7,47 20,34 16,87 6OR_9PACU_18 0,00 30,55 25,45 5,29 6OR_9PACU_19 16,17 7,40 11,36 0,00 6OR_9PACU_20 3,77 20,76 0,00 19,83 6OR_9PACU_21 0,00 3,75 23,97 4,72 6OR_9PACU_22 3,89 3,64 0,00 12,82 6OR_9PACU_23 15,63 14,28 10,44 0,00 6OR_9PACU_24 3,54 0,78 13,89 0,00 6OR_9PACU_25 0,00 14,72 7,69 6,52 6OR_9PACU_26 10,00 0,00 16,31 1,52 6OR_9PACU_27 0,00 19,14 24,88 12,16 6OR_9PACU_28 14,23 13,55 0,00 30,00 6OR_9PACU_29 19,79 0,00 57,74 11,55 6OR_9PACU_30 30,84 8,80 51,30 0,00 RPD - FO IG IG_TA ABC ABC_ELIT 6OR_12PACU_1 10,13 30,30 25,88 0,00 6OR_12PACU_2 28,68 0,00 13,66 14,05 6OR_12PACU_3 12,05 5,69 20,11 0,00 6OR_12PACU_4 0,00 4,09 31,43 0,66 6OR_12PACU_5 0,00 5,56 11,72 21,73 6OR_12PACU_6 8,38 2,35 9,19 0,00 6OR_12PACU_7 37,44 25,77 0,00 12,91 6OR_12PACU_8 24,99 0,00 30,81 13,14 6OR_12PACU_9 35,33 0,00 17,00 17,07 6OR_12PACU_10 20,18 21,25 0,00 10,96 6OR_12PACU_11 29,02 0,00 34,99 31,10 6OR_12PACU_12 1,43 0,00 11,27 1,99 6OR_12PACU_13 0,00 55,35 42,94 12,43 6OR_12PACU_14 27,06 27,86 34,60 0,00 6OR_12PACU_15 0,26 0,00 25,77 9,40 6OR_12PACU_16 0,23 10,26 19,64 0,00 6OR_12PACU_17 23,09 16,62 39,04 0,00 6OR_12PACU_18 0,00 5,35 28,26 18,96 6OR_12PACU_19 0,00 18,14 17,72 13,42 6OR_12PACU_20 0,00 4,41 0,82 5,90 6OR_12PACU_21 28,09 0,00 32,60 13,87 6OR_12PACU_22 0,00 24,65 46,18 14,28 6OR_12PACU_23 13,88 3,29 8,05 0,00 6OR_12PACU_24 7,89 0,00 32,67 7,11 6OR_12PACU_25 0,00 2,47 24,45 20,21 6OR_12PACU_26 0,32 1,04 13,66 0,00 6OR_12PACU_27 5,64 0,00 35,84 8,35 6OR_12PACU_28 20,65 0,00 9,98 9,81 6OR_12PACU_29 43,02 17,73 24,66 0,00 6OR_12PACU_30 51,96 63,35 79,56 0,00 RPD de FO de experimentación - V RPD de FO de experimentación - VI RPD de FO de experimentación - IV