GPSS-Plus es un motor de simulación por eventos discretos y sistemas continuos orientado a modelar y analizar el comportamiento de sistemas reales a lo largo del tiempo. Su propósito no es programar aplicaciones, sino describir cómo funciona un sistema y observar su evolución bajo distintas condiciones.
En esencia, opera como un laboratorio digital donde fluye un conjunto de entidades (clientes, productos, vehículos, señales o paquetes de datos) a través de una red de procesos y recursos con capacidades y demoras definidas.
El objetivo es transformar una descripción real del sistema en un modelo ejecutable: desde una línea de producción industrial o una cadena logística, hasta el tráfico de datos en una red o un protocolo de emergencia. Al simular miles de escenarios en minutos, permite realizar previsiones, optimizar decisiones y diseñar protocolos de contingencia dentro de un entorno de simulación generalista.
Existen diferentes enfoques para abordar el modelado de sistemas tanto por su interfaz como por su objetivo.
Basados en Drag & Drop discretos y/o continuos:
Discretos basados en lenguajes propios o generalistas:
Gracias a la herencia de los conceptos clave de GPSS clásico, GPSS-Plus se sitúa en ambos mundos, utilizando un lenguaje propio para definir el modelo, en lugar de construirlo sobre lenguajes generalistas, y reforzando el papel de la descripción textual como representación del sistema frente a los esquemas gráficos.
El objetivo último es que todo pueda comprenderse:
GENERATE 5
ADVANCE 10 {From:"Madrid", To:"Paris"}
TERMINATE
Las principales innovaciones introducidas son:
El cambio más significativo está en el manejo de los parámetros de los bloques y comandos. En GPSS clásico, los parámetros A, B, C... ofrecían una sintaxis fija y limitada.
Ejemplo clásico:
ADVANCE 30,10 ; Espera entre 30 y 40 tiempos
En GPSS-Plus, se mantiene esa estructura, pero se extiende con un formato JSON-like, que permite especificar más detalles sin romper la simplicidad:
ADVANCE 30,10 {From:"Madrid", To:"Paris"}
Esto hace posible integrar parámetros gráficos, de comportamiento, o lógicos, sin inventar nuevas variantes sintácticas para cada caso.
GPSS-Plus representa visualmente las entidades y los bloques mediante elementos gráficos simples: “bolitas” que se desplazan por un canvas siguiendo exactamente el flujo del modelo. Esta representación permite observar el sistema en tiempo real, comprender su dinámica de forma inmediata y facilitar la detección de cuellos de botella o comportamientos inesperados durante la ejecución.
Esta visualización, inexistente en GPSS clásico, introduce una nueva forma de interactuar con el modelo, donde la observación directa se convierte en una herramienta clave de validación y comprensión.
Las entidades virtuales son la ayuda para describir lo invisible del modelo. Para quien venga de otros paradigmas, recogen lo que antes se describía fuera en otro lenguaje. Para quien se inicia, simplemente son otra entidad más.
En GPSS-Plus, todo lo que ocurre en el sistema se desarrolla en el tiempo y puede observarse mientras sucede. Para que eso sea posible todos los comportamientos se tratan de la misma forma: Como entidades que se mueven, abstractas, visibles o un simple volumen.
Las entidades virtuales no se visualizan, pero son entidades completas que ejecutan lógica interna o eventos del sistema. Nacen para describir ese comportamiento con ON_ENTER, un TIMER o TIMEOUT, y viven en el propio modelo.
Según su ciclo de vida, existen tres tipos principales:
Reactores, que atienden eventos concretos y desaparecen.
Agentes, que no mueren y gestionan el universo del modelo.
Componentes, entidades vivas que existen como parte de otra entidad y mueren con ella.
Con este enfoque, el modelo se convierte en un ecosistema de ejecuciones concurrentes. Una entidad puede esperar un autobús mientras atiende una llamada; un agente puede controlar el tráfico; el sol puede salir y ponerse sin que ninguna entidad principal tenga que ocuparse de ello.
Se observa que muchos conceptos habituales de la Programación Procedural o la Orientada a Objetos se reinterpretan: el comportamiento no se encapsula en objetos jerárquicos, sino que se modela explícitamente como entidades que coexisten y actúan en paralelo dentro del sistema.
No hay this, ni parent, ni child: todos son brothers. Las funciones dejan de ser llamadas puntuales y pasan a convertirse en procedimientos vivos. No existe “this.respirar()” que siempre respira igual, sino una persona que camina y, al mismo tiempo, respira un aire diferente.
Al describirse íntegramente como un modelo explícito, GPSS-Plus permite que su comportamiento pueda ser analizado, interpretado y contrastado de forma automática. Una inteligencia artificial o auditor externo puede reconstruir qué hace el sistema, detectar incoherencias internas y contrastar la intención del modelador con la ejecución real del modelo.
Esta capacidad introduce una evolución natural de los procesos clásicos de verificación y validación (V&V), que dejan de basarse únicamente en revisión manual y análisis estadístico. Se incorpora así la coherencia entre historia (H), modelo (M) y ejecución (E) sentando las bases de un nuevo enfoque de validación.
En GPSS-Plus, las variables (SAVEVALUE, ASSIGN) pueden contener números, strings, arrays u objetos.
Esto permite trabajar con estructuras de datos complejas directamente en el modelo, sin recurrir a lenguajes externos ni perder legibilidad. Por ejemplo:
ASSIGN DATOS, {tipo:"Estudiante", Nombre:"Antonio", Edad: 22, Calificaciones: [8,5.8,6,8]}
El modelado continuo en GPSS-Plus se apoya en una idea simple: dividir el tiempo en pequeños fotogramas. Al ejecutar estos pasos de forma sucesiva dentro de la cola de eventos, el comportamiento continuo emerge de manera natural.
INTEGRATE, DYNAMIC, SOLVE son las herramientas básicas que permiten definir estructuras de sistema continuo integrada en el mismo corazón de lo discreto. RK4, matrices Jacobianas y Newton Raphson permiten simular fluidos, voltajes o velocidades con el mismo DSL.
Los modelos de simulación pueden perseguir tres objetivos principales:
- Sandbox, orientado a la simulación acotada y a la obtención de resultados, con o sin precarga de datos históricos.
- Gemelo digital, donde el modelo acompaña a un sistema real para su verificación, control u operación predictiva.
- Edge software, en el que el modelo constituye el propio sistema, ejecutándose de forma permanente como lógica operativa.
En los dos últimos casos, el modelo debe interactuar con el entorno externo. GPSS-Plus permite este acoplamiento mediante el BRIDGER, que actúa como un canal bidireccional genérico entre el modelo y sistemas físicos reales.
Desde el punto de vista del lenguaje, el acceso a recursos externos es uniforme: una base de datos, un fichero o un sensor se manejan con la misma sintaxis. Abrir un relé o almacenar un dato se expresa mediante BRIDGE_WRITE, y sustituir un GENERATE que simula la entrada de un paquete por una suscripción a un sensor real de movimiento es prácticamente inmediato. La transición entre sandbox a gemelo digital o edge software resulta así transparente.
Gracias a la Rehidratación, el modelo no solo se conecta al mundo real, sino que se sincroniza con su estado en curso, permitiendo que el simulador 'despierte' en mitad de un proceso sin perder la continuidad operativa tras el último dato almacenado incluso tras la modificación del modelo.
Además de estas innovaciones, GPSS-Plus incorpora múltiples extensiones que amplían su expresividad, modularidad y potencia:
Nuevos recursos: bloques nuevos como STOCK, RESTROOM, CONDITIONS, máquinas de estados finitos que amplían la paleta clásica (STORAGE, FACILITY...).
Creación dinámica de recursos: Lo que en motores Drag&Drop exigiría arrastrar cientos de bloques manualmente, en GPSS-Plus se logra con un simple bucle FOREACH y un NEWFACILITY. Esta es la diferencia clave entre un lenguaje textual y las interfaces gráficas clásicas.
Behavior functions: a partir de datos reales o simulados, crea interpolaciones automáticas que encapsulan comportamientos complejos (por ejemplo, distintos tramos de una carretera o máquina).
Funciones nativas: herramientas como CONCAT, MERGE , PUSH extienden el lenguaje sin depender de código externo.
Gráficas automáticas: basta con tabular datos para generar gráficos dentro del entorno de simulación.
Contextos (CX$): cada modelo puede definir contextos independientes, ideales para crear librerías, módulos o componentes reutilizables.
Depuración total: el sistema permite el seguimiento detallado de la cola de eventos, de cada entidad, recurso o variable, facilitando el análisis y la corrección de modelos.
Para entender GPSS ó GPSS-Plus y cualquier motor de simulación, primero debemos entender qué significa un sistema de eventos discretos (DES).
Un Sistema de Eventos Discretos (DES) modela procesos complejos basándose en la idea de que la actividad del sistema es una secuencia de eventos discretos.
¿Y qué es un evento discreto? Un evento discreto es una acción o cambio que ocurre en un momento determinado y finaliza en otro:
El sistema completo se representa como la combinación de estos eventos puntuales que, al ejecutarse en el tiempo, simulan un proceso de la vida real.
Motores de simulación como GPSS y GPSS-Plus son las herramientas clave para este análisis. Su función es modelar y analizar cómo los recursos (máquinas, estaciones de trabajo, ventanillas, o vehículos) son gestionados en conjunción con las entidades (clientes, productos) y sus respectivas colas de espera.
Este enfoque genera informes y estadísticas que, entre muchas:
Identifican cuellos de botella y minimizan tiempos de espera.
Optimizan el uso de los recursos de manera eficiente.
Estudian y protocolizan acciones y contingencias ante cualquier eventualidad o fallo en el sistema.
Simularemos un sistema básico donde las entidades (clientes) llegan a un recurso (ventanilla), esperan si está ocupada, son atendidas y luego se retiran. En este modelo:
Para los que conozcan GPSS clásico tendríamos:
GENERATE 60,10 ; Entidades generadas cada 60 a 70 unidades de tiempo. ADVANCE 10 ; Tarda 10 tiempos en colocarse en la cola de la ventanilla. SEIZE VENTANILLA ; La entidad llega a la cola de la ventanilla. ADVANCE 60,30 ; Tiempo de atención: 60 a 90 tiempos. RELEASE VENTANILLA ; Abandona la ventanilla. ADVANCE 10 ; Tarda 10 segundos en irse. TERMINATE 1 ; La entidad finaliza. START 100 ; Ejecuta esta acción hasta que 100 entidades hayan sido atendidas.
En GPSS-Plus, es muy similar, pero con representación gráfica. Para ello, configuramos recursos y parámetros adicionales:
FACILITY {NAME:VENTANILLA, X:300, Y:300} ; Definimos la ventanilla en el espacio.
POSITION {NAME:SALIDA, X:500, Y:300} ; Definimos la salida en el espacio.
GENERATE 60,10 {NAME:GEN1, X:100, Y:300} ; Posicionamos el generador de entidades.
ADVANCE 10 {TO:VENTANILLA} ; Avanza hasta la ventanilla.
SEIZE VENTANILLA ; Asignación del recurso "VENTANILLA".
ADVANCE 60,30 ; Tiempo de atención: 60 a 90 tiempos.
RELEASE VENTANILLA ; Liberación del recurso.
ADVANCE 10 {TO:SALIDA} ; Avanza hasta la salida.
TERMINATE 1 ; La entidad finaliza.
START 100 ; Ejecuta esta acción hasta que 100 entidades hayan sido atendidas.
Paso final: Pulsa Play y observa el resultado. La primera entidad saldrá del "Generate" entre los momentos 60 y 70.
Como se puede observar, el programa no ha cambiado demasiado para añadir la parte gráfica. Básicamente, hemos definido la situación espacial de los elementos.
En una simulación, hay dos elementos principales en juego:
1. Las ENTIDADES o transacciones son los elementos que que cambian su estado con la simulación, pueden representar personas, cajas o cualquier elemento. En GPSS-Plus se representan con bolitas de colores. Estas entidades interactúan con el entorno. Por ejemplo, entran en juego a través del bloque GENERATE, avanzan por un circuito con ADVANCE y mueren en un TERMINATE.
2. Los RECURSOS, que conforman el entorno y son utilizados por las entidades, como una ventanilla de atención o una caja más grande que la que represente una entidad.
Por ejemplo, una persona (entidad) se acerca a una ventanilla (recurso). Esta ventanilla tendrá un COMANDO que la define, y una serie de BLOQUES que describen qué hace la persona en relación con la ventanilla.
Un ejemplo de definición de un recurso como ventanilla es el COMANDO:
FACILITY {name:Ventanilla1, x:100, y:100}
Un ejemplo de instrucción para que una entidad ocupe esa ventanilla es el BLOQUE:
SEIZE Ventanilla1
En resumen, nos basamos en dos elementos principales: bloques y comandos.
1. Bloques ligados a las entidades
Los bloques son instrucciones que las ENTIDADES ejecutan directamente durante la simulación. Cada bloque define una acción específica que afecta al flujo o estado de esa entidad.
Sintaxis general:
BLOQUE [PARÁMETROS A,B,C..] {OPCIONES JSON-LIKE}
Ejemplos de bloque:
ASSIGN nombreVariable,10 TERMINATE 1
Los parámetros se denominan por letras (A, B, C, ...). Dependiendo del bloque, también pueden incluir parámetros en formato JSON-like, que usualmente definen aspectos gráficos o avanzados.
Ejemplo:
MOD {COLOR:#FF0000} ; pone de color rojo una entidad.
Otro ejemplo:
ADVANCE 10,5 {TO:Ventanilla1} ; la entidad avanza en tiempo hacia "Ventanilla1"
2. Comandos ligados a los recursos y entorno del motor
Los comandos configuran el entorno de la simulación. A diferencia de los bloques, no son ejecutados directamente por las entidades. En cambio, definen recursos, posiciones gráficas y reglas del sistema.
Sintaxis general:
COMANDO {NAME:theName, OPCIONES JSON-LIKE}
Ejemplo para definir un almacén:
STORAGE {NAME:ALMACEN1, CAPACITY:10, X:270, Y:200}
Este formato permite agregar información adicional como la posición (X, Y).
En GPSS-Plus, las variables permiten almacenar información que puede ser utilizada por las entidades o para configurar elementos del entorno. Estas variables se dividen principalmente en dos tipos:
1. SAVEVALUE (variables globales):Las variables globales son accesibles por cualquier entidad y se mantienen durante toda la simulación. Son útiles para almacenar datos compartidos, como contadores, acumuladores o estados globales.
Sintaxis:
SAVEVALUE variable, valor
Ejemplo:
SAVEVALUE TiempoEspera, 8
Este comando establece la variable global "TiempoEspera" con un valor de 8.
Una vez definido, el valor puede actualizarse en cualquier momento:
SAVEVALUE TiempoEspera, 10
El valor de "TiempoEspera" ahora es 10.
Para recuperar el valor de una variable global se utilizan los SNA que tienen un formato particular:
X$nombreSavevalue ó X$(nombreSavevalue)
; X$TiempoEspera ó X$(TiempoEspera) IF (X$TiempoEspera>10) ...
Por ejemplo, incrementar el valor de un savevalue se puede hacer mediante:
SAVEVALUE TiempoEspera, X$TiempoEspera + 1
O a través del método:
SAVEVALUE.inc TiempoEspera
Particularmente, y debido a que estas variables son usadas por más de una entidad, tiene un COMANDO asociado para su creación e inicialización llamado INITIAL.
Su sintaxix es simple:
INITIAL TiempoEspera,10
Recuerda que INITIAL es un comando. Se usa al comienzo del programa para inicializar.
2. ASSIGN (variables locales de la entidad):
Las variables locales pertenecen a una entidad específica. Cada entidad puede tener su propio valor para una misma variable, lo que las hace útiles para gestionar datos exclusivos de cada entidad.
Sintaxis:
ASSIGN variable, valor
Ejemplo:
ASSIGN Identificador, 1
Este bloque asigna el valor 1 a la variable "Identificador" de la entidad actual que invoca el ASSIGN.
Para recuperar el valor de una variable local con su SNA de formato:
P$nombreAssign ó P$(nombreAssign)
; P$Identificador ó P$(Identificador) if(P$Identificador>5) ...
P$Identificador es la forma que permite obtener el valor de una variable local de la entidad.
Diferencias clave entre SAVEVALUE y ASSIGN:
Ejemplo combinado:
En este ejemplo, combinamos SAVEVALUE y ASSIGN para calcular el tiempo promedio de espera en una ventanilla. Si el tiempo está definido por un ADVANCE 15,10 significa que estarán entre 15 y 25 tiempos. Eso debería ir aproximando el promedio a 20.
INITIAL TiempoTotal, 0
INITIAL NumEntidades, 0
Facility {NAME:Ventanilla,X:354,Y:204}
Graphic {NAME:Text1,Type:TEXT,X:358,Y:321,Text:"Promedio"}
Position {NAME:Salida,X:577,Y:201}
START 50
;---------------------------------
Generate 20,0 {NAME:GEN1,X:100,Y:207}
ADVANCE 10 {to:Ventanilla}
SEIZE Ventanilla
ASSIGN TiempoInicio, AC1$
ADVANCE 15,10
ASSIGN TiempoFinal, AC1$
RELEASE Ventanilla
ADVANCE 15 {to:Salida}
ASSIGN TiempoEntidad, (P$TiempoFinal - P$TiempoInicio)
SAVEVALUE NumEntidades, X$NumEntidades + 1
SAVEVALUE TiempoTotal, X$TiempoTotal + P$TiempoEntidad
SAVEVALUE promedio, round(X$TiempoTotal / X$NumEntidades,3)
MOVE {NAME:Text1,text:"Promedio: X$promedio"}
TERMINATE 1
En este código:
- Cada entidad calcula su tiempo de espera en la ventanilla.
- Los tiempos se acumulan en la variable global "TiempoTotal".
- El número de entidades atendidas se cuenta en "NumEntidades".
- Al final de la simulación, puedes calcular el tiempo promedio de espera:
TiempoPromedio = X$TiempoTotal / X$NumEntidades
De esta manera, SAVEVALUE y ASSIGN trabajan juntos para ofrecer un manejo eficiente de datos en la simulación.
Más adelante veremos como ambos BLOQUES admiten mucho más que variables numerales.
En este ejemplo, simulamos un sistema donde cada entidad elige aleatoriamente una de tres ventanillas para ser atendida. Cada ventanilla tiene su propia cola, y el flujo se distribuye de forma desigual en función del tiempo de servicio.
La lógica de decisión se resuelve exclusivamente mediante procedimientos estructurados.
FACILITY {NAME:VENTANILLA1,X:320,Y:450}
FACILITY {NAME:VENTANILLA2,X:320,Y:300}
FACILITY {NAME:VENTANILLA3,X:320,Y:150}
POSITION {NAME:POS1,X:160,Y:300}
POSITION {NAME:POS2,X:497,Y:300}
START 30 ; Se ejecutará hasta que se completen 30 entidades
GENERATE 10,0 {NAME:GEN1,X:60,Y:300}
ADVANCE 20,0 {TO:POS1}
ASSIGN ALEATORIO,RANDOM
IF (P$ALEATORIO<0.3)
CALL CAMINO1
ELSE
IF (P$ALEATORIO<0.6)
CALL CAMINO2
ELSE
CALL CAMINO3
ENDIF
ENDIF
ADVANCE 20 {TO:POS2}
TERMINATE 1
;---------------------------------------------
PROCEDURE CAMINO1
ADVANCE 20 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE 25,10
RELEASE VENTANILLA1
ENDPROCEDURE
PROCEDURE CAMINO2
ADVANCE 20 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE 10,2
RELEASE VENTANILLA2
ENDPROCEDURE
PROCEDURE CAMINO3
ADVANCE 20 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE 10,2
RELEASE VENTANILLA3
ENDPROCEDURE
Con respecto al anterior sólo tenemos unos pocos BLOQUES nuevos:
ASSIGN ALEATORIO,RANDOM
Este bloque crea una variable local en la entidad (llamada ALEATORIO) y le asigna un número aleatorio entre 0 y 1, usando la función RANDOM.
Estas variables locales también se llaman parámetros y se accede a ellas con la notación SNA P$Nombre
P$ALEATORIO
IF (P$ALEATORIO<0.3)
Este es un bloque de estructura de control "IF".
Como se puede ver, es un "if" igual al de cualquier otro lenguaje. Comparamos una variable con un número.
Y este otro bloque de estructura de control no requiere tampoco muchas presentaciones.
Llama a un "PROCEDURE".
CALL CAMINO1
Por último
PROCEDURE CAMINO1 ... ENDPROCEDURE 100 ; produciría un assign en la entidad invocadora llamado CAMINO1 de valor 100
Que tampoco requieren mucha presentación. Encapsulan un conjunto de acciones (PROCEDURE).
ENDPROCEDURE Puede tener de un valor en el parámetro A que será retornado del mismo modo que si hubieramos generado una instrucción ASSIGN. Recuperable como P$NombredelProcedure (P.E. P$CAMINO1). En este caso, no se utiliza.
Uno de los recursos clásicos es el STORAGE.
FACILITY representa cosas como una ventanilla, una máquina o un operario: solo puede atender a una entidad a la vez y según su capacidad (con CAPACITY:3 la FACILITY atendería 3 entidades). STORAGE, en cambio, representa algo como un almacén, un tanque, o una red que puede usarse parcialmente.
¿Qué es un STORAGE? Un STORAGE es un tipo de recurso que no se ocupa por entidad, sino por cantidad. Se usa cuando una entidad debe reservar parte de un recurso sin ocuparlo por completo. Por ejemplo, un camión puede dejar 10 cajas en un almacén que admite hasta 40.
¿Cómo funciona? Se define con una capacidad total, por ejemplo 40 unidades.
STORAGE {NAME:almacen1, X:300, Y:200, capacity:40}
Las entidades usan el bloque ENTER para ocupar cierta cantidad, y LEAVE para devolverla.
ENTER almacen1,5 LEAVE almacen1
Si una entidad quiere ocupar más de lo que hay disponible, esperará automáticamente en una cola interna hasta que el STORAGE tenga suficiente capacidad libre. No necesitas programar nada adicional para gestionar esa espera.
En el ejemplo: Almacén de cajas definido (STORAGE) con 40 unidades de capacidad. Los camiones llegan cada 10 unidades de tiempo y depositan entre 1 y 16 cajas. Después de un tiempo, se retiran liberando el espacio ocupado. Verás en el texto de pantalla cuántas cajas hay almacenadas en cada momento.
Además de realizarse el cálculo a través de sumas y restas, se puede hacer directamente consultando el SNA correspondiente a la ocupación del STORAGE:
R$(almacen1,OCCUPIED) ; Unidades actualmente ocupadas R$(almacen1,LEFT) ; Unidades disponibles
/* Almacenes: STORAGE
Gestión de capacidad y uso de un almacén.
*/
INITIAL capacidad,40 ; define un savevalue llamado capacidad de valor 40
STORAGE {NAME:almacen1, X:300, Y:200, capacity:X$capacidad}
POSITION {NAME:SALIDA, X:500, Y:200}
; Texto que muestra el contenido actual
GRAPHIC {NAME:texto1, Type:TEXT, X:320, Y:240, Text:"Cajas actuales: 0"}
START 100
; Camiones llegando cada 10 unidades de tiempo
GENERATE 10,0 {NAME:Camion, X:100, Y:200}
ADVANCE 15 {TO:almacen1}
ENTER almacen1,(random*15)+1 ; Ocupa entre 1 y 16 unidades del storage
savevalue uso,X$capacidad - R$(almacen1,LEFT)
MOVE {name:texto1,text:"Cajas actuales X$uso : R$(almacen1,OCCUPIED)"}
ADVANCE 40,10
LEAVE almacen1 ; Libera las unidades ocupadas
savevalue uso,X$capacidad - R$(almacen1,LEFT)
MOVE {name:texto1,text:"Cajas actuales X$uso : R$(almacen1,OCCUPIED)"}
ADVANCE 15 {TO:SALIDA}
TERMINATE 1
La cola de eventos:
En el corazón de cualquier simulación de eventos discretos se encuentra la cola de eventos, una estructura que define cómo y cuándo se ejecutan las acciones de las entidades en el modelo. Este enfoque de eventos discretos es lo que lo distingue de otros lenguajes de programación.
Hay que pensar que es como si cada entidad tuviese su propia programación, como se haría en C o Python, pero saltando entre entidades según el orden temporal que marca esta cola. Si simulamos 10 entidades, hay 10 programas corriendo “a la vez” (o casi).
¿Qué es la cola de eventos?
Es una lista ordenada de acciones programadas, cada una asociada a un instante de tiempo simulado (AC1). La cola garantiza que las acciones se ejecuten en el orden correcto según el tiempo en que deben ocurrir.
Lo más importante: está ordenada por tiempo. Por ejemplo:
Tiempo 15 - Atender a la entidad 14 en el paso 88 Tiempo 45 - Atender a la entidad 22 en el paso 16 Tiempo 77 - Atender a la entidad 16 en el paso 25
Ejemplo: Evolución de la cola de eventos
Supongamos el caso clásico de un banco donde los clientes entran por la puerta y son atendidos en una ventanilla. Las entidades son generadas con GENERATE y pasan un tiempo siendo atendidas mediante ADVANCE.
Imaginemos que estamos en el instante T = 5, con la siguiente cola:
T 5: Entidad 5: Esperando en la cola de la ventanilla T 7: Generate: Generar elemento, cliente que va a entrar por la puerta T 8: Entidad 9: Dirigiéndose a la cola de la ventanilla T 16: Entidad 3: Dirigiéndose a la cola de la ventanilla
Ahora mismo, tenemos una entidad, la 5, que es la siguiente en la cola. Extraeremos ese elemento de la cola para procesarlo. ¡Ahora sabemos que el tiempo es 5! No porque el tiempo haya pasado, sino porque nuestra siguiente tarea está programada para T=5.
Vamos a pensar que, por ejemplo, la Entidad 5 se encuentra en el "paso" (posición de su programación) que indica un ADVANCE 10 para ser atendida.
Ante esto, la solución es colocarse en la cola en T=15 (5+10) y cesar en su ejecución mientras es atendida.
Ahora la cola de eventos tendrá esta situación:
T 7: Generate: Generar elemento T 8: Entidad 9: Dirigiendose a la cola de la ventanilla T 15: Entidad 5: Ir a la salida T 16: Entidad 3: Dirigiendose a la cola de la ventanilla
Ya estamos en el evento siguiente. Toca actualizar el tiempo del sistema de nuevo: "AC1" que ahora toma el valor 7. Como decíamos, no es que el tiempo haya pasado, es que la cola de eventos hace saltar el tiempo porque no tiene nada que hacer hasta T=7.
Extraemos el elemento de la cola y nos encontramos con un GENERATE 20,5. Por lo tanto, debemos calcular qué tiempo será ese definido por 20,5; algo entre 20 y 25. Supongamos el resultado de 22. Es decir, entrará otra persona dentro de 22 tiempos. La cola pasa ahora a tener este otro aspecto después de moverse el GENERATE y realizar 2 tareas:
1.- En propio GENERATE avanza 20 (parámetro A) (7+20=27).
2.- Crear una nueva entidad (la número 10) para que pueda llevar a cabo sus tareas en 22 momentos después (7+22=29).
T 8: Entidad 9: Dirigiendose a la cola de la ventanilla T 15: Entidad 5: Ir a la salida T 16: Entidad 3: Dirigiendose a la cola de la ventanilla T 27: Generate: Generar elemento T 29: Entidad 10: Dirigiendose a la cola de la ventanilla
Ahora sería el momento de volver a mover el tiempo del sistema y atender el siguiente elemento: la Entidad 9.
Así seguirá viva la cola de eventos hasta que se cumpla alguna de las opciones que finalicen el programa.
Esta cola de eventos en GPSS-Plus tiene, además, otras características para poder realizar las tareas en el mundo gráfico y añadir los eventos tipo "ON_*".
En esta cola GPSS-Plus, tenemos 5 tipos de elementos:
Las colas de los recursos:
Otras colas existentes son las de los recursos que cada uno de ellos puede tener una o dos según el caso.
Estas listas están ordenadas por orden de llegada y su gestión depende exclusivamente del propio recurso.
Por ejemplo, una facility tiene dos listas, la de entidades ocupantes y la de entidades en cola.
Cuando una entidad intenta hacer SEIZE, si el recurso está ocupado, pasará a la lista de espera. Si no hay entidades en el recurso, lo ocupará ingresando en la lista de ocupantes.
Cuando realize RELEASE, abandonará esta lista y forzará al recurso a buscar alguna entidad en su cola pendiente de entrar. Si hay alguna, la incorporará a la lista de ocupantes y planificará su entrada en la cola de eventos en ese mismo momento, por lo que esa entidad tomará el control del motor en cuanto la saliente cese su actividad.
Conclusión
La cola de eventos no solo define el orden y el tiempo en que ocurren las acciones, sino que también actúa como el núcleo del sistema. Garantiza que los bloques se ejecuten correctamente y que la simulación sea consistente y precisa. Comprender este concepto es esencial para diseñar modelos eficientes y aprovechar al máximo las capacidades de GPSS-Plus.
En GPSS-Plus es sencillo ver esta cola, sólo debes activar la ventana de menú y pulsar sobre el botón "Event Queue".
Imagina un grupo de personas esperando a que se abra una puerta automática. No están haciendo cola ni ocupando nada, simplemente están esperando que ocurra algo.
Eso es lo que permite el recurso CONDITIONS: detener entidades hasta que una condición lógica se cumpla.
Algo parecido existía en GPSS clásico con el bloque ASSEMBLE para este propósito. En GPSS-Plus, este comportamiento se sustituye por este recurso más general y flexible.
El recurso CONDITIONS permite retener entidades hasta que se cumpla una condición. Esta condición se define como una expresión evaluada cada vez que una entidad intenta pasar.
CONDITIONS {NAME:Conditions1,X:208,Y:317,expression:(X$contador>=5)}
CONDITIONStiene más opciones que se verán más adelante.
Su bloque asociado WAITUNTIL se encarga de validar para esta entidad la condición, en este caso, si el contador es mayor o igual a 5.
Llegada la 5ª entidad, la condición se cumplirá y seguirá adelante sin ser retenida. Ahora solo falta liberar al resto chequeando a todas las retenidas con otro bloque asociado: WAITCHECK.
Resulta interesante observar que salen todas juntas del CONDITIONS en el primer tramo. En el segundo, la aleatoriedad del ADVANCE 20,40 hace que se separen.
También empezamos a utilizar el BLOQUE ENDGENERATE más acorde con el método estructurado que TERMINATE. Son equivalentes pero sintácticamente es más adecuado estructurar el bloque con GENERATE/ENDGENERATE. Permite identificar claramente qué bloque fue generado, especialmente si hay múltiples GENERATE en el programa.
/* Sincronización: CONDITIONS / WAITUNTIL
Las entidades esperan en grupo y avanzan juntas.
*/
POSITION {NAME:POS1,X:208,Y:222}
POSITION {NAME:POS2,X:452,Y:215}
POSITION {NAME:POS3,X:752,Y:215}
CONDITIONS {NAME:Conditions1,X:208,Y:317,expression:(X$contador>=5)} ; Recurso retenedor
INITIAL contador,0
START 100
;*****************************************************
GENERATE 10,2 {NAME:GEN1,X:86,Y:228,ERADIO:10,ECOLOR:#FF9900}
ADVANCE 16,0 {TO:POS1}
savevalue contador,X$contador + 1
WAITUNTIL Conditions1
WAITCHECK Conditions1
savevalue contador,0
ADVANCE 20,0 {TO:POS2} ; Avanzan hacia la segunda posición TODOS JUNTOS
ADVANCE 20,40 {TO:POS3} ; Avanzan hacia la tercera posición con un tiempo aleatorio entre 20 y 40 pero esta vez SEPARÁNDOSE
ENDGENERATE 1 ; Finaliza la vida de la entidad ES IDENTICO A TERMINATE 1
Ahora que ya sabes usar procedimientos y decisiones condicionales simples (IF), vamos a ver un caso donde una entidad elige entre varias rutas posibles.
Para eso, usamos la estructura SWITCH, muy parecida a un “menú de decisiones”.
¿Qué hace este ejemplo?
P$ALEATORIO).CALL CAMINO1, ..., CALL CAMINO4).Además definimos cierto aspecto puramente visual, "el flow".
Activándolo en los ADVANCE veremos una línea de ruta que comenzará siendo vertical u horizontal según el LAYOUT, podrá pasar por un cierto VIA y VIA2, además de poder converger o divergir según MERGE o DECISION.
/* Bloques: SWITCH y LOCK/UNLOCK
Enrutamiento condicional y gestión de recursos bloqueables.
*/
FACILITY {NAME:VENTANILLA1,X:320,Y:450,capacity:3}
FACILITY {NAME:VENTANILLA2,X:320,Y:300,capacity:3}
FACILITY {NAME:VENTANILLA3,X:320,Y:150}
FACILITY {NAME:VENTANILLA4,X:320,Y:50}
POSITION {NAME:POS1,X:158,Y:301}
POSITION {NAME:POS2,X:497,Y:300}
POSITION {NAME:POS3,X:627,Y:300,TYPE:TERMINATE,TITLE:END}
Graphic {NAME:Text1,Type:TEXT,X:320,Y:104,Text:"Unlock"}
START 30
GENERATE 10,0 {NAME:GEN1,X:57,Y:300}
ADVANCE 20,0 {TO:POS1,flow:1}
ASSIGN ALEATORIO,(RANDOM)
SWITCH P$ALEATORIO
CASE <,0.4
CALL CAMINO1
ENDCASE
CASE <,0.8
CALL CAMINO2
ENDCASE
CASE <,0.92
CALL CAMINO3
MOVE {NAME:Text1,text:"Lock"}
LOCK VENTANILLA1
LOCK VENTANILLA2
LOCK VENTANILLA3
ENDCASE
DEFAULT
CALL CAMINO4
MOVE {NAME:Text1,text:"Unlock"}
UNLOCK VENTANILLA1
UNLOCK VENTANILLA2
UNLOCK VENTANILLA3
ENDCASE
ENDSWITCH
ADVANCE 20,10 {TO:POS2,flow:1,MERGE:"salida"}
ADVANCE 20,0 {TO:POS3,flow:1}
ENDGENERATE 1
;****************************************
PROCEDURE CAMINO1
ADVANCE 20 {TO:VENTANILLA1,flow:1,DECISION:"inicio"}
SEIZE VENTANILLA1
ADVANCE 45,10
RELEASE VENTANILLA1
ENDPROCEDURE
PROCEDURE CAMINO2
ADVANCE 20 {TO:VENTANILLA2,flow:1,DECISION:"inicio"}
SEIZE VENTANILLA2
ADVANCE 40,10
RELEASE VENTANILLA2
ENDPROCEDURE
PROCEDURE CAMINO3
ADVANCE 20 {TO:VENTANILLA3,flow:1,DECISION:"inicio"}
SEIZE VENTANILLA3
ADVANCE 40,20
RELEASE VENTANILLA3
ENDPROCEDURE
PROCEDURE CAMINO4
ADVANCE 20 {TO:VENTANILLA4,flow:1,DECISION:"inicio"}
SEIZE VENTANILLA4
ADVANCE 10,2
RELEASE VENTANILLA4
ENDPROCEDURE
;***************************************************************Ahora que ya conoces los fundamentos de GPSS-Plus, veamos cómo se integran todos en un ejemplo unificado.
Vamos a ver un código bastante más largo de lo que hemos visto hasta ahora, casi 100 líneas de código.
Este modelo representa un sistema con vehículos de distintos tipos que recorren una zona de trabajo durante varios ciclos. Cada uno decide su ruta, ejecuta tareas, y finaliza tras completar un número de ciclos entrando en el aparcamiento de utilitarios o de camiones según el caso ocupando la facility o el storage dependiendo del tamaño del camión.
Este código tiene varias novedades:
Lo primero que observamos es que hay a nivel general una estructura con dos GENERATES varios PROCEDURES.
Los generates se separan visualmente para que cada uno tenga su propia actividad y marcan las entidades nacidas de ellos de forma diferenciada.
GENERATE ... NAME:G_UTILITARIOSG_UTILITARIOS ; genera los utilitarios .... assign ... .... CALL PROC.MAIN ENDGENERATE 1
Y después tenemos la serie de PROCEDURES:
Procedures de proceso:
PROC.MAIN ; Proceso principal PROC.UTILITARIOS PROC.CAMIONES
Procedures de decisión:
DECIDE.RUTA DECIDE.LOOP_OR_END
PROC.MAIN:
Toda la lógica principal estará concentrada en el proceso PROC.MAIN que normalmente estará formado por un bucle while que encerrará las entidades mientras no cumplan la condición de finalizar todas sus tareas.
WHILE ("P$estado"!="EXIT")
...
...
ASSIGN estado,????
ENDWHILE
Dentro de éste se situará la decisión sobre las rutas principales que pude tomar cada entidad que suele ser un SWITCH de opciones o varios IFs anidados. Todas estas decisiones conviene colocarlas en PROCEDURES con el nombre: DECIDE.* de forma que sea sencillo reconocerlos y saber de antemano que lo normal es que retornen, directamente el nombre del PROCEDURE a ser invocado por la entidad:
ENDPROCEDURE "P$VALUE" ; Retornará 'PROC.UTILITARIOS' ó 'PROC.CAMIONES'
Es decir: CALL DECIDE.AAA devolverá en P$AAA (lo anterior al punto se obvia) el valor: 'PROC.BBB' que podrá usarse directamente como CALL P$AAA.
CALL DECIDE.RUTA ; Ejecuta lógica de decisión CALL "P$(RUTA)" ; Llama al procedimiento retornado por la decisión
Muy parecido a la decisión de salir o continuar dando vueltas al ciclo.
Otros elementos nuevos son:
ADVANCE0 {TO:POS_ENTRADA,flow:1,layout:H}
Que en este caso es un ADVANCE0 y no ADVANCE puesto que no queremos más que dar un salto visual sin que la entidad pierda tiempo entrando y saliendo de la cola de eventos.
Y la última novedad es el uso de otro recurso que tiene una utilidad de agrupamiento y estadística: QUEUER
Tiene capacidad ilimitada y con él se peude saber de forma sencilla cuántas y cuáles son las entidades que están en una determinada zona.
QUEUE EN_APARCAMIENTO ; entran ... ... DEPART EN_APARCAMIENTO ; salen
Resumidamente se puede observar en el ejemplo:
GENERATE con lógica diferenciada (camiones y utilitarios).SWITCH, CALL, PROCEDURE).WHILE) para simular iteración.FACILITY, STORAGE, QUEUE).P$(RUTA)).
QUEUER {NAME:EN_APARCAMIENTO,X:413,Y:497}
FACILITY {NAME:UTILITARIOS, capacity:4,X:413,Y:390}
STORAGE {name: CAMIONES , capacity:5, X:412,Y:120}
POSITION {NAME:POS_ENTRADA,X:240,Y:270}
POSITION {NAME:POS_SALIDA,X:557,Y:316,type:decision,title:"FIN?"}
POSITION {NAME:POS_LOOP,X:225,Y:549}
POSITION {NAME:FIN,X:730,Y:324,type:terminate}
START 20
;************************************************************************
GENERATE 20,10,0,10 {NAME:G_UTILITARIOS,X:80,Y:363,ESUBTITLE:"P$ciclos/3"}
assign TIPO_VEHICULO,1
mod {RADIO:5,color:#000066}
call PROC.MAIN
ENDGENERATE 1 ; ALIAS DE TERMINATE
;----------------------------
GENERATE 20,10,0,10 {NAME:G_CAMIONES,X:80,Y:199,ESUBTITLE:"P$ciclos/3"}
ASSIGN TIPO_VEHICULO,floor(RANDOM * 2 + 2)
mod {RADIO:10,color:#006600}
call PROC.MAIN
ENDGENERATE 1 ; ALIAS DE TERMINATE
;************************************************************************
PROCEDURE PROC.MAIN
ASSIGN ciclos,1
ADVANCE0 {TO:POS_ENTRADA,flow:1,layout:H}
ASSIGN estado, "loop"
WHILE ("P$estado"!="EXIT")
QUEUE EN_APARCAMIENTO
CALL DECIDE.RUTA ; return assign:RUTA
CALL "P$(RUTA)"
ADVANCE 20,18 {TO:POS_SALIDA,flow:1,MERGE:"salida"}
DEPART EN_APARCAMIENTO
CALL DECIDE.LOOP_OR_END
ASSIGN estado,"P$(LOOP_OR_END)"
ENDWHILE
ADVANCE 20,0 {TO:FIN,flow:1}
ENDPROCEDURE
;*****************************************************
PROCEDURE DECIDE.RUTA
ASSIGN VALUE,""
SWITCH P$TIPO_VEHICULO
CASE <=,1
ASSIGN VALUE,"PROC.UTILITARIOS"
ENDCASE
DEFAULT
ASSIGN VALUE,"PROC.CAMIONES"
ENDCASE
ENDSWITCH
ENDPROCEDURE "P$VALUE"
;---------------------------------------------
PROCEDURE DECIDE.LOOP_OR_END
assign value,"loop"
assign ciclos,P$ciclos + 1
if (P$ciclos > 3)
assign value,"EXIT"
mod {SUBTITLE:"-EXIT-"}
else
advance 30 {to:POS_ENTRADA,flow:1,via:POS_LOOP,layout:V}
endif
ENDPROCEDURE "P$value"
;---------------------------------------------
PROCEDURE PROC.UTILITARIOS
ADVANCE 20 {TO:UTILITARIOS,flow:1,DECISION:"recurso"}
SEIZE UTILITARIOS
ADVANCE 55,40
RELEASE UTILITARIOS
ENDPROCEDURE
PROCEDURE PROC.CAMIONES
ADVANCE 20 {TO:CAMIONES,flow:1,DECISION:"recurso" }
ENTER CAMIONES ,P$TIPO_VEHICULO
ADVANCE 40,20
LEAVE CAMIONES
ENDPROCEDURE
;----------------------------------
Ya hemos visto sobradamente qué es una entidad: Una bolita que se mueve por la pantalla. Sabemos que nace de un GENERATE, que recorre bloques, avanza por posiciones y puede entrar en recursos.
¿Y qué es una entidad virtual? Pues una entidad que no se mueve por la pantalla.
Parece muy simple la diferencia pero es básicamente esa aunque conlleva muchas implicaciones. Imagina una bolita invisible que no ocupa espacio pero sí ejecuta instrucciones.
Vamos a definirlas formalmente:
TIMER, PRE_RUN, TRIGGER (ON_ENTER, ON_QUEUE, ON_*...) O del BLOQUE: TIMEOUT TERMINATE no resta sucesos. De hecho, veremos que se usa TERMINATE_VE que solo termina con las virtuales.¿Y para qué sirven?
Pues tienen infinidad de utilidades.
Aunque no lo parezca, son el motivo por el que GPSS-Plus no utiliza lenguajes secundarios para programar elementos complementarios como pueda ser el Python o JAVA.
Se programan en GPSS-Plus como cualquier subprograma. Se encargan de todas las tareas accesorias a los ciclos de vida de las entidades, por ejemplo, si queremos montar un reloj, tiene poco sentido que sea el nacimiento o movimiento de las entidades que seguro que tienen un montón de cosas de las que preocuparse entrando y saliendo de recursos.
En su lugar, un TIMER que se lanzará automáticamente cada N instantes puede hacer estas tareas.
Si queremos tener un cierto control de múltiples recursos y el estado de sus colas, podemos tener una entidad virtual que cada vez que alguna entidad ingrese en una cola haga de controlador.
Las entidades virtuales trabajan entre bastidores. No se ven moverse, pero sin ellas, muchos procesos simplemente no ocurrirían.
En el ejemplo:
Una entidad normal, la número 1 (D$N==1), crea una entidad virtual que nacerá dentro de 30 instantes para ejecutar un procedure:
timeout actualizarContador,30
No recorre posiciones del flujo ni usa recursos. Solo aparece tras ser reclamada por una entidad y, desde entonces, la entidad virtual llama a una nueva entidad virtual cada 3 instantes.
Su única tarea: contar.
La primera llama a la segunda, que llama a la tercera.... todo a través de un BLOQUE TIMEOUT que sólo requiere el nombre del procedimiento a ejecutar por la entidad virtual y el tiempo en el que ocurrirá.
Se puede observar cómo el texto cambia aunque no hay entidades visibles que lo actualicen. Eso es porque lo hace una entidad virtual, que trabaja en segundo plano, como un pequeño proceso autónomo.
Este ejemplo demuestra cómo una VE puede actuar como proceso recurrente sin necesidad de ninguna intervención humana ni entidad física. A partir de ahora, verás que muchas de las decisiones del sistema, alarmas o estadísticas estarán gestionadas por este tipo de entidades invisibles.
/* Entidades Virtuales:
Procesos invisibles que operan en segundo plano.
La primera entidad creará con un retraso de 30 momentos una VE.
Esta VE creará su siguiente cada 3 momentos.
Solo son visibles las entidades, no las VE.
*/
POSITION {NAME:POS1,X:321,Y:332}
POSITION {NAME:POS2,X:517,Y:326}
Graphic {NAME:Text1,Type:TEXT,X:366,Y:454,Text:"Contador: 0"}
INITIAL contador,0
START 200
;*****************************************************
GENERATE 20,0 {NAME:GEN1,x:101,y:332}
ADVANCE 10 {TO:POS1}
; Lanzamos una entidad virtual que actualizará el contador
if (D$N==1)
timeout actualizarContador,30
endif
ADVANCE 10 {TO:POS2}
ENDGENERATE 1
;*****************************************************
PROCEDURE actualizarContador
SAVEVALUE contador, X$contador + 1
MOVE {name:Text1, text:"Soy la Entidad Virtual D$N. Contador: X$contador"}
TIMEOUT actualizarContador, 3
TERMINATE_VE
ENDPROCEDURE
Ya hemos visto qué es una entidad virtual: una entidad que no se ve moverse por la pantalla pero que ejecuta instrucciones y participa del motor de simulación.
Ahora veremos cómo nacen estas entidades virtuales y quién puede crearlas.
Las VE no nacen de un GENERATE, como las entidades normales. En su lugar, pueden ser creadas por cuatro mecanismos distintos:
El sistema, usando:
PRE_RUN – al comienzo de la simulación, una sola vez.
TIMER – en intervalos fijos definidos por INTERVAL.
Una entidad, usando:
TIMEOUT – programa la ejecución de una VE para dentro de X instantes.
Un recurso, usando eventos ON_* como:
ON_SEIZE, ON_RELEASE, ON_LEAVE, etc.
¿Qué tienen en común?
Todas las VEs ejecutan un PROCEDURE, que puede venir indicado como TRIGGER o como nombre de procedimiento.
Ese PROCEDURE es el "código" que ejecuta la VE. Debe terminar con un TERMINATE o TERMINATE_VE, igual que cualquier entidad normal.
Sobre los parámetros
Cuando usamos NEW, TIMEOUT o CALL, podemos pasar parámetros adicionales.
Estos estarán disponibles dentro del procedimiento como P$PARAM_A, P$PARAM_B, etc., igual que ya hemos visto con CALL.
Esto permite que una VE "sepa" quién la ha llamado o por qué.
En el ejemplo:
PRE_RUN crea una entidad al inicio (color magenta).
TIMER1 crea una cada 53 unidades (color azul).
TIMEOUT se lanza justo antes de entrar al recurso y deja un mensaje.
ON_RELEASE se dispara al liberar la FACILITY1.
Cada vez que una de estas VEs se crea, aparece un texto en pantalla con su identificador y lo que está haciendo.
/* Creación de las VE */
SYSTEM {TYPE:ON_TIMER, TRIGGER:TIMER1, INTERVAL: 53}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
POSITION {NAME:POS1,X:311,Y:497}
POSITION {NAME:POS2,X:571,Y:147,type:terminate,title:END}
Facility {NAME:Facility1,X:573,Y:483,ON_RELEASE:on_RELEASE_Facility1}
Graphic {NAME:Text_Pre_run,Type:TEXT,X:253,Y:196}
Graphic {NAME:Text_TIMER1,Type:TEXT,X:253,Y:158}
Graphic {NAME:Text_Timeout1,Type:TEXT,X:254,Y:252}
Graphic {NAME:Text_on_release,Type:TEXT,X:575,Y:544}
START 40
GENERATE 30,0 {NAME:GEN1,X:110,Y:510, ECOLOR:#FF3333, ERADIO:8}
if ("P$PARAM_A"=="PRE_RUN")
MOD {COLOR:magenta,subtitle:pre_run}
ENDIF
if ("P$PARAM_A"=="TIMER1")
MOD {COLOR:blue,subtitle:timer1}
ENDIF
ADVANCE 30,2 {TO:POS1,flow:1}
ADVANCE 30,2 {TO:Facility1,flow:1}
TIMEOUT Timeout1,0,D$N ; le pasamos como parámetro el número de entidad.
seize Facility1
advance 30,30
release Facility1
ADVANCE 30,2 {TO:POS2,flow:1}
ENDGENERATE 1
;******************************************
PROCEDURE PRE_RUN
MOVE {name:Text_Pre_run,Text:"Soy [D$N] PRE_RUN
Creo una nueva entidad."}
NEW GEN1,0,"pre_run"
TERMINATE_VE
ENDPROCEDURE
;---------------------------------
PROCEDURE TIMER1
MOVE {name:Text_TIMER1,Text:"Soy [D$N] Timer1.
Creo una nueva entidad."}
NEW GEN1,0,"timer1" ; Generate, tiempo, PARAM_A, PARAM_B...
TERMINATE_VE
ENDPROCEDURE
;---------------------------------
PROCEDURE Timeout1
MOVE {name:Text_Timeout1,Text:"Soy [D$N] Timeout1.
La entidad [P$PARAM_A] va a entrar"}
TERMINATE_VE
ENDPROCEDURE
PROCEDURE on_RELEASE_Facility1
MOVE {name:Text_on_release,Text:"Soy [D$N] on_RELEASE.
La entidad [P$ENTITYNUMBER] abandona la Facility1"}
TERMINATE_VE
ENDPROCEDURE
;******************************************
Un tipo especial de entidad virtual es aquella que no muere al final de su procedimiento.
Estas VE que permanecen activas durante toda la simulación (o parte de ella) las llamamos agentes.
PRE_RUN mediante un TIMEOUT.SAVEVALUE, para poder interactuar o consultar su estado.Más adelante veremos que estos agentes se encargan de tareas de control, monitorización o coordinación entre recursos.
En resumen, un agente es una VE que no hace TERMINATE y permanece viva en bucle.
Eso le permite estar atento, esperando, o vigilando el entorno.
/* Agentes: Entidades Virtuales Permanentes
Monitorización de recursos con un bucle infinito.
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
POSITION {NAME:POS1,X:311,Y:497}
POSITION {NAME:POS2,X:571,Y:147,type:terminate,title:END}
Facility {NAME:Facility1,X:573,Y:483}
Graphic {NAME:Text_agente,Type:TEXT,X:229,Y:305}
START 40
GENERATE 30,0 {NAME:GEN1,X:110,Y:510, ECOLOR:#FF3333, ERADIO:8}
ADVANCE 30,2 {TO:POS1,flow:1}
ADVANCE 30,2 {TO:Facility1,flow:1}
seize Facility1
advance 30,30
release Facility1
ADVANCE 30,2 {TO:POS2,flow:1}
ENDGENERATE 1
;******************************************
PROCEDURE PRE_RUN
TIMEOUT AGENTE.INIT,0
TERMINATE_VE
ENDPROCEDURE
;---------------------------------
PROCEDURE AGENTE.INIT
savevalue nAgente,D$N
WHILE (1==1)
MOVE {name:Text_agente,Text:"Soy el agente [D$N].
Tiempo AC1$
Entidades en Facility1: R$(Facility1,IN)
Entidades en cola: R$(Facility1,QUEUE)"}
ADVANCE 10
ENDWHILE
TERMINATE_VE
ENDPROCEDURE
Además de las entidades normales y de los agentes, el modelo admite un tercer tipo de entidad: las entidades componente.
Una entidad componente es una entidad que existe para ejecutar una función en nombre de otra entidad. No es un objeto físico ni un recurso externo, sino una parte activa de su comportamiento: algo que la entidad “hace” de manera continua o paralela.
Este concepto permite modelar situaciones muy naturales:
un buzo que respira y consume oxígeno mientras avanza, un vehículo cuyas ruedas se desgastan mientras circula, o una persona que lee una noticia y cambia de decisión mientras espera en la cola del autobús.
Más adelante veremos cómo se crean y cómo trabajan junto a la entidad principal, pero por ahora basta con entender que permiten que una entidad no sea un único hilo secuencial, sino un pequeño sistema formado por varias entidades colaborando entre sí.
El DSL de GPSS-Plus no está pensado como un lenguaje de programación general, ni como una notación alternativa para dibujar bloques. Está diseñado como el lenguaje operativo del motor de simulación.
Cuando se trabaja con el DSL, el usuario no “programa”: define comportamientos, recursos y eventos que el motor ejecuta en el tiempo: MODELA.
Para entenderlo correctamente, conviene pensar el DSL como un conjunto de herramientas organizadas en cuatro niveles.
El primer nivel del DSL está orientado a definir qué hacen las entidades que recorren el modelo.
Estas entidades:
nacen (GENERATE),
avanzan en el tiempo (ADVANCE),
interactúan con recursos (SEIZE, ENTER, REST…),
toman decisiones (IF, SWITCH),
y finalmente mueren (TERMINATE).
Todo su comportamiento se describe paso a paso, pero ese paso a paso no es secuencial real: es ejecución sobre una cola de eventos.
El DSL proporciona aquí:
bloques de flujo,
control temporal,
estructuras condicionales y bucles,
llamadas a procedimientos.
Este es el nivel más visible del modelo y el más cercano a la intuición del usuario.
No todo comportamiento pertenece a una “bolita”.
GPSS-Plus introduce entidades virtuales (VE) para modelar lógica que:
no tiene representación gráfica,
no recorre el circuito,
pero vive dentro del motor de simulación.
Las VE permiten:
reaccionar a eventos (ON_SEIZE, ON_RELEASE, SIGNAL),
ejecutar lógica periódica (TIMEOUT),
actuar como agentes, controladores, incluso como comonentes,
coordinar otras entidades sin bloquearlas.
Desde el punto de vista del DSL, una VE es una entidad completa, con ciclo de vida propio, pero orientada al control del sistema o entidades.
El tercer nivel del DSL define el entorno del modelo.
Los recursos representan aquello que las entidades usan, ocupan, esperan o consultan. Pueden ser:
físicos: FACILITY, STORAGE, RESTROOM, STOCK
lógicos: QUEUER, CONDITIONS, FSM
externos: BRIDGER, FILE, TABLE, PLOTTER
Cada recurso sigue el mismo patrón conceptual:
se define mediante un COMANDO,
se usa mediante BLOQUES emparejados,
se observa mediante SNAs.
El DSL no oculta el estado de los recursos: todo puede ser leído, consultado y usado en decisiones.
Por debajo de entidades y recursos existe un cuarto nivel, menos visible pero fundamental: el motor de simulación.
El DSL permite interactuar con él de forma explícita:
controlando el tiempo simulado,
programando eventos futuros (TIMEOUT),
configurando el sistema (SYSTEM),
accediendo a la cola de eventos y al estado interno mediante SNAs.
Esto permite modelos donde:
el comportamiento no es solo “flujo”,
sino reacción, sincronización y planificación
/* Season 3: DSL
Ejemplo de modelo simple con el DSL de GPSS-Plus.
*/
FACILITY {NAME:VENTANILLA, X:300, Y:300} ; Definimos la ventanilla en el espacio.
POSITION {NAME:SALIDA, X:500, Y:300} ; Definimos la salida en el espacio.
START 100 ; Ejecuta esta acción hasta que 100 entidades hayan sido atendidas
;------------------------------------------------------------------------------------
GENERATE 60,10 {NAME:GEN1, X:100, Y:300} ; Posicionamos el generador de entidades.
ADVANCE 10 {TO:VENTANILLA} ; Avanza hasta la ventanilla.
SEIZE VENTANILLA ; Asignación del recurso "VENTANILLA".
ADVANCE 60,30 ; Tiempo de atención: 60 a 90 segundos.
RELEASE VENTANILLA ; Liberación del recurso.
ADVANCE 10 {TO:SALIDA} ; Avanza hasta la salida.
TERMINATE 1 ; La entidad finaliza.
En cuanto al lenjuage, la evolución de GPSS clásico a GPSS-Plus tiene dos grandes puntos de inflexión:
SAVEVALUE y ASSIGN, que ahora no solo almacenan números, sino también cadenas, arrays y objetos y permiten ejecutar funciones sobre ellos.Este segundo avance convierte en obsoletos elementos antiguos como MATRIX y permite que GPSS-Plus incorpore funciones nativas de cualquier lenguaje moderno como push, concat, merge o split, todo dentro de la propia lógica del simulador y sin recurrir a programación externa. Lo que antes se resolvía con herramientas rígidas, ahora puede hacerse de forma expresiva, estructurada y mantenible. GPSS-Plus deja de ser un lenguaje de tarjetas con números y pasa a ser un DSL de propósito completo, orientado a flujos de eventos y manipulación estructurada de datos.
Ya sabemos que GPSS-Plus permite declarar variables con dos niveles de ámbito:
Su funcionamiento es idéntico salvo por su visibilidad. En ambos casos, las variables pueden contener diferentes tipos de datos:
Nótese que no existe booleano, que se usará 0 para falso y 1 para verdadero. Ejemplos:
ASSIGN miNumero, 10
ASSIGN miTexto, "hola"
ASSIGN miLista, [10, 20, 30]
ASSIGN miObjeto, {clave1: 20, clave2: "hola"}
ASSIGN mixto, [10, {clave: "dato"}, 30]
Acceso mediante rutas en sus SNA
Los SNA asociados a estas variables son P$ (para ASSIGN) y X$ (para SAVEVALUE). Pero dado que las variables ahora pueden ser algo más que números, los SNA serán más versátiles para poder acceder a cualquier parte de la variable. Su acceso será guiado por la ruta de claves separadas por puntos tanto para arrays como para objetos:
ASSIGN notas, [8,4,5] ASSIGN media, (P$(notas.0) +P$(notas.1) +P$(notas.2))/3
ASSIGN notas, {notaExamen1:8,notaExamen2:4,notaExamen3:5}
ASSIGN media, (P$(notas.notaExamen1) +P$(notas.notaExamen2) +P$(notas.notaExamen3))/3
Y todo esto puede hacerse tan complejo como sea necesario introduciendo los SNA en cualquier parte de la notación para construir la ruta adecuada:
ASSIGN numero_0,0
ASSIGN numero_1,1
ASSIGN numero_2,2
ASSIGN notas, {notaExamen_0:8,notaExamen_1:4,notaExamen_2:5}
ASSIGN media, (P$(notas.notaExamen_P$numero_0) + P$(notas.notaExamen_P$numero_1) + P$(notas.notaExamen_P$numero_2)) / 3
O el acceso a un array dentro de un objeto o un objeto dentro de un array siguiendo el mismo proceder:
ASSIGN grupo, [{nombre: "Luis"}, {nombre: "Marta"}]
ASSIGN nombrePrimero, P$(grupo.0.nombre)
Existe una única palabra reservada para estas rutas: LENGTH
ASSIGN longitud, P$(unArray.LENGTH)
Modificación parcial mediante rutas
También puede modificarse una parte de una estructura accediendo a ella directamente:
ASSIGN unArray.0, {clave1: "texto1"}
convirtiendo el primer elemento del array en un objeto
SNA de acceso sin evaluación:V$
Mientras que P$ y X$ evalúan los contenidos para ser mostrados, V$ devuelve el contenido bruto (número, cadena, objeto o array) sin tratamiento alguno. Esto es necesario para transmitir un array o un objeto completo a través de un parámetro o hacer copias perfectas.
;realizar una copia:
ASSIGN alumnos,[{nombre:"Ana",edad:20},{nombre:"Alberto",edad:22},{nombre:"Antonio",edad:19}]
ASSIGN copiaAlumnos, V$(alumnos)
;obtener un elemento concreto
ASSIGN elemento, V$(unArray.1)
; o pasarlo como parámetro
CALL irAClase, V$(unArray.1)
Es importante diferenciar P$unTexto de V$unTexto.
assign miNombre:"Antonio"
move {name:text1,text:"Mi nombre es P$miNombre"} ; se muestra 'Mi nombre es Antonio'
move {name:text1,text:"Mi nombre es V$miNombre"} ; se muestra 'Mi nombre es "Antonio"'
Esta diferencia viene, precisamente, de que V$ es la obtención del dato bruto, que incluye la declaración del tipo de dato que es, en este caso, un string.
V$ no debe usarse dentro de cadenas directamente. Se reserva para contextos donde se espera un valor estructurado.
Como última nota, la característica de nulo es se trata con el operador "?" de tal forma que se puede asignar un valor si antes no ha sido asignado con:
ASSIGN max_value,P$max_value ? P$max_value : 100
/* Variables Estructuradas: Arrays y Objetos
Declaración, acceso y modificación de datos complejos.
*/
POSITION {NAME:POS1,X:300,Y:100}
initial posY,400
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 100}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 120}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text9,Type:TEXT,X:324,Y:X$posY - 160}
START 1
;*****************************************************************
GENERATE 1,0,0,1 {NAME:GEN1,X:100,Y:100}
; --- Declaraciones de todos los tipos ---
ASSIGN miNumero, 10 * 2
ASSIGN miTexto, "Resultado"
ASSIGN miArray, [10, 20, 30]
ASSIGN miObjeto, {clave1: 222, clave2: 333}
ASSIGN miArrayDeObjetos, [10, {clave1: 555, clave2: (111 * 6)}, 30]
; --- Accesos por rutas ---
ASSIGN suma, P$(miArray.0) + P$(miArray.1)
; --- Modificaciones por ruta ---
ASSIGN miArray.2, 777
ASSIGN miObjeto.clave1, {profundidad: "valor interno"}
ASSIGN miObjeto.clave2, V$(miArray.2)
; --- Copias por valor bruto (estructuras completas) ---
ASSIGN copiaObjeto, V$(miObjeto)
ASSIGN elementoCopiado, V$(miArrayDeObjetos.1)
; --- Evaluación de longitud ---
ASSIGN tamArray, P$(miArrayDeObjetos.LENGTH)
; --- Visualización ---
move {name:Text1, text: "miNumero = P$miNumero"}
move {name:Text2, text: "miTexto = P$miTexto"}
move {name:Text3, text: "miArray.2 = P$(miArray.2)"}
move {name:Text4, text: "miObjeto.clave1.profundidad = P$(miObjeto.clave1.profundidad)"}
move {name:Text5, text: "miObjeto.clave2 (copiado) = P$(miObjeto.clave2)"}
move {name:Text6, text: "miArrayDeObjetos.1.clave2 = P$(miArrayDeObjetos.1.clave2)"}
move {name:Text7, text: "copiaObjeto.clave2 = P$(copiaObjeto.clave2)"}
move {name:Text8, text: "elementoCopiado.clave1 = P$(elementoCopiado.clave1)"}
move {name:Text9, text: "Longitud array = P$tamArray"}
ADVANCE 100,0 {TO:POS1}
ENDGENERATE 1
Ya sabemos que un ASSIGN crea una característica (variable) asociada a cada entidad virtual y que un SAVEVALUE es un valor global del sistema.
También hemos visto que se accede a ellos a través de expresiones como P$nombre o X$nombre.
Estas expresiones se llaman SNA, String/Numeric Accessors, y son un mecanismo fundamental del lenguaje para acceder dinámicamente a cualquier valor del modelo.
P$nombre — Devuelve el contenido como texto. Es útil para trazas, etiquetas gráficas, o concatenaciones.V$nombre — Devuelve el valor bruto (string, número, objeto o array), tal como fue asignado.X$nombre — Devuelve el contenido de un SAVEVALUE (valor global).
assign unaVariable,{nombre:"Antonio",edad:30}
call fun, V$(unaVariable) ;envía el objeto como tal
Además, existen SNA específicos para acceder a datos del sistema:
AC1$ — Tiempo actual del sistema.TG1$ — Número de entidades pendientes de procesar.Y también a propiedades de recursos:
R$(recurso, propiedad) — Devuelve el valor de la propiedad solicitada.R$(almacen1, IN) — Entradas al STORAGE.R$(facility3, ENTRIES) — Número de usos de la FACILITY.R$(market1, QUEUE) — Longitud actual de la cola.Los SNA no son instrucciones: son expresiones.
Se evalúan en cualquier parte donde se espere un valor. Esto los convierte en una herramienta esencial para tomar decisiones, construir mensajes, hacer cálculos o acceder a estructuras complejas.
En el ejemplo, se usan dos SNA muy representativos:
AC1$ el tiempo actual del sistema.R$(VENTANILLA,QUEUE): estado actual de la cola del recurso.D$N: devuelve el número identificador de la entidad virtual actual. En este caso, lo almacenamos en una variable numero mediante un ASSIGN, y luego accedemos a su valor con P$numero.Esto muestra cómo los SNA pueden ser usados tanto para obtener datos del sistema como para manipular variables propias de cada entidad, y cómo se integran fácilmente con los comandos visuales como move.
Por último tenemos el SNA SYS$ que contiene variables básicas del sistema en un objeto como los datos de fecha y hora:
ASSIGN sys, SYS$
MOVE {name: REALTIME1, text:"P$(sys.date.year)/P$(sys.date.month)/P$(sys.date.day)"}
MOVE {name: REALTIME2, text:"P$(sys.date.hour):P$(sys.date.min):P$(sys.date.sec)"}
Acceso directo a datos de entidades:
El SNA D$ permite consultar propiedades internas de cualquier entidad del sistema.
Su sintaxis general es:
D$(propiedad) ; entidad actual D$(propiedad, numeroEntidad) ; entidad específica
Si se omite el segundo parámetro, se asume D$N (la entidad actual).
Todas estas propiedades se aplican tanto a la entidad actual como a cualquier otra entidad si se indica su número:
| Propiedad | Descripción |
|---|---|
| N, ID | Identificador de la entidad |
| M0, M1 | Parámetros M0 y M1 |
| BLOCK | Índice del bloque actual |
| STEP | Número de paso ejecutado |
| RESOURCETIME | Tiempo dentro de la FACILITY/STORAGE actual |
| RESOURCENAME | Nombre del recurso que ocupa |
| ADVANCESTART | Tiempo en que comenzó el ADVANCE actual |
| ADVANCELAPSE | Duración restante del ADVANCE |
| X, Y, Z | Coordenadas visuales |
| T | Tiempo absoluto de creación |
| CX | Contexto de la entidad |
De forma especial se puede verificar la existencia de una entidad dentro del mismo SNA D$:
if (D$(EXIST,1000)==1)
; la entidad 1000 está activa
endif
/* SNA: Atributos del Sistema y Recursos
Entidad virtual monitoriza el estado de la cola de un recurso.
*/
SYSTEM {TYPE:ON_TIMER, TRIGGER:Timer1, INTERVAL: 5}
FACILITY {NAME:Ventanilla,X:470,Y:308}
Graphic {NAME:Text1,Type:TEXT,X:471,Y:254}
Graphic {NAME:Text2,Type:TEXT,X:471,Y:234}
START 100
GENERATE 10,0 {name:gen1,X:115,Y:307}
ADVANCE 20 {to:Ventanilla}
SEIZE Ventanilla
ADVANCE 20
RELEASE Ventanilla
ENDGENERATE 1
; Mostrar la cola actual cada cierto tiempo
PROCEDURE Timer1
assign numero,D$N
move {name:Text1,text:"Soy la entidad virtual número: P$numero"}
move {name:Text2,text:"T: AC1$ Entidades en cola: R$(Ventanilla,QUEUE)"}
TERMINATE
ENDPROCEDURE 1Las funciones nativas permiten operar directamente sobre el contenido de una variable, sea numérica, de texto, array u objeto. Algunas funciones modifican su contenido.
El formato es siempre el mismo:
ASSIGN.<FUNCION> nombreVariableDestino, parámetro [,número entidad destino]
SAVEVALUE.<FUNCION> nombreVariableDestino, parámetro
Si la variable no existe, se crea.
Es muy importante decir que los objetos JSON "array" y "object" solo se pueden crear bajo estos dos bloques y el comando INITIAL. Cualquier otro COMANDO o BLOQUE que requiera parámetros como objeto o array recibirá el objeto ya creado bajo el SNA V$(variable).
CALL fun, V$(estructura) CALL fun, P$(variable)CALL fun, {nombre_"Antonio"}; no se interpretan los objetos fuera de ASSIGN, SAVEVALUE ó INITIALCALL fun, [1,2,V$(otraLista)]; no se interpretan los objetos fuera de ASSIGN, SAVEVALUE ó INITIAL
1. Variables numéricas
.INC valor – incrementa el valor actual, uno por defecto. Modifica su contenido ASSIGN.INC contador ASSIGN.INC contador,10 ASSIGN.INC miObjeto.puntuacion
2. Variables string
.TRIM – elimina espacios al principio y al final Modifica su contenido
ASSIGN.TRIM miCadena
.LENGTH – devuelve la longitud del string Devuelve el resultado en otro assign
ASSIGN.LENGTH miCadena, longitud
.JOIN – une los elementos en un string Devuelven un nuevo valor
ASSIGN.JOIN cadenaFinal, {DATA:["uno","dos","tres"], SEP:" - "}
ASSIGN.JOIN cadenaFinal, {DATA:V$miArray, SEP:" - "}
3. Variables array
.PUSH valor – añade al final
.UNSHIFT valor – añade al inicio
.EXTEND array – añade los elementos de otro array
ASSIGN.PUSH miArray, 45 ; [45]
ASSIGN.PUSH miArray, {edad:45} ; [45,{edad:45}]
ASSIGN.UNSHIFT miArray, "inicio" ; ["inicio",45,{edad:45}]
ASSIGN.EXTEND miArray, V$otroArray ; ["inicio",45,{edad:45},"otroArray_a","otroArray_b"]
.SPLIT – divide un string por separador y lo convierte en array
ASSIGN.SPLIT resultado, {DATA:"uno,dos,tres", SEP:","}
.SLICE– tona una sección de un array entre START y END (no incluido)
ASSIGN myArray [0,1,2,3,4]
ASSIGN.SLICE myArray, {START:1,END:3} ; [1,2]
4. Variables objeto
.MERGE objeto – fusiona claves desde otro objeto
.DELETE ruta – elimina una propiedad del objeto Modifican su contenido
ASSIGN.MERGE datos, V$nuevosDatos ASSIGN.DELETE datos, direccion.calle
.KEYS – obtiene una lista con los nombres de clave Devuelve el resultado en otra variable
ASSIGN.KEYS datos, claves
Además de asignar un valor completo a una variable, el DSL permite asignar únicamente a una parte interna de un objeto o un array mediante rutas.
Una ruta es una secuencia de claves o índices separada por puntos:
ASSIGN variable.clave , 3 ASSIGN variable.clave.subclave , 3 ASSIGN variable.3 , 3 ASSIGN variable.3.otroNivel , 3
Rutas en objetos
Las rutas en objetos no crean niveles intermedios automáticamente.
Si una ruta no existe, se genera un error y la simulación se detiene:
ASSIGN miObjeto, {}
ASSIGN miObjeto.clave1, 123 ; OK
ASSIGN miObjeto.clave2.otroNivel, 999 ; ERROR → clave2 no existe
Los objetos deben existir previamente en cada nivel de la ruta:
ASSIGN miObjeto, {clave2:{}} ; Ahora sí existe clave2
ASSIGN miObjeto.clave2.otroNivel, 999
Rutas en arrays
Los arrays sí permiten acceder directamente a cualquier índice, incluso si está vacío:
ASSIGN miArray, [] ASSIGN miArray.2, 99 ; Crea MIARRAY como [ , , 99 ]
Además, es posible operar sobre una ruta interna con funciones nativas:
ASSIGN miArray.3, [] ASSIGN.PUSH miArray.3, 199
VD$Algunos datos son de acceso directo a través del SNA asociado para, por ejemplo, validar la existencia de un elemento extraido de .POP antes de continuar
VD$(ruta) – Devuelve el valor bruto sin procesar. Similar a V$ pero dentro de una expresión.VD$(ruta,LENGTH) – Longitud del contenido:
VD$(ruta,TYPEOF) – Tipo de dato en forma de texto: "STRING", "NUMBER", "ARRAY", "OBJECT", etc.VD$(ruta,ISEMPTY) – Devuelve "1" si el valor está vacío:
"0" en cualquier otro caso.VD$(ruta,EXIST) – Comprueba si la ruta existe completamente sin errores.
"1" si es válida, "0" si no lo es.VD$(ruta,HASKEY,clave) – Comprueba si un objeto tiene una determinada clave. Devuelve "1" o "0".VD$(ruta,INCLUDES,valor) – Comprueba si un array o cadena incluye un valor dado. Devuelve "1" o "0".
POSITION {NAME:POS1,X:300,Y:100}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 100}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 120}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text9,Type:TEXT,X:324,Y:X$posY - 160}
Graphic {NAME:Text10,Type:TEXT,X:324,Y:X$posY - 180}
Graphic {NAME:Text11,Type:TEXT,X:324,Y:X$posY - 200}
Graphic {NAME:Text12,Type:TEXT,X:324,Y:X$posY - 220}
Graphic {NAME:Text13,Type:TEXT,X:324,Y:X$posY - 240}
Graphic {NAME:Text14,Type:TEXT,X:324,Y:X$posY - 260}
START 1
GENERATE 1,0,0,1 {NAME:GEN1,X:100,Y:100}
; Variables base
ASSIGN saludo, "Hola"
ASSIGN complemento, "mundo"
ASSIGN mensajeOriginal, " Hola Mundo GPSS+ "
ASSIGN nombres, ["Ana", "Luis", "Eva"]
ASSIGN otrosNombres, ["Sonia", "David"]
ASSIGN textoConSeparador, "uno,dos,tres,cuatro,cinco"
; Métodos de arrays
assign.push nombres, "Ricardo" ; ["Ana", "Luis", "Eva" "Ricardo"]
assign.unshift nombres, "Laura" ; ["laura", "Ana", "Luis", "Eva" "Ricardo"]
assign.extend nombres, V$otrosNombres ; ["laura", "Ana", "Luis", "Eva", "Ricardo","Sonia", "David"]
assign.slice nombres, {START:1,END:6} ; ["Ana", "Luis", "Eva", "Ricardo","Sonia"]
; String methods
assign.join mensajeFinal, {DATA:["P$saludo", " ", "P$complemento", "!"]} ; "Hola Mundo!"
assign.join nombresTexto, {DATA:V$nombres,SEP:" - "} ; "Ana - Luis - Eva"
assign.split palabras, {DATA:V$textoConSeparador, SEP: ","}; ["uno","dos","tres","cuatro","cinco"]
assign.trim mensajeOriginal ; "Hola Mundo GPSS+"
assign.LENGTH mensajeOriginal, largoMensaje ; 16
; Objetos
ASSIGN datos, {nombre:"Juan", edad:25}
ASSIGN nuevosDatos, {ciudad:"Madrid", edad:30}
assign.merge datos, V$nuevosDatos
assign.delete datos, ciudad
; Numérico
ASSIGN contador, 10
assign.inc contador,-5
assign.inc contador
; Claves
ASSIGN alumnos, {ana:{edad:20}, luis:{edad:25}}
assign.keys alumnos, clavesAlumnos
; Rutas de acceso
ASSIGN miObjeto, {}
ASSIGN miObjeto.clave1, 123
;ASSIGN miObjeto.clave2.otroNivel, "error" -> error por no existir
ASSIGN miArray, []
ASSIGN miArray.2, 99
ASSIGN miArray.3, []
ASSIGN.push miArray.3, 199
; Mostrar resultados
move {name:Text1, text:"Mensaje final: P$mensajeFinal"}
move {name:Text2, text:"Nombres (2): P$(nombres.2) LENGTH: VD$(nombres,LENGTH)"}
move {name:Text3, text:"Nombres texto: P$nombresTexto"}
move {name:Text4, text:"Palabras[1]: P$(palabras.1)"}
move {name:Text5, text:"Texto trimmed: P$mensajeOriginal"}
move {name:Text6, text:"Largo mensaje: P$largoMensaje"}
move {name:Text7, text:"datos.edad: P$(datos.edad)"}
move {name:Text8, text:"datos.ciudad: P$(datos.ciudad)"} ; debería estar vacío
move {name:Text9, text:"Contador final: P$contador"}
move {name:Text10, text:"clavesAlumnos[1]: P$(clavesAlumnos.1)"}
move {name:Text11, text:"Tipo de nombres: VD$(nombres,TYPEOF)"}
move {name:Text12, text:"Alumnos tiene luis?: VD$(alumnos,HASKEY,luis)"}
move {name:Text13, text:"miObjeto.clave1: P$(miObjeto.clave1)"}
move {name:Text14, text:"miArray.3.0: P$(miArray.3.0)"}
ADVANCE 100,0 {TO:POS1}
ENDGENERATE 1Además de trabajar con variables propias o globales, GPSS-Plus permite leer y escribir variables ASSIGN de cualquier entidad viva del sistema.
Lectura a través del SNA:
El SNA habitual P$ admite un segundo parámetro que indica el número de entidad objetivo:
P$(nombreVariable, númeroEntidad)
Esto permite consultar valores internos de otra entidad sin necesidad de copiarlos a un SAVEVALUE.
move {name:text5, text:"La entidad 2 tiene: P$(unNumeroPrivado,2)"}
Esto accede directamente a la variable unNumeroPrivado activa en la entidad número 2.
Escritura en otra entidad:
Mediante un tercer parámetro en la instrucción ASSIGN, podemos guardar información en la entidad deseada:
ASSIGN unNumero, 123, 1
Comprobación de la existencia de la entidad:
Se puede consultar la existencia con el SNA específico:
if (D$(EXIST,1000)==1)
/* Acceso cruzado entre entidades usando P$ y ASSIGN destino */
POSITION {NAME:POS_MOD, X:286, Y:244}
POSITION {NAME:POS_END, X:741, Y:233, type:terminate}
Graphic {NAME:T1, type:TEXT, X:455, Y:300}
Graphic {NAME:T2, type:TEXT, X:455, Y:188}
START 60 ; habrá 6 entidades (3 de cada tipo)
/* --- ENTIDAD TIPO A --- */
GENERATE 30,0,15 {NAME:GA, X:100, Y:70, ECOLOR:#ff3333, subtitle:"A"}
ASSIGN miValor, 100
mod {subtitle:"Mi valor P$miValor"}
ADVANCE 20 {to:POS_MOD}
; Si existe la entidad B (2, 4, 6...)
IF (D$(EXIST, D$N+1)==1)
ASSIGN nEntidadDestino,D$N+1
ASSIGN miValor, P$(miValor)+10, P$nEntidadDestino ; A incrementa el valor de B
MOVE {name:T1, text:"Cambio el valor de P$nEntidadDestino a: [P$(miValor,P$nEntidadDestino)]"}
ENDIF
ADVANCE 50 {to:POS_END}
ENDGENERATE 3
/* --- ENTIDAD TIPO B --- */
GENERATE 30,0 {NAME:GB, X:103, Y:407, ECOLOR:#3366ff, subtitle:"B"}
ASSIGN miValor, 200
mod {subtitle:"Mi valor P$miValor"}
ADVANCE 20 {to:POS_MOD}
; Si existe la entidad A (1, 3, 5...)
IF (D$(EXIST, D$N-1)==1)
ASSIGN nEntidadDestino,D$N-1
ASSIGN miValor, P$(miValor)-5, P$nEntidadDestino ; B reduce el valor de A
MOVE {name:T2, text:"Cambio el valor de P$nEntidadDestino a: [P$(miValor,P$nEntidadDestino)]"}
ENDIF
ADVANCE 50 {to:POS_END}
ENDGENERATE 3
Las estructuras de control permiten a las entidades tomar decisiones, repetir bloques de código o recorrer colecciones. GPSS-Plus incorpora estas estructuras con una sintaxis simple y adaptada al paradigma declarativo del lenguaje.
1. IF / ELSE / ENDIF
Permite evaluar una condición y ejecutar bloques alternativos según el resultado.
IF (P$edad > 18 || "P$nombre"=="Eva")
move {name:text1,text:"Mayor de edad o es Eva"}
ELSE
move {name:text1,text:"Menor de edad o no es Eva"}
ENDIF
&&, ||) y comparaciones (==, !=, >, <, >=, <=).2. FOREACH / ENDFOREACH
ASSIGN listaAlumnos,[{nombre:"Ana"},{nombre:"Luis"}]
FOREACH alumno IN V$listaAlumnos
move {name:text1,text:"Alumno: P$alumno.nombre"}
ENDFOREACH
color, entidad, clave) debe ser un ASSIGN, creado automáticamente si no existe.IN y IN_OBJECT (el origen de los datos) debe ser una referencia válida por su SNA V$(ruta)FACILITY, STORAGE, etc.).FACILITY, STORAGE, etc.).FOREACH color, IN, V$(persona.colores) assign resultado, "P$color" ENDFOREACH FOREACH clave, IN_OBJECT, V$(persona) assign resultado, "P$clave" ENDFOREACH FOREACH entidad, IN_RESOURCE, Ventanilla assign ids, "P$entidad" ENDFOREACH FOREACH entidad, IN_QUEUE, Ventanilla assign ids, "P$entidad" ENDFOREACH
3. REPEAT / UNTIL
ASSIGN intento, 0
REPEAT
ASSIGN.INC intento
move {name:text1,text:"Intento número: P$intento"}
UNTIL (P$intento >= 3)
4. SWITCH / CASE / ENDCASE/ ENDSWITCH
SWITCH P$color
CASE ==,"rojo"
move {name:text1,text:"Color ROJO"}
CASE ==,"azul"
move {name:text1,text:"Color AZUL"}
CASE ==,"verde"
move {name:text1,text:"Color VERDE"}
ENDCASE
ENDSWITCH
5. WHILE / ENDWHILE
assign intentos, ""
assign intento, 0
assign maximo, 5
WHILE (P$intento < P$maximo)
assign.inc intento
assign intentos, "P$intentos P$intento"
ENDWHILE
move {name:text1, text:"Intentos: P$intentos"}
REPEAT, la condición se evalúa antes de cada iteración.P$, VD$, D$) y operadores lógicos.
/* Estructuras de Control: FOREACH y REPEAT
Iteración sobre arrays, objetos y recursos.
*/
POSITION {NAME:POS1,X:651,Y:480}
initial posY,400
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 160,color:#000000}
Facility {NAME:Facility1,X:382,Y:483,capacity:3}
START 100
GENERATE 10,0 {name:GEN1,X:110,Y:477}
assign edad,2
assign nombre,"Eva"
; IF (P$nombre=="Eva") -> Expresión incorrecta, faltan las comillas
IF ("P$nombre"=="Eva")
move {name:Text1, text:"Expresiones correctas"}
endif
IF (P$edad > 18 || V$nombre=="Eva")
move {name:Text1, text:"Expresiones correctas"}
endif
ASSIGN intentos, 0
move {name:Text1, text:"Color actual: P$valor"}
; FOREACH: recorrer todos los colores
assign gustos_luis,""
ASSIGN persona, {nombre:"Luis"
, gustos:["leer", "música", "viajar", "dormir"]
, ciudad:"Madrid"
, pais:"España"
}
FOREACH gusto, IN, V$(persona.gustos)
assign gustos_luis,"P$gustos_luis P$gusto"
ENDFOREACH
move {name:Text1, text:"Gustos de Luis: P$gustos_luis"}
assign claves_valores,""
FOREACH clave, IN_OBJECT, V$(persona)
assign claves_valores,"P$(claves_valores) \n P$clave P$(persona.P$clave)"
ENDFOREACH
move {name:Text6, text:"Claves y valores de persona:\n P$(claves_valores)"}
assign entidades_cola,""
FOREACH entidad, IN_QUEUE, Facility1
assign entidades_cola,"P$entidades_cola P$entidad"
ENDFOREACH
move {name:Text3, text:"entidades en cola: P$entidades_cola"}
assign entidades_dentro,""
FOREACH entidad, IN_RESOURCE, Facility1
assign entidades_dentro,"P$entidades_dentro P$entidad"
ENDFOREACH
move {name:Text4, text:"entidades dentro: P$entidades_dentro"}
assign intento, 0
assign maximo, 3
assign intentos,""
REPEAT
assign.inc intento
assign intentos,"P$intentos P$intento"
UNTIL (P$intento >= P$maximo)
move {name:Text5, text:"intentos: P$intentos"}
advance 10,0 {to:Facility1}
SEIZE Facility1
advance 20,30
RELEASE Facility1
ADVANCE 100,0 {TO:POS1}
ENDGENERATE 1
GPSS-Plus incorpora un sistema completo de subprocesos que no existía en GPSS clásico, gracias a la introducción de pilas de ejecución (stacks).
Esto permite que una entidad —o incluso el propio sistema— pueda invocar procedimientos independientes y retomar la ejecución de forma controlada.
Existen varios tipos de llamada, según quién invoca y quién ejecuta el procedimiento:
Todos ellos saltan a un bloque PROCEDURE, que debe finalizar con ENDPROCEDURE.
Los procedimientos pueden organizarse como rutas:
agente.abrir cliente.saludar robot1.motor.arrancar
El valor devuelto se almacenará automáticamente en una variable cuyo nombre coincide con el último fragmento ("abrir", "saludar", "arrancar").
Dentro de un PROCEDURE pueden consultarse los parámetros recibidos:
P$PARAM_A — valor evaluado
V$PARAM_A — valor bruto (array, objeto, número, string sin evaluar)
El procedimiento puede terminar con:
El comando CALL permite que la propia entidad activa salte a un procedimiento y lo ejecute inmediatamente.
Al finalizar, la entidad vuelve exactamente al paso siguiente del que llamó.
Es el comportamiento más simple y directo del sistema de subprocesos.
Sintaxis:
CALL nombreProcedimiento, parámetroA, parámetroB, ...
P$PARAM_A, P$PARAM_B, etc.Ejemplos de nombre → variable de resultado:
CALL calcular, 10 → P$calcular CALL cliente.sumar, 20,30 → P$sumar CALL robot.motor.arrancar → P$arrancar
Finalización del PROCEDURE:
ENDPROCEDURE valor
Finaliza y devuelve el valor indicado:
ENDPROCEDURE 3 ; P$calcular = 3
RETURN valor
Hace lo mismo, pero resulta más explícito y legible:
RETURN 3 ENDPROCEDURE
Ambas formas son equivalentes cuando se usa CALL.
Acceso a los parámetros:
Dentro del procedimiento:
P$PARAM_A devuelve el valor evaluado.V$PARAM_A devuelve el valor bruto (arrays, objetos, strings sin evaluar).Ejemplos:
P$(PARAM_A.nombre) si pasaste un objeto.P$(PARAM_B.2) si pasaste un array.
/* CALL */
POSITION {NAME:POS1, X:286, Y:244}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 100}
START 1
GENERATE 10,0,0,1 {NAME:GA, X:100, Y:70}
ASSIGN datos, {nombre:"Ana", edad:30}
ASSIGN numero, 123
ASSIGN colores, ["rojo","verde"]
CALL procesar, V$datos, V$numero, V$colores,30
move {name:Text1, text:"Resultado del procedimiento: P$(procesar)"}
ADVANCE 10 {to:POS1}
ENDGENERATE 1
;---------------------------------------
PROCEDURE procesar
move {name:Text2, text:"Nombre: P$(PARAM_A.nombre)"}
move {name:Text3, text:"Edad: P$(PARAM_A.edad)"}
move {name:Text4, text:"Número: P$PARAM_B"}
move {name:Text5, text:"Primer color: P$(PARAM_C.0)"}
move {name:Text6, text:"Dato directo: P$(PARAM_D)"}
RETURN "OK"
ENDPROCEDURE
SIGNAL invoca un PROCEDURE para que sea ejecutado por otra entidad, pero no interrumpe a la entidad que lo llama.
La ejecución ocurrirá en el mismo instante de tiempo (AC1).
Entrará al final del conjunto de eventos programados para ese mismo tiempo.
La entidad actual continúa normalmente, termina su ciclo y luego la entidad señalada tomará el control en el momento programado.
Es una llamada diferida.
En resumen: SIGNAL programa la ejecución del procedimiento, pero no interrumpe a la entidad que lo invoca.
SIGNALNOW invoca un PROCEDURE para que sea ejecutado de inmediato por otra entidad, interrumpiendo a la entidad actual.
Puntos clave:
Normalmente, el PROCEDURE debería finalizar con:
RETURN_RESTORE
Si la entidad interrumpida debe reanudar exactamente donde estaba.
RETURN_RETRY
Si debe repetir la operación en la que estaba (típico para SEIZE, ENTER o recursos).
En resumen: SIGNALNOW provoca una interrupción inmediata; la entidad señalada ejecuta el procedimiento sin esperar.
Funcionamiento de SIGNAL / SIGNALNOW con agentes activos y dormidos
Cuando un procedimiento es ejecutado mediante SIGNAL o SIGNALNOW, el comportamiento final depende del estado en el que se encuentre la entidad receptora.
En este ejemplo se muestran dos casos muy distintos:
El agente vivo tiene una tares de procesar un texto que tarda un tiempo considerable. Mientras no lo está procesando, muestra constantemente el tiempo AC1, pero cuando entra en el proceso, no es capaz de hacerlo notándose que el tiempo no se actualiza.
Si cambiamos la salida del procedure a:
RETURN P$PARAM_A + P$PARAM_B
Notaremos que las llamadas a sumar interrumpen la tarea.
En resumen, esto permite ver las diferencias reales entre:
PARA EL AGENTE VIVO (en ADVANCE)
PARA EL AGENTE DORMIDO (en RESTROOM)
/* SIGNAL y SIGNALNOW en agentes permanentes */
SYSTEM {TYPE:PRE_RUN, TRIGGER:PRE_RUN}
POSITION {NAME:POS_1, X:615, Y:398}
Graphic {NAME:T_resultado1, type:TEXT, X:425, Y:281,text:"resultado1"}
Graphic {NAME:T_resultado2, type:TEXT, X:425, Y:256,text:"resultado2"}
Graphic {NAME:T2, type:TEXT, X:430, Y:501}
Graphic {NAME:T3, type:TEXT, X:430, Y:531}
Graphic {NAME:T4, type:TEXT, X:426, Y:310}
Graphic {NAME:T5, type:TEXT, X:425, Y:188}
RESTROOM {name:RestRoomAgenteDormido,x:423,y:118}
START 200
/* ---- PRE RUN: Crear los agentes ---- */
PROCEDURE PRE_RUN
TIMEOUT agenteVivo.loop, 0
TIMEOUT agenteDormido.loop, 0
TERMINATE_VE
ENDPROCEDURE
/* ---- GENERADOR DE ENTIDADES NORMALES ---- */
GENERATE 15,0 {NAME:G1, X:153, Y:411}
ASSIGN miValor, D$N * 10
MOVE {name:T4, text:"Entidad D$N comienza con miValor = P$miValor"}
ADVANCE 10 {to:POS_1}
; Llamada SIGNAL diferida
SIGNAL agenteVivo.procesar, X$nAgenteVivo, "Hola"
; Llamada SIGNALNOW inmediata
SIGNALNOW agenteVivo.sumar, X$nAgenteVivo, P$miValor, 5
MOVE {name:T_resultado1, text:"Resultado del agente Vivo = P$sumar"}
SIGNALNOW agenteDormido.sumar, X$nAgenteDormido, P$miValor, 5
MOVE {name:T_resultado2, text:"Resultado del agente dormido = P$sumar"}
ENDGENERATE 4
/* ---- AGENTE VIVO ---- */
PROCEDURE agenteVivo.loop
SAVEVALUE nAgenteVivo, D$N
assign procesando,0
WHILE (1==1)
MOVE {name:T3, text:"Agente Vivo [P$(nAgente)] activo en t = AC1$"}
ADVANCE 2
ENDWHILE
terminate_ve
ENDPROCEDURE
/* ---- AGENTE DORMIDO ---- */
PROCEDURE agenteDormido.loop
SAVEVALUE nAgenteDormido, D$N
assign procesando,0
MOVE {name:T5, text:"Agente Dormido [P$(nAgente)] activo"}
rest RestRoomAgenteDormido
terminate_ve
ENDPROCEDURE
/* ---- PROCEDIMIENTOS INVOCADOS POR ENTIDADES ---- */
PROCEDURE agenteVivo.procesar
if (P$procesando==1)
return
endif
assign procesando,1
MOVE {name:T2, text:"SIGNAL → agente INICIA proceso mensaje: P$PARAM_A"}
ADVANCE 80
MOVE {name:T2, text:"SIGNAL → agente FINALIZA proceso mensaje: P$PARAM_A"}
assign procesando,0
RETURN ; viene de signal y no afecta el tipo a la entidad
ENDPROCEDURE
PROCEDURE agenteVivo.sumar
; PARAM_A = valor base
; PARAM_B = incremento
; RETURN P$PARAM_A + P$PARAM_B ; finaliza el proceso para admitir otro
RETURN_RESTORE P$PARAM_A + P$PARAM_B ; el proceso permanece en su tiempo
; RETURN_RETRY P$PARAM_A + P$PARAM_B ; explota por no estar en un SEIZE o similar
ENDPROCEDURE
;-------------------------------------------------------------------
PROCEDURE agenteDormido.sumar
; PARAM_A = valor base
; PARAM_B = incremento
; RETURN P$PARAM_A + P$PARAM_B ; explota por morir el agente al salir del restroom
RETURN_RESTORE P$PARAM_A + P$PARAM_B ; procesa quedandose el agente en su restroom
; WAKE RestRoomAgenteDormido,0,X$nAgenteDormido ; saca al agente de estar en REST
; RETURN_RETRY P$PARAM_A + P$PARAM_B ; procesa reiniciando el agente su restroom
ENDPROCEDURE
TIMEOUT / ON_* / PRE_RUN / TIMER
Estos procedimientos son ejecutados por entidades virtuales (VE) recién creadas.
Las VE deben finalizar como cualquier entidad con un TERMINATE o TERMINATE_VE. La diferencia entre ambos BLOQUES es que la primera finaliza cualquier entidad y la segunda solo entidades virtuales. Usando el bloque TERMINATE_VE , el PROCEDURE podrá ser invocado para ser ejecutado por ambos tipos de entidades.
Es importante decir que deben ser TERMINADAS obligatoriamente pues de llegar al ENDPROCEDURE no sabrían donde retornar.
TIMEOUT abrirRecurso, 50, P$valor, V$Objeto
PROCEDURE abrirRecurso
move {name:text1, text:"Ejecutado en t=AC1$"}
TERMINATE_VE
ENDPROCEDURE
POSITION {NAME:Salida, X:624, Y:199}
Graphic {NAME:T1, type:TEXT, X:493, Y:505}
Graphic {NAME:T2, type:TEXT, X:493, Y:476}
Graphic {NAME:T3, type:TEXT, X:492, Y:444}
Facility {NAME:Ventana, ON_RELEASE: cuandoSale, X:249, Y:335 }
SYSTEM {TYPE:PRE_RUN, TRIGGER:PRE_RUN}
START 50
;----------------------------------------
PROCEDURE PRE_RUN
move {name:T1, text:"PRE_RUN ejecutado en t = AC1$"}
TIMEOUT avisoInicial, 10
TERMINATE_VE
ENDPROCEDURE
;----------------------------------------
GENERATE 15,0 {NAME:"GEN1", X:100, Y:100}
ADVANCE 10 {to:Ventana}
SEIZE Ventana
ADVANCE 5
RELEASE Ventana ; disparará ON_RELEASE
ADVANCE 10 {to:Salida}
ENDGENERATE 1
;----------------------------------------
PROCEDURE avisoInicial
move {name:T2, text:"TIMEOUT ejecutado en t = AC1$"}
TERMINATE_VE
ENDPROCEDURE
PROCEDURE cuandoSale
move {name:T3, text:"ON_RELEASE ejecutado en t = AC1$"}
TERMINATE_VE
ENDPROCEDURE
SCAPE
Es una llamada especial que tiene como destino un GENERATE.
SCAPE se usa cuando una entidad debe abandonar completamente su flujo actual y comenzar uno nuevo.
Esto es útil cuando una condición requiere reiniciar, cambiar de comportamiento o bifurcar el flujo de manera irreversible.
El salto SCAPE lleva a los únicos puntos seguros limpios de stack de direcciones: los GENERATES.
Conservará todos los valores de assign.
SCAPE nombreGenerate, 50, P$valor, V$Objeto
BACKPACK (LOAD / UNLOAD)
Una entidad solo puede ser cargada (LOAD) si está en REST porque en ese estado:
Está dormida.
No consume ciclo.
No puede despertar sola, solo mediante WAKE por terceros.
Es “segura” de manipular.
Por eso el BACKPACK es una herramienta de logística no de control de flujo. Se transporta cualquier entidad.
Mecanismo para transportar entidades dormidas en un RESTROOM.
LOAD: la entidad cargada es retirada del RESTROOM y pasa al backpack.
UNLOAD: al llegar al destino, la entidad descargada empieza una nueva vida desde un GENERATE especificado.
En el ejemplo:
En este ejemplo se simula una ciudad y un museo conectados por una carretera.
Los ciudadanos generan su propio flujo de vida: algunos viajan en vehículo particular y otros esperan en la parada de autobús.
Cuando llegan al museo o regresan a la ciudad, las entidades “cambian de mundo” utilizando SCAPE, iniciando un nuevo flujo en el generador correspondiente.
Los autobuses funcionan como transportistas: cargan ciudadanos dormidos (LOAD), los desplazan por la carretera y los descargan en el generador de destino (UNLOAD), donde cada uno continúa su propio flujo.
De este modo, cada entidad continúa su ciclo natural según su medio de transporte.
POSITION {NAME:Salida, X:178, Y:39}
POSITION {NAME:inicioCarretera, X:150, Y:313}
POSITION {NAME:finCarretera, X:514, Y:314}
Facility {NAME:museo, capacity:50, X:534, Y:77 }
Restroom {NAME:paradaAutobusCiudad, X:68, Y:321 }
Restroom {NAME:paradaAutobusMuseo, X:588, Y:312 }
START 500
;----------------------------------------
GENERATE 15,0 {NAME:"GenUsuarios", X:100, Y:100 }
if (P$museoVisitado==1)
advance 100,50 {from:inicioCarretera ,to:Salida}
terminate 1
endif
ASSIGN TIPO,0 ; EN VEHICULO PARTICULAR
if (RANDOM >0.2)
ASSIGN TIPO,1 ; EN AUTOBUS
endif
advance 10 {to:inicioCarretera}
if (P$TIPO==1)
REST paradaAutobusCiudad
endif
advance 50,10 {to:finCarretera}
SCAPE GenMuseo
ENDGENERATE 1
;----------------------------------------
GENERATE 0,0,0,0 {NAME:"GenMuseo", X:600, Y:160 }
advance 20,30 {from:finCarretera,to:museo}
seize museo
advance 30,50
assign museoVisitado,1
mod {color:green}
release museo
advance 10 {to:finCarretera}
if (P$TIPO==1)
REST paradaAutobusMuseo
endif
advance 50 {to:inicioCarretera}
SCAPE GenUsuarios
ENDGENERATE 1
;----------------------------------------
;----------------------------------------
GENERATE 50,0,0,2 {NAME:"GenAutobuses", X:300, Y:460,ecolor:red,eradio:10,visible:0 }
advance 50 {to:inicioCarretera}
while (1==1)
load miBackpackIda, paradaAutobusCiudad
mod {subtitle:"Pasajeros P$(miBackpackIda.LENGTH)"}
advance 80,10 {to:finCarretera}
unload miBackpackIda, GenMuseo
advance 10
load miBackpackVuelta, paradaAutobusMuseo
mod {subtitle:"Pasajeros P$(miBackpackVuelta.LENGTH)"}
advance 80,10 {to:inicioCarretera}
unload miBackpackVuelta, GenUsuarios
advance 10
endwhile
ENDGENERATE 0
Tipos de recursos disponibles en GPSS-Plus:
Facility → Recurso exclusivo con capacidad. Ej.: máquina, servidor.
Storage → Recurso acumulativo. Ej.: almacén, depósito.
Restroom → Recurso retenedor. Las entidades esperan a ser liberadas.
Conditions → Recurso retenedor condicional. Paso según lógica.
Stock → Almacén inteligente de productos etiquetados.
Stater → Máquina de estados finitos. Control de procesos.
Queuer → Recurso abierto. Para conteo, agrupación o análisis.
Permiten modelar capacidades compartidas, espacios físicos, almacenes, puntos de sincronización, etc. Cada tipo de recurso define una lógica propia de uso, pero todos pueden conectarse con procedimientos a través de eventos (hooks) como ON_SEIZE, ON_LEAVE, etc.
Histogramas automáticos:
Todos los recursos permiten generar histogramas automáticos sobre su uso definiendo su tabulación:
R_BIN_*: Tabula la ocupación del [R] Recurso.
R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:40
E_BIN_*: Tabula los tiempos de ocupación de la [E] Entidad.
E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:40
SNA asociados:
Mediante R$(nombreRecurso,propiedad) puedes consultar en tiempo real:
X, Y → Posición gráficaLEFT, IN, QUEUE, ENTRIES, CAPACITY → Estado operativoLOCK → 0 o 1 según si la entrada está bloqueadaOCCUPIED → (solo en STORAGE) cantidad actual ocupadaR$(Fac1,LEFT) ; Espacio disponible R$(Fac1,QUEUE) ; Tamaño de de cola R$(Fac1,LOCK) ; 0|1 Bloqueada la entrada
HOOKs
Dependiendo del recurso en particular, pueden disponer de determinados Hooks atendidos por procedures ejecutados por entidades virtuales (VE).
Se crearán entidades virtuales (VE) que recorrerán el trigger y nacerán con el assign ENTITYNUMBER.
Facility {NAME:Fac2, CAPACITY:3, X:100, Y:100, ON_ATTEMPT:Fac1_attempt}
;------------------------
PROCEDURE Fac1_attempt
MOVE {NAME:Text1,Text:"La entidad P$ENTITYNUMBER ha intentado entrar"}
TERMINATE_VE
ENDPROCEDURE
POSITION {NAME:Salida,X:615,Y:388}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 160}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 180}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 200}
Facility {NAME:Facility1,X:324,Y:560
,capacity:3
,ON_ATTEMPT:FACILITY1_attempt
,ON_SEIZE:FACILITY1_seize
,ON_RELEASE:FACILITY1_release
,ON_QUEUE:FACILITY1_queue
}
Storage {NAME:Storage1,X:324,Y:235
,capacity:30
,ON_ATTEMPT:STORAGE1_attempt
,ON_ENTER:STORAGE1_enter
,ON_LEAVE:STORAGE1_leave
,ON_QUEUE:STORAGE1_queue
}
START 1000
;-------------------------------
GENERATE 10,0 {NAME:GEN1,X:62,Y:396}
if (D$N%2==1)
advance 10 {to:Facility1}
seize Facility1
advance 55,10
release Facility1
else
advance 10 {to:Storage1}
enter Storage1,random * 25 + 1
advance 35,10
leave Storage1
endif
advance 10 {to:Salida}
ENDGENERATE 1
;-------------------------------
PROCEDURE FACILITY1_release
move {name:Text1,text:"AC1$ Soy la VE [D$N] de ON_RELEASE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_queue
move {name:Text2,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_seize
move {name:Text3,text:"AC1$ Soy la VE [D$N] de ON_SEIZE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_attempt
move {name:Text4,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
;-------------------------------
PROCEDURE STORAGE1_leave
move {name:Text5,text:"AC1$ Soy la VE [D$N] de ON_LEAVE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_queue
move {name:Text6,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_enter
move {name:Text7,text:"AC1$ Soy la VE [D$N] de ON_ENTER la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_attempt
move {name:Text8,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
FACILITY
Modela un recurso exclusivo (como una máquina, un operador o una cabina).
Permite múltiples entidades según su capacity y cola si está ocupado.
Internamente gestiona dos listas: Entidades ocupantes y de entidades en espera.
Soporta varios métodos de selección de entidad entrante a través del parámetro METHOD:
Bloques asociados:
Eventos disponibles:ON_ATTEMPT, ON_QUEUE, ON_SEIZE, ON_RELEASE
Facility {NAME:Fac1, CAPACITY:3, X:100, Y:100}
Facility {NAME:Fac2, CAPACITY:3, X:100, Y:100, ON_ATTEMPT:Fac1_attempt, E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:40 }
;------------------------
SEIZE Fac1
ADVANCE 10
RELEASE Fac1
/*
Recursos. Facility
*/
POSITION {NAME:Salida,X:615,Y:388}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 160}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 180}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 200}
Facility {NAME:Facility1,X:324,Y:160
,capacity:3
,ON_ATTEMPT:FACILITY1_ATTEMPT
,ON_SEIZE:FACILITY1_SEIZE
,ON_RELEASE:FACILITY1_RELEASE
,ON_QUEUE:FACILITY1_QUEUE
,R_BIN_SIZE:1,R_BIN_COUNT:10
,E_BIN_SIZE:1,E_BIN_COUNT:10
}
START 1000
;-------------------------------
GENERATE 10,0 {NAME:GEN1,X:62,Y:396}
advance 10 {to:Facility1}
seize Facility1
advance 55,10
release Facility1
advance 10 {to:Salida}
ENDGENERATE 1
;-------------------------------
PROCEDURE FACILITY1_RELEASE
move {name:Text1,text:"AC1$ Soy la VE [D$N] de ON_RELEASE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_QUEUE
move {name:Text2,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_SEIZE
move {name:Text3,text:"AC1$ Soy la VE [D$N] de ON_SEIZE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE FACILITY1_ATTEMPT
move {name:Text4,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
STORAGE
Modela un recurso exclusivo tipo almacén en el que la ocupación viene definica por cantidades a aportar por cada entidad.
Permite múltiples capacidades según su capacity y cola si no hay espacio disponible.
Internamente gestiona dos listas: Entidades ocupantes con su carga y de entidades en espera.
Soporta varias lógicas de selección de entrada a través del parámetro opcional METHOD:
Bloques asociados:
Eventos disponibles:ON_ATTEMPT, ON_QUEUE, ON_ENTER, ON_LEAVE
Soporta varios métodos de selección de entidad entrante a través del parámetro METHOD:
Estadísticas adicionales: EQ_BIN_* RQ_BIN_* Por capacidad utilizada.
Storage {NAME:Sto1, CAPACITY:3, X:100, Y:100,METHOD:MIN_SPACE}
Storage {NAME:Sto2, CAPACITY:3, X:100, Y:100, ON_ATTEMPT:Fac1_attempt, E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:40 }
;------------------------
Enter Sto2,6
ADVANCE 10
RELEASE Sto2
/*
Recursos. Storage
*/
POSITION {NAME:Salida,X:615,Y:388}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 160}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 180}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 200}
Storage {NAME:Storage1,X:324,Y:235
,capacity:30
,ON_ATTEMPT:STORAGE1_attempt
,ON_ENTER:STORAGE1_enter
,ON_LEAVE:STORAGE1_leave
,ON_QUEUE:STORAGE1_queue
,E_BIN_START:34,E_BIN_SIZE:1,E_BIN_COUNT:12
,R_BIN_SIZE:1,R_BIN_COUNT:10
,EQ_BIN_START:34,EQ_BIN_SIZE:1,EQ_BIN_COUNT:12
,RQ_BIN_START:0,RQ_BIN_SIZE:1,RQ_BIN_COUNT:32
}
START 1000
;-------------------------------
GENERATE 10,0 {NAME:GEN1,X:62,Y:396}
advance 10 {to:Storage1}
enter Storage1,random * 25 + 1
advance 35,10
leave Storage1
advance 10 {to:Salida}
ENDGENERATE 1
;-------------------------------
PROCEDURE STORAGE1_leave
move {name:Text5,text:"AC1$ Soy la VE [D$N] de ON_LEAVE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_queue
move {name:Text6,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_enter
move {name:Text7,text:"AC1$ Soy la VE [D$N] de ON_ENTER la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE STORAGE1_attempt
move {name:Text8,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
RESTROOM
Modela un recurso retenedor.
Las entidades entran y quedan retenidas automáticamente en espera de que otra entidad la libre.
Internamente gestiona una lista: Entidades ocupantes.
Este recurso es especialmente útil para dormir agentes o mantener entidades en espera pasiva (no activa).
Bloques asociados:
Eventos disponibles: ON_REST, ON_WAKE
Restroom {NAME:Agentes, X:100, Y:100, ON_REST:Restroom1_onrest, E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:40 }
;------------------------
REST Agentes ; el agente para a inactivo
;------------------------
WAKE Agentes ; Despiesta todos los agentes
WAKE Agentes,0,X$nAgente ; Despierta un agente en concreto
/*
Recursos. Restroom
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
;SYSTEM {TYPE:OPTIONS,Speed:8}
Restroom {NAME:Restroom_agentes,X:100,Y:100}
POSITION {NAME:POS1,X:237,Y:449}
POSITION {NAME:POS2,X:269,Y:334}
POSITION {NAME:POS3,X:272,Y:202,type:terminate,title:end}
Graphic {NAME:textAgente,Type:TEXT,X:430,Y:471,Text:"Agente"}
Graphic {NAME:Text2,Type:TEXT,X:431,Y:523,Text:"Entidad"}
initial conta,0
initial nAgente,0
START 500
;*****************************************************
PROCEDURE PRE_RUN
timeout agente.main,1
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 10,0,0,0 {NAME:GEN1,X:43,Y:300}
ADVANCE 20,0 {TO:POS1}
if (D$N%4==2)
signalnow agente.Suma,X$nAgente
signalnow agente.Resta,X$nAgente
savevalue resultado,P$(agenteDato,X$nAgente)
move {name:Text2,text:"Soy Entidad D$N : resultado X$resultado"}
endif
ADVANCE 20,0 {TO:POS2}
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
;*******************************
procedure agente.main
savevalue nAgente,D$N
assign agenteDato,10
while (1==1)
move {name:textAgente,text:"Soy Agente X$nAgente [AC1: AC1$]"}
REST Restroom_agentes
endwhile
endprocedure
;**********************************
procedure agente.Suma
ASSIGN agenteDato,P$agenteDato + 2
return_restore
endprocedure
;**********************************
procedure agente.Resta
ASSIGN agenteDato,P$agenteDato - 1
return_restore
endprocedure
Conditions
Modela un recurso retenedor condicional.
Una entidad puede acceder directamente o quedar retenida según se cumplan ciertas condiciones lógicas, tanto generales como particulares.
Lógica de retención:
El recurso evalúa tres tipos de condiciones:
EXPRESIÓN GENERAL (EXPRESSION)
Definida en el propio recurso (afecta a todas las entidades).
EXPRESIÓN PARTICULAR
Especificada en el bloque WAITUNTIL para una entidad concreta.
EXPRESIÓN ABSOLUTA
Indicada en WAITCHECK, y sobreescribe las demás. Si se cumple, libera las entidades.
Para que una entidad continúe, deben cumplirse ambas expresiones (GENERAL y PARTICULAR), salvo que se utilice una EXPRESIÓN ABSOLUTA, que actúa como única condición liberadora.
La liberación o paso de la entidad se produce a través del chequeo. Este chequeo se produce siempre que una entidad es liberada o cuando se solicita expresamente.
Las expresiones siempre se indican entre paréntesis.
Internamente gestiona una lista: Entidades ocupantes.
Este recurso es especialmente útil como multi-semáforo general o sala de espera para varios recursos en conjunto.
El chequeo puede ser cada tick o tiempo determinado a través de un TIMER.
Lo habitual es establecer los chequeos en los HOOKs de los recursos implicados.
Bloques asociados:
Eventos disponibles: ON_ATTEPMT, ON_CHECK, ON_QUEUE
Conditions {NAME:semaforo, EXPRESSION:(D$N %2 == 1) X:100, Y:100, ON_CHECK:semaforo_check}
;------------------------
WAITUNTIL semaforo; La entidad continúa si es impar; (La entidad queda retenida si su número es par.)
WAITUNTIL semaforo,(D$N %3 == 0); La entidad continúa si es impar y múltiplo de 3. (3,9,15...)
;------------------------
WAITCHECK semaforo; Chequea todas las entidades retenidas
WAITCHECK semaforo,(1==1) ; Libera todas las entidades retenidas
WAITCHECK semaforo,(D$N %2 == 0) ; Libera todas las entidades pares
/*
Recursos. Conditions
*/
SYSTEM {TYPE:ON_TIMER, TRIGGER:TIMER1, INTERVAL: 350}
SYSTEM {TYPE:ON_TIMER, TRIGGER:TIMER2, INTERVAL: 650}
POSITION {NAME:Salida,X:615,Y:388}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 100}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 120}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 140}
Conditions {NAME:Conditions1,X:324,Y:160
,expression:(D$N % 3 == 0)
,ON_ATTEMPT:Conditions1_ATTEMPT
,ON_CHECK:Conditions1_CHECK
,ON_QUEUE:Conditions1_QUEUE
}
START 100
;-------------------------------
GENERATE 20,0 {NAME:GEN1,X:62,Y:396}
advance 10 {to:Conditions1}
waituntil Conditions1,(D$N!=3)
move {name:Text1,text:"AC1$ Soy la Entidad [D$N] y avanzo"}
advance 100,50 {to:Salida}
endgenerate 1
;-------------------------------
PROCEDURE TIMER1
waitcheck Conditions1,(D$N%2==1)
move {name:Text6,text:"AC1$ Soy TIMER [D$N] WAITCHECK las impares"}
TERMINATE_VE
ENDPROCEDURE
PROCEDURE TIMER2
waitcheck Conditions1,(1==1)
move {name:Text7,text:"AC1$ Soy TIMER [D$N] WAITCHECK TODOS"}
TERMINATE_VE
ENDPROCEDURE
PROCEDURE Conditions1_QUEUE
move {name:Text3,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE Conditions1_CHECK
move {name:Text4,text:"AC1$ Soy la VE [D$N] de ON_CHECK la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
PROCEDURE Conditions1_ATTEMPT
move {name:Text5,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure
;-------------------------------
STOCK
STOCK modela un almacén inteligente de productos etiquetados, que permite entradas y salidas de items con tipo y cantidad. Soporta múltiples entradas simultáneas, comprobaciones de disponibilidad y eventos (hooks) conectables a lógica externa.
Es ideal para modelar inventarios, mercados, almacenes de componentes, etc.
Estructura y comportamiento
El recurso almacena productos en forma de objetos {clave: cantidad}, organizados internamente por tipo (clave).
Permite múltiples operaciones simultáneas de carga y descarga, acumulando cantidades por producto.
Bloques asociados:
{clave1: cantidad1 , clave2:cantidad2}.{clave1: cantidad1 , clave2:cantidad2}.SNA asociados:
SC$(recurso, objeto) Retorna 0 ó 1 si el recurso tiene suficientes cantidades del objeto para poder realizar STOCKOUT.
R$(nombre,STOCK,clave) Devuelve el stock actual de un tipo concreto.
VS$(nombre) → Devuelve el stock completo como objeto {clave1: qty1 , clave2: qty2, ...}.
Eventos disponibles:ON_ATTEMPT, ON_STOCKIN, ON_STOCKOUT, ON_QUEUE
STOCK {NAME:MARKET1,X:331,Y:210
,ON_QUEUE:MARKET1_QUEUE
,ON_STOCKIN:MARKET1_STOCKIN
,ON_STOCKOUT:MARKET1_STOCKOUT
,ON_ATTEMPT:MARKET1_ATTEMPT
,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:20
,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:100
}
;------------------------
assign mesa,round(random * 30)
assign silla,round(random * 100)
assign armario,round(random * 5)
assign lampara,round(random * 5)
assign camion,{mesa:P$mesa,silla:P$silla,armario:P$armario}
assign.merge camion,{lampara:P$lampara}
STOCKIN MARKET1, CAMION
;------------------------
/*
Recursos. Stock
*/
SYSTEM {TYPE:ON_TIMER, TRIGGER:MARKET1_TIMER, INTERVAL: 20}
STOCK {NAME:MARKET1,X:331,Y:210
,ON_QUEUE:MARKET1_QUEUE
,ON_STOCKIN:MARKET1_STOCKIN
,ON_STOCKOUT:MARKET1_STOCKOUT
,ON_ATTEMPT:MARKET1_ATTEMPT
,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:20
,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:100
}
POSITION {NAME:POS1,X:70,Y:113}
POSITION {NAME:POS2,X:595,Y:283}
initial posY,500
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 140}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 160}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 180}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 200}
START 300
;*****************************************************************
; PROVEEDORES: Generan productos para añadir al STOCK
GENERATE 115,0 {NAME:PROVEEDORES,X:619,Y:194,ECOLOR:#006666,ERADIO:15}
ADVANCE 20,0 {TO:MARKET1}
assign mesa,ROUND(RANDOM * 30)
assign silla,round(random * 100)
assign armario,round(random * 5)
assign lampara,round(random * 5)
assign camion,{mesa:P$mesa,silla:P$silla,armario:P$armario}
assign.merge camion,{lampara:P$lampara}
STOCKIN MARKET1, V$camion
MOD {RADIO:5}
ADVANCE 20,0 {TO:POS2}
TERMINATE 1
;*****************************************************************
; CLIENTES: Consumen productos del STOCK
GENERATE 20,2 {NAME:CLIENTES,X:81,Y:207,ECOLOR:#666666}
assign carrito,{}
assign.merge carrito,{mesa:2}
assign.merge carrito,{silla:4}
if (SC$(MARKET1,carrito)==0)
move {name:Text1,text:"SIN STOCK"}
else
move {name:Text1,text:"STOCK SUFICIENTE"}
endif
ADVANCE 10,0 {TO:MARKET1}
STOCKOUT MARKET1, V$carrito
MOD {RADIO:8}
ADVANCE 50,50 {TO:POS1}
TERMINATE 1
;-------------------------------
PROCEDURE MARKET1_STOCKOUT
; MAPPER es objeto {mesa:N,silla:N}
move {name:Text2,text:"ON_STOCKOUT Mesas: P$(MAPPER.mesa) Sillas: P$(MAPPER.silla)"}
TERMINATE_VE
endprocedure
PROCEDURE MARKET1_STOCKIN
; MAPPER es objeto {mesa:N,silla:N}
move {name:Text3,text:"ON_STOCKIN Mesas: P$(MAPPER.mesa) Sillas: P$(MAPPER.silla)"}
TERMINATE_VE
endprocedure
PROCEDURE MARKET1_ATTEMPT
; MAPPER es objeto {mesa:N,silla:N}
move {name:Text4,text:"ON_ATTEMPT Mesas: P$(MAPPER.mesa) Sillas: P$(MAPPER.silla)"}
TERMINATE_VE
endprocedure
PROCEDURE MARKET1_QUEUE
; MAPPER es objeto {mesa:N,silla:N}
move {name:Text5,text:"ON_QUEUE Mesas: P$(MAPPER.mesa) Sillas: P$(MAPPER.silla)"}
TERMINATE_VE
endprocedure
;-------------------------------
PROCEDURE MARKET1_TIMER
assign tmp,VS$(MARKET1)
assign txt,""
foreach clave,IN_OBJECT,V$(tmp)
assign txt,"P$(txt) | P$(clave) : P$(tmp.P$(clave))"
endforeach
move {name:Text6,text:"AC1$ ON_TIMER [P$txt]"}
TERMINATE_VE
endprocedure
Backpack no es un recurso como tal ya que en realidad no realiza funciones internas, es más bien una herramienta de transporte de entidades, pero sí es cierto que las retiene.
Para poder llevar entidades de un punto a otro de la simulación, por ejemplo, una furgoneta con paquetes se necesita algo que indique al sistema que esas entidades ya no están disponibles en la cola de eventos ni en ninguna otra cola.
Este concepto podríamos llamarlo mochila (Backpack) donde una entidad podrá transportar entidades. Existen dos limitaciones:
Solo se puede cargar desde un RESTROOM ya que es un recurso retenedor en la que las entidades no salen por sí mismas y sólo se pueden descargar en un GENERATE ya que es el único punto seguro con respecto a la pila de direcciones de la entidad (véase SCAPE).
Tiene dos bloques asociados:
LOAD nombreMochila,nombreRestroom[,númeroEntidad]UNLOAD nombreMochila,nombreGenerate[,númeroEntidad]El primero carga en la mochila todo el contenido de un Restroom o la entidad con ese número y el segundo es su paso inverso en el GENERATE.
Las entidades en la mochila pueden obtenerse a través del propio ASSIGN generado con el mismo nombre de la mochila. Es un array con los números de las entidades.
El ejemplo es el recorrido de una furgoneta que pasa por dos puntos de recogida y lleva los paquetes al punto de distribución.
RESTROOM {NAME:almacenReceptor1,X:220,Y:348,E_BIN_SIZE:1,E_BIN_COUNT:10}
RESTROOM {NAME:almacenReceptor2,X:217,Y:182,E_BIN_SIZE:1,E_BIN_COUNT:10}
RESTROOM {NAME:almacenDistribucion,X:662,Y:93}
Graphic {NAME:Text1,Type:TEXT,X:100,Y:100,Text:"Terminado: NO",font:"20",color:red}
START 1000
;************************************************************************
GENERATE 8,3 {NAME:Recepcion1,X:66,Y:356}
ADVANCE 20,0 {TO:almacenReceptor1}
rest almacenReceptor1
terminate 1
;************************************************************************
GENERATE 5,3 {NAME:Recepcion2,X:65,Y:181}
ADVANCE 20,0 {TO:almacenReceptor2}
rest almacenReceptor2
terminate 1
;************************************************************************
GENERATE 40,0,0,1 {NAME:Furgonetas,X:40,Y:567
,eradio:16,ecolor:red,visible:0
,esubtitle:"P$(mochila1.LENGTH)"}
ADVANCE0 {TO:PuntoDistribucion}
while (1==1)
ADVANCE 20,30 {TO:almacenReceptor1}
load mochila1,almacenReceptor1
ADVANCE 20,30 {TO:almacenReceptor2}
load mochila1,almacenReceptor2
ADVANCE 20,30 {TO:PuntoDistribucion}
unload mochila1,PuntoDistribucion
endwhile
terminate 1
;************************************************************************
GENERATE 0,0,0,0 {NAME:PuntoDistribucion,X:669,Y:434}
ADVANCE0 {TO:PuntoDistribucion}
ADVANCE 20,50 {TO:almacenDistribucion}
if (D$N>1000)
move {name:Text1,text:"TERMINADO: SÍ",color:green}
stop
endif
rest almacenDistribucion
terminate 1
FACILITY {NAME:VENTANILLA1,X:380,Y:348,capacity:4}
POSITION {NAME:POS1,X:218,Y:437}
POSITION {NAME:POS2,X:591,Y:429}
POSITION {NAME:POS3,X:713,Y:329}
Graphic {NAME:Line1,Type:L,color:#FF0000, X1:218,Y1:500,X2:592,Y2:500}
Graphic {NAME:Text1,Type:T,X:410,Y:527,Text:"Estadistic section"}
TABLE {name: TABLA1,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:100,EXPRESSION:(M1$ - P$TIEMPOINICIO)}
START 100
;***************************************************************
GENERATE 8,3 {NAME:GEN1,X:66,Y:350}
ADVANCE 30,0 {TO:POS1}
ASSIGN TIEMPOINICIO,M1$
ADVANCE 20,0 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE 20,20
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS2}
TABULATE TABLA1
ADVANCE 20,0 {TO:POS3}
TERMINATE 1
;***************************************************************FSM - Máquina de Estados Finitos
El bloque FSM permite modelar una Máquina de Estados Finitos. Se trata de un recurso que gestiona estados internos definidos mediante una tabla de transiciones, activadas por entradas (INPUT).
La lógica de funcionamiento se define mediante un objeto JSON con los siguientes campos:
STATES: Lista de estados posibles (opcional si se usa EVAL: 1)TRANSITIONS: Lista de transiciones entre estadosINITIAL: Estado inicial (*Sólo para LOCAL:0)EVAL: (opcional) Si es 1, permite usar expresiones matemáticas en TO.Cada transición puede contener:
"" actúa como comodín (acepta cualquier estado)."" actúa como comodín.EVAL:1).
; EJEMPLO BÁSICO
initial logic, { STATES: ["open", "close"],
TRANSITIONS: [
{FROM: "open", INPUT: "close", TO: "close", TRIGGER: "puertaAbierta"},
{FROM: "close", INPUT: "open", TO: "open"},
{FROM: "close", INPUT: "switch", TO: "open"},
{FROM: "open", INPUT: "switch", TO: "close"}
],
INITIAL: "open"
}
FSM {NAME:FSM1,X:434,Y:540, LOGIC:V$(logic)}
; EJEMPLO CICLICO
initial logic2, { states: ["uno", "dos", "tres"],
TRANSITIONS: [
{FROM: "uno", INPUT: "siguiente", TO: "dos"},
{FROM: "dos", INPUT: "siguiente", TO: "tres"},
{FROM: "tres", INPUT: "siguiente", TO: "uno", TRIGGER: "reinicio"}
],
INITIAL: "open"
}
FSM {NAME:FSM2,X:134,Y:140, LOGIC:V$(logic2)}
; EJEMPLO LÓGICA EVALUADA
; En este modo (eval: 1), los estados y transiciones pueden evaluarse como expresiones matemáticas dinámicas.
initial logicTemp3, {
TRANSITIONS: [
{FROM: "", INPUT: "sube", TO: "(X + 1 <= 24 ? X + 1.5 : X)"},
{FROM: "", INPUT: "baja", TO: "(X - 1 >= 18 ? X - 1.5 : X)"},
{FROM: "", INPUT: "reset", TO: "21"}
],
EVAL: 1,
INITIAL: 21
}
FSM {NAME:Stater3, X:434, Y:540, LOGIC:V$(logicTemp3)}
; EJEMPLO LÓGICA EVALUADA CON PARÁMETRO ADICIONAL Y
; SE PUEDEN AÑADIR PARÁMETROS Y, Z A AS FUNCIONES
initial logicTemp4, {
TRANSITIONS: [
{FROM: "", INPUT: "sube", TO: "(X + 1 <= 24 ? X + Y : X)"},
{FROM: "", INPUT: "baja", TO: "(X - 1 >= 18 ? X - Y : X)"},
{FROM: "", INPUT: "reset", TO: "21"}
],
EVAL: 1,
INITIAL: 21
}
FSM {NAME:Stater4, X:434, Y:540, LOGIC:V$(logicTemp4)}
Bloques asociados:
FSM Fsm1,"OPEN" ; pasa a estado "CLOSE" FSM Fsm1,"SWITCH" ; si estaba en "OPEN" pasa a "CLOSE" y viceversa. FSM Fsm2,"siguiente" ; si estaba en "DOS" pasa a "TRES",... FSM Fsm4,"subir",10 ; incrementa el state en 10
SNA asociado:
FSM es el único recurso que ofrece una operación atómica de consulta y modificación del estado.
Esto permite modelar zonas críticas, semáforos y sincronización entre procesos.
Usos típicos
waituntil Conditions1,("R$(Fsm1,IN_AFTER,close)"=="open") ; la entidad pasa y retiene las posteriores
...
FSM Fsm1,"open" ; finaliza la zona de exclusión mutua
waitcheck Conditions1 ; da paso al siguiente
Y como almacén de funciones matemáticas con ejecución de dos parámetros adicionales:
{ FROM: "", INPUT: "incr", TO: "(X + Y * Z)" } ; X: State actual, Y,Z Parámetros extra de input
Eventos disponibles: TRIGGER
Cada transición puede incluir un trigger, que llamará a un PROCEDURE:
PROCEDURE puertaAbierta
move {name:text7,text:"La entidad P$ENTITYNUMBER ha abierto la puerta"}
TERMINATE_VE
endprocedure
Modos de almacenamiento:
Su modo por defecto es global, un estado por STATER, pero si se define:
LOCAL:1
su modo es de un estado para cada entidad. Perfecto como discriminador de caminos que evita el uso de SWITCH anidados.
Dado que no se assigna valor inicialmente, éste se asignará mediente el acceso directo al assign asociado:
assign Fsm1,10
POSITION {NAME:Salida,X:648,Y:383}
initial posY,500
initial logic, { STATES: ["open", "close"],
TRANSITIONS: [
{FROM: "open", INPUT: "close", TO: "close", TRIGGER: "puertaAbierta"},
{FROM: "close", INPUT: "open", TO: "open"},
{FROM: "close", INPUT: "switch", TO: "open"},
{FROM: "open", INPUT: "switch", TO: "close"}
],
INITIAL: "open"
}
FSM {NAME:FSM1,X:434,Y:540, LOGIC:V$(logic)}
Graphic {NAME:Text1,Type:TEXT,X:324,Y:X$posY - 0}
Graphic {NAME:Text2,Type:TEXT,X:324,Y:X$posY - 20}
Graphic {NAME:Text3,Type:TEXT,X:324,Y:X$posY - 40}
Graphic {NAME:Text4,Type:TEXT,X:324,Y:X$posY - 60}
Graphic {NAME:Text5,Type:TEXT,X:324,Y:X$posY - 80}
Graphic {NAME:Text6,Type:TEXT,X:324,Y:X$posY - 100}
Graphic {NAME:Text7,Type:TEXT,X:324,Y:X$posY - 120}
Graphic {NAME:Text8,Type:TEXT,X:324,Y:X$posY - 140}
Conditions {NAME:Conditions1,X:317,Y:539
,expresion:("R$(FSM1,IN_AFTER,close)"=="open")
,ON_ATTEMPT:Conditions1_ATTEMPT
,ON_CHECK:Conditions1_CHECK
,ON_QUEUE:Conditions1_QUEUE
}
START 100
;-------------------------------
GENERATE 20,0 {NAME:GEN1,X:62,Y:396}
advance 10 {to:Conditions1}
waituntil Conditions1
advance 15,25 {to:Salida}
STATE FSM1,"open"
waitcheck Conditions1
terminate 1
;-------------------------------
PROCEDURE Conditions1_QUEUE
move {name:Text3,text:"AC1$ Soy la VE [D$N] de ON_QUEUE la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure 1
PROCEDURE Conditions1_CHECK
move {name:Text4,text:"AC1$ Soy la VE [D$N] de ON_CHECK la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure 1
PROCEDURE Conditions1_ATTEMPT
move {name:Text5,text:"AC1$ Soy la VE [D$N] de ON_ATTEMPT la entidad P$ENTITYNUMBER"}
TERMINATE_VE
endprocedure 1
;-------------------------------
PROCEDURE puertaAbierta
move {name:Text7,text:"La entidad P$ENTITYNUMBER ha abierto la puerta"}
TERMINATE_VE
endprocedure 1
;-------------------------------
QUEUER
Modela un recurso abierto.
Permite múltiples entidades sin criterio.
Internamente gestiona una lista de entidades en el recurso.
Su uso principal es estadístico o como agrupador de entidades.
Bloques asociados:
Eventos disponibles:ON_QUEUE, ON_DEPART
QUEUER {NAME:Qventanilla1,X:374,Y:424
,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:20
,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:20}
;------------------------
queue Qventanilla1
seize ventanilla1
depart Qventanilla1
/*
Recursos. Queuer
*/
;SYSTEM {TYPE:OPTIONS,Speed:50,Pause:0}
POSITION {NAME:POS1,X:620,Y:360}
QUEUER {NAME:Qventanilla1,X:374,Y:424
,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:20
,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:20}
Facility {NAME:ventanilla1,X:373,Y:365,capacity:5}
START 200
;*****************************************************
GENERATE 5,3,0,0 {NAME:GEN1,X:111,Y:366}
ADVANCE 10 {TO:ventanilla1}
queue Qventanilla1
seize ventanilla1
depart Qventanilla1
ADVANCE 20,10
release ventanilla1
ADVANCE 10 {TO:POS1}
ENDGENERATE 1
Existen varios recursos más con sus propias secciones como Bridger y Dynamic.
El comando SYSTEM permite configurar distintos aspectos del motor de simulación. Su sintaxis básica es:
SYSTEM {TYPE:..., ...}
Parámetros de simulación (velocidad, precisión...)
Puedes controlar la velocidad de simulación, la precisión temporal y la precisión decimal del tiempo:
SYSTEM {TYPE:OPTIONS, SPEED:3}
SYSTEM {TYPE:OPTIONS, TIME_DECIMALS:2}
SYSTEM {TYPE:OPTIONS, width:1400, height:1400}
SYSTEM {TYPE:OPTIONS, SEED:1400}
SPEED: Velocidad inicial de reproducción (por defecto: 5).TIME_DECIMALS: Permite que el tiempo AC1 tenga valores como 3.14 en lugar de valores enteros (por defecto: 0).WIDTH: Ancho del canvas de simulación. Por defecto 800.HEIGHT : Alto del canvas de simulación. Por defecto 600.SEED: Genera la secuencia de números aleatorios por Xorshift128+.TRACE_ROUTES: Genera las trazas para V&V2.- Mostrar información básica del sistema
Puedes activar o desactivar la visualización de elementos clave del motor:
SYSTEM {TYPE:OPTIONS, SHOW_BASICS:1}
Esto hará que se muestren en la parte inferior:
TG1: Tiempo global inicialAC1: Tiempo actualSPEED: Velocidad de simulación3.- Temporizadores (TIMER)
Los temporizadores permiten ejecutar procedimientos periódicamente, sin intervención de entidades:
SYSTEM {TYPE:ON_TIMER, TRIGGER:llenado, INTERVAL:0.1}
TRIGGER: nombre del procedimiento a ejecutar.INTERVAL: intervalo de tiempo (en unidades de simulación) entre ejecuciones.Esto es útil para tareas de monitorización, logging, cargas periódicas, etc.
SYSTEM {TYPE:OPTIONS, SPEED:5, TIME_DECIMALS:1, SHOW_BASICS:0}
SYSTEM {TYPE:ON_TIMER, TRIGGER:actualizarGraficos, INTERVAL:0.5}
PROCEDURE actualizarGraficos
move {name:text1:text: "Texto actualizado a AC1$"}
TERMINATE_VE
ENDPROCEDURELa plataforma cuenta con herramientas visuales y técnicas que permiten el seguimiento detallado de la simulación, tanto a nivel de ejecución como de resultados.
Panel de depuración
Desde el tercer panel (canvas de reproducción, seguimiento y debug, editor de código), puedes acceder a:
Cola de eventos
Muestra en tiempo real qué entidades están programadas, su posición temporal (TG1) y en qué paso del programa se encuentran.
Recursos
Detalla las entidades ocupantes, aquellas en espera (cola), y la capacidad de cada recurso.
Entidades
Visualiza el estado de cada entidad: variables, posición, paso actual, retardo, etc.
Ficheros
Se muestran todos los archivos creados mediante el comando FILE y el bloque WRITE.
Incluye también el archivo especial DEBUG, que se genera automáticamente si se activan instrucciones con la etiqueta {debug:1} o {debug:2}.
{debug:1} registra información general de la ejecución de la línea.
{debug:2} incluye valores de variables evaluadas y resultados detallados.
if (P$variable == 1) {debug:1}
assign P$variable, X$origen {debug:2}
Mediante el botón Informe, se genera una visión estadística completa de la simulación:
AC1), número de entidades procesadas...Este ejemplo presenta el uso básico de un recurso tipo STORAGE, útil para modelar inventarios, almacenes o zonas de capacidad limitada. Las entidades consumen unidades aleatorias del recurso, representando una carga variable.
El interés del ejemplo está en cómo obtener el contenido del recurso en tiempo real por dos métodos diferentes:
Suma de datos individuales: usando FOREACH, se recorre cada entidad que ocupa el recurso y se suman sus valores asociados (en este caso, el número de unidades que ha tomado).
Consulta directa: mediante R$(ALMACEN1,LEFT) se obtiene el número de unidades libres, por lo que el total ocupado es CAPACITY - LEFT.
Ambos enfoques permiten contrastar y validar la información, y se pueden aplicar para cálculos estadísticos, control de inventarios o gestión de alertas de saturación.
El TIMER periódicamente actualiza la información mostrada en pantalla, ofreciendo una visión continua del estado del recurso.
/*
Accediendo y midiendo un recurso
*/
STORAGE {NAME: ALMACEN1, CAPACITY:40, X:316,Y:401}
SYSTEM {TYPE:ON_TIMER,INTERVAL:50,TRIGGER:PROCTIMER}
GRAPHIC {NAME:BANNER1,TYPE:TEXT,X:184,Y:343,TEXT:ALEATORIO.}
GRAPHIC {NAME:BANNER2,TYPE:TEXT,X:305,Y:283,TEXT:SUMA}
GRAPHIC {NAME:BANNER3,TYPE:TEXT,X:306,Y:241,TEXT:DIRECT}
POSITION {NAME:POS1,X:176,Y:296}
POSITION {NAME:POS2,X:452,Y:300}
POSITION {NAME:POS3,X:559,Y:299}
START 100
;******************************************************
GENERATE 30,10 {NAME:GEN1,X:61,Y:297}
ASSIGN ALEATORIO,FLOOR(RANDOM * 9 + 1) {debug:1}
MOVE {NAME:BANNER1,TEXT:"ALEATORIO: P$ALEATORIO"}
ADVANCE 20 {TO:POS1}
ADVANCE 20 {TO:ALMACEN1}
ENTER ALMACEN1 ,P$ALEATORIO
ADVANCE 110,90
LEAVE ALMACEN1
ADVANCE 20 {TO:POS2}
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
;*****************************************************
PROCEDURE PROCTIMER
ASSIGN SUMA,0
FOREACH NUMERO,IN_RESOURCE,ALMACEN1
ASSIGN SUMA,(P$(ALEATORIO,P$NUMERO) + P$SUMA)
ENDFOREACH
MOVE {NAME:BANNER2,TEXT:"SUMA ALEATORIOS: P$SUMA"}
ASSIGN DIRECT,40-R$(ALMACEN1,LEFT)
MOVE {NAME:BANNER3,TEXT:"DIRECT: P$DIRECT"}
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
No todos los recursos deben atender con la misma duración a cada entidad. En ciertos escenarios —como servicios al cliente, centros de atención médica o estaciones de procesamiento— puede ser razonable adaptar el tiempo de atención en función de la carga del sistema.
En este ejemplo, tres ventanillas (Facility_1, Facility_2, Facility_3) reciben flujos de entidades con distinta intensidad. Cada una está acompañada por un Queuer, que permite medir y registrar el tamaño actual de la cola.
Antes de ser atendida, cada entidad llama a un procedimiento que calcula dinámicamente su tiempo de atención, basándose en el tamaño actual de la cola del recurso.
La lógica usada en este caso es una de las siguientes:
assign tamCola,R$(P$ventanilla,QUEUE) + 1
ASSIGN tiempoAtencion, round(max(2, 120 / P$tamCola))
ASSIGN tiempoAtencion, round(max(2, 120 / sqrt(P$tamCola)))
ASSIGN tiempoAtencion, round(120 / log(P$tamCola + 1))
Esto significa que:
A mayor cola, menor tiempo de atención, priorizando fluidez.
A menor cola, más tiempo por entidad, mejorando la calidad del servicio.
Es una estrategia de balanceo automático entre calidad y rendimiento.
Esta técnica demuestra:
Cómo vincular un recurso con su Queuer para medir carga.
Cómo usar funciones matemáticas para adaptar la duración de los ADVANCE.
Cómo mantener bajo control el tamaño de la cola sin sacrificar la atención.
El ejemplo puede ser fácilmente extendido a:
Funciones más complejas de atención.
Decisión sobre qué ventanilla elegir.
Optimización del rendimiento global del sistema.
/*
Gestionando el tiempo de uso
*/
Facility {NAME:Facility_1,X:400,Y:84,capacity:3,E_BIN_SIZE:1,E_BIN_COUNT:200}
Facility {NAME:Facility_2,X:400,Y:282,capacity:3,E_BIN_SIZE:1,E_BIN_COUNT:200}
Facility {NAME:Facility_3,X:400,Y:471,capacity:3,E_BIN_SIZE:1,E_BIN_COUNT:200}
Queuer {NAME:Queuer_1,X:400,Y:174,R_BIN_SIZE:1,R_BIN_COUNT:30}
Queuer {NAME:Queuer_2,X:400,Y:363,R_BIN_SIZE:1,R_BIN_COUNT:30}
Queuer {NAME:Queuer_3,X:400,Y:557,R_BIN_SIZE:1,R_BIN_COUNT:30}
Graphic {NAME:Text_1,Type:TEXT,X:400,Y:132}
Graphic {NAME:Text_2,Type:TEXT,X:400,Y:323}
Graphic {NAME:Text_3,Type:TEXT,X:400,Y:512}
POSITION {NAME:POS_1,X:700,Y:137}
POSITION {NAME:POS_2,X:700,Y:328}
POSITION {NAME:POS_3,X:700,Y:506}
START 1000
;*********************************************************
GENERATE 10,0 {NAME:GEN1,X:100,Y:126}
CALL caminoVentanilla,1
ENDGENERATE 1
GENERATE 15,0 {NAME:GEN2,X:100,Y:319}
CALL caminoVentanilla,2
ENDGENERATE 1
GENERATE 20,0 {NAME:GEN3,X:100,Y:492}
CALL caminoVentanilla,3
ENDGENERATE 1
;----------------------------------
PROCEDURE caminoVentanilla
assign ventanilla,"Facility_P$PARAM_A"
assign cola,"Queuer_P$PARAM_A" {debug:1}
assign nVentanilla,"P$PARAM_A"
ADVANCE 20 {TO:"P$ventanilla"}
CALL calcularTiempo , "P$ventanilla",P$nVentanilla
QUEUE P$cola
SEIZE P$ventanilla
DEPART P$cola
ADVANCE P$calcularTiempo
RELEASE P$ventanilla
ADVANCE 20 {TO:"POS_P$nVentanilla"}
ENDPROCEDURE
;----------------------------------
PROCEDURE calcularTiempo
assign ventanilla,"P$PARAM_A"
assign nVentanilla,P$PARAM_B
assign tamCola,R$(P$ventanilla,QUEUE) + 1
;ASSIGN tiempoAtencion, round(max(2, 120 / P$tamCola))
;ASSIGN tiempoAtencion, round(max(2, 120 / sqrt(P$tamCola)))
ASSIGN tiempoAtencion, round(120 / log(P$tamCola + 1))
move {name:"Text_P$nVentanilla",text:"tiempo Atención: P$tiempoAtencion"}
RETURN P$tiempoAtencion
ENDPROCEDURE
En este ejemplo, las entidades deben decidir por cuál de tres caminos avanzar, cada uno asociado a una ventanilla con su propia cola (FACILITY con distinta capacidad). La decisión se toma en la función DECIDE.SELECCIONA, que recorre una lista con los nombres de los caminos y el tamaño actual de sus colas (QUEUE). El resultado es el nombre del camino con menos carga.
Ese valor es un STRING que se devuelve con ENDPROCEDURE y se almacena automáticamente en la variable P$SELECCIONA, que luego se usa directamente como llamada: CALL P$SELECCIONA. Esta mecánica permite una selección dinámica sin necesidad de condicionales o múltiples llamadas explícitas.
Las estructuras .decide se usan así para devolver directamente el nombre del procedimiento a ejecutar, ahorrando al programador tener que hacer un doble SWITCH o una cascada de IF.
Esto simplifica notablemente la lógica de decisión ya que con simplemente:
CALL DECIDE.SELECCIONA CALL P$SELECCIONA
sustituye a:
CALL DECIDE.SELECCIONA SWITCH P$SELECCIONA CASE ==, "CAMINO1" CALL CAMINO1 CASE ==, "CAMINO2" CALL CAMINO2 ... ENDSWITCH
/*
Redirigir las colas
*/
FACILITY {NAME:VENTANILLA1,X:320,Y:450,capacity:3}
FACILITY {NAME:VENTANILLA2,X:320,Y:300,capacity:1}
FACILITY {NAME:VENTANILLA3,X:320,Y:150,capacity:2}
POSITION {NAME:POS1,X:152,Y:300}
POSITION {NAME:POS2,X:497,Y:300}
Graphic {NAME:Text1,Type:TEXT,X:100,Y:100}
START 200
;*************************************************************************
GENERATE 6,0 {NAME:GEN1,X:54,Y:300}
ADVANCE 20,0 {TO:POS1,flow:1}
CALL DECIDE.SELECCIONA ; return strings : "CAMINO1", "CAMINO1" ó "CAMINO1" creando un ASSIGN llamado P$SELECCIONA
CALL P$SELECCIONA
ADVANCE 20 {TO:POS2,flow:1,merge:"camino"}
ENDGENERATE 1
;*************************************************************************
PROCEDURE DECIDE.SELECCIONA
assign mimap, [
{cola:"CAMINO1",encola: R$(VENTANILLA1,QUEUE)},
{cola:"CAMINO2",encola: R$(VENTANILLA2,QUEUE)},
{cola:"CAMINO3",encola: R$(VENTANILLA3,QUEUE)}
]
ASSIGN MINKEY,"CAMINO1"; inicializamos el KEY seleccionado
ASSIGN MINVAL,100000 ; inicializamos el VAL de menor valor para las comparaciones
FOREACH tmp,IN,V$mimap
IF (P$(tmp.encola) < P$MINVAL)
ASSIGN MINKEY,"P$(tmp.cola)"
ASSIGN MINVAL,P$(tmp.encola)
move {name:Text1,Text:"Decisión: P$(MINKEY) Cola: P$(MINVAL)"}
ENDIF
ENDFOREACH
ENDPROCEDURE "P$MINKEY" ; retornará, por ejemplo, "C1"
;**************************************************************************
PROCEDURE CAMINO1
ADVANCE 20 {TO:VENTANILLA1,flow:1,decision:"camino"}
SEIZE VENTANILLA1
ADVANCE 25,10
RELEASE VENTANILLA1
ENDPROCEDURE
PROCEDURE CAMINO2
ADVANCE 20 {TO:VENTANILLA2,flow:1,decision:"camino"}
SEIZE VENTANILLA2
ADVANCE 80,70
RELEASE VENTANILLA2
ENDPROCEDURE
PROCEDURE CAMINO3
ADVANCE 20 {TO:VENTANILLA3,flow:1,decision:"camino"}
SEIZE VENTANILLA3
ADVANCE 30,20
RELEASE VENTANILLA3
ENDPROCEDURE
;***************************************************************
Hemos visto anteriormente el bloque ADVANCE como algo que parecía inmutable. Por ejemplo:
ADVANCE 20,10
El rol de UPDATE
Una entidad que está dentro de un ADVANCE no puede auto-modificar su tiempo de activación, ya que está inactiva. Por tanto, debe ser otra entidad (usualmente una entidad virtual, activada por un TIMER o un HOOK) la que ejecute UPDATE para ajustar su tiempo.
Técnicamente, UPDATE no modifica un ADVANCE: modifica la cola de eventos.
Extrae la entidad de su activación futura y la reprograma en un nuevo tiempo absoluto.
El motor no interpreta el cambio: solo mantiene la cola ordenada.
Escenario: Condiciones cambiantes en una carretera
Simulamos un tramo de carretera bajo ciertas condiciones almacenadas en el SAVEVALUE conditions.
En condiciones normales (conditions = 1.0), se tarda 100 unidades de tiempo en cruzar.
Si conditions = 1.5, el trayecto dura 150 unidades de tiempo.
Una entidad calcula su tiempo de avance cuando entra en el tramo.
Si las condiciones cambian mientras una entidad está en camino, queremos ajustar su tiempo de llegada, para reflejar este cambio.
¿Cómo se calcula?
Imagina que una entidad entró cuando conditions = 1.2 (tiempo de recorrido = 120), pero ahora las condiciones mejoran a 1.0. Si ya ha recorrido el 50%, le quedarían 50 unidades con las nuevas condiciones. En total, habría tardado 110 en lugar de 120.
El bloque UPDATE
UPDATE entidadID, nuevoTiempo
Este comando reprograma a una entidad con un nuevo tiempo de activación, siempre que dicho tiempo sea ≥ AC1$ (el tiempo actual de simulación).
Qué demuestra este ejemplo:
Que el comportamiento de las entidades puede ser modificado incluso mientras están en curso.
Que TIMER + FOREACH + UPDATE forman una combinación para reaccionar dinámicamente.
Que se permite representar escenarios del mundo real, donde las condiciones cambian y el sistema debe adaptarse.
/*
Actualizando la cola de eventos: UPDATE
*/
SYSTEM {TYPE:ON_TIMER, TRIGGER:TIMER1, INTERVAL: 100}
Queuer {NAME:Queuer1,X:350,Y:324}
Graphic {NAME:Text1,Type:TEXT,X:352,Y:381}
POSITION {NAME:POS1,X:90,Y:513}
POSITION {NAME:POS2,X:638,Y:512}
POSITION {NAME:POS3,X:635,Y:322}
START 100
;*********************************************************
INITIAL defaultTime,100
INITIAL conditions,1.0
;*********************************************************
PROCEDURE TIMER1
savevalue oldConditions,X$conditions
savevalue conditions,round(random + 0.5,2)
MOVE {NAME:Text1,TEXT:"Old Conditions: X$oldConditions \nNew conditions: X$conditions"}
FOREACH number,IN_RESOURCE,Queuer1
assign myConditions,X$conditions,P$number
ASSIGN from, D$(ADVANCESTART, P$number)
ASSIGN lapse, D$(ADVANCELAPSE, P$number)
ASSIGN completedRatio , (AC1$ - P$from) / P$lapse
ASSIGN remainingRatio , 1 - P$completedRatio
ASSIGN adjustedRemainingTime , P$remainingRatio * X$defaultTime * X$conditions
ASSIGN newTime, AC1$ + P$adjustedRemainingTime
if (X$conditions<1)
MOD {number:P$number,color:#00DD00}
else
MOD {number:P$number,color:#000000}
endif
if (P$newTime < AC1$)
assign newTime,AC1$
endif
Update P$number,P$newTime
ENDFOREACH
TERMINATE_VE
ENDPROCEDURE
;*********************************************************
GENERATE 20,0 {NAME:GEN1,X:102,Y:203, ECOLOR:#FF3333, ERADIO:8,esubtitle:P$myConditions}
ADVANCE 16,5 {TO:POS1}
assign myTime,X$defaultTime * X$conditions
assign myConditions,X$conditions
Queue Queuer1
ADVANCE P$myTime,0 {TO:POS2}
Depart Queuer1
MOD {color:#FF3333}
ADVANCE 16,5 {TO:POS3}
ENDGENERATE 1
GPSS-Plus permite definir elementos gráficos que se pueden mover, transformar y actualizar durante la simulación. Estos objetos pueden ser textos, curvas, imágenes, arcos o líneas, y pueden agruparse para formar unidades visuales completas.
Los gráficos pueden moverse de forma individual o en grupo, y se pueden modificar sus propiedades como:
X, Y)ROTATE)RESIZE)OPACITY)TEXT)ARC o LINELos comandos MOVE permiten usar:
X:100GD$, por ejemplo: X:GD$(objeto,X) + 10Este ejemplo muestra cómo una entidad, a lo largo de sus pasos, modifica variables internas (SAVEVALUE) y usa esas variables para actualizar objetos gráficos en cada avance.
Se ilustra el uso de gráficos de tipo TEXT, CURVE3, CURVE, ARC, IMAGE y GROUP, así como el uso combinado de animaciones relativas, escalado y rotación.
GPSS-Plus admite dos tipos de curvas:
X1, Y1, X2, Y2, X3, Y3Para declarar los puntos, existen dos formas equivalentes:
POINTS:"[x1,y1],[x2,y2],[x3,y3],..."X1:1,Y1:2, X2:3,Y2:4, X3:5,Y3:6Las curvas, líneas o arcos pueden ser abiertos o cerrados. Para representarlos como figuras cerradas con relleno, es necesario establecer:
CLOSE:1FCOLOR: para el color de relleno/*
Crear y modificar gráficos
*/
SYSTEM {TYPE:OPTIONS, SPEED:8, TIME_DECIMALS:1}
;==============================
; VARIABLES DINÁMICAS
;==============================
INITIAL GRADO, 0
INITIAL ALTURA, 80
INITIAL ESCALA, 100
INITIAL CRECIENDO, 1
;==============================
; ELEMENTOS GRÁFICOS
;==============================
GRAPHIC {NAME:txt, TYPE:TEXT, X:300, Y:50,text:"--test--"}
GRAPHIC {NAME:radarGrupo, TYPE:GROUP, X:300, Y:400}
GRAPHIC {
GROUP:radarGrupo,
NAME:radarTexto,
TYPE:TEXT,
TEXT:"GRADO: X$GRADO",
COLOR:#00AAFF,
X:0, Y:-50
}
GRAPHIC {
GROUP:radarGrupo,
NAME:radarImagen,
TYPE:IMAGE,
SRC:"PUERTA",
X:100, Y:100,
OPACITY:0.9,
ROTATE:0
}
GRAPHIC {
GROUP:radarGrupo,
NAME:ondaVisual,
TYPE:CURVE,
POINTS:"[0,0],[40,X$ALTURA],[80,0]",
COLOR:red,
WIDTH:2,
OPACITY:0.6
}
GRAPHIC {
NAME:puerta,
TYPE:IMAGE,
SRC:"PUERTA",
X:400, Y:100,
OPACITY:0.9,
ROTATE:0
}
GRAPHIC {
NAME:barreraCircular,
TYPE:ARC,
X:100, Y:400,
COLOR:#FFAA00,
RADIUS:60,
START_ANGLE:0,
END_ANGLE:X$GRADO,
OPACITY:1,
CLOSE:0
}
;==============================
; ENTIDAD PRINCIPAL
;==============================
GENERATE 5, 0 {NAME:GEN1, X:50, Y:200}
;move {name:txt,text:"Pos X GD$(barreraCircular,X)"}
; Aumentar grado (rotación)
SAVEVALUE GRADO, (X$GRADO + 15) % 360
; Subir/bajar la onda
IF ((X$GRADO % 180) < 90)
SAVEVALUE ALTURA, X$ALTURA + 4
ELSE
SAVEVALUE ALTURA, X$ALTURA - 4
ENDIF
; Animar el tamaño
IF (X$CRECIENDO == 1)
SAVEVALUE ESCALA, X$ESCALA + 5
IF (X$ESCALA >= 140)
SAVEVALUE CRECIENDO, 0
ENDIF
ELSE
SAVEVALUE ESCALA, X$ESCALA - 5
IF (X$ESCALA <= 100)
SAVEVALUE CRECIENDO, 1
ENDIF
ENDIF
ADVANCE 5, 0
;==============================
; ACTUALIZACIONES VISUALES
;==============================
MOVE {NAME:radarTexto, TEXT:"Grado: X$GRADO"}
MOVE {NAME:radarImagen, ROTATE:X$GRADO}
MOVE {NAME:radarImagen, RESIZE:X$ESCALA}
MOVE {NAME:ondaVisual, Y2:X$ALTURA}
MOVE {NAME:radarGrupo, Y:(GD$(radarGrupo,Y)+(RANDOM*10)-5)}
MOVE {NAME:puerta, RESIZE_X:X$ESCALA * 2}
MOVE {NAME:barreraCircular, END_ANGLE:X$GRADO}
TERMINATE 1
START 500
Antes de introducir el sistema de contextos CX$ para instanciar componentes gráficos reutilizables en LIBRERIAS INDEPENDIENTES, es importante comprender cómo se puede construir un componente visual manualmente, paso a paso, utilizando procedimientos (PROCEDURE) y estructuras de configuración (INITIAL).
Este ejemplo no representa la forma óptima ni la más recomendable para implementar componentes gráficos reutilizables, pero cumple un papel esencial en el aprendizaje:
Permite entender cómo se declaran los gráficos agrupados (GROUP).
Muestra cómo parametrizar su geometría y posición mediante diccionarios (INITIAL).
Enseña cómo encapsular transformaciones visuales dentro de procedimientos.
Introduce la llamada de “funciones visuales” desde entidades con CALL.
Este enfoque será más adelante reemplazado por el uso de contextos CX$, pero conviene dominar primero este nivel “manual” para comprender en profundidad cómo GPSS-Plus convierte procedimientos en componentes visuales autónomos y reutilizables.
En este ejemplo se representa un depósito con nivel visual, compuesto por un GROUP que contiene:
un marco exterior (LINE cerrado)
un nivel de relleno (LINE cerrado, con altura variable)
un texto con el porcentaje de llenado visible
Toda la geometría y la posición del depósito están definidas mediante una estructura de configuración (config) y se aplican directamente a gráficos con nombres fijos. Aunque este enfoque simula una forma de parametrización, no es reutilizable tal como está: los nombres de los objetos gráficos están codificados de forma estática, lo que impide instanciar múltiples depósitos sin conflictos.
Esta limitación se resolverá más adelante con el uso de contextos CX$ y librerías, que permitirán crear múltiples instancias visuales de un mismo componente, cada una con su propio estado y configuración.
El procedimiento nivel_init se encarga de generar los vértices del gráfico al inicio (PRE_RUN), y el procedimiento deposito_set actualiza visualmente el nivel de llenado según un porcentaje (P$PARAM_A).
También se incluye deposito_locate, que permite mover todo el grupo a una nueva posición.
/*
Gráficos. Modificación por procedimiento
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
;==============================
; CONFIGURACIÓN DEL DEPÓSITO
;==============================
INITIAL config, {deposito_coords:[0, 0, 20, 200],deposito_position:[180,180]}
;==============================
; GRÁFICOS
;==============================
GRAPHIC {NAME:Deposito, TYPE:GROUP, X:0, Y:0}
; Marco exterior
GRAPHIC {
NAME:Marco,
GROUP:Deposito,
TYPE:LINE,
POINTS:"[0,0],[0,10],[10,10],[10,10]",
COLOR:#555555,
CLOSE:1
}
; Nivel interior (relleno)
GRAPHIC {
NAME:Nivel,
GROUP:Deposito,
TYPE:LINE,
POINTS:"[0,300],[0,300],[40,300],[40,300]",
COLOR:#3399FF,
FCOLOR:#99CCFF,
CLOSE:1
}
; Texto de porcentaje
GRAPHIC {
NAME:TextoNivel,
GROUP:Deposito,
TYPE:TEXT,
X:0, Y:0,
TEXT:"Nivel: 0%",
COLOR:#000000
}
START 100
PROCEDURE PRE_RUN
timeout nivel_init,0
TERMINATE
ENDPROCEDURE 1
;==============================
; PROCEDIMIENTO DE ACTUALIZACIÓN
;==============================
PROCEDURE nivel_init
move {name:Marco
,X1:X$(config.deposito_coords.0)-4,Y1:X$(config.deposito_coords.1)-4
,X2:X$(config.deposito_coords.2)+4,Y2:X$(config.deposito_coords.1)-4
,X3:X$(config.deposito_coords.2)+4,Y3:X$(config.deposito_coords.3)+4
,X4:X$(config.deposito_coords.0)-4,Y4:X$(config.deposito_coords.3)+4
}
move {name:Nivel
,X1:X$(config.deposito_coords.0),Y1:X$(config.deposito_coords.1)
,X2:X$(config.deposito_coords.2),Y2:X$(config.deposito_coords.1)
,X3:X$(config.deposito_coords.2),Y3:X$(config.deposito_coords.3)
,X4:X$(config.deposito_coords.0),Y4:X$(config.deposito_coords.3)
}
MOVE {NAME:TextoNivel
, x:(GD$(Marco,X1) + GD$(Marco,X2))/ 2
, y:X$(config.deposito_coords.1)-14, TEXT:"Nivel: PP$A%"}
MOVE {NAME:Deposito
, x:X$(config.deposito_position.0)
, y:X$(config.deposito_position.1)}
terminate
endprocedure 1
PROCEDURE deposito_set
; PP$A = porcentaje deseado (0 a 100)
ASSIGN yBase, X$(config.deposito_coords.1) ; y inferior
ASSIGN yTop, X$(config.deposito_coords.3) ; y superior
ASSIGN alturaMaxima, P$yBase - P$yTop
ASSIGN alturaNivel, P$alturaMaxima * P$PARAM_A / 100
ASSIGN yActual, P$yBase - P$alturaNivel
; Mover vértices del nivel
MOVE {
NAME:Nivel,
Y3:P$yActual,
Y4:P$yActual
}
MOVE {NAME:TextoNivel,TEXT:"Nivel: P$PARAM_A%"}
ENDPROCEDURE 1
PROCEDURE deposito_locate
MOVE {NAME:Deposito
, x:P$PARAM_A
, y:P$PARAM_B}
ENDPROCEDURE 1
;==============================
; ENTIDAD QUE LO USA
;==============================
GENERATE 25,0 {NAME:Gen1}
CALL deposito_set, 25
ADVANCE 5
CALL deposito_set, 50
ADVANCE 5
CALL deposito_set, 75
ADVANCE 5
CALL deposito_set, 90
ADVANCE 5
CALL deposito_set, 10
ADVANCE 5
CALL deposito_locate, GD$(Deposito,X)+4,GD$(Deposito,Y)+4
TERMINATE 1
Este ejemplo representa un reloj de agujas tradicional (analógico) utilizando primitivas gráficas. A través de un PROCEDURE se inicializa el escenario con un arco de fondo, marcas horarias (TEXT) y las dos agujas (LINE). Una entidad virtual (TIMER) se genera periódicamente y actualiza las posiciones de las agujas usando trigonometría y variables temporales (SAVEVALUE).
Se combinan:
ARC para la circunferencia principal.
TEXT para los números y el reloj digital.
LINE para representar las agujas.
Es un caso claro donde la animación no viene de entidades móviles, sino de transformaciones visuales programadas.
Se puede observar la velocidad de reproducción, con un valor de speed: 5 aproximadamente se acompasaría el AC1 con el segundero.
/*
Gráficos. Reloj analógico animado
*/
SYSTEM {type:OPTIONS, TIME_DECIMALS:0, SPEED:7}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:ON_TIMER, TRIGGER:TIMER1, INTERVAL: 10}
; ANALOGIC CLOCK
GRAPHIC {NAME:OUT,TYPE:ARC,X:0,Y:0,COLOR:#FFFF99,RADIUS:200,START_ANGLE:0,END_ANGLE:360,CLOSE:0}
GRAPHIC {NAME:AT12,TYPE:TEXT,X:300,Y:500,TEXT:"12"}
GRAPHIC {NAME:AT3,TYPE:TEXT,X:500,Y:300,TEXT:"3"}
GRAPHIC {NAME:AT6,TYPE:TEXT,X:300,Y:100,TEXT:"6"}
GRAPHIC {NAME:AT9,TYPE:TEXT,X:100,Y:300,TEXT:"9"}
GRAPHIC {NAME:MINLINE,TYPE:LINE,COLOR:#F00,X1:268,Y1:361,X2:265,Y2:332}
GRAPHIC {NAME:HOURLINE,TYPE:LINE,COLOR:#000,X1:240,Y1:361,X2:233,Y2:328}
; DIGITAL CLOCK
GRAPHIC {NAME:TIME,TYPE:TEXT,X:300,Y:536,TEXT:"IT'S.. ",FONT:"26PX"}
GRAPHIC {NAME:REALTIME1,TYPE:TEXT,X:575,Y:536,TEXT:"REALTIME.. ",FONT:"14PX"}
GRAPHIC {NAME:REALTIME2,TYPE:TEXT,X:575,Y:516,TEXT:"REALTIME.. ",FONT:"14PX"}
START 1
; THE CLOCK
GENERATE 100,0 {NAME:GEN1,VISIBLE:0,EVISIBLE:0,X:647,Y:99}
TERMINATE 0
PROCEDURE PRE_RUN
SAVEVALUE MIN,0
SAVEVALUE A,0
SAVEVALUE B,0
SAVEVALUE CENTROX,300
SAVEVALUE CENTROY,300
SAVEVALUE RADIOM,180
SAVEVALUE RADIOH,120
MOVE {NAME:OUT,X:X$CENTROX,Y:X$CENTROY}
MOVE {NAME:MINLINE,x1:0,y1:0,x2:0,y2:0}
MOVE {NAME:HOURLINE,x1:0,y1:0,x2:0,y2:0}
TERMINATE_VE
ENDPROCEDURE
PROCEDURE TIMER1
assign sys,SYS$
ASSIGN sys, SYS$
MOVE {name: REALTIME1, text:"P$(sys.date.year)/P$(sys.date.month)/P$(sys.date.day)"}
MOVE {name: REALTIME2, text:"P$(sys.date.hour):P$(sys.date.min):P$(sys.date.sec)"}
MOVE {NAME:TIME,TEXT:"[X$HOR:X$MIN2]"}
MOVE {NAME:MINLINE
,X1: (X$CENTROX)
,Y1:(X$CENTROY)
,X2:(X$CENTROX+SIN(X$A)*X$RADIOM)
,Y2:(X$CENTROY+COS(X$A)*X$RADIOM)}
MOVE {NAME:HOURLINE
,X1:(X$CENTROX)
,Y1:(X$CENTROY)
,X2:(X$CENTROX+SIN(X$B)*X$RADIOH)
,Y2:(X$CENTROY+COS(X$B)*X$RADIOH)}
SAVEVALUE MIN, (X$MIN+1)
SAVEVALUE MIN2,(X$MIN %60)
SAVEVALUE HOR, (FLOOR(X$MIN/60))
SAVEVALUE A, (MODULO(X$MIN,60) *2*PI)/60
SAVEVALUE B, (MODULO(X$MIN,720) *2*PI)/720
TERMINATE_VE
ENDPROCEDURE
Aunque todo es representable en 2D, en ciertas simulaciones —especialmente las de naturaleza mecánica o con énfasis visual— puede ser deseable utilizar representaciones en 3D.
GPSS-Plus no pretende ser una herramienta de modelado 3D como tal; el enfoque está en facilitar simulaciones visualmente efectivas y fáciles de implementar. Por ello, el diseño 3D puede provenir de dos fuentes:
Ficheros externos .GLB
Primitivas geométricas integradas
| Tipo | Descripción |
|---|---|
BOX |
Caja tridimensional con WIDTH, HEIGHT, DEPTH |
SPHERE |
Esfera con RADIUS, SEGMENTS opcionales |
TRIANGLE |
Triángulo |
PLANE |
Plano formado por dos triángulos que comparten arista |
PRISM |
Prisma regular entre dos puntos, con SIDES lados |
OBJECT |
Objeto externo en formato .GLB |
| Propiedad | Aplicación | Descripción |
|---|---|---|
WIDTH, HEIGHT, DEPTH |
Permanente | Redimensiona la geometría base |
displace_x/y/z |
Permanente | Desplaza el modelo con respecto a su pivote interno |
rotate_x/y/z |
Permanente | Gira el modelo respecto a su pivote interno |
| Acción | Descripción |
|---|---|
MOVE_BETWEEN |
Posiciona el objeto entre dos puntos manteniendo su tamaño |
STRETCH_BETWEEN |
Escala el objeto en Y para ajustarse entre dos puntos |
rotate_y |
Añade rotación adicional sobre su eje Y, útil para tornillos o engranajes |
y |
En MOVE_BETWEEN, permite desplazar a lo largo de la línea |
Estas acciones son análogas a las uniones PRISMATIC y REVOLUTE en sistemas mecánicos, pero aplicadas directamente desde el conocimiento matemático de la posición del objeto.
Ejemplos
; Coloca un muelle descrito en un fichero entre dos puntos definidos
MOVE {name:muelle, STRETCH_BETWEEN:"[0,0,0],[0,P$x_masa,0]"}
; coloca un pistón o segmento telescópico a lo largo de un segmento en la posición "X" con respecto al su punto central
MOVE {name:amortiguador, MOVE_BETWEEN:"[0,0,0],[0,P$x_masa,0]",y:P$x_masa -10}
Activar el modo 3D
SYSTEM {TYPE:VISUAL, MODE:3D, V_WIDTH:70, V_HEIGHT:30, CAMERA:1}
V_WIDTH, V_HEIGHT: área que ocuparán los objetos en unidades internas
CAMERA: preset de cámara (1 = isométrico, 2 = cenital, etc.)
El ejemplo:
Para el ejemplo, se usan dos puntos para rotar, en ese mismo eje, una tuerca.
/*
Gráficos. 3D
*/
SYSTEM {TYPE:PRE_RUN, TRIGGER:PRE_RUN}
SYSTEM {TYPE:VISUAL, MODE:3D, V_WIDTH:40, V_HEIGHT:20, CAMERA:0}
SYSTEM {TYPE:OPTIONS, TIME_DECIMALS:1, SPEED:5}
GRAPHIC {NAME:suelo, TYPE:PLANE, X:25, POINTS:"[0,0,0],[0,0,40],[40,0,0]", color:#fabada}
GRAPHIC {NAME:nut, TYPE:OBJECT, src:NUT, X:0, Y:0, Z:0,WIDTH:20, HEIGHT:20, DEPTH:20,rotate_x:90,opacity:0.9}
Graphic {NAME:linea1,Type:LINE,X1:0,Y1:0,X2:0,Y2:500}
Graphic {NAME:linea2,Type:LINE,X1:0,Y1:0,X2:500,Y2:0}
Graphic {NAME:linea3,Type:LINE,X1:0,Y1:0,X2:0,Y2:0,Z2:500}
Graphic {NAME:linea4,Type:LINE,POINTS:"[10,0,10],[150,150,150]",color:red}
Graphic {NAME:Text1,Type:TEXT,X:20,Y:20,text:"sss",font:"2px"}
START 1
;===========================
; PROCEDIMIENTOS
;===========================
PROCEDURE agente.init
savevalue DT,0.1
savevalue velocidad,1
savevalue X,4
savevalue estado,0
WHILE (1==1)
if (X$estado==0)
savevalue X,X$X + X$DT * X$velocidad
else
savevalue X,X$X - X$DT * X$velocidad
endif
if (X$X>10)
savevalue estado,1
endif
if (X$X<4)
savevalue estado,0
endif
assign altura, round(X$X,3)
move {name:Text1,text:"Altura: P$altura"}
; La tuerca se coloca y rota sobre su eje
MOVE {name:nut, MOVE_BETWEEN:"[10,0,10],[150,150,150]", rotate_y:P$altura * 60,y:P$altura}
ADVANCE 1, 0
ENDWHILE
STOP
ENDPROCEDURE
;===========================
PROCEDURE PRE_RUN
TIMEOUT agente.init, 0
TERMINATE_VE
ENDPROCEDURE
En GPSS-Plus, una entidad virtual (VE) suele ser efímera: nace, ejecuta una serie de bloques, y termina. Sin embargo, algunos modelos requieren procesos permanentes que puedan actuar como controladores, gestores, o procesos pasivos que esperan órdenes. A estas VEs especiales las llamamos agentes.
Un agente es simplemente una entidad virtual que nunca termina su ejecución. Para ello, se inicializa con el bloque TIMEOUT (fuera de los bloques GENERATE), lo que hace que comience sin depender de ningún flujo de llegada.
Una vez creada, el agente puede comportarse de dos formas principales:
Activa: ejecuta ciclos continuamente (por ejemplo, usando ADVANCE) para revisar y tomar decisiones. Como un TIMER que es siempre la misma entidad.
Pasiva: entra en espera con HOLD y ahí permanecerá mientras no se le ordene hacer otra cosa.
En el PRE_RUN llamamos a su PROCEDURE inicializador:
timeout agente1.main,0 ; Se ejecutará en el instante 0.
Que por convenio se llamará ".main".
Este PROCEDURE se encargará de almacenar en un SAVEVALUE su identificador para que cualquier entidad pueda acceder a ella y tras esto entrará en el bucle infinito con espera activa o pasiva:
PROCEDURE agente1.main
SAVEVALUE nAgente1,D$N
WHILE (1==1)
ADVANCE 100
... ; Acciones periódicas
ENDWHILE
TERMINATE_VE ; No se alcanzará nunca
ENDPROCEDURE
;----------------------------------------
PROCEDURE agente2.main
SAVEVALUE nAgente2,D$N
HOLD HOLDER_AGENTES
TERMINATE_VE ; No se alcanzará nunca
ENDPROCEDURE
Y sus métodos o PROCEDURES asociados siguen la convención agente.metodo, por ejemplo:
procedure agente2.liberacion_total
MOVE {NAME:INFO2, TEXT:"T= AC1$ Agente 2 liberando todo"}
UNHOLD HOLDER1
UNHOLD HOLDER2
RETURN_RESTORE ; Vuelve exactamente a su estado anterior
ENDPROCEDURE
Donde lo más importante a destacar es su finalización especial: RETURN_RESTORE
Este bloque devulverá al agente a la misma situación en la que se encontraba al ser interrumpida mediante el SIGNAL / SIGNALNOW.
Si estaba en un ADVANCE, se reanuda con el tiempo restante ajustado.
Si estaba en una cola de un recurso, no se alterará su situación.
Nótese que si se utilizase RETURN o ENDPROCEDURE, saltaría a la siguiente línea de código. Útil si se está en una espera activa y se quiere reiniciar el tiempo de actuación periódica.
En el ejemplo:
Vemos a dos agentes de ambos tipos liberando entidades de dos HOLDERS de formas arbitrarias.
El segundo, cada 20 entidades que completan el recorrido, libera todas las atrapadas.
/*
Entidad virtual persistente. El agente
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
START 100
;===> Configuración de recursos y posiciones
Restroom {NAME:Restroom1, X:337, Y:344}
Restroom {NAME:Restroom2, X:337, Y:258}
Restroom {NAME:Restroom_AGENTE2, X:567, Y:104}
POSITION {NAME:ENTRADA, X:179, Y:301}
POSITION {NAME:SALIDA, X:559, Y:299}
GRAPHIC {NAME:INFO1, TYPE:TEXT, X:335, Y:178, TEXT:"Agente1"}
GRAPHIC {NAME:INFO2, TYPE:TEXT, X:569, Y:62, TEXT:"Agente2"}
INITIAL contador,0
;*****************************************************
;===> Inicialización del agente
PROCEDURE PRE_RUN
timeout agente1.main,0
timeout agente2.main,0
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
;===> Flujo principal de las entidades
GENERATE 15,5 {NAME:GEN1, X:50, Y:300}
ADVANCE 10 {TO:ENTRADA}
ASSIGN aleatorio, FLOOR(RANDOM * 2) + 1
IF (P$aleatorio==1)
MOD {color:#990000}
ADVANCE 10 {TO:Restroom1}
REST Restroom1
ELSE
MOD {color:#999900}
ADVANCE 10 {TO:Restroom2}
REST Restroom2
ENDIF
ADVANCE 30,30 {TO:SALIDA}
if (D$N %20 == 0)
SIGNAL agente2.liberacion_total,X$nAgente2
endif
ENDGENERATE 1
;*******************************
procedure agente2.main
; Asigna su nombre único como variable si hace falta
SAVEVALUE nAgente2,D$N
MOVE {NAME:INFO2, TEXT:"Agente 1 activo"}
WHILE (1==1) ; inútil si siempre se usa RETURN_RESTORE
REST Restroom_AGENTE2
ENDWHILE
TERMINATE_VE ; No se alcanzará nunca
ENDPROCEDURE
;*******************************
procedure agente2.liberacion_total
MOVE {NAME:INFO2, TEXT:"T= AC1$ Agente 2 liberando todo"}
WAKE Restroom1
WAKE Restroom2
RETURN_RESTORE
ENDPROCEDURE
;**********************************
PROCEDURE agente1.main
; Asigna su nombre único como variable si hace falta
SAVEVALUE nAgente1,D$N
MOVE {NAME:INFO1, TEXT:"Agente 2 activo"}
WHILE (1==1)
ADVANCE 50 ; El agente revisa cada 50 unidades
SAVEVALUE contador, X$contador + 1
MOVE {NAME:INFO1, TEXT:"Agente: X$contador"}
IF (X$contador % 3==0)
assign liberar,R$(Restroom1,IN)
MOVE {NAME:INFO1, TEXT:"Liberando Restroom 1 (P$liberar DE R$(Restroom1,IN))"}
WAKE Restroom1
ENDIF
IF (X$contador % 5 ==0)
assign liberar,round(R$(Restroom2,IN) * 2 / 3)
MOVE {NAME:INFO1, TEXT:"Liberando Restroom 2 (P$liberar DE R$(Restroom2,IN))"}
WAKE Restroom2,P$liberar
ENDIF
ENDWHILE
TERMINATE_VE ; No se alcanzará nunca
ENDPROCEDURE
;**********************************
En este capítulo exploramos cómo un agente puede actuar como un controlador visual autónomo, que reacciona a los cambios de su entorno y produce efectos animados. Veremos un ejemplo típico: una puerta automática que se abre cuando detecta entidades cerca y se cierra cuando no hay nadie.
Idea base
El agente es una VE que no termina nunca y que ejecuta repetidamente una tarea: comprobar si hay entidades en una zona determinada (simulada con un QUEUER) y mover la puerta en consecuencia.
Este tipo de lógica es ideal para comportamientos animados, sensores, semáforos o cualquier objeto visual que actúe en base al entorno.
/*
Agente como controlador
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Queuer {NAME:sensor,X:403,Y:575}
POSITION {NAME:POS1,X:265,Y:454}
POSITION {NAME:POS2,X:523,Y:449}
POSITION {NAME:POS3,X:720,Y:450}
Graphic {NAME:Line1,Type:LINE,color:#FF0000, X1:418,Y1:370,X2:413,Y2:383,X3:387,Y3:383,X4:381,Y4:370}
Graphic {NAME:Line2,Type:LINE,color:#FF0000, X1:422,Y1:509,X2:414,Y2:486,X3:387,Y3:486,X4:382,Y4:508}
Graphic {NAME:Text1,Type:TEXT,X:431,Y:262,Text:" "}
initial nAgente,0
START 200
;*****************************************************
PROCEDURE PRE_RUN
timeout agente.main,0
TERMINATE_VE
ENDPROCEDURE 1
;*****************************************************
GENERATE 30,50 {NAME:GEN1,X:86,Y:450}
ADVANCE 20,0 {TO:POS1}
queue sensor
ADVANCE 20,0 {TO:POS2}
depart sensor
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
;*******************************
procedure agente.main
savevalue nAgente,D$N
assign usuarios,0
assign estado,100
assign posY2,GD$(Line1,Y2)
while (1==1)
advance 1
call agente.moverPuerta
endwhile
terminate_ve
endprocedure
procedure agente.moverPuerta
IF (R$(sensor,IN)<=0)
assign estado,P$estado + 5
assign estado,MIN(100,P$estado)
else
assign estado,P$estado -5
assign estado,MAX(10,P$estado)
ENDIF
move {name:Line1,Y2:P$estado + P$posY2,Y3:P$estado + P$posY2}
;move {name:Text1,TEXT:"p$usuarios P$estado | P$posY1"}
endprocedure
En este capítulo vamos a desgranar el funcionamiento conjunto de la cola de eventos y la pila de contexto (stack) dentro de GPSS-Plus, y cómo esto afecta a los comportamientos asíncronos con SIGNAL y síncronos con SIGNALNOW.
Cuando una entidad ejecuta un bloque ADVANCE, pasa a la cola de eventos, que se ordena cronológicamente. En caso de coincidencia de tiempo, la entidad que entra primero tendrá prioridad (FIFO).
Esta cola contiene solo tres datos por elemento:
Cada entidad dispone de un stack privado, una pila de contexto que permite reanudar su ejecución tras un salto (CALL, FOREACH, etc.).
Este stack es LIFO: el último contexto en entrar es el primero en salir.
En llamadas SIGNAL o SIGNALNOW a otras entidades (agentes), se guarda en esta pila el estado de ejecución actual, para que pueda retomarse tras la llamada.
SIGNAL?SIGNAL es asíncrono. La entidad que lo emite no se detiene: simplemente agenda la ejecución de un procedimiento en otro agente/entidad.
Si enviamos tres SIGNAL seguidos al mismo agente:
SIGNAL agente.suma2,X$nAgente SIGNAL agente.multiplica10,X$nAgente SIGNAL agente.suma4,X$nAgente
la entidad emisora seguirá su camino, pero el agente receptor quedará planificado tres veces en la cola de eventos.
Cuando el agente tome el control, usará su stack para ejecutar los procedimientos pendientes. Pero al ser una pila LIFO, ejecutará en orden inverso al deseado:
(((0 + 4) * 10) + 2) = 42
SIGNALNOW?SIGNALNOW es síncrono. Detiene temporalmente la entidad que lo invoca, ejecuta inmediatamente al agente y luego la entidad continúa.
SIGNALNOW agente.suma2,X$nAgente SIGNALNOW agente.multiplica10,X$nAgente SIGNALNOW agente.suma4,X$nAgente
Aquí, las llamadas se ejecutan en el orden escrito, porque se intercalan correctamente:
(((0 + 2) * 10) + 4) = 24
Este modelo define un agente con tres métodos (suma2, multiplica10, suma4). Dos entidades lo invocan: una usando SIGNAL y otra con SIGNALNOW. En pantalla se puede ver la diferencia en el resultado.
En el caso de SIGNAL, se observa que inicialmente el resultado sigue siendo 0 puesto que los SIGNAL no se han ejecutado. Basta un ADVANCE 0 (la entidad se vuelve a programar en la cola de eventos) para que los agentes se ejecuten y tras ello, el resultado está disponible.
/*
SIGNAL Vs SIGNALNOW
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Restroom {NAME:RestroomAgentes,X:252,Y:60,visible:1}
POSITION {NAME:POS1,X:552,Y:194}
POSITION {NAME:POS2,X:552,Y:394}
Graphic {NAME:Text1,Type:TEXT,X:313,Y:252,Text:"Entidad"}
Graphic {NAME:Text2,Type:TEXT,X:313,Y:232,Text:"Entidad"}
Graphic {NAME:Text3,Type:TEXT,X:326,Y:444,Text:"Entidad"}
Graphic {NAME:TextAgente,Type:TEXT,X:311,Y:325,Text:"---",color:#ff3333}
initial nAgente,0
START 2
;*****************************************************
PROCEDURE PRE_RUN
timeout agente.main,0
TERMINATE_VE
ENDPROCEDURE 1
;*****************************************************
GENERATE 10,0,0,1 {NAME:GEN1,X:100,Y:200}
assign resultado,0,X$nAgente
SIGNAL agente.suma2,X$nAgente
SIGNAL agente.multiplica10,X$nAgente
SIGNAL agente.suma4,X$nAgente
move {name:Text1,TEXT:"SIGNAL Previo: P$(resultado,X$nAgente)"}
advance 0
move {name:Text2,TEXT:"SIGNAL Resultado: P$(resultado,X$nAgente)"}
ADVANCE 20,0 {TO:POS1}
ENDGENERATE 1
GENERATE 60,0,0,1 {NAME:GEN2,X:100,Y:400}
assign resultado,0,X$nAgente
SIGNALNOW agente.suma2,X$nAgente
SIGNALNOW agente.multiplica10,X$nAgente
SIGNALNOW agente.suma4,X$nAgente
move {name:Text3,TEXT:"SIGNALNOW Resultado: P$(resultado,X$nAgente)"}
ADVANCE 20,60 {TO:POS2}
ENDGENERATE 1
;*******************************
procedure agente.main
savevalue nAgente,D$N
assign resultado,0
REST RestroomAgentes
terminate_ve
endprocedure
procedure agente.suma2
assign resultado,P$resultado + 2
move {name:TextAgente,TEXT:"ResultadoSUMA2: P$resultado T= AC1$"}
RETURN_RESTORE
endprocedure
procedure agente.suma4
assign resultado,P$resultado + 4
move {name:TextAgente,TEXT:"ResultadoSUMA4: P$resultado T= AC1$"}
RETURN_RESTORE
endprocedure
procedure agente.multiplica10
assign resultado,P$resultado * 10
move {name:TextAgente,TEXT:"Resultadomultiplica10: P$resultado T= AC1$"}
RETURN_RESTORE
endprocedure
Vamos a ver un código con una narrativa compleja pero cuya realización resulta natural en GPSS-Plus gracias a sus primitivas para agentes, listas de tareas y sincronización por eventos.
El ejemplo simula un restaurante simplificado: los clientes llegan, ocupan una mesa, piden una comanda y esperan a ser servidos. Los cocineros (agentes) despiertan si estaban dormidos, toman una comanda de una lista compartida, y generan entidades "plato" para cada tarea. Cuando todos los platos de un cliente están listos, este puede empezar a comer.
Toda la coordinación se resuelve con una lista FIFO de tareas (SAVEVALUE.push/pop) y un RESTROOM común que regula el sueño y despertar de los agentes.
No se necesita SIGNAL, ni ON_QUEUE, ni ON_RELEASE y, sin embargo, todo fluye perfectamente.
Este modelo muestra cómo los agentes pueden autogestionar su agenda sin interrupciones externas, simplemente leyendo una cola de trabajo compartida.
Aquí, el RESTROOM funciona como una cerradura de sueño: el primero que se despierte atiende, los demás esperan.
/*
Colas de mensajes
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
START 1000
Facility {NAME:mesas,X:221,Y:60,capacity:7}
Restroom {NAME:esperaServir,X:323,Y:60}
Restroom {NAME:agentesDormidos,X:348,Y:357}
POSITION {NAME:COMIENDO, X:579, Y:46}
GRAPHIC {NAME:INFOA_1, TYPE:TEXT, X:149, Y:296, TEXT:"Agente1 listo"}
GRAPHIC {NAME:INFOB_1, TYPE:TEXT, X:147, Y:237, TEXT:"infoB1"}
GRAPHIC {NAME:INFOC_1, TYPE:TEXT, X:147, Y:268, TEXT:"infoC1"}
GRAPHIC {NAME:INFOA_2, TYPE:TEXT, X:577, Y:292, TEXT:"Agente2 listo"}
GRAPHIC {NAME:INFOB_2, TYPE:TEXT, X:576, Y:238, TEXT:"infoB2"}
GRAPHIC {NAME:INFOC_2, TYPE:TEXT, X:576, Y:266, TEXT:"infoC2"}
Graphic {NAME:TextComanda,Type:TEXT,X:270,Y:132,Text:"Comanda"}
Graphic {NAME:TextA,Type:TEXT,X:271,Y:102,Text:"Sin servir"}
Graphic {NAME:TextB,Type:TEXT,X:469,Y:84,Text:"Comiendo"}
;--------------------------------
; CLIENTES
;--------------------------------
GENERATE 8,5,0,8 {NAME:Clientes, X:53, Y:61}
ADVANCE 3 {TO:mesas,FLOW:1}
seize mesas
call anadir_comanda
rest esperaServir
ADVANCE 120,30 {FROM:esperaServir,TO:COMIENDO,FLOW:1} ; comiendo
release mesas
if (R$(mesas,IN)<=0)
stop
endif
ENDGENERATE 1
;**********************************
procedure anadir_comanda
ASSIGN cHorno, FLOOR(RANDOM * 4)
ASSIGN cFogon, FLOOR(RANDOM * 6)
ASSIGN cFreidora, FLOOR(RANDOM * 3)
ASSIGN nPlatosPendientes, P$cHorno + P$cFogon + P$cFreidora
savevalue.push comandas, [1,D$N,P$cHorno,P$cFogon,P$cFreidora] ; Añade comanda
MOVE {name:TextComanda,text:"Horno: P$cHorno ; Fogon: P$cFogon ; Freidora: P$cFreidora"}
; avisamos a cocina despertando a todos, el primero que llegue atenderá la comanda
; el segundo volverá a dormirse
wake agentesDormidos
ENDPROCEDURE
;--------------------------------
; COCINEROS / AGENTES
;--------------------------------
PROCEDURE PRE_RUN
timeout agente.main,0,1 ; agente 1
timeout agente.main,0,2 ; agente 2
TERMINATE_VE
ENDPROCEDURE
procedure agente.main
; Asigna su nombre único como variable
if (P$PARAM_A==1)
savevalue nAgente1,D$N
assign nAgente,1
else
savevalue nAgente2,D$N
assign nAgente,2
endif
MOVE {NAME:"INFOA_P$nAgente", TEXT:"Agente activo P$PARAM_A"}
savevalue comandas, []
assign tareaEnCurso,0
WHILE (1==1)
if (VD$(comandas,LENGTH)<=0)
; Si no hay tareas, duerme (espera que un cliente lo despierte)
MOVE {NAME:"INFOA_P$nAgente", TEXT:"T: AC1$ Cocinero P$nAgente : DURMIENDO...", color:red}
rest agentesDormidos
endif
assign nTareas,VD$(comandas,LENGTH)
MOVE {NAME:"INFOA_P$nAgente", TEXT:"Cocinero: tengo P$nTareas tareas, Despierto", color:green}
savevalue.pop comandas,comanda
assign tareaEnCurso,P$(comanda.0)
assign nEntidad,P$(comanda.1)
assign cHorno,P$(comanda.2)
assign cFogon,P$(comanda.3)
assign cFreidora,P$(comanda.4)
if (P$tareaEnCurso > 0)
MOVE {NAME:"INFOB_P$nAgente", TEXT:"Tengo P$nTareas tareas, ejecuto la Cliente P$nEntidad"}
MOVE {NAME:"INFOC_P$nAgente", TEXT:"Horno: P$cHorno ; Fogon: P$cFogon ; Freidora: P$cFreidora"}
assign contador,0
while (P$contador Configuración de recursos y posiciones
Facility {NAME:Horno,X:348,Y:566,capacity:2}
Facility {NAME:Fogon,X:347,Y:499,capacity:4}
Facility {NAME:Freidora,X:347,Y:437,capacity:3}
Graphic {NAME:TextPlatos1,Type:TEXT,X:160,Y:579,Text:"Platos..."}
Graphic {NAME:TextPlatos2,Type:TEXT,X:533,Y:578,Text:"Platos2..."}
POSITION {NAME:FINCOCINA, X:649, Y:509}
GENERATE 0,0,0,0,0 {NAME:platos, X:69, Y:508}
move {name:TextPlatos1,text:"Realizando plato para P$PARAM_A tipo P$PARAM_B"}
assign entidadDestino,P$PARAM_A
assign tipoComanda,"P$PARAM_B"
switch "P$tipoComanda"
case ==,"Horno"
ADVANCE 4 {TO:Horno,FLOW:1,DECISION:COCINA}
seize Horno
ADVANCE 40,5
release Horno
endcase
case ==,"Fogon"
ADVANCE 4 {TO:Fogon,FLOW:1,DECISION:COCINA}
seize Fogon
ADVANCE 30,5
release Fogon
endcase
case ==,"Freidora"
ADVANCE 4 {TO:Freidora,FLOW:1,DECISION:COCINA}
seize Freidora
ADVANCE 22,3
release Freidora
endcase
endswitch
ADVANCE 5 {TO:FINCOCINA,FLOW:1,MERGE:COCINA}
move {name:TextPlatos2,text:"Terminado plato para P$entidadDestino tipo P$tipoComanda"}
assign nPlatosPendientes, P$(nPlatosPendientes,P$entidadDestino) - 1,P$entidadDestino
; si ya no hay más plazos que cocinar, empiezan a comer
if (P$(nPlatosPendientes,P$entidadDestino)<=0)
wake esperaServir,0,P$entidadDestino
endif
ENDGENERATE 0
En este capítulo exploramos cómo un agente puede encargarse de monitorizar el paso de entidades por una zona del modelo y calcular métricas globales como el tiempo total de uso o la media de permanencia.
El agente no actúa directamente sobre las entidades, sino que escucha eventos generados automáticamente por un QUEUER (a través de sus ON_QUEUE y ON_DEPART), y en base a esos eventos, registra cuándo entran y salen las entidades.
Para ello, el agente mantiene una estructura de datos (array asociativo) donde guarda los tiempos de entrada, y luego calcula las diferencias al salir. De esta manera, puede mantener un seguimiento completo sin interferir en el flujo lógico del resto del modelo.
Este tipo de lógica es útil para implementar estadísticas personalizadas, auditoría de uso, alertas o cualquier control que dependa de eventos externos y del historial de entidades.
/*
Gestión de entidades
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Restroom {NAME:RestroomAgentes,X:100,Y:121,visible:1}
queuer {NAME:Queuer1,X:321,Y:123,on_queue:Queuer1on_queue,on_depart:Queuer1on_depart}
POSITION {NAME:POS1,X:251,Y:335}
POSITION {NAME:POS2,X:469,Y:341}
Graphic {NAME:Text1,Type:TEXT,X:425,Y:514,Text:"Entidad"}
Graphic {NAME:TextAgente1,Type:TEXT,X:433,Y:462,Text:"---"}
Graphic {NAME:TextAgente2,Type:TEXT,X:433,Y:416,Text:"---"}
initial nAgente,0
START 500
;*****************************************************
PROCEDURE PRE_RUN
timeout agente.main,0
TERMINATE_VE
ENDPROCEDURE 1
;*****************************************************
GENERATE 10,0 {NAME:GEN1,X:43,Y:300}
move {name:Text1,TEXT:"Entidad Resultado P$(resultado,X$nAgente) AC1$"}
ADVANCE 20,0 {TO:POS1}
queue Queuer1
ADVANCE 20,60 {TO:POS2}
depart Queuer1
ENDGENERATE 1
;*******************************
procedure agente.main
savevalue nAgente,D$N
assign datos,{}
assign resultado,0
assign media,0
assign nEntidades,0
assign tiempoTotal,0
while (1==1)
REST RestroomAgentes
endwhile
terminate_ve
endprocedure
;**********************************
procedure agente.IN
assign datos.entidad_P$PARAM_A,P$PARAM_B
move {name:TextAgente1,TEXT:"IN Entidad P$PARAM_A : Tiempo Inicio: P$(datos.entidad_P$PARAM_A)"}
RETURN_RESTORE
endprocedure
;**********************************
procedure agente.OUT
assign nEntidades,P$nEntidades + 1
assign tiempoInicio,P$(datos.entidad_P$PARAM_A)
assign tiempoFin,P$PARAM_B
assign tiempoTotal,P$tiempoTotal + P$tiempoFin - P$tiempoInicio
assign media,P$tiempoTotal / P$nEntidades
move {name:TextAgente2,TEXT:"OUT Entidad: P$PARAM_A Tiempo Total: P$tiempoTotal Media: P$media"}
ASSIGN.DELETE datos,entidad_P$PARAM_A ; eliminamos la entrada de la lista
RETURN_RESTORE
endprocedure
procedure Queuer1on_queue
SIGNAL agente.IN,X$nAgente,P$ENTITYNUMBER,AC1$
TERMINATE_VE
endprocedure
procedure Queuer1on_depart
SIGNAL agente.OUT,X$nAgente,P$ENTITYNUMBER,AC1$
TERMINATE_VE
endprocedure
En este capítulo presentamos un agente que actúa como gestor inteligente de un recurso compartido: un semáforo.
El objetivo es permitir el paso de entidades desde distintos orígenes hacia distintos destinos, pero solo uno a la vez, de forma controlada. Se simula así un cruce en el que el semáforo cambia de dirección cada cierto tiempo, desbloqueando una de las cuatro posibles trayectorias.
Estructura general:
Un INITIAL define la tabla tramos, que contiene la información de cada dirección (nombres de posiciones, holders, colores, etc.).
Las entidades que circulan pasan por un HOLDER si su dirección no está activa, y continúan si está permitida.
El agente agenteSemaforo.main se encarga de:
Apagar el semáforo actual (red)
Elegir aleatoriamente el siguiente (green)
Actualizar el texto y los colores
Hacer UNHOLD a las entidades en espera para ese tramo
Aprendizaje clave:
El agente actúa como planificador visual y operativo, y permite entender cómo un loop de trabajo constante puede gobernar múltiples entidades, simplemente ajustando cuándo se permite continuar y cuándo no.
Este patrón es útil para gestionar accesos a zonas exclusivas, turnos de paso, recursos únicos o flujos alternantes.
/*
Gestor de recursos
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
INITIAL tramos, [
{ tiempo: 10, color: "#009900", nInicio: "InicioN", nFin: "FinN", nCentro: "CentroN", nRestroom: "RestroomN", nSemaforo: "SemaforoN", nTexto: "TextN" },
{ tiempo: 20, color: "#FF0099", nInicio: "InicioS", nFin: "FinS", nCentro: "CentroS", nRestroom: "RestroomS", nSemaforo: "SemaforoS", nTexto: "TextS" },
{ tiempo: 30, color: "#000099", nInicio: "InicioE", nFin: "FinE", nCentro: "CentroE", nRestroom: "RestroomE", nSemaforo: "SemaforoE", nTexto: "TextE" },
{ tiempo: 40, color: "#0099FF", nInicio: "InicioO", nFin: "FinO", nCentro: "CentroO", nRestroom: "RestroomO", nSemaforo: "SemaforoO", nTexto: "TextO" }
]
POSITION {NAME:InicioN,X:368,Y:541}
POSITION {NAME:InicioS,X:393,Y:30}
POSITION {NAME:InicioE,X:756,Y:270}
POSITION {NAME:InicioO,X:41,Y:269}
POSITION {NAME:FinN,X:398,Y:540}
POSITION {NAME:FinS,X:416,Y:31}
POSITION {NAME:FinE,X:754,Y:298}
POSITION {NAME:FinO,X:40,Y:301}
POSITION {NAME:CentroN,X:381,Y:347}
POSITION {NAME:CentroS,X:393,Y:249}
POSITION {NAME:CentroE,X:447,Y:294}
POSITION {NAME:CentroO,X:325,Y:290}
Graphic {NAME:SemaforoN,TYPE:ARC,X:413,Y:346,FCOLOR:"red",RADIUS:16,START_ANGLE:0,END_ANGLE:360,CLOSE:1}
Graphic {NAME:SemaforoS,TYPE:ARC,X:363,Y:246,FCOLOR:"red",RADIUS:16,START_ANGLE:0,END_ANGLE:360,CLOSE:1}
Graphic {NAME:SemaforoE,TYPE:ARC,X:453,Y:263,FCOLOR:"red",RADIUS:16,START_ANGLE:0,END_ANGLE:360,CLOSE:1}
Graphic {NAME:SemaforoO,TYPE:ARC,X:326,Y:320,FCOLOR:red,RADIUS:16,START_ANGLE:0,END_ANGLE:360,CLOSE:1}
Restroom {NAME:RestroomN,X:713,Y:570,on_rest:actualiza}
Restroom {NAME:RestroomS,X:713,Y:473,on_rest:actualiza}
Restroom {NAME:RestroomE,X:713,Y:521,on_rest:actualiza}
Restroom {NAME:RestroomO,X:713,Y:422,on_rest:actualiza}
Graphic {NAME:TextN,Type:TEXT,X:412,Y:347,Text:"N",font:"18px",color:#ffffff}
Graphic {NAME:TextS,Type:TEXT,X:363,Y:247,Text:"S",font:"18px",color:#ffffff}
Graphic {NAME:TextE,Type:TEXT,X:453,Y:263,Text:"E",font:"18px",color:#ffffff}
Graphic {NAME:TextO,Type:TEXT,X:326,Y:320,Text:"O",font:"18px",color:#ffffff}
Graphic {NAME:textSem,Type:TEXT,X:165,Y:381,Text:"Semaforo",font:"18px",color:#000000}
START 3000
;----------------------------- PROCEDURE PRINCIPAL DE MOVIMIENTO
procedure toSemaforo ; P$(PARAM_A) = origen, P$PARAM_B = destino
ADVANCE X$(tramos.P$(PARAM_A).tiempo),10 {from:X$(tramos.P$(PARAM_A).nInicio), to:X$(tramos.P$PARAM_A.nCentro)}
if (X$semaforoAbierto!=P$PARAM_A)
rest X$(tramos.P$(PARAM_A).nRestroom)
endif
ADVANCE 10,3 {to:X$(tramos.P$PARAM_B.nCentro)}
ADVANCE X$(tramos.P$(PARAM_B).tiempo),10 {to:X$(tramos.P$(PARAM_B).nFin)}
endprocedure
;----------------------------- AGENTE SEMÁFORO
procedure agenteSemaforo.main
savevalue semaforoAbierto,0
while (1==1)
savevalue semaforoAbiertoAnterior, X$semaforoAbierto
move {name:X$(tramos.X$(semaforoAbierto).nSemaforo),FCOLOR:yellow}
savevalue semaforoAbierto,-1
advance 50
call calcularDestino,X$semaforoAbiertoAnterior
move {name:X$(tramos.X$(semaforoAbiertoAnterior).nSemaforo),FCOLOR:red}
savevalue semaforoAbierto,P$calcularDestino
move {name:textSem,text:"Semaforo Abierto: X$semaforoAbierto"}
move {name:X$(tramos.X$(semaforoAbierto).nSemaforo),FCOLOR:green}
wake X$(tramos.X$(semaforoAbierto).nRestroom)
advance 200
endwhile
endprocedure
;----------------------------- PROCEDURE PARA CAMBIAR DESTINO
procedure calcularDestino ; P$PARAM_A origen
assign tmp, (P$PARAM_A + 1 + floor(random*3)) % 4
endprocedure P$tmp
;----------------------------- ACTUALIZACIÓN DE VALORES EN PANTALLA
procedure actualiza
move {name:X$(tramos.0.nTexto),text:"R$(X$(tramos.0.nRestroom),IN)"}
move {name:X$(tramos.1.nTexto),text:"R$(X$(tramos.1.nRestroom),IN)"}
move {name:X$(tramos.2.nTexto),text:"R$(X$(tramos.2.nRestroom),IN)"}
move {name:X$(tramos.3.nTexto),text:"R$(X$(tramos.3.nRestroom),IN)"}
terminate
endprocedure
;----------------------------- GENERADOR ÚNICO
GENERATE 10,5 {NAME:GEN,X:577,Y:575,ERADIO:10}
assign origen,floor(random*4)
mod {color:X$(tramos.P$origen.color)}
call calcularDestino,P$origen
call toSemaforo,P$origen,P$calcularDestino
ENDGENERATE 1
;----------------------------- ARRANQUE DEL AGENTE
PROCEDURE PRE_RUN
TIMEOUT agenteSemaforo.main, 0
TERMINATE_VE
ENDPROCEDURE
En un motor de simulación por eventos discreto, una entidad suele ser un único hilo de ejecución: un flujo secuencial que avanza bloqueándose y reanudándose según el modelo.
Este enfoque funciona, pero no coincide con el comportamiento real de muchas situaciones.
Una persona que espera en la cola del autobús no solo espera:
respira, consulta el móvil, piensa, se distrae y puede cambiar de opinión.
Un vehículo que se desplaza también actúa en paralelo:
consume energía, se desgasta, recibe señales, ajusta su ruta.
Modelar todo esto con un único flujo lineal es una simplificación excesiva.
Por lo tanto, vamos a ampliar la forma en la que podemos ver una entidad para que pueda tener varias funciones en paralelo.
Una entidad componente es una entidad virtual que ejecuta una función en nombre de otra entidad principal.
Su existencia depende de la entidad principal, pero tiene su propio flujo de ejecución.
Esto permite que una entidad principal tenga varios comportamientos activos simultáneamente, cada uno representado por una entidad componente independiente.
Características clave:
La entidad componente se crea cuando nace la entidad principal.
Ejecuta su lógica en paralelo, sin bloquear el flujo principal.
Puede leer y modificar atributos de la entidad principal.
Puede despertarla, cambiar su estado o provocar decisiones.
Finaliza automáticamente cuando la entidad principal deja de existir.
No es un “hijo” en sentido POO, ni un objeto, ni una propiedad.
Es un comportamiento vivo, modelado como entidad.
La cola de eventos sigue siendo estrictamente secuencial, pero ahora la entidad deja de ser monolítica: ya no representa un único flujo sino un pequeño sistema compuesto por varias entidades cooperando y modelando más profundamente a la entidad principal.
Esto permite modelar fenómenos naturales de simultaneidad sin recurrir a hilos, semáforos ni programación externa.
El funcionamiento es sencillo y uniforme:
La entidad principal nace.
Inmediatamente se crea una entidad componente indicandole el número de la entidad principal.
La entidad componente ejecuta su lógica en bucle WHILE:
D$(EXIST,P$idEntidadPrincipal) == 1
Puede influir en la entidad principal asignándole valores, modificando su estado o despertándola.
Cuando la entidad principal desaparece, el componente termina automáticamente.
Así se definen comportamientos paralelos naturales, como respiración, desgaste, decisiones internas, sensores activos o distracciones.
En el ejemplo vemos a personas que llegan a una parada de autobús muy simplificado. Cuando llegan a 6, simplemente se van y mientras se encuentran en REST fuera de la cola de eventos.
Pero las entidades también están leyendo el móvil y algunos pueden decidir dejar la cola e irse con urgencia.
Cuando la entidad componente decide que hay que abandonar la cola, ejecuta lo necesario mientras la principal está aun dormida en el RESTROOM.
/*
Componente
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
;SYSTEM {TYPE:OPTIONS,Speed:8}
Restroom {NAME:Restroom_autobus,X:333,Y:282}
POSITION {NAME:SALIDA_URGENTE,X:335,Y:119}
POSITION {NAME:SALIDA,X:615,Y:286,type:terminate,title:end}
Graphic {NAME:textAgente,Type:TEXT,X:494,Y:336,Text:"Grupo"}
Graphic {NAME:Text2,Type:TEXT,X:331,Y:76,Text:"Urgencia"}
START 500
;*****************************************************
PROCEDURE PRE_RUN
timeout verifica_cola,1
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 10,0,0,0 {NAME:GEN1,X:61,Y:288}
ASSIGN salidaUrgente,0
timeout componente_mirar_movil,0,D$N
ADVANCE 20,0 {TO:Restroom_autobus}
REST Restroom_autobus
if (P$salidaUrgente == 1)
ADVANCE 20,10 {TO:SALIDA_URGENTE}
else
ADVANCE 20,10 {TO:SALIDA}
endif
ENDGENERATE 1
;*******************************
procedure verifica_cola
while (1==1)
advance 5
if (R$(Restroom_autobus,OCCUPIED) > 5)
move {name:textAgente,text:"Liberando [AC1: AC1$]"}
WAKE Restroom_autobus ; todos salen
endif
endwhile
endprocedure
;**********************************
procedure componente_mirar_movil
assign miEntidad,P$PARAM_A
while (D$(EXIST,P$miEntidad)==1)
advance 5
if (R$(Restroom_autobus,IS_OCCUPIED_BY,P$miEntidad) == 1 && RANDOM < 0.02)
move {name:Text2,text:"Liberando la entidad P$miEntidad [AC1: AC1$]"}
assign salidaUrgente,1,P$miEntidad
mod {number:P$miEntidad, color:red}
WAKE Restroom_autobus,-1,P$miEntidad ; sale solo esta entidad
endif
endwhile
terminate_ve
endprocedure
;**********************************
Antes de comenzar a ver los contextos debemos tener en cuenta cierta característica de GPSS-Plus.
En general, los motores basados en Drag&drop los recursos se arrastran fácilmente sobre el canvas y ya están listos para usar. En GPSS-Plus hemos visto que es diferente, hay que escribir el código del COMANDO.
Por contra, esa característica de los Drag&drop hace muy complicada la creación en tiempo de ejecución de un recurso. No estamos ahí para arrastrarlo al canvas. En GPSS-Plus basta con cambiar el COMANDO FACILITY por el BLOQUE NEWFACILITY.
Solo se exige que esté creado antes de ser usado y para eso se suele hacer con el PRE_RUN.
/*
Contextos y módulos. Creación dinámica 1
*/
SYSTEM {TYPE:OPTIONS, SPEED:5}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Position {NAME:pos_exit,X:581,Y:407}
; --- Facility definido de forma clásica ---
FACILITY {NAME:resource_static, CAPACITY:1, X:496, Y:164}
START 200
PROCEDURE PRE_RUN
; Crear un segundo facility dinámico en el arranque
NEWFACILITY {NAME:resource_dynamic, CAPACITY:1, X:300, Y:400}
TERMINATE_VE
ENDPROCEDURE
;------------------------------------------------------
; --- Flujo de entidades ---
GENERATE 10,0 {NAME:GEN1} ; cada 10 ticks una entidad
advance 15 {to:resource_static}
SEIZE resource_static
ADVANCE 15
RELEASE resource_static
advance 15 {to:resource_dynamic}
SEIZE resource_dynamic
ADVANCE 17
RELEASE resource_dynamic
advance 15 {to:pos_exit}
TERMINATE 1
Ahora que ya sabemos que se pueden crear en runtime, veamos su utilidad... la más evidente, la de crear multitud con un iterador.
Solo debemos tener en cuenta qué nombre les hemos puesto para poder usarlos.
En GPSS‑Plus, los nombres de recursos son strings reales, por lo que pueden construirse dinámicamente y usarse directamente en SEIZE, RELEASE, R$(), etc.
SYSTEM {TYPE:OPTIONS, SPEED:5}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Position {NAME:POS1,X:147,Y:311}
Position {NAME:POS2,X:542,Y:327}
Position {NAME:POS3,X:756,Y:322}
Graphic {NAME:Text1,Type:TEXT,X:305,Y:571,Text:"Hello"}
; --- Facility definido de forma clásica ---
FACILITY {NAME:f_static, CAPACITY:4, X:657, Y:325,color:blue}
START 200
PROCEDURE PRE_RUN
; Crear un segundo facility dinámico en el arranque
assign t,0
while (P$t < 5)
assign t,P$t+1
assign pos_y,(P$t * 80 + 20)
assign nombre,"f_dynamic_P$t"
NEWFACILITY {NAME:P$nombre, CAPACITY:1, X:300, Y:(P$pos_y)}
NEWPOSITION {NAME:pos_P$nombre, X:200, Y:(P$pos_y)}
endwhile
TERMINATE_VE
ENDPROCEDURE
;------------------------------------------------------
; --- Flujo de entidades ---
GENERATE 6,0 {NAME:GEN1,x:58,y:310}
advance 15,0 {to:POS1,flow:1}
ASSIGN MINKEY,0
ASSIGN MINVAL,100000
assign t,0
while (P$t < 5)
assign t,P$t+1
assign nombre,"f_dynamic_P$t"
if (R$(P$nombre,QUEUE) < P$MINVAL)
ASSIGN MINKEY,P$t
ASSIGN MINVAL,R$(P$nombre,QUEUE)
endif
endwhile
assign siguiente_recurso,"f_dynamic_P$MINKEY"
move {name:Text1,text:"Consulta entidad D$N: P$(siguiente_recurso) Cola: R$(P$siguiente_recurso,QUEUE)"}
advance 15,0 {to:"pos_P$siguiente_recurso",flow:1,DECISION:"inicio"}
advance 15,0 {to:"P$siguiente_recurso",flow:1}
SEIZE P$siguiente_recurso
ADVANCE 25,15
RELEASE P$siguiente_recurso
advance 15 {to:POS2,flow:1,MERGE:"salida"}
advance 15 {to:f_static,flow:1}
SEIZE f_static
ADVANCE 7,8
RELEASE f_static
advance 15 {to:POS3,flow:1}
TERMINATE 1
Y por último, también se pueden crear si surge la necesidad.
Solo hay que tener en cuenta que un recurso no se puede modificar, si se crea, solo puede bloquearse con el bloque LOCK.
/*
Contextos y módulos. Creación dinámica 3
*/
SYSTEM {TYPE:OPTIONS, SPEED:5}
Position {NAME:pos_exit,X:581,Y:407}
; --- Facility definido de forma clásica ---
FACILITY {NAME:resource_static, CAPACITY:1, X:496, Y:164}
START 200
;------------------------------------------------------
; --- Flujo de entidades ---
GENERATE 10,0 {NAME:GEN1}
advance 15 {to:resource_static}
SEIZE resource_static
ADVANCE 15
RELEASE resource_static
if (R$(resource_dynamic,EXIST)==0)
NEWFACILITY {NAME:resource_dynamic, CAPACITY:1, X:300, Y:400}
endif
advance 15 {to:resource_dynamic}
SEIZE resource_dynamic
ADVANCE 17
RELEASE resource_dynamic
advance 15 {to:pos_exit}
TERMINATE 1
Seguro que a estas alturas te habrás dado cuenta de lo que puede suponer hacer una lógica que simule un taller completo.
Y nos podemos volver locos si nos dicen que son dos talleres. Y luego 10.
Parece que ahí deberíamos abandonar, pero no, para eso están los contextos de GPSS-Plus.
No son clases porque en este entorno de eventos discretos no tiene sentido como tal, pero veremos que se les parecen y mucho. Es más un espacio de nombres y aquí todo es mucho más simple.
Imaginemos un caso algo más trivial, un gráfico de un nivel de un depósito.
Ponemos las líneas, los colores y el texto y después queremos usarlo.
Ahí empiezan los problemas, que ese objeto, clase, dibujo, elemento o el nombre que quieras darle tiene tan incrustados los parámetros que resulta abrumador trabajar con él.
Y no solo eso, sino que si tenemos dos depósitos, el trabajo no es solo el doble sino que hay que ir con mucho cuidado de no pisar la variables y crear tantas LINE como sean necesarias.
Este es el problema que vamos a resolver.
Por analogía se parecerá a decir que es a través de clases con métodos, variables locales e instanciaciones, pero no es así. Se resolverá con una simple variable tipo string.
Hasta ahora, para mover un gráfico podríamos hacer:
CALL grafico_set,75
Y si nuestro procedure estaba correcto, redibujaría cierto gráfico para mostrar ese 75% de llenado.
Gráficos que previamente debíamos haber creado.
Ahora vamos a hacerlo diferente y de tal forma que todo lo que vamos a ver se podrá llevar a otro fichero que no nos moleste más y llamarlo con un INCLUDE.
Si nuestro objeto es un depósito, crearemos un procedure llamado:
PROCEDURE deposito.init
Este procedure va a ser invocado, normalmente, por una EV desde el PRE_RUN / CALL y se va a encargar de crear todo el gráfico en lugar de con COMANDOS GRAPHIC, con bloques NEWGRAPHIC.
Como todas las llamadas, podrán tener sus parámetros para ser ejecutado, por ejemplo:
call deposito.init, 20, 130,120, 100,"Carga","#ff0000"
...donde deposito.init es el procedure y el resto son: ancho, alto, X, Y, nombre y color. También se puede hacer más cómodamente definiendo previamente un objeto con todas las características.
Y con esto podemos empezar a pintar nuestro depósito en base a esos parámetros:
NEWGRAPHIC { NAME:TextoNivel, GROUP:Deposito, TYPE:TEXT, X:0, Y:0, TEXT:"Nivel: 0% ", COLOR:#000000 }
Y así todo lo que queramos añadir a nuestro gráfico.
Después, solo tenemos que añadir algún método para gestionarlo:
PROCEDURE deposito.move
MOVE {NAME:Deposito, x:P$PARAM_A, y:P$PARAM_B}
ENDPROCEDURE
Con esto tendremos un gráfico... ¡pero solo uno!
Ese es el problema.
La solución viene de algo que hemos dicho referente al punto de separación en el nombre del procedure.
Para llamar a este procedure init o locate podemos llamarlo de tantas formas como instanciaciones queramos:
CALL aaa.deposito.init, 20, 130,120, 100,"Carga","#ff0000" CALL bbb.deposito.init, 20, 80,420, 100,"Nivel","#ffff00" CALL ccc.deposito.init, 20, 180,320, 100,"Nivel","#ff00ff" ... CALL aaa.deposito.locate,100,200 CALL bbb.deposito.locate,200,100 CALL ccc.deposito.locate,300,200
Lo que hacemos es ponerle otro punto de separación al nombre de la instancia de ese objeto.
CX$Llamar a este procedure "aaa.deposito.locate" significa que:
El nombre real del procedure es "deposito.locate"
El contexto (CX$) es el string "aaa"
Y hay que tenerlo muy claro y muy presente.
Cuando entre una entidad en un PROCEDURE cuya llamada esté hecha con puntos de separación, se ejecutará la actividad bajo ese valor de CX$.
Y con esto es suficiente para no pisar variables ni recursos entre unas instancias y otras.
Por ejemplo:
SAVEVALUE CX$_altura , 100
...realmente habrá creado:
SAVEVALUE aaa_altura , 100
O:
NEWGRAPHIC {NAME:CX$_Deposito, TYPE:GROUP, X:0, Y:0}
...será realmente:
NEWGRAPHIC {NAME:aaa_Deposito, TYPE:GROUP, X:0, Y:0}
Por lo tanto, se habrán generado tantos SAVEVALUE, ASSIGN,NEWGRAPHIC, NEWRESTROOM como formas de llamar a los init de cada clase.
Así que solo tendremos que renombrar nuestros ASSIGNs internos y SAVEVALUEs y NAMEs.... añadiéndoles delante "CX$_"
Y cuando queramos acceder a esos mismos parámetros deberemos hacerlo usando paréntesis para seguir evitando un anidamiento indeseado.
Por ejemplo:
savevalue CX$_ALTO, 100
...se recupera con el SNA:
X$(CX$_ALTO)
Todo este material lo introducimos en un fichero separado y lo llamamos desde un include.
La separación con puntos en un CALL del tipo aaa.bbb.ccc se interpreta así:
- El contexto (CX$) es "aaa"
- El nombre real del procedure ejecutado es bbb.ccc
- CX$ es un SNA clave que representa el contexto de llamada actual y puede usarse para construir nombres dinámicos.
- Las variables (ASSIGN o SAVEVALUE) se pisan si no están dentro del contexto. Usa siempre:
ASSIGN CX$_nombre, valor
...para evitar colisiones entre instancias.
- Si el método necesita devolver un valor (como un "get"), puedes hacer:
CALL aaa.bbb.get
...y luego obtener el retorno con:
P$(get)
- Todos los SAVEVALUE internos deberían usar CX$_ para ser privados a cada instancia.
Ejemplo:
SAVEVALUE CX$_estado, 1
IMPORTANTE: Las EV creadas con SIGNAL o TIMEOUT mueren al finalizar el procedure, y sus ASSIGN desaparecen.
Solo CALL usa una entidad real que mantiene sus variables.
- Por coherencia y encapsulamiento, todo lo que se use desde fuera de una "clase" debe hacerse a través de sus métodos:
CALL instancia.objeto.metodo,...
...y no accediendo directamente a los nombres internos de gráficos, variables o recursos.
Existen dos ficheros, el principal y una librería llamada "./library_graphics/tank.lib".
Puedes ver el código abriéndolo en el menú "OPEN".
Lo más interesante es que se configura la librería desde el programa principal desde PRE_RUN:
assign config,{title:"deposito"
,x:100,y:100
,width:50 ,height:180
,value:88
,"color":"#ff0000"}
call aaa.tank.init,V$config
Y se usa sin más desde el programa con CALL:
CALL aaa.tank.set, 10
CALL aaa.tank.get
MOVE {NAME:Text1, TEXT:"Valor actual aaa: P$(get)"}
/*
Contextos y módulos. Librerías gráficas
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Graphic {NAME:Text1,Type:TEXT,X:100,Y:319,Text:"Valor actual"}
Graphic {NAME:Text2,Type:TEXT,X:300,Y:312,Text:"Valor actual"}
START 100
include ./library_graphics/tank.lib
;-----------------------------------------------------------
; Instanciamos dos depósitos con parámetros distintos
; Cada uno se genera en su propio contexto: aaa y bbb
; Los procedimientos serán los mismos, pero los datos y gráficos serán independientes
PROCEDURE PRE_RUN
assign config,{title:"tank"
,x:100,y:100
,width:50 ,height:180
,value:88
,max_value:100
,"color":"#ff0000"}
call aaa.tank.init,V$config
assign config2,{title:"tank"
,x:300,y:100
,width:50 ,height:180
,value:88
,max_value:100
,"color":"#ff00ff"}
call bbb.tank.init,V$config2
TERMINATE_VE
ENDPROCEDURE
;============================================================
GENERATE 25,0 {NAME:Gen1,x:100,y:400}
CALL aaa.tank.set, 25
CALL bbb.tank.set, 35
ADVANCE 5
CALL aaa.tank.set, 52
CALL bbb.tank.setcolor, "blue"
ADVANCE 5
CALL aaa.tank.get
MOVE {NAME:Text1, TEXT:"Valor actual aaa: P$(get)"}
CALL bbb.tank.get
MOVE {NAME:Text2, TEXT:"Valor actual bbb: P$(get)"}
CALL aaa.tank.set, 75
CALL bbb.tank.set, 10
ADVANCE 5
CALL aaa.tank.set, 90
ADVANCE 5
CALL aaa.tank.set, 10
ADVANCE 5
ENDGENERATE 1
Ya hemos visto los contextos CX$ que resumiéndolos mucho son un assign especial de tipo string que sirve para que no coincidan los nombres de las variables y que se hereda de invocante a invocado. Es decir, si hacemos TIMEOUT, la EV que ejecute el PROCEDURE lo hará con el mismo valor de CX$ que el que lo llamó.
También se puede establecer con el BLOQUE CX:
CX "sevilla"
Y a partir de ese punto, el SNA CX$ tendrá ese valor.
Ahora que sabemos crear elementos gráficos con NEWGRAPHIC pasando el contexto, vamos a ver cómo crear un módulo completo con el mismo sistema.
Un módulo es conjunto de GENERATEs, NEWFACILITYs, PROCEDUREs... que funcionarían por sí mismos, normalmente bajo un contexto y que suelen contener al menos un GENERATE. Todo esto en un único fichero al que llamaremos con la extensión ".mod".
Lo más particular de estos módulos es que se usan con un INCLUDE y los parámetros afectarán a lo que esté en zona de bloques, pero no a lo que sea un GENERATE ya que es el BLOQUE/COMANDO que no es exactamente un BLOQUE sino que también es un punto de programa que crea entidades y donde es seguro donde enviar una entidad con SCAPE o UNLOAD.
Así que no tiene sentido hacer algo como:
GENERATE {name: CX$_gen,x:100,y:200}
porque una entidad virtual no va a pasar por ella para crear el GENERATE ni mucho menos podrá realizar un NEWGENERATE.
Este sí es el aspecto de un GENERATE en modulo:
GENERATE 0,0,0,0 {name:hub,visible:0,x:700,y:100}
advance 10,30 {from: hub , to:"CX$_posicion"}
ENDGENERATE
;-------------------------------------------
PROCEDURE hub.init
;... creación de recursos
ENDPROCEDURE
Al ejecutarse el INCLUDE, lo único que sucede es que este fichero pasa a formar parte del código general. Se concatenan los textos de código GPSS-Plus sin más.
Así que el código pasará a tener un nuevo GENERATE llamado "hub" que podrá ser el punto de aterrizaje de cualquier entidad que llegue con SCAPE o con UNLOAD. Lo importante es que aterricen esas entidades con el CX$ correcto.
Por esto, un módulo sin GENERATE no puede recibir entidades desde fuera, por lo que no puede funcionar como subsistema autónomo. En este ejemplo, "hub" es la referencia al punto de programa.
En suma, lo que tendremos en un módulo será, normalmente, uno o varios GENERATEs que procesarán entidades provinientes de otros contextos o módulos o del principal con acceso a recursos bajo ese mismo contexto.
En el ejemplo veremos un sistema básico.
Del GENERATE principal parten las entidades para ir a parar a un conjuntos de recursos que en bloque son un módulo llamado "plantaReciclaje".
Este módulo tiene, además de un ".init" similar al de creación de cualquier librería con NewGraphic, NewFacility... tiene el GENERATE para que lleguen a él las entidades a través de SCAPEen el principal:
scape plantaReciclaje {cx:"plantaA"}
Y hará que cada entidad llegue al generate "plantaReciclaje" con su stack de direcciones vacío pero todos los assign intactos y el valor de contexto establecido.
generate 0,0,0,0 {name:plantaReciclaje,visible:0}
call procesar
terminate 1
endgenerate
procedure procesar
advance 10,10 {to:CX$_pos_in}
advance 10,10 {to:CX$_fac1}
seize CX$_fac1
advance 10,10
release CX$_fac1
advance 10,10 {to:CX$_pos_out}
endprocedure
En cierto modo es algo como la clásica instrucción "GOTO" pero en el que solo se permite el salto a un punto de programa seguro que es el GENERATE.
/*
Contextos y módulos. Módulo simple
*/
SYSTEM {TYPE:OPTIONS, SPEED:5}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
Position {NAME:POS1,X:147,Y:311}
START 200
include "./manual_es/Season_07_Contextos_y_modulos/modulo.mod"
PROCEDURE PRE_RUN
assign config,{title:"Planta_A"
,x:350,y:500
}
CALL plantaA.plantaReciclaje.init,V$config
assign config,{title:"Planta B"
,x:350,y:100
}
CALL plantaB.plantaReciclaje.init,V$config
TERMINATE_VE
ENDPROCEDURE
;------------------------------------------------------
; --- Flujo de entidades ---
GENERATE 6,0 {NAME:GEN1,x:58,y:310}
advance 15,0 {to:POS1}
if (RANDOM > 0.5)
scape plantaReciclaje {cx:"plantaA"}
else
scape plantaReciclaje {cx:"plantaB"}
endif
TERMINATE 1
Vamos a ver un ejemplo de un sistema de control logístico.
Cada módulo se carga en un INCLUDE que se especializa en cada cosa.
Un JSON con los datos de varias ciudades será el punto de partida para crear los conjuntos de recursos de manera análoga a la que hemos visto.
Los paquetes se generan en los "punto_distribucion" almacenándose en un RESTROOM particular de ese punto.
Una furgoneta recorre los puntos de distribución cargando en su BACKPACK los paquetes provinientes del RESTROOM. Los BACKPACK y los RESTROOM funcionan entre ellos. No es posible poner en una mochila las entidades de ningún otro recurso.
La furgoneta descarga todas la entidades en el punto seguro GENERATE "hub_distribucion" que se encargará de clasificar los paquetes en diferentes RESTROOMs. Tras esto, la furgoneta cargará cada saca ya clasificada hará de nuevo el recorrido entregando los paquetes en su destino.
Al final, es un pequeño ejemplo de 150 líneas de código para hacer algo aparentemente complejo.
SYSTEM {TYPE:OPTIONS, SPEED:5}
SYSTEM {TYPE:VISUAL, WIDTH:900,HEIGHT:600}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
START 1
include ./logistica/namespace.mod
include ./logistica/punto_distribucion.mod
include ./logistica/hub_distribucion.mod
include ./logistica/transportes.mod
PROCEDURE PRE_RUN
CALL crear_puntos_de_distribucion
CALL hub_distribucion.init
TERMINATE_VE
ENDPROCEDURE
;------------------------------------------------------
procedure crear_puntos_de_distribucion
FOREACH ciudad, IN_OBJECT, V$(aCiudades)
assign datosCiudad, V$(aCiudades.P$ciudad)
assign config,{
title:"P$(ciudad)"
,x:P$(datosCiudad.x)
,y:P$(datosCiudad.y)
}
call P$ciudad.punto_distribucion.init,V$config
ENDFOREACH
endprocedure
Un concepto avanzado y complejo cuando el sistema crece es la rehidratación.
Rehidratar significa cargar el estado del sistema no desde el inicio, sino desde un punto avanzado en el tiempo, reconstruyendo la simulación tal y como estaba en ese instante.
En el ejemplo logístico esto es fundamental: no es lo mismo “empezar a simular paquetes” que cargar el estado real de un ERP y continuar la simulación desde ahí.
De esta forma, el modelador se hace responsable de guardar todo aquello que quiera poder restaurar.
Este proceso implica resolver tres problemas profundos:
En esta fase, más que modelar, estamos programando. La rehidratación es un problema técnico, no declarativo.
En el ejemplo:
Así que supongamos que lo que tenemos almacenado es que 4 camiones daban vueltas en un circuito de 4 posiciones. Los datos almacenados por última vez son:
Para reconstruir (rehidratar) este estado necesitamos resolver los tres problemas anteriores.
1. Avanzar el tiempo global
El bloque FORWARD_AC1 permite mover el reloj simulado hacia adelante.
Lo habitual es colocarlo como primera instrucción del PRE_RUN, de modo que el modelo arranque directamente en el instante deseado.
2. Reprogramar los tiempos de las entidades
Una vez que AC1 ha avanzado, todas las entidades deben tener tiempos posteriores al nuevo AC1.
Esto se consigue con UPDATE, que reprograma el final de un ADVANCE. Y en este caso, añadiremos también el tiempo de inicio para que la visualización comience allí donde se quedó.
La única condición es crítica:
UPDATE debe ejecutarse desde una VE cuando la entidad ya esté dentro de un ADVANCE.
Por eso la rehidratación requiere una pequeña coreografía temporal. Primero la introducimos en la cola correspondiente y después actualizamos los tiempos a través de un TIMEOUTinmediato.
3. Reconstruir el punto del programa
Este es el problema más complejo.
Si existiera un GOTO, sería trivial: saltaríamos al punto exacto donde estaba cada entidad.
Pero no existe, y no debe existir.
La solución correcta es:
Crear una máquina de estados finitos (FSM) local para cada entidad.
El estado de la FSM representa en qué punto del programa estaba la entidad cuando se guardó el sistema.
La lógica del modelo se reescribe para que cada entidad avance según su estado.
Esto convierte el flujo del programa en un autómata explícito, perfectamente restaurable.
4. El detalle clave: toda entidad termina siempre en una cola
En un motor DES, el “estado último” de una entidad siempre es uno de estos:
ADVANCE.SEIZE.Por tanto:
Los estados de la FSM deben corresponderse con estas colas.
No necesitamos reconstruir colas internas ni manipular estructuras ocultas:
basta con reinsertar las entidades en el punto correcto del programa, y el motor reconstruye el resto automáticamente.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
POSITION {NAME:Pos1,X:368,Y:429}
POSITION {NAME:Pos2,X:634,Y:452}
POSITION {NAME:Pos3,X:592,Y:164}
POSITION {NAME:Pos4,X:343,Y:217}
initial fsm_camion_logic, {
STATES: [
"F1", "F2", "F3", "F4"
],
TRANSITIONS: [
{FROM:"F1", INPUT:"tick", TO:"F2"},
{FROM:"F2", INPUT:"tick", TO:"F3"},
{FROM:"F3", INPUT:"tick", TO:"F4"},
{FROM:"F4", INPUT:"tick", TO:"F1"}
],
INITIAL: "F1"
}
FSM {NAME:FSM_CAMION, LOCAL:1, LOGIC:V$(fsm_camion_logic)}
FACILITY {NAME:Fac1,X:633,Y:385}
Graphic {NAME:Text1,Type:TEXT,X:324,Y:100}
START 100
;-------------------------------
PROCEDURE PRE_RUN
FORWARD_AC1 105
assign params,{status:"F2",tiempoIni:90,tiempo:110,matricula:"1111"}
new GEN1,0,V$params
assign params,{status:"F3",tiempoIni:88,tiempo:108,matricula:"2222"}
new GEN1,0,V$params
assign params,{status:"F2",tiempoIni:89,tiempo:109,matricula:"3333"}
new GEN1,0,V$params
assign params,{status:"F2",tiempoIni:104,tiempo:124,matricula:"4444"}
new GEN1,0,V$params
TERMINATE_VE
ENDPROCEDURE
;-------------------------------
PROCEDURE camion_update
; PARAM_A -> Numero entidad
; PARAM_B -> tiempo final
; PARAM_C -> tiempo inicial
move {name:Text1,text:"camion_update P$(FSM_CAMION,P$PARAM_A) P$PARAM_C"}
if (D$(IN_ADVANCE,P$PARAM_A)==1)
update P$PARAM_A, P$PARAM_B, P$PARAM_C
endif
TERMINATE_VE
endprocedure 1
;-------------------------------
GENERATE 0,0,0,0 {NAME:GEN1,X:62,Y:396}
assign params,V$PARAM_A
assign FSM_CAMION,"P$(params.status)"
mod {subtitle:"P$(params.matricula)"}
; debe ser una VE y que se ejecute una vez hayan arrancado los advance.
timeout camion_update,0,D$N,P$(params.tiempo),P$(params.tiempoIni)
; move {name:Text1,text:"Camión inicializado P$PARAM_C R$(FSM_CAMION,STATE) P$FSM_CAMION"}
while (1==1)
STATE FSM_CAMION,"tick"
switch "R$(FSM_CAMION,STATE)"
case ==,"F1"
advance 20 {from:Pos4, to:Pos1}
endcase
case ==,"F2"
advance 20 {from:Pos1, to:Pos2}
endcase
case ==,"F3"
seize Fac1
advance 40 {from:Pos2, to:Pos3}
release Fac1
endcase
case ==,"F4"
advance 20 {from:Pos3, to:Pos4}
endcase
endswitch
endwhile
endgenerate 1
;-------------------------------
La simulación y la estadística van de la mano. Poco podremos hacer si no tenemos claros algunos conceptos básicos. En GPSS-Plus, los cálculos estadísticos se dividen en tres grandes grupos:
Tiempos que una entidad permanece en un recurso (orientado a la entidad).
Tiempos que un número de entidades está ocupando un recurso (orientado al recurso).
Cualquier otro dato personalizado que quieras medir (orientado al modelo).
En este primer ejemplo, trabajamos con el caso 3, donde queremos medir el tiempo que una entidad permanece en un tramo del circuito, incluyendo un recurso (una FACILITY) y su entorno.
GPSS-Plus automatiza gran parte del trabajo. Solo tienes que crear una TABLE con los parámetros adecuados:
TABLE {name: TABLA1, E_BIN_START:0, E_BIN_SIZE:1, E_BIN_COUNT:100, EXPRESSION:(AC1$ - P$TIEMPOINICIO)}
Esto configura una tabla de 100 celdas de 1 unidad de ancho, que empieza en 0. Cada vez que ejecutemos TABULATE TABLA1, se ejecutará EXPRESSION :
(AC1$ - P$TIEMPOINICIO)
donde calculará la diferencia entre el tiempo actual (AC1$) y un instante anterior (P$TIEMPOINICIO) guardado previamente.
Este valor, que representa la permanencia de la entidad en el conjunto de recursos o zona del modelo, se acumulará en la celda correspondiente.
Creamos entidades que avanzan DESDE UN PUNTO pasando por una FACILITY Y HASTA OTRO PUNTO. después tabulamos la duración:
GENERATE 8,3
ADVANCE 30,0 {TO:POS1}
ASSIGN TIEMPOINICIO,AC1$ ; Inicio del cómputo
ADVANCE 20,0 {TO:VENTANILLA1} ; + 20
SEIZE VENTANILLA1
ADVANCE 20,20 ; +20 a +40 (aleatorio) + Tiempo en cola
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS2} ; + 20
TABULATE TABLA1 ; Fin del cómputo
ADVANCE 20,0 {TO:POS3}
ENDGENERATE
Si observas los resultados en el informe (botón superior derecho), verás una distribución rectangular (uniforme) entre 60 y 80, con algunos tiempos superiores de las entidades que han permanecido en la cola. Es decir, que todas las entidades han permanecido un tiempo dentro de ese rango, como se esperaba.
Este es un buen punto de partida para empezar a analizar comportamientos internos del modelo con datos objetivos.
/*
Acumulando datos en tablas
*/
FACILITY {NAME:VENTANILLA1,X:380,Y:348,capacity:4}
POSITION {NAME:POS1,X:218,Y:437}
POSITION {NAME:POS2,X:591,Y:429}
POSITION {NAME:POS3,X:713,Y:329}
Graphic {NAME:Line1,Type:LINE,color:#FF0000, X1:218,Y1:500,X2:592,Y2:500}
Graphic {NAME:Text1,Type:TEXT,X:410,Y:527,Text:"Estadistic section"}
TABLE {name: TABLA1,E_BIN_START:50,E_BIN_SIZE:1,E_BIN_COUNT:60,EXPRESSION:(AC1$ - P$TIEMPOINICIO)}
;TABLE {name: TABLA1,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:160,EXPRESSION:(AC1$ - D$M0)}
START 500
;***************************************************************
GENERATE 8,3 {NAME:GEN1,X:66,Y:350}
ADVANCE 40,0 {TO:POS1}
ASSIGN TIEMPOINICIO,AC1$
ADVANCE 20,0 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE 20,20
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS2}
TABULATE TABLA1
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
;***************************************************************
Ahora pasamos a las estadísticas centradas en el tiempo de las entidades en el recurso.
El las estadísticas calculadas sobre un único recurso se calculan automáticamente por GPSS-Plus:
Facility {NAME:ventanilla1,X:377,Y:362,capacity:10
,E_BIN_START:18,E_BIN_SIZE:1,E_BIN_COUNT:14
}
Medimos el tiempo que una entidad (E) que permanece en el recurso con los parámetros E_BIN_* que tiene el mismo significado que en el bloque TABLE.
En el ejemplo siguiente, se generan entidades a un ritmo constante, y la FACILITY contiene un ADVANCE 20,10 lo que significa que el resultado estadístico serán datos entre 20 y 30.
/*
Tiempo de entidades
*/
SYSTEM {TYPE:OPTIONS,Speed:7}
POSITION {NAME:POS1,X:620,Y:360}
Facility {NAME:ventanilla1,X:377,Y:362,capacity:10,E_BIN_START:18,E_BIN_SIZE:1,E_BIN_COUNT:14}
START 200
;*****************************************************
GENERATE 8,0,0,0 {NAME:GEN1,X:111,Y:366}
ADVANCE 10 {TO:ventanilla1}
seize ventanilla1
ADVANCE 20,10
release ventanilla1
ADVANCE 10 {TO:POS1}
TERMINATE 1
Ahora pasamos a las estadísticas centradas en el uso de un recurso, es decir, en cuánto tiempo ha estado ocupado por una o más entidades.
Si antes medíamos el tiempo que una entidad (E) permanece en el recurso con los parámetros E_BIN_*, ahora, para observar cómo de ocupado ha estado el recurso en cada instante, usamos R_BIN_*.
La tabla creada con R_BIN_* nos indica, para cada nivel de ocupación (0, 1, 2, ...), cuánto tiempo el recurso ha estado exactamente con esa cantidad de uso.
En el ejemplo siguiente, se generan entidades a un ritmo constante, y la FACILITY tiende a tener entre 4 y 6 unidades ocupadas, algo que se verá reflejado como un pico en ese rango en la gráfica generada automáticamente.
Por supuesto, nada impide que uses a la vez E_BIN_* y R_BIN_* sobre un mismo recurso, si deseas tener una visión desde ambos ángulos: el de la entidad y el del recurso.
/*
Uso del recurso
*/
SYSTEM {TYPE:OPTIONS,Speed:7}
POSITION {NAME:POS1,X:620,Y:360}
Facility {NAME:ventanilla1,X:377,Y:362,capacity:10,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:11}
START 200
;*****************************************************
GENERATE 5,3,0,0 {NAME:GEN1,X:111,Y:366}
ADVANCE 10 {TO:ventanilla1}
seize ventanilla1
ADVANCE 20,10
release ventanilla1
ADVANCE 10 {TO:POS1}
ENDGENERATE 1
Las colas también pueden ser objeto de análisis estadístico. En GPSS-Plus, funcionan exactamente igual que los recursos, pero se gestionan de forma separada mediante estructuras específicas llamadas QUEUER.
Para activar la recogida de estadísticas, basta con envolver el acceso al recurso con QUEUE y DEPART:
queue Qventanilla1 seize ventanilla1 depart Qventanilla1
Esto habilita el cálculo automático de:
El número de entidades en cola a lo largo del tiempo (R_BIN_*)
El tiempo de espera que cada entidad pasa en cola (E_BIN_*)
En el informe, podrás visualizar gráficamente cómo se comportó la cola: cuándo se formó, cuánto tiempo esperaron las entidades, y si el dimensionamiento del recurso fue suficiente.
El siguiente ejemplo muestra una FACILITY con colas frecuentes. Al aplicar QUEUE/DEPART, el sistema captura toda la estadística sin ningún código adicional.
/*
Las colas
*/
SYSTEM {TYPE:OPTIONS,Speed:7}
POSITION {NAME:POS1,X:620,Y:360}
QUEUER {NAME:Qventanilla1,X:374,Y:424
,R_BIN_START:0,R_BIN_SIZE:1,R_BIN_COUNT:20
,E_BIN_START:0,E_BIN_SIZE:1,E_BIN_COUNT:20
}
Facility {NAME:ventanilla1,X:373,Y:365,capacity:5}
START 200
;*****************************************************
GENERATE 5,3,0,0 {NAME:GEN1,X:111,Y:366}
ADVANCE 10 {TO:ventanilla1}
queue Qventanilla1
seize ventanilla1
depart Qventanilla1
ADVANCE 20,10
release ventanilla1
ADVANCE 10 {TO:POS1}
ENDGENERATE 1
Los STORAGE son un tipo especial de recurso. A diferencia de las FACILITY, que gestionan entidades de forma individual, un STORAGE gestiona cantidades variables que una entidad puede introducir o extraer.
Por ejemplo, una entidad puede representar un camión que deja 10 unidades en un depósito. Por tanto, el sistema no analiza cuántas entidades han pasado por el almacén, sino cuántas unidades se han almacenado y durante cuánto tiempo.
Se recogen dos tipos de estadísticas:
EQ_BIN_*: Tiempo que cada unidad permanece en el almacenamiento (tiempo de ocupación por unidad).
RQ_BIN_*: Nivel de ocupación del almacenamiento en cada momento (por cantidad).
Este comportamiento se activa igual que en otros recursos, pero usando los parámetros EQ_BIN_* y RQ_BIN_* para configurar los intervalos de las tablas.
El siguiente código muestra un almacenamiento (STORAGE) con capacidad de 14 unidades. Las entidades entrantes ocupan una cantidad aleatoria (entre 1 y 5 unidades). La estadística resultante indicará cuánto tiempo estuvieron almacenadas esas unidades y cómo varió el nivel de ocupación del almacén:
STORAGE {NAME:almacen1,X:377,Y:362,capacity:14,
EQ_BIN_START:18,EQ_BIN_SIZE:1,EQ_BIN_COUNT:14,
RQ_BIN_START:0, RQ_BIN_SIZE:1, RQ_BIN_COUNT:18}
GENERATE 8,0,0,0
ADVANCE 10 {TO:almacen1}
ENTER almacen1,(RANDOM*5)+1
ADVANCE 20,10
LEAVE almacen1
ADVANCE 10 {TO:pos1}
ENDGENERATE 1
En el informe, verás dos tablas diferentes:
Una con los tramos de tiempo por unidad almacenada (EQ), que te dirá si las unidades permanecieron poco o mucho tiempo en el almacén.
Otra con los niveles de ocupación del almacén (RQ), donde podrás ver cuántas unidades había simultáneamente en cada instante.
Este tipo de análisis es esencial en modelos de logística, inventario, o cualquier sistema con almacenamiento variable.
/*
Storages
*/
SYSTEM {TYPE:OPTIONS,Speed:7}
POSITION {NAME:POS1,X:620,Y:360}
STORAGE {NAME:almacen1,X:377,Y:362,capacity:14
,EQ_BIN_START:18,EQ_BIN_SIZE:1,EQ_BIN_COUNT:14
,RQ_BIN_START:0,RQ_BIN_SIZE:1,RQ_BIN_COUNT:18
}
START 200
;*****************************************************
GENERATE 8,0,0,0 {NAME:GEN1,X:111,Y:366}
ADVANCE 10 {TO:almacen1}
ENTER almacen1,(RANDOM*5)+1
ADVANCE 20,10
LEAVE almacen1
ADVANCE 10 {TO:POS1}
ENDGENERATE 1
En GPSS-Plus, las estadísticas se acumulan automáticamente si se definen las tablas con los parámetros adecuados. Existen varios tipos de estadísticas, clasificadas por lo que se mide:
E_BIN_*)Mide cuánto tiempo ha permanecido cada entidad en un recurso.
Aplicable a: Todos los recursos (FACILITY, STORAGE, QUEUE, etc.)
Ejemplo: Una entidad pasa 40 unidades de tiempo → se suma 1 al bin 40.
R_BIN_*)Mide cuánto tiempo un recurso ha tenido una cantidad concreta de entidades activas.
Aplicable a: Todos los recursos
Ejemplo: El recurso tiene 3 entidades durante 10 unidades → se suma 10 al bin 3.
EQ_BIN_*)Mide cuánto tiempo permanece cada unidad dentro de un STORAGE.
Aplicable a: Solo STORAGE
Ejemplo: Un camión con 5 unidades permanece 40 tiempos → se suma 5 al bin 40.
RQ_BIN_*)Mide cuánto tiempo un STORAGE contiene cierta cantidad total de unidades.
Aplicable a: Solo STORAGE
Ejemplo: 3 unidades durante 10 tiempos → se suma 30 al bin 3.
Colas (QUEUER)
Las colas tienen su propia tabla de estadísticas si se declara un QUEUER. Para recoger datos:
queue Cola1 seize Recurso1 depart Cola1 ADVANCE ... release Recurso1
Se peude aplicar:
E_BIN_*: Tiempo que cada entidad pasó en cola.
R_BIN_*: Tiempo que la cola tuvo X entidades esperando.
TABLE + TABULATEPara registrar datos arbitrarios:
TABLE {name:TABLA1, E_BIN_START:0, E_BIN_SIZE:1, E_BIN_COUNT:100, EXPRESSION:(AC1$ - P$TIEMPOINICIO)}
TABULATE TABLA1
Esto permite medir cualquier cosa definida por una expresión matemática.
Una función es un componente que produce un valor numérico a partir de una entrada. Ese valor puede representar tiempos, cantidades, costes, o cualquier otro parámetro variable que queramos incorporar al modelo.
En términos matemáticos, podríamos escribir algo como:
f(x) = 10 + (x * 5)
Si x toma valores entre 0 y 1, entonces f(x) tomará valores entre 10 y 15.
En GPSS-Plus, cuando usamos:
ADVANCE 10,5
estamos usando esa función implícita, se genera un valor aleatorio entre 0 y 1, lo multiplica y resuelve un valor entre 10 y 15.
En términos estadísticos, eso es una distribución uniforme entre 10 y 15, es decir: todos los valores dentro del rango tienen la misma probabilidad de ocurrir.
En muchos casos la realidad no es uniforme.
Si medimos cuánto tardan 1000 vehículos en recorrer 100 km. La mayoría tardará cerca de 1 hora. Algunos tardarán algo menos, otros algo más. Pero casi ninguno lo hará en 30 minutos o en 2 horas.
Eso no es una distribución uniforme. Es lo que se conoce como una distribución normal o gaussiana.
Por eso, GPSS-Plus permite definir funciones que modelen con mayor precisión comportamientos como estos. Existen dos métodos para hacerlo:
VALUESCon este método, definimos manualmente una función basada en una tabla acumulada de probabilidades.
Function {
NAME: funValues,
TYPE: "VALUES",
EXPRESSION: "RANDOM",
VALUES: "0.0001,0.0000/0.0002,0.0101/.../1.0000,1.0000/"
}
Esta tabla asocia claves (probabilidades acumuladas entre 0 y 1 EXPRESSION: "RANDOM") con valores resultantes. Si el valor aleatorio generado es 0.33, la función busca el intervalo que le corresponde (por ejemplo, entre 0.0293 y 0.2121) y devuelve el valor asociado que representa una distribución de tipo campana (gaussiana), donde los valores extremos tienen menos probabilidad que los valores centrales.
Entonces, una instrucción como:
ADVANCE 20 + (FN$funValues * 20)
Generará valores entre 20 y 40, concentrados en torno al 30.
GAUSSEste método permite definir directamente una distribución gaussiana mediante sus parámetros característicos:
Function {
NAME: funGauss,
TYPE: "GAUSS",
A: 1,
B: 30,
SIGMA1: 3.3,
SIGMA2: 3.3,
INTERVALS: 100
}
A: altura de la curva (no afecta al resultado, es visual)B: valor central (media)SIGMA1, SIGMA2: desviaciones estándar izquierda y derechaINTERVALS: cantidad de divisiones para la funciónCon esta función, simplemente usamos:
ADVANCE FN$funGauss
Ambas técnicas producirán una distribución de tiempos en torno al valor 30, pero con métodos diferentes: una tabla predefinida frente a una curva construida automáticamente.
Las FACILITY con parámetros como:
E_BIN_START:16, E_BIN_SIZE:1, E_BIN_COUNT:40
registran estadísticas automáticas sobre cuánto tiempo ha permanecido cada entidad en el recurso. Estas estadísticas se visualizan en el informe y permiten comprobar cómo se comporta realmente la función.
Veremos que, en ambos casos, la gráfica resultante es una distribución gaussiana. Esto permite validar que tanto la tabla manual como la función generada ofrecen comportamientos coherentes.

/*
Definiendo las funciones
*/
SYSTEM {TYPE:OPTIONS,Speed:5}
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:16,E_BIN_SIZE:1,E_BIN_COUNT:40,capacity:3}
Facility {NAME:VENTANILLA2,X:384,Y:186,E_BIN_start:16,E_BIN_SIZE:1,E_BIN_COUNT:40,capacity:3}
POSITION {NAME:POS1,X:278,Y:381}
POSITION {NAME:POS2,X:482,Y:380}
Function {name:funGauss, type:"GAUSS", a:1, b:30, sigma1:3.3, sigma2:3.3,intervals:100 }
Function {NAME:funValues,type:"VALUES",expression:"RANDOM",VALUES:"
0.0001,0.0000/0.0002,0.0101/0.0004,0.0202/0.0006,0.0303/0.0008,0.0404/
0.0011,0.0505/0.0015,0.0606/0.0020,0.0707/0.0025,0.0808/0.0032,0.0909/
0.0039,0.1010/0.0049,0.1111/0.0060,0.1212/0.0073,0.1313/0.0088,0.1414/
0.0106,0.1515/0.0128,0.1616/0.0152,0.1717/0.0180,0.1818/0.0213,0.1919/
0.0250,0.2020/0.0293,0.2121/0.0341,0.2222/0.0396,0.2323/0.0458,0.2424/
0.0527,0.2525/0.0603,0.2626/0.0689,0.2727/0.0783,0.2828/0.0887,0.2929/
0.1000,0.3030/0.1124,0.3131/0.1258,0.3232/0.1403,0.3333/0.1559,0.3434/
0.1726,0.3535/0.1904,0.3636/0.2093,0.3737/0.2292,0.3838/0.2501,0.3939/
0.2720,0.4040/0.2948,0.4141/0.3185,0.4242/0.3429,0.4343/0.3680,0.4444/
0.3937,0.4545/0.4199,0.4646/0.4464,0.4747/0.4731,0.4848/0.5000,0.4949/
0.5269,0.5051/0.5536,0.5152/0.5801,0.5253/0.6063,0.5354/0.6320,0.5455/
0.6571,0.5556/0.6815,0.5657/0.7052,0.5758/0.7280,0.5859/0.7499,0.5960/
0.7708,0.6061/0.7907,0.6162/0.8096,0.6263/0.8274,0.6364/0.8441,0.6465/
0.8597,0.6566/0.8742,0.6667/0.8876,0.6768/0.9000,0.6869/0.9113,0.6970/
0.9217,0.7071/0.9311,0.7172/0.9397,0.7273/0.9473,0.7374/0.9542,0.7475/
0.9604,0.7576/0.9659,0.7677/0.9707,0.7778/0.9750,0.7879/0.9787,0.7980/
0.9820,0.8081/0.9848,0.8182/0.9872,0.8283/0.9894,0.8384/0.9912,0.8485/
0.9927,0.8586/0.9940,0.8687/0.9951,0.8788/0.9961,0.8889/0.9968,0.8990/
0.9975,0.9091/0.9980,0.9192/0.9985,0.9293/0.9989,0.9394/0.9992,0.9495/
0.9994,0.9596/0.9996,0.9697/0.9998,0.9798/0.9999,0.9899/1.0000,1.0000/
"}
START 500
;*****************************************************
GENERATE 10,0,0,0 {NAME:GEN1,X:91,Y:383}
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE 20 + (FN$funValues * 20) ; definición clásica
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$funGauss ; definición por parámetros
;ADVANCE 2 ; definición por parámetros
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
ENDGENERATE 1
El comando FUNCTION permite definir funciones que devuelven valores numéricos, ya sea mediante cálculos directos o a través de tablas de distribución precalculadas. Estas funciones se utilizan para simular comportamientos variables y realistas en operaciones como ADVANCE, ASSIGN, TABULATE, etc., y representan tiempos, cantidades, costes, probabilidades y más.
En GPSS-Plus, las funciones pueden clasificarse en dos grandes grupos:
Estas funciones generan previamente una tabla interna con un número configurable de puntos (intervalos), sobre la que interpolan el resultado. Son ideales para representar comportamientos aleatorios con patrones conocidos. Los tipos disponibles son:
GAUSS: Distribución normal (campana de Gauss).
EXP: Distribución exponencial decreciente.
UNIFORM: Distribución uniforme (rectangular).
TRIANGULAR: Distribución triangular.
LOGNORMAL: Distribución log-normal, asimétrica hacia la derecha.
FDISTRIBUTION: Permite definir una función de distribución personalizada mediante una fórmula matemática.
Estas funciones evalúan el resultado directamente, sin generar ni consultar una tabla. Son más flexibles cuando se desea trabajar con fórmulas o con modelos de conteo de eventos discretos. Incluyen:
POISSON: Genera un valor entero aleatorio según una distribución de Poisson (número de eventos por intervalo).
MATH: Evalúa directamente una expresión matemática. Admite múltiples parámetros que se sustituyen por letras (A, B, C, etc.) en orden.
Function {name:funGauss, type:"GAUSS", a:1, b:30, sigma1:3.3, sigma2:3.3,intervals:100 }
Function {name:funExp, type:"EXP", b:0, lambda:0.15, intervals:100}
Function {name:funUni, type:"UNIFORM", min:20, max:40, intervals:100}
Function {name:funTri, type:"TRIANGULAR", min:20, max:40, mode:30, intervals:100}
Function {name:funLog, type:"LOGNORMAL", mu:3.4, sigma:0.3, intervals:100}
Function {name:funLog2, type:"LOGNORMAL", b:30, spread:5, intervals:100}
Function {name:funCus, type:"FDISTRIBUTION", function:"A * Math.sin(B * x)", a:10, b:0.5, min:0, max:10, intervals:100}
Function {name:funPoi, type:"POISSON", lambda:3.5}
Function {Name:math1, type: math, EXPRESSION:"A * 2 + B"}
La distribución gaussiana (o normal) representa una curva en forma de campana, donde los valores más probables están cerca de un valor central y los extremos tienen menor probabilidad. Es ideal para modelar fenómenos naturales como tiempos de atención, errores de medición o duración de procesos, donde la mayoría de los casos se agrupan alrededor de una media.
Parámetros:
Por construcción, la distribución tomará valores aproximadamente entre b - sigma1 * 3 y b + sigma2 * 3.
Este ejemplo muestra cómo tres ventanillas con funciones gaussianas diferentes producen distintas distribuciones de tiempos de servicio:
ADVANCE FN$gfun1 ; simétrica ADVANCE FN$gfun2 ; dispersión derecha ADVANCE FN$gfun3 ; dispersión izquierda
/*
Gauss
*/
SYSTEM {TYPE:OPTIONS,Speed:5}
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA2,X:343,Y:185,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA3,X:535,Y:183,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
POSITION {NAME:POS1,X:250,Y:381}
POSITION {NAME:POS2,X:423,Y:383}
POSITION {NAME:POS3,X:566,Y:384}
Function {name:gfun1, TYPE:GAUSS, a:1, b:50, sigma1:3.3, sigma2:3.3,intervals:100 }
Function {name:gfun2, TYPE:GAUSS, a:1, b:50, sigma1:1.0, sigma2:10.0,intervals:100 }
Function {name:gfun3, TYPE:GAUSS, a:1, b:50, sigma1:10.0, sigma2:1.0,intervals:100 }
START 500
;*****************************************************
GENERATE 10,0,0,0 {NAME:GEN1,X:91,Y:383}
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$gfun1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$gfun2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
ADVANCE 20,0 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE FN$gfun3
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1La distribución exponencial se utiliza para modelar eventos que ocurren de manera aleatoria pero con una probabilidad decreciente en el tiempo. Es ideal para situaciones como tiempos de espera, fallos de sistemas o llegadas al azar donde lo más probable es que algo ocurra al principio, y sea menos probable cuanto más se demore.
Un ejemplo clásico: "Si estás esperando a ser atendido, es más probable que ocurra pronto; y cada minuto adicional reduce esa probabilidad."
Esto no lo representa bien una distribución uniforme (como ADVANCE 10,5), pero sí una función exponencial, definida con el tipo:
Function {name:efun1, type:"EXP", a:100, b:20, lambda:0.3, intervals:100}
a: Altura de la curva (solo para visualización, no afecta al cálculo).
b: Valor base mínimo. Es el punto desde el cual comienza la distribución.
lambda: Tasa de decrecimiento. A mayor lambda, la caída es más rápida.
intervals: Número de tramos en los que se divide la curva (cuantos más, más precisa).
Aproximadamente, el 99.9% de los valores estarán dentro del rango:
λ = 1 → de b a b + 6.91
λ = 0.5 → de b a b + 13.82
λ = 2 → de b a b + 3.45
Ejemplo de uso comparativo:
Function {Name:efun1, type:"EXP", a:100, b:20, lambda:0.3, intervals:70}
Function {Name:efun2, type:"EXP", a:100, b:20, lambda:0.6, intervals:70}
Function {Name:efun3, type:"EXP", a:100, b:20, lambda:1.0, intervals:70}
ADVANCE FN$efun1 ; caída lenta
ADVANCE FN$efun2 ; caída media
ADVANCE FN$efun3 ; caída rápida
/*
Exponential
*/
SYSTEM {TYPE:OPTIONS, Speed:5}
;=== Facilities con seguimiento estadístico ===
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA2,X:343,Y:185,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA3,X:535,Y:183,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
;=== Posiciones finales para visualización ===
POSITION {NAME:POS1, X:250, Y:381}
POSITION {NAME:POS2, X:423, Y:383}
POSITION {NAME:POS3, X:566, Y:384}
;=== EFunctions con distintas tasas de caída ===
Function {Name:efun1, TYPE:EXP, a:100, b:20, Lambda:0.3, Intervals:70}
Function {Name:efun2, TYPE:EXP, a:100, b:20, Lambda:0.6, Intervals:70}
Function {Name:efun3, TYPE:EXP, a:100, b:20, Lambda:1.0, Intervals:70}
;=== Inicio de simulación ===
START 500
;=== Flujo de entidades por las tres ventanillas ===
GENERATE 10,0,0,0 {NAME:GEN1, X:91, Y:383}
; VENTANILLA 1 – Caída lenta
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$efun1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
; VENTANILLA 2 – Caída media
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$efun2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
; VENTANILLA 3 – Caída rápida
ADVANCE 20,0 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE FN$efun3
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
¿Qué es la distribución log-normal?
Una variable sigue una distribución log-normal si el logaritmo de esa variable tiene una distribución normal. En otras palabras:
“Muchos eventos pequeños ocurren frecuentemente, pero también es posible que aparezcan algunos eventos con valores muy grandes, aunque con baja probabilidad.”
Tiempo de reparación de una máquina.
Duración de una llamada telefónica.
Ingresos económicos (la mayoría gana poco, unos pocos ganan mucho).
Function {Name:lfunc1, Type:LOGNORMAL, b:30, sigma:0.1}
Parámetros:
b: Valor más probable de la función (el pico de la curva).sigma: Controla la dispersión de los valores hacia la derecha. A mayor sigma, más amplia es la “cola” de la distribución.GPSS-Plus convierte automáticamente estos valores a los parámetros matemáticos μ (media logarítmica) y σ (desviación estándar) para construir la curva log-normal.
A diferencia de la gaussiana, la log-normal no es simétrica. Tiene una “cola” larga hacia valores más altos. Este comportamiento refleja bien los fenómenos donde los valores extremos son raros, pero posibles.
Este ejemplo muestra tres funciones log-normales con el mismo tipo de curva, pero distinta dispersión.
Function {Name:lfunc1, Type:LOGNORMAL, b:30, sigma:0.1} ; curva estrecha
Function {Name:lfunc2, Type:LOGNORMAL, b:20, sigma:0.15} ; dispersión media
Function {Name:lfunc3, Type:LOGNORMAL, b:40, sigma:0.2} ; cola larga
/*
Log-normal
*/
SYSTEM {TYPE:OPTIONS, Speed:5}
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:100,capacity:10}
Facility {NAME:VENTANILLA2,X:343,Y:185,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:100,capacity:10}
Facility {NAME:VENTANILLA3,X:535,Y:183,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:100,capacity:10}
POSITION {NAME:POS1, X:250, Y:381}
POSITION {NAME:POS2, X:423, Y:383}
POSITION {NAME:POS3, X:566, Y:384}
Function {Name:lfunc1, Type: LOGNORMAL, b:30, sigma:0.1}
Function {Name:lfunc2, Type: LOGNORMAL, b:20, sigma:0.15}
Function {Name:lfunc3, Type: LOGNORMAL, b:40, sigma:0.2}
START 500
GENERATE 10,0,0,0 {NAME:GEN1, X:91, Y:383}
; VENTANILLA 1 – Dispersión pequeña
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$lfunc1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
; VENTANILLA 2 – Dispersión media
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$lfunc2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
; VENTANILLA 3 – Dispersión grande (cola larga)
ADVANCE 20,0 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE FN$lfunc3
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
La distribución uniforme representa la máxima incertidumbre: todos los valores dentro de un rango tienen exactamente la misma probabilidad.
"Es como lanzar un dado perfecto: ningún número tiene más posibilidades que otro."
Este tipo de distribución es útil cuando no hay razones para pensar que unos valores sean más probables que otros.
Parámetros
Function {Name:ufun1, TYPE:UNIFORM, Min:20, Max:40}
Min: valor mínimo que puede tomar la función.
Max: valor máximo que puede tomar la función.
Todos los valores entre Min y Max tienen la misma probabilidad.
En el gráfico del informe, la curva tiene forma de rectángulo plano. Cuanto mayor sea el rango, más dispersa será la distribución.
Este ejemplo compara tres distribuciones uniformes con distintos rangos. Todas tienen forma rectangular, pero difieren en amplitud y posición:
Function {Name:ufun1, TYPE:UNIFORM, Min:20, Max:40} ; Rango pequeño
Function {Name:ufun2, TYPE:UNIFORM, Min:20, Max:60} ; Rango amplio
Function {Name:ufun3, TYPE:UNIFORM, Min:30, Max:50} ; Rango desplazado
/*
Uniforme
*/
SYSTEM {TYPE:OPTIONS, Speed:5}
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA2,X:343,Y:185,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA3,X:535,Y:183,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
POSITION {NAME:POS1, X:250, Y:381}
POSITION {NAME:POS2, X:423, Y:383}
POSITION {NAME:POS3, X:566, Y:384}
Function {Name:ufun1, TYPE:UNIFORM, Min:20, Max:40}
Function {Name:ufun2, TYPE:UNIFORM, Min:20, Max:60}
Function {Name:ufun3, TYPE:UNIFORM, Min:30, Max:50}
START 500
GENERATE 10,0,0,0 {NAME:GEN1, X:91, Y:383}
; VENTANILLA 1 – Uniforme de 20 a 40
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$ufun1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
; VENTANILLA 2 – Uniforme de 20 a 60
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$ufun2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
; VENTANILLA 3 – Uniforme de 30 a 50
ADVANCE 20,0 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE FN$ufun3
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS3}
TERMINATE 1
La distribución triangular permite definir una estimación mínima, máxima y más probable (modo) de un valor. Tiene forma de triángulo, donde el valor del modo es el más frecuente y los extremos son menos probables.
Es útil cuando tienes una idea aproximada del comportamiento de un proceso, pero no datos suficientes para definir una distribución más compleja.
“Una entrega normalmente tarda 3 días, pero podría hacerlo en 2 o en 5.”
Ese tipo de incertidumbre es ideal para una distribución triangular.
Parámetros
Function {Name:tfun1, TYPE:TRIANGULAR, Min:20, Max:40, Mode:30}
Min: Valor mínimo posible.Max: Valor máximo posible.Mode: Valor más probable (pico de la distribución).Este ejemplo compara tres funciones triangulares con diferentes posiciones del pico. Todas con el mismo rango, pero con distintas distribuciones:
Function {Name:tfun1, TYPE:TRIANGULAR, Min:20, Max:40, Mode:30} ; Pico centrado
Function {Name:tfun2, TYPE:TRIANGULAR, Min:20, Max:40, Mode:25} ; Pico sesgado a la izquierda
Function {Name:tfun3, TYPE:TRIANGULAR, Min:20, Max:40, Mode:35} ; Pico sesgado a la derecha
/*
Triangular
*/
SYSTEM {TYPE:OPTIONS, Speed:5}
Facility {NAME:VENTANILLA1,X:165,Y:184,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA2,X:343,Y:185,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
Facility {NAME:VENTANILLA3,X:535,Y:183,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:10}
POSITION {NAME:POS1, X:250, Y:381}
POSITION {NAME:POS2, X:423, Y:383}
POSITION {NAME:POS3, X:566, Y:384}
Function {Name:tfun1, TYPE:TRIANGULAR, Min:20, Max:40, Mode:30}
Function {Name:tfun2, TYPE:TRIANGULAR, Min:20, Max:40, Mode:25}
Function {Name:tfun3, TYPE:TRIANGULAR, Min:20, Max:40, Mode:35}
START 500
GENERATE 10,0,0,0 {NAME:GEN1, X:91, Y:383}
; VENTANILLA 1 – Triángulo centrado
ADVANCE 20,4 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$tfun1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS1}
; VENTANILLA 2 – Triángulo con pico a la izquierda
ADVANCE 20,0 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$tfun2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS2}
; VENTANILLA 3 – Triángulo con pico a la derecha
ADVANCE 20,0 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE FN$tfun3
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
La distribución Poisson modela la cantidad de eventos que ocurren en un intervalo de tiempo fijo, siempre que:
Los eventos ocurren independientemente.
La tasa media de ocurrencia (λ) es constante.
¿Cuándo usar Poisson?
Número de llamadas por minuto en un call center.
Llegadas de clientes a una tienda.
Errores de fabricación por día.
Parámetro:
λ (lambda): número medio esperado de eventos por intervalo.
Por ejemplo, si λ = 3, en promedio habrá 3 eventos por unidad de tiempo, aunque puede haber 2, 4, 0, etc. La variabilidad está incluida en la distribución.
Relación con la distribución exponencial
Poisson cuenta cuántos eventos ocurren.
Exponencial mide el tiempo entre eventos.
Ambas están controladas por el mismo parámetro λ, y son complementarias:
Poisson(λ): Número de eventos por intervalo λ
Exponencial(λ): Tiempo entre eventos consecutivos 1 / λ
Comparación visual de ambas distribuciones para 3 valores distintos de λ:
Function {Name:fPoisson1, TYPE:POISSON, LAMBDA:X$LAMBDA1}
Function {Name:fPoisson2, TYPE:POISSON, LAMBDA:X$LAMBDA2}
Function {Name:fPoisson3, TYPE:POISSON, LAMBDA:X$LAMBDA3}
Function {Name:fexp1, TYPE:EXP, LAMBDA:X$LAMBDA1, b:0}
Function {Name:fexp2, TYPE:EXP, LAMBDA:X$LAMBDA2, b:0}
Function {Name:fexp3, TYPE:EXP, LAMBDA:X$LAMBDA3, b:0}
Los resultados se visualizan en texto dinámico con la media acumulada:
Tendencia a λ = 3.0 → Media ≈ 3.0 Tendencia a λ = 0.25 → Media ≈ 0.25 Exp = 1 / λ = 0.333 → Media ≈ 0.333
Esta simulación sirve para entender visualmente cómo:
Poisson estabiliza el número de eventos hacia λ.
Exponencial estabiliza el tiempo entre eventos hacia 1 / λ.
/*
Poisson
*/
SYSTEM {TYPE:OPTIONS,Speed:5}
Graphic {NAME:pText1,Type:TEXT,X:309,Y:281,Text:"Media1"}
Graphic {NAME:pText2,Type:TEXT,X:309,Y:238,Text:"Media2"}
Graphic {NAME:pText3,Type:TEXT,X:308,Y:198,Text:"Media3"}
Graphic {NAME:eText1,Type:TEXT,X:506,Y:280,Text:"Exp1"}
Graphic {NAME:eText2,Type:TEXT,X:506,Y:238,Text:"Exp2"}
Graphic {NAME:eText3,Type:TEXT,X:504,Y:196,Text:"Exp3"}
POSITION {NAME:POS1,X:598,Y:395}
INITIAL contador,0
INITIAL LAMBDA1,1/4
INITIAL iLAMBDA1,round(1/X$LAMBDA1,3)
INITIAL pTotal1,0
INITIAL eTotal1,0
INITIAL LAMBDA2,3/4
INITIAL iLAMBDA2,round(1/X$LAMBDA2,3)
INITIAL pTotal2,0
INITIAL eTotal2,0
INITIAL LAMBDA3,3
INITIAL iLAMBDA3,round(1/X$LAMBDA3,3)
INITIAL pTotal3,0
INITIAL eTotal3,0
Function {Name:fPoisson1, TYPE:POISSON, LAMBDA:X$LAMBDA1}
Function {Name:fPoisson2, TYPE:POISSON, LAMBDA:X$LAMBDA2}
Function {Name:fPoisson3, TYPE:POISSON, LAMBDA:X$LAMBDA3}
Function {Name:fexp1, TYPE:EXP, LAMBDA:X$LAMBDA1, b:0}
Function {Name:fexp2, TYPE:EXP, LAMBDA:X$LAMBDA2, b:0}
Function {Name:fexp3, TYPE:EXP, LAMBDA:X$LAMBDA3, b:0}
START 2000
;*****************************************************
GENERATE 2,0,0,0 {NAME:GEN1,X:218,Y:395}
SAVEVALUE contador,X$contador + 1
; --- Acumulation Poisson ---
SAVEVALUE pTotal1,X$pTotal1 + FN$fPoisson1
SAVEVALUE pMedia1, X$pTotal1 / X$contador
SAVEVALUE pMediaRound1, round(X$pMedia1,3)
SAVEVALUE pTotal2,X$pTotal2 + FN$fPoisson2
SAVEVALUE pMedia2, X$pTotal2 / X$contador
SAVEVALUE pMediaRound2, round(X$pMedia2,3)
SAVEVALUE pTotal3,X$pTotal3 + FN$fPoisson3
SAVEVALUE pMedia3, X$pTotal3 / X$contador
SAVEVALUE pMediaRound3, round(X$pMedia3,3)
; --- Acumulation Exponentian ---
SAVEVALUE eTotal1,X$eTotal1 + FN$fexp1
SAVEVALUE eMedia1, X$eTotal1 / X$contador
SAVEVALUE eMediaRound1, round(X$eMedia1,3)
SAVEVALUE eTotal2,X$eTotal2 + FN$fexp2
SAVEVALUE eMedia2, X$eTotal2 / X$contador
SAVEVALUE eMediaRound2, round(X$eMedia2,3)
SAVEVALUE eTotal3,X$eTotal3 + FN$fexp3
SAVEVALUE eMedia3, X$eTotal3 / X$contador
SAVEVALUE eMediaRound3, round(X$eMedia3,3)
move {name:pText1,text:"Tendencia a Lambda = X$LAMBDA1 --> X$pMediaRound1"}
move {name:pText2,text:"Tendencia a Lambda = X$LAMBDA2 --> X$pMediaRound2"}
move {name:pText3,text:"Tendencia a Lambda = X$LAMBDA3 --> X$pMediaRound3"}
move {name:eText1,text:"Exp = 1/X$LAMBDA1 = X$iLAMBDA1 --> X$eMediaRound1"}
move {name:eText2,text:"Exp = 1/X$LAMBDA2 = X$iLAMBDA2 --> X$eMediaRound2"}
move {name:eText3,text:"Exp = 1/X$LAMBDA3 = X$iLAMBDA3 --> X$eMediaRound3"}
ADVANCE 30,10 {TO:POS1}
ENDGENERATE 1
El tipo FDISTRIBUTION permite definir funciones de distribución completamente personalizadas mediante una expresión matemática, sin limitarse a fórmulas predefinidas como GAUSS, EXP o TRIANGULAR.
Estas funciones generan una tabla de distribución internamente, evaluando una expresión matemática en un rango determinado. Es ideal para representar distribuciones no estándar o variantes de las clásicas.
¿Cómo funciona?
GPSS-Plus construye una tabla de distribución basada en:
Una expresión matemática que define la forma de la función (como una campana, un triángulo, etc.)
Un rango de valores (min, max) donde se evalúa dicha función
Un número de intervals que determina la resolución de la tabla
Parámetros opcionales (A, B, C...) que puedes usar dentro de la fórmula
Una vez construida la tabla, la función actúa como cualquier otra distribución: se toma un valor aleatorio, se consulta la tabla y se devuelve el resultado correspondiente.
Sintaxis:
Function {
Name: nombre,
Type: FDISTRIBUTION,
Expression: "fórmula",
min: valorMinimo,
max: valorMaximo,
intervals: cantidad,
A: valor, B: valor, ...
}
Ejemplo 1: Distribución gaussiana personalizada
Function {
Name:gauss1,
Type: FDISTRIBUTION,
EXPRESSION: "(1 / (A * SQRT(2 * PI))) * EXP(-0.5 * ((X - B) / A)^2)",
A: 2.3,
B: 30,
min: B - (3 * A),
max: B + (3 * A),
intervals: 100
}
Este ejemplo crea una función tipo campana centrada en 30, con dispersión 2.3. Es equivalente a una gaussiana clásica, pero totalmente configurable.
Ejemplo 2: Distribución triangular definida por fórmula
Function {
Name:tri1,
Type: FDISTRIBUTION,
EXPRESSION: "(1 / C) * (1 - ABS((X - B) / (C / 2)))",
B: 25, C: 30,
min: B - C / 2,
max: B + C / 2,
intervals: 100
}
Esta función genera una distribución triangular con pico en 25, anchura total de 30, y base de [10, 40].
La variable X es el eje horizontal (dominio).
La función se evalúa intervals veces entre min y max.
La integral se normaliza automáticamente para comportarse como una distribución.
Todos los parámetros (A, B, C, etc.) pueden ser expresiones.
/*
FDistribution
*/
SYSTEM {TYPE:OPTIONS,Speed:5}
Facility {NAME:VENTANILLA1,X:244,Y:190,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:20}
Facility {NAME:VENTANILLA2,X:412,Y:188,E_BIN_start:5,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:20}
POSITION {NAME:POS1,X:200,Y:387}
POSITION {NAME:POS2,X:341,Y:381}
POSITION {NAME:POS3,X:507,Y:380}
;GAUSSIANA
Function {Name:tName1, type: FDISTRIBUTION, EXPRESSION:"(1 / (A * SQRT(2 * PI))) * EXP(-0.5 * ((X - B) / A)^2)", b:30, a: 2.3, min:B - (3 * A), max:B + (3 * A),intervals:100}
; TRIANGULAR B: centro del rango C: ancho total del rango
Function {Name:tName2, A:0, B:25, C:30, D:0, type: FDISTRIBUTION, EXPRESSION:"(1 / C) * (1 - ABS((X - B) / (C / 2)))", min:B - C/2, max:B + C/2, intervals:100}
START 1000
;*****************************************************
GENERATE 2,0,0,0 {NAME:GEN1,X:56,Y:319}
ADVANCE 20,0 {TO:POS1}
ADVANCE 30,10 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$tName1
RELEASE VENTANILLA1
ADVANCE 20,0 {TO:POS2}
ADVANCE 20,10 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE FN$tName2
RELEASE VENTANILLA2
ADVANCE 20,0 {TO:POS3}
ENDGENERATE 1
El tipo MATH permite definir funciones que se evalúan como fórmulas matemáticas directas, sin utilizar distribuciones ni tablas de valores precalculadas. Son útiles cuando el valor que necesitamos depende de variables de forma determinista o con una expresión algebraica.
Este tipo de función no genera aleatoriedad. Simplemente toma los parámetros indicados en orden, los sustituye en la expresión y devuelve el resultado.
Sintaxis:
Function {Name:nombre, Type:MATH, Expression:"A + B * C"}
Name: nombre identificador de la función.Type: debe ser MATH.Expression: fórmula a evaluar, usando letras mayúsculas como variables (A, B, C...). Estas letras representan los parámetros pasados en orden.¿Cómo se llama una función MATH?
Se utiliza como cualquier función FN$, pero con parámetros entre paréntesis:
FN$(nombre, valor_A, valor_B, ...)
/*
Math
*/
Graphic {NAME:Text1,Type:TEXT,X:370,Y:312,Text:"Hello"}
Function {Name:math1, type: MATH, EXPRESSION:"A * 2 + B"}
START 1
;*****************************************************
GENERATE 1,0,0,1 {NAME:GEN1,X:56,Y:319}
assign AAA,4
assign BBB,6
move {name:Text1,text:"Result: FN$(math1,P$AAA,P$BBB)"}
ENDGENERATE 1
Hasta ahora hemos visto cómo GPSS-Plus es, en su esencia, un motor de simulación de eventos discretos. Pero también es capaz de simular sistemas continuos, es decir, aquellos en los que los cambios ocurren de forma gradual a lo largo del tiempo.
Un sistema continuo es aquel en el que los valores cambian sin interrupciones. Por ejemplo:
El nivel de un depósito que se va llenando.
La temperatura de un horno que se calienta.
La velocidad de un coche acelerando suavemente.
En estos casos no hay “saltos” puntuales como los de una cola o un cliente que entra o sale. El cambio ocurre de forma progresiva.
La respuesta es: haciendo trampa, pero de forma inteligente. En lugar de simular un cambio constante y fluido, lo dividimos en pequeños pasos temporales, lo bastante rápidos como para que parezca continuo.
Es como el cine: cada fotograma es una imagen estática, pero al reproducirse 24 por segundo, vemos movimiento.
Con un ajustes clave:
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
TIME_DECIMALS:1 indica que el tiempo se mide con un decimal (0.1).SPEED:5 hace que el sistema se actualice con una interpretación media. Del 0 que indica pausa a 10 que es el máximo de velocidad. La interpretación media (5) es que 10 instantes AC1 tandan aproximadamente 1 segundo. Con SPEED:2, 1 segundo equivale aproximadamente a 1 AC1.El motor sigue siendo discreto. El tiempo no avanza por sí solo; avanza cuando se atiende una entidad o se dispara un evento. Lo que hacemos es generar muchos eventos pequeños, muy seguidos.
Ejemplo: Llenado de un depósito
Vamos a llenar un depósito con un caudal variable (por ejemplo, siguiendo una función seno para hacerlo más dinámico).
Para ello, usamos un timer, que lanza un bloque de código cada 0.1 unidades de tiempo. No genera transacciones ni estadísticas: solo ejecuta.
SYSTEM {TYPE:ON_TIMER, TRIGGER:llenado, INTERVAL: 0.1}
Cada vez que se activa el PROCEDURE, haremos esto:
Calcular un caudal según una función oscilante, definida previamente con FUNCTION TYPE:MATH.
Añadir ese caudal al nivel actual del depósito.
Actualizar visualmente el nivel.
Mostrar el valor en pantalla.
Si el depósito se llena, detener la simulación.
En ese caso, cada ciclo calculaba el caudal y lo sumaba al nivel con una fórmula sencilla:
SAVEVALUE nivel, X$nivel + X$caudal * 0.1
Este método se llama método de Euler, y es la forma más simple de integración numérica.
Estima que el intervalo de tiempo entre AC1$ y AC1$ + 0.1 el depósito se va a llenar con la cantidad de caudal en el momento AC1$ multiplicado por el intervalo 0.1
En el siguiente paso, volverá a hacer el mismo cálculo, usando el nuevo valor del caudal en ese instante. Y así sucesivamente, sumando pequeñas cantidades como si fuera una suma de rectángulos: eso es, en esencia, una integral.
/*
Sistema continuo dentro del discreto
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
SYSTEM {TYPE:ON_TIMER, TRIGGER:llenado, INTERVAL: 0.1}
Graphic {NAME:aviso,Type:TEXT,X:291,Y:342,Text:"aviso"}
Function {Name:fcCaudal, Type:Math, Expression:"0.6 + SIN(A) * 4"}
INITIAL nivel, 0
INITIAL caudal, 1.2
INITIAL fase, 0
INITIAL constante, 0.6
include ./library_graphics/speedometer.lib
START 100
PROCEDURE PRE_RUN
assign config,{title:"Depósito"
,x:100,y:60
,width:100 ,height:180
,min_value: 0
,max_value: 100
,"color":"#ff0000"}
call deposito.speedometer.init,V$config
assign config,{title:"Caudal"
,x:300,y:60
,width:100 ,height:180
,min_value: -10
,max_value: 10
,"color":"orange"}
call caudal.speedometer.init,V$config
TERMINATE_VE
ENDPROCEDURE
PROCEDURE llenado
; Oscilar caudal como una función del tiempo simulado
; SAVEVALUE caudal, X$constante + SIN(X$fase) * 4
SAVEVALUE caudal, FN$(fcCaudal,X$fase)
; Aumentamos la fase suavemente
SAVEVALUE fase, X$fase + 0.1
; Sumamos el caudal al nivel, según el intervalo de tiempo
SAVEVALUE nivel, X$nivel + X$caudal * 0.1
; Actualizamos visualmente la línea del depósito
; Valores redondeados para mostrar
ASSIGN tCaudal, round(X$caudal, 2)
ASSIGN tNivel, round(X$nivel, 2)
CALL deposito.speedometer.set, P$tNivel
CALL caudal.speedometer.set, P$tCaudal
; Visualización del estado
IF (X$nivel>=100)
MOVE {name:aviso, text:"¡DEPÓSITO LLENO!"}
STOP
ELSE
MOVE {name:aviso, text:"Nivel: P$tNivel - Caudal: P$tCaudal"}
ENDIF
TERMINATE
ENDPROCEDURE 1
En el capítulo anterior vimos cómo GPSS-Plus puede simular comportamientos aparentemente continuos mediante el uso de TIMERs y pasos de tiempo muy pequeños. Observamos, por ejemplo, cómo se llenaba un depósito en pantalla, simulando un proceso constante.
Ahora vamos un paso más allá:
No solo vamos a ver lo que ocurre… vamos a registrarlo y graficarlo automáticamente para su análisis posterior.
Porque, en muchos casos, no basta con observar cómo se mueve algo: necesitamos tener una curva, un gráfico, un historial del comportamiento.
Para eso existe en GPSS-Plus el sistema de trazado de datos:
PLOTTER y PLOTPLOTTER {}: declara una tabla gráfica donde se irán guardando valores.
PLOT: añade un punto a esa tabla, usando el tiempo actual u otra variable como eje X.
Cada punto añadido es como un “fotograma” del sistema, y al terminar la simulación, GPSS-Plus dibuja una curva con todos los puntos registrados.
En nuestro caso vamos a registrar:
PLOTTER {NAME:thePlot, Y_0:nivel, Y_1:caudal, X:TIME_AC1}
Esto significa:
Vamos a graficar dos series de valores: nivel y caudal.
El eje horizontal (X) será el tiempo (AC1$), llamado TIME_AC1.
Para añadir puntos en cada instante:
PLOT thePlot, X$nivel, X$caudal
Esto se ejecuta dentro del PROCEDURE, en cada llamada, y va dibujando la historia del proceso.
"NIVEL se obtiene acumulando el CAUDAL. El sistema simula una integración continua."
"Cada punto corresponde a un instante generado automáticamente por el TIMER."
/*
Recolección de datos PLOT
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
SYSTEM {TYPE:ON_TIMER, TRIGGER:llenado, INTERVAL: 0.1}
PLOTTER {NAME:thePlot, Y_0:nivel, Y_1:caudal, X:TIME_AC1}
Graphic {NAME:aviso,Type:TEXT,X:291,Y:342,Text:"aviso"}
Function {Name:fcCaudal, Type:Math, Expression:"0.6 + SIN(A) * 4"}
INITIAL nivel, 0
INITIAL caudal, 1.2
INITIAL fase, 0
include ./library_graphics/speedometer.lib
START 100
PROCEDURE PRE_RUN
assign config,{title:"Depósito"
,x:100,y:60
,width:100 ,height:180
,min_value: 0
,max_value: 100
,"color":"#ff0000"}
call deposito.speedometer.init,V$config
assign config,{title:"Caudal"
,x:300,y:60
,width:100 ,height:180
,min_value: -10
,max_value: 10
,"color":"orange"}
call caudal.speedometer.init,V$config
TERMINATE_VE
ENDPROCEDURE
PROCEDURE llenado
; Oscilar caudal como una función del tiempo simulado
SAVEVALUE caudal, FN$(fcCaudal,X$fase)
; Aumentamos la fase
SAVEVALUE fase, X$fase + 0.1
; Sumar caudal al nivel
SAVEVALUE nivel, X$nivel + X$caudal * 0.1
; Registrar los datos en el ploter
PLOT thePlot, X$nivel, X$caudal
; Mostrar valores redondeados
ASSIGN tCaudal, round(X$caudal, 2)
ASSIGN tNivel, round(X$nivel, 2)
; Actualizar visualmente el nivel en pantalla
CALL deposito.speedometer.set, P$tNivel
CALL caudal.speedometer.set, P$tCaudal
IF (X$nivel>=100)
MOVE {name:aviso, text:"¡DEPÓSITO LLENO!"}
STOP
ELSE
MOVE {name:aviso, text:"Nivel: P$tNivel - Caudal: P$tCaudal"}
ENDIF
TERMINATE
ENDPROCEDURE 1
En los capítulos anteriores vimos cómo GPSS-Plus permite simular sistemas con comportamiento aparentemente continuo gracias a la ejecución frecuente de procedimientos temporizados (TIMER) y el uso de SAVEVALUE para acumular valores como el llenado de un depósito.
Hasta ahora usábamos una fórmula simple para simular el llenado:
SAVEVALUE nivel, X$nivel + X$caudal * 0.1
Este método es efectivo, pero básico. Asume que el caudal es constante durante cada intervalo de tiempo, lo cual no es del todo cierto si el caudal varía rápidamente, por ejemplo, con una función seno como es el caso.
Y cuando hablamos de intervalo, en este caso hablamos de ese "0.1". No es lo mismo SIN(0) que SIN(0.1).Y si queremos una estimación más precisa del valor medio, probablemente sería mejor usar SIN(0.05), es decir, el punto central del intervalo.
Cuando el valor que estás acumulando cambia dentro del mismo intervalo, el sistema no es perfectamente exacto: estás sumando un valor medio aproximado, no el real.
Para mejorar esto, GPSS-Plus incorpora el bloque INTEGRATE, que utiliza un método de integración numérica llamado Runge-Kutta de cuarto orden (RK4) para calcular una estimación mucho más precisa.
INTEGRATE { EXPRESSION: "SIN(T) + 2", DT: 0.1, SAVEVALUE: caudal_RK4 }
Este bloque:
Evalúa la expresión en cuatro puntos clave del intervalo: al inicio (X), dos veces en el punto medio (X + DT/2), y al final (X + DT).
Aplica el método de Runge-Kutta de cuarto orden (RK4), combinando esos valores con pesos específicos para estimar con precisión la variación de la función en ese tramo.
Guarda el resultado en el SAVEVALUE indicado, como mejor estimación del valor medio de la expresión en ese intervalo.
El valor resultante será una mejor estimación del caudal medio en ese intervalo de tiempo, y lo podemos usar para llenar el depósito con más precisión.
Vamos a construir un ejemplo que utiliza ambos métodos en paralelo:
caudal: usando el método simple
caudal_RK4: usando integración numérica RK4
nivel: acumulado con el caudal clásico
nivel_RK4: acumulado con el caudal RK4
Además, graficaremos las 4 curvas usando PLOTTER para comparar visualmente los resultados.
nivel_RK4 sube ligeramente más suavemente y más rápido que nivel, porque tiene en cuenta que el caudal crece dentro del intervalo.
caudal_RK4 se ajusta mejor a los picos y valles que caudal, que solo calcula un punto.
La línea blanca en pantalla (nivel RK4) va un poco por delante de la línea azul (nivel simple).
El método Runge-Kutta de cuarto orden (RK4), que implementa el bloque INTEGRATE en GPSS-Plus, es un algoritmo numérico diseñado para resolver ecuaciones diferenciales ordinarias de la forma:
dy/dt = f(t)
Y lo hace suponiendo que la función f(t) es continua y suave en el intervalo que se está integrando.
Esto significa que hay muchas variables (X$nivel, X$entidadesEnCola, X$facilidad, etc.) que pueden cambiar bruscamente, sin continuidad ni suavidad.
Esos cambios dependen de eventos que se disparan cuando otras entidades llegan, bloquean recursos, o terminan.
Por tanto:
No se debe usar INTEGRATE con expresiones que dependan de elementos del sistema discreto.
INTEGRATE con RK4 es una herramienta potente y precisa, siempre que se utilice con funciones puramente matemáticas. No es una simulación mágica del futuro: es un estimador inteligente de una función bien definida.
/*
Mejora de resultados con INTEGRATE
*/
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
SYSTEM {TYPE:ON_TIMER, TRIGGER:llenado, INTERVAL: 0.1}
PLOTTER {NAME:thePlot, Y_0:nivel, Y_1:caudal,X:TIME_AC1}
PLOTTER {NAME:thePlot_RK4, Y_0:nivel, Y_1:caudal,X:TIME_AC1}
PLOTTER {NAME:thePlot_ALL, Y_0:nivel, Y_1:caudal, Y_2:nivel_RK4, Y_3:caudal_RK4,X:TIME_AC1}
Graphic {NAME:aviso,Type:TEXT,X:291,Y:342,Text:"aviso"}
Graphic {NAME:aviso_RK4,Type:TEXT,X:291,Y:302,Text:"Aviso2"}
Graphic {NAME:Cubo1,Type:LINE,POINTS:"[100,100],[100,500],[200,500],[200,100]"
, Close:1
, color:#00FFFF
, Fcolor:#FFFFFF}
Graphic {NAME:Line1,Type:LINE,fcolor:#FF6666, X1:102,Y1:100,X2:198,Y2:100}
Graphic {NAME:Line_RK4,Type:LINE,color:#333300, X1:98,Y1:100,X2:202,Y2:100}
INITIAL nivel, 0
INITIAL caudal, 1.2
INITIAL nivel_RK4, 0
INITIAL caudal_RK4, 1.2
INITIAL fase, 0
INITIAL constante, 0.6
START 100
;*****************************************************
PROCEDURE llenado
; Oscilar caudal como una función del tiempo simulado
SAVEVALUE caudal, X$constante + SIN(X$fase) * 4
; Aumentamos la fase suavemente
SAVEVALUE fase, X$fase + 0.1
; Integración continua del nivel
INTEGRATE { EXPRESSION: "X$constante + SIN(T) * 4", METHOD: RK4, DT: 0.1, SAVEVALUE: caudal_RK4 }
SAVEVALUE nivel, X$nivel + X$caudal * 0.1
SAVEVALUE nivel_RK4, X$nivel_RK4 + X$caudal_RK4 * 0.1
PLOT thePlot,X$nivel,X$caudal
PLOT thePlot_RK4,X$nivel_RK4,X$caudal_RK4
PLOT thePlot_ALL,X$nivel,X$caudal,X$nivel_RK4,X$caudal_RK4
MOVE {NAME:Line1, X1:98,Y1:(100+X$nivel*4) ,X2:202,Y2:(100+X$nivel*4)}
MOVE {NAME:Line_RK4, X1:98,Y1:(100+X$nivel_RK4*4) ,X2:202,Y2:(100+X$nivel_RK4*4)}
ASSIGN tCaudal, round(X$caudal, 2)
ASSIGN tNivel, round(X$nivel, 2)
ASSIGN tCaudal_RK4, round(X$caudal_RK4, 2)
ASSIGN tNivel_RK4, round(X$nivel_RK4, 2)
; Visualización del estado
IF (X$nivel>=100)
MOVE {name:aviso, text:"¡DEPÓSITO LLENO!"}
stop
ELSE
MOVE {name:aviso, text:"Nivel: P$tNivel - Caudal: P$tCaudal"}
MOVE {name:aviso_RK4, text:"Nivel: P$tNivel_RK4 - Caudal: P$tCaudal_RK4"}
ENDIF
TERMINATE
ENDPROCEDURE 1
Este ejemplo muestra cómo simular el movimiento de un vehículo en un circuito circular utilizando una aceleración constante. Es el primer paso hacia la construcción de modelos físicos simples, como los que se emplean en videojuegos o simulaciones básicas de sistemas mecánicos.
El coche comienza detenido (velocidad 0).
Se le aplica una aceleración constante positiva hasta alcanzar una velocidad máxima (en este caso, 4 radianes por segundo).
Al llegar a ese límite, se invierte la aceleración para que el coche frene suavemente hasta una velocidad mínima (0.2 rad/s).
Luego vuelve a acelerar... y el ciclo se repite.
Se emplea el bloque INTEGRATE para sumar la aceleración en cada paso de tiempo:
INTEGRATE {EXPRESSION: X$aceleracion, DT: 0.1, SAVEVALUE: deltaVel}
En este caso, al ser una aceleración constante:
deltaVel = X$aceleracion * 0.1 deltaVel = ±0.01 × 0.1 = ±0.001
Esto nos da el incremento de velocidad en ese intervalo, y luego simplemente se acumula:
SAVEVALUE velocidad, X$velocidad + X$deltaVel
El coche se mueve suavemente en la pista circular, ganando y perdiendo velocidad.
El gráfico resultante (PLOTTER) muestra dos curvas:
Aceleración: una onda cuadrada que alterna entre +0.01 y -0.01.
Velocidad: una onda triangular que sube y baja en respuesta a la aceleración.
Este comportamiento es el típico de un sistema con control básico de velocidad, y es una base para la aceleración variable, resistencia, o control adaptativo.
/*
Movimiento con aceleración constante
*/
SYSTEM {type:OPTIONS, TIME_DECIMALS:2, SPEED:5}
SYSTEM {TYPE:ON_TIMER, TRIGGER:moverCoche, INTERVAL: 0.01}
; --- Variables iniciales ---
INITIAL velocidad, 0.0 ; Velocidad angular
INITIAL angulo, 0 ; Posición angular (en radianes)
INITIAL aceleracion, 0.01 ; Aceleración angular constante
INITIAL radio, 200 ; Radio del circuito circular
; --- Funciones gráficas ---
Graphic {NAME:txtVel, Type:TEXT, X:510, Y:120, TEXT:"Velocidad:"}
Graphic {NAME:coche, Type:LINE, X1:-10, Y1:-10, X2:-10, Y2:10, X3:10, Y3:10, X4:10, Y4:-10, COLOR:#FF0000, close:1}
Graphic {NAME:pista, Type:ARC, X:300, Y:300, RADIUS:X$radio, CLOSE:0, COLOR:#999999}
; --- Gráfica de evolución ---
PLOTTER {NAME:curva, Y_0:aceleracion, Y_1:velocidad,X:TIME}
START 500
; --- Procedimiento de movimiento ---
PROCEDURE moverCoche
IF (X$velocidad >= 4)
SAVEVALUE aceleracion, -0.1 ; Empieza a frenar
endif
if (X$velocidad <= 0.2)
SAVEVALUE aceleracion, 0.1 ; Vuelve a acelerar
ENDIF
; Calcular incremento de velocidad
INTEGRATE {EXPRESSION: X$aceleracion, DT: 0.01, SAVEVALUE: deltaVel}
; Acumular en velocidad total
SAVEVALUE velocidad, X$velocidad + X$deltaVel
; Sumar velocidad al ángulo
SAVEVALUE angulo, X$angulo + X$velocidad * 0.05
; Calcular posición
ASSIGN rad, X$angulo
ASSIGN posX, 300 + X$radio * COS(P$rad)
ASSIGN posY, 300 + X$radio * SIN(P$rad)
MOVE {NAME:coche, X:P$posX, Y:P$posY}
ASSIGN tVel, round(X$velocidad, 2)
MOVE {NAME:txtVel, TEXT: "Velocidad: P$tVel rad/s"}
PLOT curva, X$aceleracion, X$velocidad
TERMINATE
ENDPROCEDURE
En este ejemplo damos un paso más en la simulación de movimiento circular, haciendo que la aceleración del vehículo evolucione de forma dinámica.
Hasta ahora habíamos utilizado una aceleración constante o un cambio inmediato entre aceleración positiva y negativa. Sin embargo, en el mundo real, un coche acelera poco a poco, y frena con más fuerza, sobre todo si deja de acelerar y retiene motor. Eso es lo que queremos modelar aquí.
Simulamos un vehículo que:
Acelera lentamente cuando parte desde velocidad cero.
Frena más bruscamente cuando alcanza una velocidad alta.
Circula por una pista circular.
Muestra gráficamente su posición y velocidad angular.
Registra la evolución en dos gráficas: aceleración y velocidad.
aceleracion: valor actual de aceleración angular.
deltaA: ritmo de cambio de la aceleración (puede ser positivo o negativo).
INTEGRATE: calcula cuánto cambia la velocidad con la aceleración actual.
velocidad: cambia suavemente en función de la aceleración.
angulo: se incrementa según la velocidad, generando el movimiento.
coche: se mueve sobre la pista en función del ángulo.
PLOTTER: representa cómo varían la aceleración y la velocidad a lo largo del tiempo.
Mientras la velocidad es baja, el sistema incrementa poco a poco la aceleración (deltaA positivo y suave).
Cuando la aceleración supera un umbral (por ejemplo, 0.04), se invierte el signo de deltaA y se frena rápidamente.
Al llegar a velocidad cero, se reinicia el ciclo: se corta la aceleración, se resetea velocidad, y se vuelve a acelerar.
De esta forma se obtiene un patrón cíclico muy natural, parecido al de un motor térmico: aceleración progresiva y frenado más brusco.
La aceleración forma una curva en dientes de sierra: sube lentamente, y cae rápidamente.
La velocidad sigue una forma suave y ondulada: acelera poco a poco y baja más rápido.
El coche cambia de color: verde cuando acelera, rojo cuando frena.
/*
Movimiento con aceleración variable
*/
SYSTEM {type:OPTIONS, TIME_DECIMALS:2, SPEED:6}
SYSTEM {TYPE:ON_TIMER, TRIGGER:moverCoche, INTERVAL: 0.1}
; --- Variables iniciales ---
INITIAL velocidad, 0.0 ; Velocidad angular
INITIAL angulo, 0 ; Posición angular (en radianes)
INITIAL aceleracion, 0.01 ; Aceleración angular
INITIAL radio, 200 ; Radio del circuito circular
INITIAL deltaA, 0.0002
; --- Funciones gráficas ---
Graphic {NAME:txtVel, Type:TEXT, X:510, Y:120, TEXT:"Velocidad:"}
Graphic {NAME:coche, Type:LINE, X1:-10, Y1:-10, X2:-10, Y2:10, X3:10, Y3:10, X4:10, Y4:-10, COLOR:green, close:1}
Graphic {NAME:pista, Type:ARC, X:300, Y:300, RADIUS:X$radio, CLOSE:0, COLOR:#999999}
; --- Gráfica de evolución ---
PLOTTER {NAME:curva, Y_0:aceleracion, Y_1:velocidad,X:TIME}
START 500
PROCEDURE moverCoche
;-------------------------------
; AJUSTE SUAVE DE LA ACELERACIÓN
;-------------------------------
; Control: si velocidad muy alta → reducir aceleración poco a poco
IF (X$aceleracion > 0.04)
SAVEVALUE deltaA, -0.003
SAVEVALUE aceleracion, 0.04
MOVE {name: coche,color:red}
ENDIF
IF (X$velocidad <= 0.0)
SAVEVALUE deltaA, 0.0002
SAVEVALUE aceleracion, 0
SAVEVALUE velocidad, 0.0
MOVE {name: coche,color:green}
ENDIF
;---------------------------------
; ACTUALIZAR VELOCIDAD Y POSICIÓN
;---------------------------------
; Modificar aceleración suavemente según deltaA
SAVEVALUE aceleracion, X$aceleracion + X$deltaA
INTEGRATE {EXPRESSION: X$aceleracion, DT: 0.1, SAVEVALUE: deltaVel}
SAVEVALUE velocidad, X$velocidad + X$deltaVel
; Actualizar ángulo (posición) según velocidad angular
SAVEVALUE angulo, X$angulo + X$velocidad * 0.1
;--------------------------------
; CALCULAR POSICIÓN Y MOVER COCHE
;--------------------------------
ASSIGN rad, X$angulo
ASSIGN posX, 300 + X$radio * COS(P$rad)
ASSIGN posY, 300 + X$radio * SIN(P$rad)
MOVE {NAME:coche, X:P$posX, Y:P$posY}
;--------------------------
; MOSTRAR ESTADO Y PLOTTERS
;--------------------------
ASSIGN tVel, round(X$velocidad, 2)
ASSIGN tAcel, round(X$aceleracion, 4)
MOVE {NAME:txtVel, TEXT: "Velocidad: P$tVel rad/s \n Aceleración: P$tAcel"}
; Gráficas para análisis
PLOT curva, X$aceleracion, X$velocidad
TERMINATE
ENDPROCEDURE
En los capítulos anteriores vimos cómo simular el llenado de un depósito con funciones continuas, cómo registrar su evolución con gráficos (PLOTTER) y cómo mejorar la precisión usando integración numérica (INTEGRATE con RK4). Ahora es el momento de integrar todo ese conocimiento en un sistema realista y complejo, en el que lo continuo y lo discreto conviven.
Este ejemplo representa un sistema híbrido en el que:
Hay una fuente de energía solar (modelo continuo)
Hay usuarios discretos que llegan, deciden cargar y se conectan si lo necesitan
La energía solar se modela como una función senoidal que simula el paso del día: una oscilación desde 0 hasta 1.5, a lo largo de 1440 minutos (24 horas). Cada unidad de tiempo representa un minuto, y cada ciclo de TIMER añade energía a la batería.
INTEGRATE {
EXPRESSION: "0.75 + 0.75 * SIN((T / 1440) * 6.2832)",
DT: 1,
SAVEVALUE: caudalSolar
}
Esta expresión genera una onda suave con un máximo de 1.5 y un mínimo de 0, centrada en 0.75. El resultado se suma al nivel de batería hasta un máximo de 100.
Además, un PLOTTER recoge los datos en cada ciclo:
PLOTTER {NAME:bateriaPlot, Y_0:nivelBateria, Y_1:caudalSolar, X:TIME_AC1}
Así se podrá ver la evolución de la batería y el caudal solar durante todo el día.
Mientras tanto, los usuarios llegan de forma aleatoria usando GENERATE. Cada uno decide si necesita cargar su batería, usando una función de tipo Poisson para modelar la decisión:
Function {Name:decisionCarga, TYPE:POISSON, LAMBDA:1/3}
Si deciden cargar:
Se mueven hasta el punto de carga.
Se conectan al recurso RESTROOM cargador, que simula un sistema de carga simultánea.
Permanecen dentro del RESTROOM, mientras la batería central les transfiere energía (si tiene suficiente).
Cuando alcanzan su nivel de carga requerido (usoPorCarga), son liberados automáticamente mediante WAKE.
Cada vez que se ejecuta el ciclo de carga (cicloSolar):
Se calcula el nuevo caudal solar.
Se aumenta el nivel de batería con ese caudal.
Se recorre la lista de usuarios conectados (FOREACH) y se les transfiere carga si hay energía disponible.
Si un usuario ya ha sido cargado completamente, se libera del recurso.
Visualmente, todo esto se representa con:
Un depósito que sube y baja según el nivel de batería.
Un arco que representa la intensidad solar actual.
Un gráfico de evolución con los datos recolectados en cada paso.
Al finalizar la simulación, el gráfico bateriaPlot muestra la evolución de:
Nivel de batería (serie Y_0)
Caudal solar (serie Y_1)
Se puede observar cómo:
La batería se recarga durante el día y se vacía durante la noche.
Si hay muchos usuarios conectados, la batería puede agotarse antes de que vuelva a salir el sol.
El número de usuarios simultáneos conectados se puede monitorizar con un contador clásico (por ejemplo, TABULATE cargador si se desea).
Este tipo de simulación es lo que se denomina un modelo híbrido continuo/discreto. Combina:
Una evolución continua del entorno (como el sol)
Una lógica discreta de eventos, decisiones y ocupación de recursos (usuarios)
Este tipo de modelado es ideal para simular:
Energía y redes eléctricas
Producción intermitente y consumo variable
Procesos biológicos o químicos
Infraestructuras compartidas con restricciones
/*
Simulación híbrida
*/
SYSTEM {type:OPTIONS, TIME_DECIMALS:0, SPEED:5} ; Tiempo con dos decimales y simulación visual lenta
SYSTEM {TYPE:ON_TIMER, TRIGGER:cicloSolar, INTERVAL: 1} ; Ejecutar procedimiento 'cicloSolar' cada 1 unidad de tiempo
;--- Gráficas de resultados ---
PLOTTER {NAME:bateriaPlot, Y_0:nivelBateria, Y_1:caudalSolar, X:TIME_AC1} ; Curvas: nivel batería y caudal solar
;--- Función aleatoria de decisión para usuarios ---
Function {Name:decisionCarga, TYPE:POISSON, LAMBDA:1/4} ; Probabilidad de decidir cargar batería
;--- Posiciones gráficas ---
Position {NAME:Pos1,X:431,Y:511} ; Entrada al punto de carga
Position {NAME:Pos2,X:690,Y:510} ; Salida
Position {NAME:PosRecarga,X:578,Y:173} ; Punto de recarga (visual)
;--- Recurso de recarga de usuarios ---
Restroom {NAME:Cargador,X:555,Y:360,R_BIN_SIZE:1,R_BIN_COUNT:40} ; Permite 40 unidades de carga concurrentes
;--- Indicadores visuales ---
Graphic {NAME:tNivelBateria,Type:TEXT,X:329,Y:305,Text:"tNivel"} ; Texto: nivel batería
Graphic {NAME:gNivelCaudal,TYPE:ARC,X:411,Y:173,
fCOLOR:#FFFF99,RADIUS:50,START_ANGLE:0,END_ANGLE:45,CLOSE:1} ; Semicírculo: intensidad solar
Graphic {NAME:tCaudal,Type:TEXT,X:411,Y:173,Text:"tCaudal"} ; Texto: valor de caudal
Graphic {NAME:Cubo1,Type:LINE
,POINTS:"[100,100],[100,500],[ 200,500],[200,100]"
,Close:1,fcolor:#666666} ; Depósito batería
Graphic {NAME:Line1,Type:LINE,color:#00FFFF, X1:98,Y1:100,X2:202,Y2:100} ; Línea de nivel de batería
;--- Variables iniciales ---
INITIAL nivelBateria, 50
INITIAL caudalSolar, 0
INITIAL usoPorCarga, 15
START 2000
;==============================
; PROCEDIMIENTO CICLO SOLAR
;==============================
PROCEDURE cicloSolar
; Caudal solar: función sinusoidal normalizada entre 0 y 1.5
INTEGRATE { EXPRESSION: "0.75 + 0.75 * SIN((T / 1440) * 6.2832)", DT: 1, SAVEVALUE: caudalSolar }
; Incrementar batería (con límite 100)
SAVEVALUE nivelBateria, MIN(100, X$nivelBateria + X$caudalSolar)
; Recorrer entidades conectadas al cargador
FOREACH NUMERO,IN_RESOURCE,Cargador
if (X$nivelBateria>1)
; Aumentar batería interna del usuario
assign actual,P$(bateria,P$NUMERO) + 0.5
assign bateria,P$actual,P$NUMERO
; Restar batería del sistema
savevalue nivelBateria,X$nivelBateria - 0.5
; Si ya tiene batería suficiente, liberar al usuario
if (P$actual>X$usoPorCarga)
wake Cargador,0,P$NUMERO
endif
endif
ENDFOREACH
; Gráfico solar: arco proporcional al caudal
ASSIGN anguloSol, round((X$caudalSolar / 1.5) * 360, 1)
MOVE {NAME: gNivelCaudal, END_ANGLE: P$anguloSol}
; Actualizar línea visual del nivel de batería
MOVE {NAME:Line1, X1:98,Y1:(100+X$nivelBateria*4) ,X2:202,Y2:(100+X$nivelBateria*4)}
; Mostrar valores redondeados
assign tnivelBateriaRound,round(X$nivelBateria,2)
assign tCaudalRound,round(X$caudalSolar,2)
move {name:tNivelBateria,text:"Nivel actual: P$tNivelBateriaRound"}
move {name:tCaudal,text:"Caudal: P$tCaudalRound / 1.5"}
; Guardar datos para informe
PLOT bateriaPlot, X$nivelBateria, X$caudalSolar
TERMINATE
ENDPROCEDURE 1
;==============================
; USUARIOS
;==============================
GENERATE 5,5 {NAME:userGen, X:294, Y:514, ECOLOR:#000000} ; Llega un usuario cada ~5 unidades
assign bateria,X$usoPorCarga ; Define su necesidad de carga
ASSIGN decision, FN$decisionCarga ; Decide si necesita recargar (Poisson)
ADVANCE 10 {to:Pos1} ; Tiempo de conexión
IF (P$decision>=1)
assign bateria,0 ; Inicializa contador de carga
ADVANCE 10 {to:Cargador} ; Se mueve al punto de carga
rest Cargador ; Espera a estar cargado
ENDIF
ADVANCE 10 {to:Pos2} ; Sale
TERMINATE 1
Los sistemas lineales son aquellos que se resuelven con ecuaciones proporcionales y directas: si duplicas la entrada, se duplica la salida. Un ejemplo clásico es:
El padre tiene el doble de edad que su hijo, y el hijo es 20 años menor que el padre.
Este tipo de sistemas se resuelven paso a paso, de forma secuencial o lineal.
Pero en la realidad, muchos sistemas son no lineales, es decir, las variables están tan interrelacionadas que no pueden resolverse por separado. Requieren ser resueltas simultáneamente.
Ejemplo: Depósitos de agua unidos por un tubo
Supongamos dos depósitos de agua, uno con 10 litros y otro con 5 litros, unidos por un tubo.
Podríamos escribir:
volumen_A_Nuevo = volumen_A - flujo * DT ; DT es el incremento de tiempo volumen_B_Nuevo = volumen_B + flujo * DT flujo = K * (volumen_A - volumen_B) ; K es una constante sobre la capacidad de fluir del tubo
Donde:
K es una constante que define la permeabilidad del tuboDT es el paso de tiempo (delta t)Esto puede funcionar bien para dos depósitos. Pero si conectamos un tercer depósito en serie, el sistema ya no responde correctamente. A veces, el depósito central parece no fluir hacia el tercero, a pesar de tener diferencia de presión. Si intentamos ver la cantidad de flujo en el instante 0 entre el segundo y tercer depósito sería 0 mientras que en realidad, sí hay flujo.
Eso se debe a que las ecuaciones están acopladas. Resolverlas por separado no funciona.
La solución es tratarlos como un conjunto de ecuaciones no lineales y resolverlas todas juntas.
GPSS-Plus automatiza esto mediante el recurso DINAMIC que en cierto modo se parece a una Facility pero en lugar de gestionar entidades y colas, gestiona valores matemáticos.
Este recurso implementa:
INITIAL dynamic_config, {
EXPRESSIONS: [
"PIPE_FLUJO + PIPE_K * (TANK1_PRESION - TANK2_PRESION)",
"TANK1_PRESION - TANK1_PRESION_PREV - PIPE_FLUJO * DT",
"TANK2_PRESION - TANK2_PRESION_PREV + PIPE_FLUJO * DT"
],
STATES: ["PIPE_FLUJO", "TANK1_PRESION", "TANK2_PRESION"],
VARIABLES: ["PIPE_K", "TANK1_PRESION_PREV", "TANK2_PRESION_PREV"]
}
INITIAL dynamic_values, {
PIPE_K: 0.1,
TANK1_PRESION_PREV: 10,
TANK2_PRESION_PREV: 1
}
DYNAMIC {
name: sys,
CONFIG: V$dynamic_config,
VALUES: V$dynamic_values,
X: 300,
Y: 300,
TOLERANCE: 1e-6, ; valor por defecto
MAX_ITER: 10 ; valor por defecto
}
El bloque DYNAMIC permite declarar:
EXPRESSIONS: las ecuaciones del sistemaSTATES: variables a resolver (como presión, velocidad, temperatura)VARIABLES: parámetros auxiliares (constantes o valores anteriores)El sufijo _PREV en las variables se gestiona automáticamente: GPSS-Plus actualiza su valor en cada iteración.
SOLVE:
SOLVE {
name: "sys",
DT: 0.1,
SAVEVALUE: "resultado"
}
PROCEDURE agente.init
timeout cambiar_deposito, 10 ; si se desea cambiar algún parámetro en tiempo de ejecución
while (1==1)
SOLVE { name: "sys", DT: 0.1, SAVEVALUE: "resultado" }
SAVEVALUE TANK1_PRESION, X$(resultado.TANK1_PRESION)
SAVEVALUE TANK2_PRESION, X$(resultado.TANK2_PRESION)
SAVEVALUE PIPE_FLUJO, X$(resultado.PIPE_FLUJO)
PLOT presiones, X$TANK1_PRESION, X$TANK2_PRESION
PLOT flujo, X$PIPE_FLUJO
IF (ABS(X$PIPE_FLUJO) < 0.001)
stop
ENDIF
advance 0.1, 0
endwhile
stop
ENDPROCEDURE
Eventualmente, se pueden alterar los valores de VARIABLES con el uso de DYNAMIC_SET:
assign nuevosParams,{TANK2_PRESION_PREV:10}
dynamic_set sys,V$nuevosParams
En la sección del depuración se peude observar cómo cambian los valores de STATES y la configuración completa del DYNAMIC.
El resultado del PLOTTER es esclarecedor y muestra claramente cómo se equilibran los niveles de los depósitos con el paso del tiempo.
DYNAMIC permite modelar esos sistemas físicos con expresiones simples.Con esto, puedes modelar fácilmente fenómenos como:
/*
Sistemas No Lineales
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
PLOTTER {NAME:presiones, Y_0:tank1, Y_1:tank2, X:TIME_AC1}
PLOTTER {NAME:flujo, Y_0:flujo, X:TIME_AC1}
Graphic {NAME:tTexto,Type:TEXT,X:327,Y:486,Text:"tTexto"} ; Texto: nivel batería
INITIAL TANK1_PRESION, 10
INITIAL TANK2_PRESION, 1
INITIAL K, 0.1
INITIAL dynamic_config, {
EXPRESSIONS: [
"PIPE_FLUJO + PIPE_K * (TANK1_PRESION - TANK2_PRESION)",
"TANK1_PRESION - TANK1_PRESION_PREV - PIPE_FLUJO * DT",
"TANK2_PRESION - TANK2_PRESION_PREV + PIPE_FLUJO * DT"
],
STATES: [ "PIPE_FLUJO", "TANK1_PRESION", "TANK2_PRESION" ],
VARIABLES: [ "PIPE_K", "TANK1_PRESION_PREV", "TANK2_PRESION_PREV" ]
}
INITIAL dynamic_values, { PIPE_K: X$K
, TANK1_PRESION_PREV:X$TANK1_PRESION
, TANK2_PRESION_PREV:X$TANK2_PRESION
}
DYNAMIC {name:sys
,CONFIG:V$dynamic_config
,VALUES:V$dynamic_values
,X:300,Y:300
, TOLERANCE: 1e-6, MAX_ITER: 10
}
START 1000
include ./library_graphics/tank.lib
;==============================================================
PROCEDURE agente.init
timeout cambiar_deposito,10
advance 0.1
while (1==1)
move {name:tTexto, text:"flujo: P$rFlujo Tot: T1: P$rH1 T2: P$rH2 "}
SOLVE { name:"sys", DT: 0.1, SAVEVALUE: "resultado"}
SAVEVALUE TANK1_PRESION, X$(resultado.TANK1_PRESION)
SAVEVALUE TANK2_PRESION, X$(resultado.TANK2_PRESION)
SAVEVALUE PIPE_FLUJO, X$(resultado.PIPE_FLUJO)
PLOT presiones, X$TANK1_PRESION, X$TANK2_PRESION
PLOT flujo, X$PIPE_FLUJO
assign rFlujo,round(X$PIPE_FLUJO,3)
assign rH1,round(X$TANK1_PRESION,3)
assign rH2,round(X$TANK2_PRESION,3)
move {name:tTexto, text:"flujo: P$rFlujo Tot: T1: P$rH1 T2: P$rH2 "}
CALL tank1.tank.set, P$rH1
CALL tank2.tank.set, P$rH2
IF (ABS(X$PIPE_FLUJO) < 0.001)
stop
ENDIF
advance 0.1,0
endwhile
stop
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
call crear_depositos
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_depositos
assign config,{title:"TANK 1"
,x:100,y:50
,width:50 ,height:180
,value:0
,max_value:10
,"color":"#ff0000"}
timeout tank1.tank.init,0,V$config
assign config,{title:"TANK 2"
,x:400,y:50
,width:50 ,height:180
,value:0
,max_value:10
,"color":"#ff0000"}
timeout tank2.tank.init,0,V$config
ENDPROCEDURE
;=================================================
PROCEDURE cambiar_deposito
assign nuevosParams,{TANK2_PRESION_PREV:10}
dynamic_set sys,V$nuevosParams
TERMINATE_VE
ENDPROCEDURE
Vamos a ver cómo se resuelve este clásico que solo requiere de las ecuaciones y posicionar correctamente la masa con respecto a los componentes.
El ejemplo muestra cómo usar los gráficos 3D.
/*
Masa-muelle-amortiguador
*/
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:VISUAL, MODE:3D, V_WIDTH:70, V_HEIGHT:30, CAMERA:0}
UI {TYPE: BUTTON, id:botonA,TEXT: "Pulsar", LABEL: "Pulsar", TRIGGER: Pulsar}
UI {
TYPE: SLIDER, ID: unSlider, LABEL: "Velocidad",
VALUE: 15, MIN: 1, MAX: 50, STEP: 1,
TRIGGER: capturarVelocidad
}
PLOTTER {NAME:V_X, Y_0:V, Y_1:X, X:TIME_AC1}
Graphic {NAME:linea1,Type:LINE,X1:0,Y1:0,X2:0,Y2:500}
Graphic {NAME:linea2,Type:LINE,X1:0,Y1:0,X2:500,Y2:0}
Graphic {NAME:linea3,Type:LINE,X1:0,Y1:0,X2:0,Y2:0,Z2:500}
Graphic {NAME:tTexto,Type:TEXT,X:327,Y:486,Text:"tTexto"}
GRAPHIC {NAME:muelle, TYPE:OBJECT, src:SPRING, X:0, Y:15, Z:0, DEPTH:10, width:10, height:10, opacity:0.6}
GRAPHIC {NAME:masa, TYPE:SPHERE, X:0, Y:3, Z:0, radius:3, color:red}
GRAPHIC {NAME:amortiguador, TYPE:BOX, X:0, Y:0, Z:0, WIDTH:2, HEIGHT:20, DEPTH:2, color:blue}
GRAPHIC {NAME:amortiguadorB, TYPE:BOX, X:0, Y:10, Z:0, WIDTH:1.6, HEIGHT:20, DEPTH:1.6, color:cyan}
; FIJACIÓN inferior (visual)
GRAPHIC {NAME:soporte, TYPE:BOX, X:0, Y:-1, Z:0, WIDTH:20, HEIGHT:2, DEPTH:20, color:yellow}
INITIAL mech_config, {
EXPRESSIONS: [
"F_spring + F_damper + F_inertial", ; suma de fuerzas = 0 (equilibrio dinámico)
"F_spring - K * X", ; resorte
"F_damper - C * V", ; amortiguador
"F_inertial - M * (V - V_PREV) / DT", ; fuerza de inercia (aceleración)
"X - X_PREV - DT * V" ; integración explícita: posición
],
STATES: [
"X", ; posición
"V", ; velocidad
"F_spring", ; fuerza del muelle
"F_damper", ; fuerza de amortiguamiento
"F_inertial" ; fuerza de inercia
],
VARIABLES: [
"K", "C", "M", "X_PREV", "V_PREV"
]
}
INITIAL mech_values, {
K: 20, ; constante del muelle
C: 0.005, ; coef. de amortiguamiento
M: 1, ; masa
X_PREV: 0, ; posición inicial
V_PREV: 30 ; velocidad inicial
}
DYNAMIC {
name: sys,
CONFIG: V$mech_config,
VALUES: V$mech_values,
X: 300,
Y: 300,
TOLERANCE: 1e-6,
MAX_ITER: 10
}
START 1000
include ./library_graphics/speedometer.lib
;==============================================================
PROCEDURE agente.init
while (1==1)
SOLVE { name:"sys", DT: 0.05, SAVEVALUE: "resultado"}
SAVEVALUE V, X$(resultado.V)
SAVEVALUE X, X$(resultado.X)
SAVEVALUE I_cap , X$(resultado.I_cap)
assign x_masa,30 + X$X
; === Posición de la masa
MOVE {name:masa, Y:P$x_masa}
; === Estirar muelle desde base [0,0,0] hasta la masa
MOVE {name:muelle, STRETCH_BETWEEN:"[0,0,0],[0,P$x_masa,0]",rotate_y:P$x_masa*200}
; === Estirar amortiguador desde base también
MOVE {name:amortiguador, MOVE_BETWEEN:"[0,0,0],[0,P$x_masa,0]",y:P$x_masa -10}
PLOT V_X, X$V, X$X
assign rX,round(X$X,3)
assign rV,round(X$V,3)
CALL tank1.speedometer.set, P$rV
CALL tank2.speedometer.set, P$rX
IF (ABS(X$V) < 0.001)
stop
ENDIF
advance 0.05,0
endwhile
stop
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
CALL crear_indicadores
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_indicadores
assign config,{title:"V"
,x:20,y:15
,width:5 ,height:18
,value:0
,min_value:-30
,max_value:30
,"color":"#ff0000"
,font:"4px"}
call tank1.speedometer.init,V$config
assign config,{title:"X"
,x:50,y:15
,width:5 ,height:18
,value:0
,min_value:-10
,max_value:10
,"color":"blue"
,font:"4px"}
call tank2.speedometer.init,V$config
ENDPROCEDURE
;=================================================
PROCEDURE Pulsar
savevalue velocidad_ui,max(X$velocidad_ui,15)
assign nuevosParams,{V_PREV:-X$velocidad_ui}
dynamic_set sys,V$nuevosParams
TERMINATE_VE
ENDPROCEDURE
PROCEDURE capturarVelocidad
savevalue velocidad_ui,P$PARAM_B
TERMINATE_VE
ENDPROCEDURE
En simulaciones complejas, a menudo necesitamos modelar sistemas formados por múltiples componentes que interactúan entre sí, como depósitos conectados por tuberías, circuitos eléctricos o sistemas mecánicos acoplados. Para ello, GPSS-Plus utiliza el concepto de composición de sistemas mediante la COMPOSICIÓN.
La composición permite fusionar múltiples DYNAMIC individuales en un solo sistema dinámico. Esta fusión se realiza conectando sus variables según un esquema común de nodos, y resolviendo de forma simultánea todas sus ecuaciones internas.
Veamos un ejemplo práctico: tres depósitos conectados por dos tuberías, donde el depósito central tiene dos entradas. El esquema sería:
TANK1 --- PIPE12 --- TANK2 --- PIPE23 --- TANK3
Este sistema se define así:
INITIAL LIQUID_SYSTEM, {
COMPONENTS: ["pipe12", "pipe23", "tank1", "tank2", "tank3"],
CONNECTIONS: [
{ NODE: "n1", CONNECTIONS: ["tank1.A","pipe12.A"] },
{ NODE: "n2", CONNECTIONS: ["pipe12.B", "tank2.A"] },
{ NODE: "n3", CONNECTIONS: ["pipe23.A", "tank2.B"] },
{ NODE: "n4", CONNECTIONS: ["pipe23.B", "tank3.A"] }
]
}
Cada NODE indica un punto de conexión física entre uno o más componentes. A cada NODE se le asignará una variable de esfuerzo compartida (como presión o voltaje), y cada componente aportará sus propias ecuaciones internas y de flujo.
Tubería (PIPE):
INITIAL PIPE_CONFIG, {
EFFORTS: {
A: { NAME: "PresionA", UNIT: "Pa" },
B: { NAME: "PresionB", UNIT: "Pa" }
},
ROLES: {
QA: { ROLE: "FLOW", EXPOSED: ["A"] },
QB: { ROLE: "FLOW", EXPOSED: ["B"] },
K: { ROLE: "CONST" }
},
EXPRESSIONS: [
"QA + K * (PresionA - PresionB)", ; Flujo desde A a B
"QA + QB" ; Conservación de flujo interna
]
}
Todas y cada una de las variables que aparecen en EXPRESSIONS deben quedar defidas claramente en uno de los dos grupos.
ROLE que va a tener el resto de las variables del sistema.
FLOW puede exponerse en un puerto (una sola malla) o en dos (creando dos mallas distintas y una ecuación de conservación).
Tanque de dos puertos:
INITIAL TANK_2_PORTS_CONFIG, {"EFFORTS": {
"A": { "NAME": "PresionA", "UNIT": "Pa" },
"B": { "NAME": "PresionB", "UNIT": "Pa" }
},
"ROLES": {
"PresionA_PREV": { "ROLE": "const", "UNIT": "Pa" },
"IN1": { "ROLE": "flow", "EXPOSED": ["A"] },
"IN2": { "ROLE": "flow", "EXPOSED": ["B"] }
},
"EXPRESSIONS": [
"PresionA - PresionA_PREV - (IN1 + IN2) * DT", ; Conservación de masa/volumen
"PresionA - PresionB" ; Misma presión en los dos puertos
]
}
Después de declarar componentes y conexiones, simplemente se usa en COMANDO:
DYNAMIC {name:sys, compositor:V$LIQUID_SYSTEM, X:712, Y:54}
Este DYNAMIC genera automáticamente todas las ecuaciones necesarias combinando:
Variables de effort compartidas por nodo.
Variables de flow conectadas por puerto.
Reglas de conservación (de masa, energía, etc.)
El resultado que se puede ver en la zona de debug sería:
EXPRESSIONS: 1: FLOW_1 + PIPE12_K * (PA_N1 - PA_N2) 2: FLOW_1 + FLOW_2 3: FLOW_3 + PIPE23_K * (PA_N3 - PA_N4) 4: FLOW_3 + FLOW_4 5: PA_N1 - TANK1_PRESION_PREV - FLOW_1 * DT 6: PA_N2 - TANK2_PRESIONA_PREV - (FLOW_2 + FLOW_3) * DT 7: PA_N2 - PA_N3 8: PA_N4 - TANK3_PRESION_PREV - FLOW_4 * DT
Donde vemos que las variables de esfuerzo se han igualado con una para cada nodo y las de flujo se han numerado por malla.
Cada componente puede reutilizarse fácilmente con distintos valores.
Es posible construir bibliotecas completas de componentes (.lib) y combinarlas para construir modelos complejos sin reescribir expresiones.
El sistema cuida automáticamente los valores previos (_PREV), nombres unificados y mallas internas.
/*
Por componentes: COMPOSITOR
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:2, SPEED:5}
PLOTTER {NAME:presiones, Y_0:tank1, Y_1:tank2, Y_2:tank3, X:TIME_AC1}
PLOTTER {NAME:flujo, Y_0:flujo, X:TIME_AC1}
Graphic {NAME:tTexto,Type:TEXT,X:376,Y:579,Text:"tTexto"} ; Texto: nivel batería
include ./library_componets_liquid/liquid.lib
INITIAL PIPE12_VALUES, { K: 0.1 }
INITIAL PIPE23_VALUES, { K: 0.1 }
INITIAL TANK1_VALUES, { Presion_PREV: 10 }
INITIAL TANK2_VALUES, { PresionA_PREV: 0 }
INITIAL TANK3_VALUES, { Presion_PREV: 3 }
DYNAMIC {name:tank1, config:V$TANK_CONFIG, VALUES:V$TANK1_VALUES, X:113, Y:491}
DYNAMIC {name:tank2, config:V$TANK_2_PORTS_CONFIG, VALUES:V$TANK2_VALUES, X:396, Y:389}
DYNAMIC {name:tank3, config:V$TANK_CONFIG, VALUES:V$TANK3_VALUES, X:696, Y:342}
DYNAMIC {name:pipe12, config:V$PIPE_CONFIG, VALUES:V$PIPE12_VALUES, X:189, Y:333}
DYNAMIC {name:pipe23, config:V$PIPE_CONFIG, VALUES:V$PIPE23_VALUES, X:603, Y:512}
INITIAL LIQUID_SYSTEM, {
COMPONENTS: ["pipe12", "pipe23", "tank1", "tank2", "tank3"],
CONNECTIONS: [
{ NODE: "n1", CONNECTIONS: ["tank1.A","pipe12.A"] },
{ NODE: "n2", CONNECTIONS: ["pipe12.B", "tank2.A"] },
{ NODE: "n3", CONNECTIONS: ["pipe23.A", "tank2.B"] },
{ NODE: "n4", CONNECTIONS: ["pipe23.B", "tank3.A"] }
]
}
DYNAMIC {name:sys, compositor:V$LIQUID_SYSTEM, X:712, Y:54}
START 1000
include ./library_graphics/tank.lib
;==============================================================
PROCEDURE agente.init
timeout cambiar_deposito,2
advance 0.1
while (1==1)
SOLVE { name:"sys", DT: 0.01, SAVEVALUE: "resultado"}
SAVEVALUE TANK1_PRESION, X$(resultado.tank1_Presion)
SAVEVALUE TANK2_PRESION, X$(resultado.tank2_PresionA)
SAVEVALUE TANK3_PRESION, X$(resultado.tank3_Presion)
SAVEVALUE PIPE12, X$(resultado.pipe12_QA)
SAVEVALUE PIPE23, X$(resultado.pipe23_QA)
PLOT presiones, X$TANK1_PRESION, X$TANK2_PRESION, X$TANK3_PRESION
PLOT flujo, X$PIPE_FLUJO
assign rFlujo12,round(X$PIPE12,3)
assign rFlujo23,round(X$PIPE23,3)
assign rH1,round(X$TANK1_PRESION,3)
assign rH2,round(X$TANK2_PRESION,3)
assign rH3,round(X$TANK3_PRESION,3)
move {name:tTexto, text:"AC1$ flujos: P$rFlujo12 ----- P$rFlujo23 "}
CALL tank1.tank.set, P$rH1
CALL tank2.tank.set, P$rH2
CALL tank3.tank.set, P$rH3
IF (ABS(X$PIPE_FLUJO) < 0.001)
;stop
ENDIF
advance 0.01,0
endwhile
stop
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
CALL crear_depositos
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_depositos
assign config,{title:"TANK 1"
,x:100,y:50
,width:50 ,height:180
,value:0
,max_value:10
,"color":"#ff0000"}
timeout tank1.tank.init,0,V$config
assign config,{title:"TANK 2"
,x:300,y:50
,width:50 ,height:180
,value:0
,max_value:10
,"color":"#ff0000"}
timeout tank2.tank.init,0,V$config
assign config,{title:"TANK 3"
,x:500,y:50
,width:50 ,height:180
,value:0
,max_value:10
,"color":"#ff0000"}
timeout tank3.tank.init,0,V$config
ENDPROCEDURE
;=================================================
PROCEDURE cambiar_deposito
assign nuevosParams,{tank3_Presion_PREV:10}
dynamic_set sys,V$nuevosParams
TERMINATE_VE
ENDPROCEDURE
Una librería es un conjunto de componentes predefinidos con sus configuraciones y comportamientos matemáticos ya listos para usarse. En el caso de los componentes de GPSS-Plus se limita a guardar los CONFIG de cada elemento. Por ejemplo:
initial PIPE_CONFIG, {
TYPE: "PIPE",
EXPRESSIONS: [ "FLUJO + K * (PRESION_A - PRESION_B)" ],
STATES: ["FLUJO"],
VARIABLES: ["K"],
OWNED_VARS: { A: ["FLUJO"], B: ["-FLUJO"] },
REQUIRED_VARS: { A: ["PRESION_A"], B: ["PRESION_B"] }
}
Solo necesitamos realizar un INCLUDE del fichero que la contiene:
include ./library_componets_liquid/liquid.lib
Y usamos esos config como si los hubiñesemos escrito en el código principal.
initial pipe1_data, { K: 0.1 }
dynamic { name: pipe1, config: V$PIPE_CONFIG, values: V$pipe1_data, x: 100, y: 300 }
initial tank1_data, { PRESION_PREV: 10 }
dynamic { name: tank1, config: V$TANK_CONFIG, values: V$tank1_data, x: 50, y: 400 }
initial tank2_data, { PRESION_PREV: 5 }
dynamic { name: tank2, config: V$TANK_CONFIG, values: V$tank2_data, x: 200, y: 400 }
Reutilización y estandarización.
Reducción de errores.
Modelado más rápido y con mayor calidad.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:3, SPEED:1,pause:0}
PLOTTER {NAME:movimiento, Y_0:tank1, Y_1:tank2, X:TIME_AC1}
Graphic {NAME:tTexto,Type:TEXT,X:327,Y:486,Text:"tTexto"}
include ./library_componets_liquid/liquid.lib
INITIAL pipe1_DATA, { K: 0.1 }
DYNAMIC {name:pipe1,config:V$pipe_CONFIG,VALUES:V$pipe1_DATA,X:373,Y:418}
INITIAL tank1_DATA, { PRESION_PREV: 10 }
DYNAMIC {name:tank1,config:V$tank_CONFIG,VALUES:V$tank1_DATA,X:123,Y:357}
INITIAL tank2_DATA, { PRESION_PREV: 1 }
DYNAMIC {name:tank2,config:V$tank_CONFIG,VALUES:V$tank2_DATA,X:624,Y:347}
initial COMPOSITOR, {
COMPONENTS: [ "pipe1","tank1","tank2"],
CONNECTIONS: [ "TANK1.A=PIPE1.A", "TANK2.A=PIPE1.B"]
}
DYNAMIC {name:sys,compositor:V$COMPOSITOR,X:347,Y:164}
START 1
include ./library_graphics/tank.lib
;==============================================================
PROCEDURE agente.init
timeout cambiar_deposito,100
timeout cambiar_deposito2,200
while (1==1)
SOLVE { name:"sys", DT: 0.1, SAVEVALUE: "resultado"}
SAVEVALUE TANK1_PRESION, X$(resultado.TANK1_PRESION)
SAVEVALUE TANK2_PRESION, X$(resultado.TANK2_PRESION)
SAVEVALUE PIPE1_FLUJO, X$(resultado.PIPE1_FLUJO)
PLOT movimiento, X$TANK1_PRESION, X$TANK2_PRESION
assign rFlujo,round(X$PIPE_FLUJO,5)
assign rH1,round(X$TANK1_PRESION,5)
assign rH2,round(X$TANK2_PRESION,5)
move {name:tTexto, text:"flujo:\n P$rFlujo Tot: P$rH1 P$rH2 "}
CALL tank1.tank.set, X$TANK1_PRESION
CALL tank2.tank.set, X$TANK2_PRESION
IF (ABS(X$PIPE1_FLUJO) < 0.001)
stop
ENDIF
advance 1,0
endwhile
stop
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
CALL crear_depositos
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_depositos
assign config,{title:"TANK 1"
,x:100,y:100
,width:50 ,height:180
,value:X$TANK1_PRESION
,max_value:10
,"color":"#ff0000"}
call tank1.tank.init,V$config
assign config,{title:"TANK 2"
,x:500,y:100
,width:50 ,height:180
,value:X$TANK2_PRESION
,max_value:10
,"color":"#ff0000"}
call tank2.tank.init,V$config
ENDPROCEDURE
;=================================================
PROCEDURE cambiar_deposito
assign nuevosParams,{PIPE1_K:0.3}
dynamic_set sys,V$nuevosParams
TERMINATE_VE
ENDPROCEDURE
PROCEDURE cambiar_deposito2
assign nuevosParams,{TANK2_PRESION_PREV:10}
dynamic_set sys,V$nuevosParams
TERMINATE_VE
ENDPROCEDURE
Hemos visto varias fórmulas físicas, eléctricas y de fluidos descritas en DYNAMIC evolucionar en el SOLVE.
Resumidamente, introducíamos todas las fórmulas que debía satisfacer un sistema para ser resueltas a la vez. Así podíamos resolver, por ejemplo, el problema de los 3 tanques de líquidos conectados.
Veremos ahora una forma de utilizar ese mismo recurso para dar solución a la física con restricciones que no es otra cosa que lo que vemos en los videojuegos cuando dos objetos colisionan. De la misma manera que podemos simular múltiples objetos moviéndose entre ellos con ciertas restricciones de giro o distancia.
No introducimos un nuevo tipo de simulación, sino una forma distinta de describir otro problema.
Para hacernos una idea de cómo funciona, es como cualquier otra simulación, debemos observar qué sucede en el mundo real y tras ello, describirlo tal cual.
Los pasos suelen ser los siguientes:
1.- El objeto u objetos. Por simplificar, usaremos esferas y cubos perfectos. Pueden tener un peso y radio. Se sitúan en un lugar en el espacio.
2.- Su velocidad puntual. Debemos saber si está estático, con una cierta velocidad libre o está sometido a una fuerza que le confiere una aceleración. Es decir:
Integraremos la posición
x=x+vΔt
Y si hay fuerzas involucradas, también las fuerzas entrarán en juego:
v=v+aΔt
3.- La detección de la restricción. Si hablamos de un objeto que choca con una pared, debemos saberlo antes de efectuar alguna consecuencia. Así que se verificarán si estamos violando alguna restricción.
Si nuestro objeto fuese una esfera, verificaremos si choca o invade el espacio de:
Con pared: comparar coordenadas
Con esfera: distancia entre centros
Con caja: comparar intervalos (AABB)
Con formas más complejas: más geometría
4.- Las restricciones. Si hasta ahora hablamos de posición y velocidad del objeto, una restricción, sea la que sea, debe modificar ambas características. Si el objeto choca contra una pared, el incremento de x irá en la dirección contraria y la velocidad cambiará de signo. Una restricción no “impide”, corrige.
Cosa que no quita para que una restricción sea, por ejemplo, no superar cierta velocidad.
5.- Escalado. Cuando hay muchos objetos se hace inviable meter miles de objetos en el sistema y se realiza por partes.
Se requiere filtrar candidatos por métodos de rejillas o árboles..
El ejemplo:
Una sola masa esférica que bota contra las paredes que lo encierran.
Mostramos las fórmulas de las velocidades constantes y sus integrales para obtener las posiciones.
Los estados son los 4 nombrados: Posición (x,y) y sus velocidades (vx,vy)
Y tras esto, las restricciones. Que observemos la primera:
{
"VARIABLE":"VX_PREV",
"EXPRESSION":"(X <= (MINX+R) and VX < 0) ? -VX*E : ((X >= (MAXX-R) and VX > 0) ? -VX*E : VX)"
}
Significa qué vamos a hacer con una determinable variable si suceden ciertas condiciones. En este caso, qué le va a suceder a la variable "VX_PREV", que es uno de los estados previos a la siguiente iteración del solver.
Las variables *_PREV representan el estado que “hereda” el siguiente paso de SOLVE.
La salida será o dejarla como está en último termino (VX) o modificarla si suceden las condiciones establecidas.
Si vamos hacia la izquierda y hemos pasado el límite izquierdo, VX_PREV será -VX*E.
En caso contrario, comprobaremos la derecha siendo VX_PREV = -VX*E de nuevo.
Y como decíamos, si no hay restricciones que aplicar, VX_PREV será VX.
Nótese que "E" es la velocidad que conservo tras cada rebote, así que se usa como multiplicador para reducir la velocidad. Si E fuese 0, sería una parada en seco.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:VISUAL, MODE:3D, V_WIDTH:70, V_HEIGHT:50, CAMERA:0}
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
;SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Graphic {NAME:linea1,Type:LINE,X1:0,Y1:0,X2:0,Y2:500}
Graphic {NAME:linea2,Type:LINE,X1:0,Y1:0,X2:500,Y2:0}
Graphic {NAME:linea3,Type:LINE,X1:0,Y1:0,X2:0,Y2:0,Z2:500}
GRAPHIC {NAME:masa, TYPE:SPHERE, X:0, Y:3, Z:0, radius:3, color:red}
GRAPHIC {NAME:soporteIz, TYPE:BOX, X:-1, Y:30, Z:0, WIDTH:2, HEIGHT:60, DEPTH:20, color:yellow}
GRAPHIC {NAME:soporteDe, TYPE:BOX, X:31, Y:30, Z:0, WIDTH:2, HEIGHT:60, DEPTH:20, color:yellow}
GRAPHIC {NAME:soporteSu, TYPE:BOX, X:15, Y:61, Z:0, WIDTH:30, HEIGHT:2, DEPTH:20, color:yellow}
GRAPHIC {NAME:soporteIn, TYPE:BOX, X:15, Y:-1, Z:0, WIDTH:30, HEIGHT:2, DEPTH:20, color:yellow}
INITIAL pelota2D_config, {
EXPRESSIONS: [
; velocidad constante (sin fuerzas)
"VX - VX_PREV",
"VY - VY_PREV",
; integración explícita
"X - X_PREV - DT * VX",
"Y - Y_PREV - DT * VY"
],
STATES: [
"X","Y","VX","VY"
],
VARIABLES: [
"X_PREV","Y_PREV","VX_PREV","VY_PREV",
"R","E",
"MINX","MAXX","MINY","MAXY"
],
; Las CONDITIONS corrigen el estado después del paso libre (mover → corregir).
CONDITIONS: [
; --- pared izquierda/derecha ---
{
"VARIABLE":"VX_PREV",
"EXPRESSION":"(X <= (MINX+R) and VX < 0) ? -VX*E : ((X >= (MAXX-R) and VX > 0) ? -VX*E : VX)"
},
{
"VARIABLE":"X_PREV",
"EXPRESSION":"(X <= (MINX+R) and VX < 0) ? (MINX+R) : ((X >= (MAXX-R) and VX > 0) ? (MAXX-R) : X)"
},
; --- pared abajo/arriba ---
{
"VARIABLE":"VY_PREV",
"EXPRESSION":"(Y <= (MINY+R) and VY < 0) ? -VY*E : ((Y >= (MAXY-R) and VY > 0) ? -VY*E : VY)"
},
{
"VARIABLE":"Y_PREV",
"EXPRESSION":"(Y <= (MINY+R) and VY < 0) ? (MINY+R) : ((Y >= (MAXY-R) and VY > 0) ? (MAXY-R) : Y)"
}
]
}
INITIAL pelota2D_values, {
R: 3,
E: 0.8,
MINX: 0, MAXX: 30,
MINY: 0, MAXY: 60,
X_PREV: 20,
Y_PREV: 20,
VX_PREV: 12,
VY_PREV: 7
}
DYNAMIC {
name: box2d,
CONFIG: V$pelota2D_config,
VALUES: V$pelota2D_values,
X: 200,
Y: 200
}
START 1000
include ./library_graphics/speedometer.lib
;==============================================================
PROCEDURE agente.init
while (1==1)
SOLVE { name:"box2d", DT: 0.1, SAVEVALUE: "resultado"}
SAVEVALUE X, X$(resultado.X)
SAVEVALUE Y, X$(resultado.Y)
; === Posición de la masa
MOVE {name:masa, X:X$X , Y:X$Y}
assign vx,round(X$(resultado.VX),3)
assign vy,round(X$(resultado.VY),3)
CALL indi1.speedometer.set, P$vx
CALL indi2.speedometer.set, P$vy
; CONDICIÓN DE PARADA
; IF (ABS(P$vx) < 0.001)
; stop
; ENDIF
advance 0.1,0
endwhile
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
CALL crear_indicadores
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_indicadores
assign config,{title:"VX"
,x:50,y:15
,width:5 ,height:18
,value:0
,min_value:-15
,max_value:15
,"color":"#ff0000"
,font:"3px"}
call indi1.speedometer.init,V$config
assign config,{title:"VY"
,x:80,y:15
,width:5 ,height:18
,value:0
,min_value:-15
,max_value:15
,"color":"blue"
,font:"3px"}
call indi2.speedometer.init,V$config
ENDPROCEDURE
;=================================================
En dinámica de sistemas, una restricción es una condición que debe cumplirse en todo momento durante la simulación. A diferencia de una expresión derivada del comportamiento natural de un componente, la restricción no cambia con el tiempo (no depende de la evolución de las variables), sino que impone una relación fija entre ellas.
Esto se conoce también como restricción algebraica y convierte el sistema en un sistema de ecuaciones diferenciales algebraicas (DAE).
Supongamos dos masas conectadas por muelles y amortiguadores, pero además unidas por una barra rígida que mantiene la distancia constante entre ambas.
Esta condición puede expresarse como:
X2 - X1 = L
Donde:
X1 y X2 son las posiciones de las masasL es la longitud de la barraEsta igualdad debe mantenerse durante toda la simulación. Para ello, se introduce una fuerza de restricción, calculada mediante un multiplicador de Lagrange (Lambda) que se añade a las ecuaciones del sistema.
INITIAL mech_config, {
EXPRESSIONS: [
; Leyes de Newton para cada masa
"F1 - M1 * (V1 - V1_prev) / DT",
"F2 - M2 * (V2 - V2_prev) / DT",
; Fuerzas internas: muelle y amortiguador
"FD1 - C1 * V1",
"F_spring1 - K1 * X1",
"FD2 - C2 * V2",
"F_spring2 - K2 * (X2 - X1)",
; Fuerza de restricción impuesta por Lagrange
"F_restriction + Lambda",
; Suma de fuerzas sobre cada masa
"F1 + F_spring1 + FD1 - F_spring2 - F_restriction",
"F2 + F_spring2 + FD2 + F_restriction",
; Integración de posición
"X1 - X1_prev - DT * V1",
"X2 - X2_prev - DT * V2",
; ***** Restricción geométrica *****
"X2 - X1 - L"
],
STATES: [
"X1", "X2",
"V1", "V2",
"F1", "F2",
"FD1", "FD2",
"F_spring1", "F_spring2",
"F_restriction",
"Lambda" ; variable de Lagrange
],
VARIABLES: [
"M1", "M2",
"K1", "K2",
"C1", "C2",
"L",
"X1_prev", "X2_prev",
"V1_prev", "V2_prev"
]
}
La ecuación "X2 - X1 - L" es una ecuación de restricción.
El sistema debe resolver todas las ecuaciones simultáneamente (incluyendo la restricción), y para ello introduce Lambda, una variable interna que representa la fuerza necesaria para mantener esa condición en todo instante.
Esta fuerza luego se suma (o resta) a las ecuaciones de fuerza total en cada masa:
"F1 + ... - F_restriction" "F2 + ... + F_restriction"
Esto asegura que si el sistema tiende a violar la restricción (por una aceleración, por ejemplo), la fuerza de restricción reacciona inmediatamente para mantenerla.
Todo esto se resuelve en el DYNAMIC, con el uso interno del método de Newton-Raphson y matrices jacobianas, que permiten resolver tanto variables dinámicas (X1, V1, etc.) como variables algebraicas (Lambda) en un mismo sistema.
Las restricciones permiten simular comportamientos realistas y complejos, como:
Barras rígidas
Pistones hidráulicos con volumen constante
Relaciones geométricas en mecanismos
Conservación de energía o momento
El uso de Lambda como multiplicador de Lagrange es una herramienta clave para introducirlas de forma explícita y controlada en tus sistemas dinámicos.
/*
Restricciones en Sistemas Dinamicos
*/
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {type:OPTIONS, TIME_DECIMALS:1, SPEED:5}
PLOTTER {NAME:posiciones, Y_0:X1, Y_1:X2, X:TIME_AC1}
Graphic {NAME:tTexto,Type:TEXT,X:327,Y:486,Text:"tTexto"}
INITIAL mech_config, {
EXPRESSIONS: [
"F1 - M1 * (V1 - V1_PREV) / DT",
"FD1 - C1 * V1",
"F_spring1 - K1 * X1",
"F_spring2 - K2 * (X2 - X1)",
"FD2 - C2 * V2",
"F2 - M2 * (V2 - V2_PREV) / DT",
"F_restriction + Lambda", ; fuerza de restricción (con signo)
"F1 + F_spring1 + FD1 - F_spring2 - F_restriction",
"F2 + F_spring2 + FD2 + F_restriction",
"X1 - X1_PREV - DT * V1",
"X2 - X2_PREV - DT * V2",
"X2 - X1 - L" ; restricción geométrica
],
STATES: [
"X1", "X2",
"V1", "V2",
"F1", "F2",
"FD1", "FD2",
"F_spring1", "F_spring2",
"F_restriction",
"Lambda"
],
VARIABLES: [
"M1", "M2",
"K1", "K2",
"C1", "C2",
"L",
"X1_PREV", "X2_PREV",
"V1_PREV", "V2_PREV"
]
}
INITIAL mech_values, {
M1: 1.5,
M2: 1.5,
K1: 5,
K2: 8,
C1: 0.1,
C2: 0.1,
X1_PREV: 1.5,
V1_PREV: 2,
X2_PREV: 3.5,
V2_PREV: -1,
L: 2 ; restricción de distancia entre masas
}
DYNAMIC {
name: sys,
CONFIG: V$mech_config,
VALUES: V$mech_values,
X: 300,
Y: 300,
TOLERANCE: 1e-6,
MAX_ITER: 10
}
START 1000
include ./library_graphics/speedometer.lib
;==============================================================
PROCEDURE agente.init
while (1==1)
SOLVE { name:"sys", DT: 0.1, SAVEVALUE: "resultado"}
SAVEVALUE X1, X$(resultado.X1)
SAVEVALUE X2, X$(resultado.X2)
SAVEVALUE V1, X$(resultado.V1)
PLOT posiciones, X$X1, X$X2
assign rX1,round(X$X1,5)
assign rX2,round(X$X2,5)
CALL tank1.speedometer.set, P$rX1
CALL tank2.speedometer.set, P$rX2
IF (ABS(X$V1) < 0.00001)
stop
ENDIF
advance 0.1,0
endwhile
stop
ENDPROCEDURE
;====================================================================
PROCEDURE PRE_RUN
CALL crear_indicadores
TIMEOUT agente.init,0
TERMINATE_VE
ENDPROCEDURE
;=================================================
PROCEDURE crear_indicadores
assign config,{title:"X1"
,x:100,y:50
,width:50 ,height:180
,value:0
,min_value:-5
,max_value:5
,"color":"#ff0000"}
call tank1.speedometer.init,V$config
assign config,{title:"X2"
,x:400,y:50
,width:50 ,height:180
,value:0
,min_value:-5
,max_value:5
,"color":"purple"}
call tank2.speedometer.init,V$config
ENDPROCEDURE
;=================================================
Hasta ahora, trabajábamos construyendo modelos que operaban dentro de un universo cerrado y controlado dentro del motor de GPSS-Plus. El motor de simulación decidía cuándo avanzaba el tiempo, qué ocurría en cada momento y cómo se comportaban los elementos del sistema. Era un entorno lógico, determinista y autónomo.
Vamos a romper con todo eso con la entrada en escena del mundo real. Ya nada será lo mismo: el enfoque del motor cambia, las entidades vivirán según otros patrones y los flujos dejarán de estar solo bajo nuestro control.
Las entidades no deberían avanzar porque lo decide un número aleatorio, sino porque un sensor físico ha detectado su presencia.
El tiempo
Una de las primeras consecuencias de esta apertura al exterior es que el tiempo deja de pertenecernos.
Ya no podemos acelerarlo ni pausarlo arbitrariamente. Si el motor quiere leer de un sensor, deberá esperar lo que ese sensor tarde en responder. Si la lectura de un fichero se retrasa, también lo hará la simulación. Por eso, al trabajar con el mundo real, la ejecución debe ser en tiempo real.
Lo que antes era solo un “momento”, ahora es estrictamente un segundo del reloj del sistema. Se declara así:
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Un ADVANCE 10 significa exactamente espera 10 segundos reales.
A partir de ahí, no hay marcha atrás: todo ocurre al ritmo del mundo físico, y la simulación se convierte en una parte subordinada de la realidad.
El motor deja de ser un simulador puro y pasa a comportarse como un orquestador de procesos reales
La nueva arquitectura del sistema
Podemos identificar ahora tres grandes componentes del sistema:
BRIDGER y cualquier elemento del mundo exterior.EL BRIDGER:
Desde el punto de vista de una entidad, además del COMANDO BRIDGER, solo tiene 5 BLOQUES:
Estos BLOQUES son los que se comunican con el middleware.js
EL MIDDLEWARE.js
Disponible en el capítulo de descargas.
Es un conjunto de archivos en javascript para ejecutar en entorno node.js que recibe las instrucciónes del BRIDGER y se las retransmite al dispositivo exterior y viceversa.
En resumen, middleware.js funciona como una capa de intercambio entre protocolos, transformando los cinco bloques definidos en GPSS-Plus en mensajes compatibles con MQTT (el más utilizado, basado en topics) y OPC-UA (el más completo, con estructuras jerárquicas y tipos de dato definidos).
Para las pruebas iniciales existe un servicio público operativo en:
wss://bridger.gpss-plus.com:3000
EL mundo exterior
Son todos los sensores, actuadores, dispositivos y recursos externos.
Para pruebas, dispones de un cliente virtual OPC-UA que representa un conjunto de sensores y servicios simulados.
Su dirección es: opc.tcp://opcua.gpss-plus.com:4840
Contiene una báscula, un sensor de puerta, sensores de distancia, control PID, lectura y escritura de archivos, e incluso ejecución de programas en segundo plano.
Naturalmente, puedes modificar y ampliar el código para añadir tus propios servicios.
La topología:
Estos tres elementos se distribuyen de la siguiente manera:
gpss-plus 192.168.x.x 192.168.n.m
BRIDGER <-> middleware.js <-> Sensor temperatura
192.168.a.b | Sistema de Ficheros
(El navegador) | Base de datos MySQL
|
| 192.168.n.m
| 192.168.y.y Generador de números aleatorios
<-> middleware.js <-> Red neuronal
| Detector de movimiento OPC-UA
|
|
| 192.168.z.z 192.168.n.m
<-> middleware.js <-> Semáforo entre sistemas
En resumen, el navegador que ejecuta GPSS-Plus debe poder acceder a la IP donde esté instalado middleware.js, y este, a su vez, al dispositivo o servicio externo.
La configuración más estable es tener middleware.js en la misma red que los sensores.
En los siguientes capítulos veremos cómo manejar el BRIDGER paso a paso y construiremos nuestros propios dispositivos físicos.
En GPSS-Plus llamamos recurso externo a cualquier elemento del sistema que no forma parte directamente del motor de simulación, pero con el que las entidades pueden interactuar.
Estos recursos pueden actuar como fuentes de datos o como actuadores reales dentro del entorno físico o digital.
Ejemplos típicos incluyen:
Un archivo en el sistema de ficheros (FS).
Una tabla MySQL o cualquier base de datos accesible por red.
Un sensor físico, como un detector de temperatura o movimiento, conectado mediante OPC-UA o MQTT.
Una API REST que devuelve estados o acepta comandos.
Un dispositivo industrial o electrónico, como una báscula, un motor o una célula de carga.
En resumen, todo aquello que vive fuera del motor pero puede influir en él se considera un recurso externo.
GPSS-Plus no diferencia entre un sensor y una base de datos.
Ambos son tratados de forma uniforme mediante el recurso BRIDGER, que actúa como interfaz universal de acceso.
Esto simplifica enormemente el modelo mental:
puedes sustituir un sensor real por una tabla MySQL, o por un archivo local, sin reescribir tu modelo, simplemente cambiando la configuración del BRIDGER.
OPC-UA (Open Platform Communications – Unified Architecture) es un estándar industrial para la comunicación estructurada entre dispositivos y sistemas.
Permite intercambiar nodos, atributos y eventos de manera segura, jerárquica y extensible.
En GPSS-Plus puedes conectarte tanto a un servidor OPC-UA real, como al dispositivo virtual incluido para pruebas y desarrollo sin hardware físico.
Este dispositivo virtual expone nodos simulados como:
Sensor_Temp_1
Motor_X.Status
Contador_Piezas
Puerta_Acceso_A.IsOpen
Podrás leer, escribir o suscribirte a sus cambios usando siempre el mismo mecanismo del BRIDGER.
En los siguientes capítulos veremos cómo:
Detectar eventos, como la apertura de una puerta simulada.
Acceder a sistemas de archivos o servicios en línea de solo lectura.
Ejecutar aplicaciones o scripts remotos.
Tanto el dispositivo OPC-UA virtual como el middleware.js pueden descargarse y modificarse libremente para adaptarlos a tus propias necesidades o hardware real.
Veamos un ejemplo concreto para entender cómo fluye la información entre GPSS-Plus, middleware.js y el servidor OPC-UA.
Vamos a modificar el comportamiento de un bloque GENERATE para que las entidades se creen cuando un sensor físico (simulado) detecte la apertura de una puerta —en lugar de hacerlo por parámetros de tiempo o probabilidad.
Lo primero es definir el BRIDGER, que será el enlace con el mundo exterior:
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:333,Y:444
,SERVER:"wss://bridger.gpss-plus.com:3000"
,CLIENT:"opc.tcp://opcua.gpss-plus.com:4840"
,OPTIONS:V$options
,ON_ERROR:bridge1_on_error
}
Los parámetros principales son:
bridge1).middleware.js.Más adelante veremos que tanto el middleware.js como el cliente OPC-UA virtual pueden modificarse para adaptarse a otras topologías o protocolos.
Una vez definido el puente, debemos iniciar la conexión dentro del PRE_RUN del modelo, y suscribirnos a la variable que representa el estado de la puerta:
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "Sensor_Door_IsOpen"
, TRIGGER: on_open
}
Esta instrucción mantiene un canal abierto con el sensor para recibir notificaciones automáticas cada vez que cambie su valor.
Donde de nuevo, los parámetros son mínimos:
Las suscripciones permiten recibir datos en tiempo real sin necesidad de ejecutar lecturas periódicas.
Cada vez que se recibe un nuevo valor, se ejecuta el trigger indicado.
El parámetro P$(PARAM_A.VALUE) contendrá el valor actual del sensor (0 o 1 en este caso). PARAM_B contiene el número de la entidad invocante y PARAM_C el tiempo.
procedure on_open
move {name:Text1,TEXT:"DOOR P$(PARAM_A.VALUE)"}
if (P$(PARAM_A.VALUE.EXIST) && P$(PARAM_A.VALUE)==1)
NEW GEN1
endif
TERMINATE_VE
endprocedure
En este ejemplo, cada vez que el sensor detecta que la puerta se abre (valor = 1), el sistema crea una nueva entidad GEN1, simulando la entrada de una persona.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Facility {NAME:facility1,X:446,Y:514}
Graphic {Type:TEXT,Name:Text1,X:376,Y:358}
Graphic {Type:TEXT,Name:TextError,X:376,Y:330,Text:"On error...."}
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:375,Y:398
,SERVER:"wss://bridger.gpss-plus.com:3000"
,CLIENT:"opc.tcp://opcua.gpss-plus.com:4840"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:264,Y:511}
POSITION {NAME:POS2,X:658,Y:509}
START 100
;*****************************************************
PROCEDURE PRE_RUN
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "Sensor_Door_IsOpen"
, TRIGGER: on_open
}
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 0,0,0,0 {NAME:GEN1,X:141,Y:351}
ADVANCE 5,0 {TO:POS1}
ADVANCE 5,0 {TO:facility1}
SEIZE facility1
advance 10,10
RELEASE facility1
ADVANCE 10,0 {TO:POS2}
ENDGENERATE 1
;----------------------------------------------------------------------------
procedure on_open
move {name:Text1,TEXT:"DOOR P$(PARAM_A.VALUE)"}
if (P$(PARAM_A.VALUE.EXIST) && P$(PARAM_A.VALUE)==1)
NEW GEN1
endif
TERMINATE_VE
endprocedure
;----------------------------------------------------------------------------
procedure bridge1_on_error
move {name:TextError,TEXT:"P$PARAM_A"}
TERMINATE_VE
endprocedure
Hemos visto que en el OPC-UA virtual podíamos leer repetidamente el valores de la variable "Sensor_Door_IsOpen".
Sin embargo, no siempre sabremos qué variables o métodos existen dentro de un dispositivo. Por ejemplo, un servicio de archivos podría listar directorios, o una base de datos podría exponer tablas cuyos nombres cambian.
Para eso existe el bloque BRIDGE_BROWSE, que permite consultar la estructura interna del dispositivo o servicio conectado.
¿Por qué usar BRIDGE_BROWSE?
El comando BRIDGE_BROWSE sirve para descubrir:
En otras palabras, es el mapa de capacidades del dispositivo.
Por lo que de los 5 BLOQUES del BRIDGER vamos a ver qué se obtiene de BRIDGE_BROWSE.
BRIDGE_BROWSE se ejecuta automáticamente durante la primera conexión, pero también puede invocarse manualmente si se desea explorar un nuevo dispositivo o verificar qué ha cambiado.
Al ejecutar BRIDGE_BROWSE, el middleware devuelve un listado jerárquico de las variables accesibles:
Sensor_Motion_Detected: ns=1;i=1000 [Boolean] Sensor_Door_IsOpen: ns=1;i=1001 [Boolean] PIDControl_CurrentTemperature: ns=1;i=1002 [Double] PIDControl_Setpoint: ns=1;i=1003 [Double] (writable) PIDControl_PIDOutput: ns=1;i=1004 [Double] Device_Motor_Enabled: ns=1;i=1005 [Boolean] (writable) System_Battery_BatteryLevel: ns=1;i=1006 [Double] Device_Scale_Weight: ns=1;i=1007 [Double] Device_Scale_Trigger: ns=1;i=1008 [Boolean] (writable) Device_Piston_IsExtended: ns=1;i=1009 [Boolean] Device_Piston_Trigger: ns=1;i=1010 [Boolean] (writable) ... ...
Cada línea muestra:
ns=1;i=1000, típico en OPC-UA).(writable)).
Antes de interactuar con un nuevo dispositivo, ejecuta BRIDGE_BROWSE una vez para conocer su estructura.
Después podrás usar BRIDGE_READ, BRIDGE_WRITE o BRIDGE_SUBSCRIBE de forma precisa sobre las variables que hayas identificado.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Graphic {Type:TEXT,Name:Text1,X:653,Y:294}
Graphic {Type:TEXT,Name:TextError,X:440,Y:329,Text:"On error...."}
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:443,Y:405
,SERVER:"wss://bridger.gpss-plus.com:3000"
,CLIENT:"opc.tcp://opcua.gpss-plus.com:4840"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:209,Y:347}
START 1
;*****************************************************
PROCEDURE PRE_RUN
BRIDGE_BROWSE { NAME: bridge1
, TRIGGER: on_browse
}
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 10,0,0,1 {NAME:GEN1,X:141,Y:351}
ADVANCE 5,0 {TO:POS1}
ENDGENERATE 1
;----------------------------------------------------------------------------
procedure on_browse
move {name:Text1,TEXT:"BROWSE P$(PARAM_A)"}
TERMINATE_VE
endprocedure
;----------------------------------------------------------------------------
procedure bridge1_on_error
move {name:TextError,TEXT:"P$PARAM_A"}
TERMINATE_VE
endprocedure
Hasta ahora hemos recibido datos del mundo exterior.
Ahora aprenderemos a enviar instrucciones y leer valores específicos desde GPSS-Plus hacia un dispositivo OPC-UA, MQTT o cualquier otro conectado por el middleware.js
Dar una orden a un actuador es tan simple como asignar un valor y enviarlo:
ASSIGN values, { Device_Motor_Enabled: 1 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
Esto escribe en la variable Device_Motor_Enabled del dispositivo, encendiendo el motor (1 = ON, 0 = OFF).
La estructura del comando es siempre la misma:
La lectura se realiza de forma análoga:
ASSIGN values, ["Device_Motor_Enabled"]
BRIDGE_READ {name:bridge1, trigger:on_read, VALUES:V$values}
En este caso, la lista ["Device_Motor_Enabled"] indica qué variables queremos consultar.
El valor llegará de forma asíncrona, y se recibirá dentro del trigger correspondiente (on_read).
Por defecto, las operaciones READ y WRITE son asíncronas:
el motor GPSS-Plus continúa su ejecución sin detenerse a esperar la respuesta del dispositivo.
Esto es lo habitual en sistemas físicos, donde la comunicación puede tardar varios cientos de milisegundos.
Si el recurso es inmediato (por ejemplo, un fichero local o una base de datos interna), puedes forzar una operación síncrona con:
BRIDGE_READ { name:bridge1, VALUES:V$values, SYNC:1, trigger:on_read }
De este modo, la entidad y el motor se bloquea hasta recibir la respuesta.
Debe usarse solo en contextos controlados, ya que detener el tiempo del simulador afecta a todo el sistema.
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Graphic {Type:TEXT,Name:Text1,X:653,Y:294}
Graphic {Type:TEXT,Name:Text2,X:653,Y:194}
Graphic {Type:TEXT,Name:TextError,X:440,Y:329,Text:"On error...."}
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:443,Y:405
,SERVER:"wss://bridger.gpss-plus.com:3000"
,CLIENT:"opc.tcp://opcua.gpss-plus.com:4840"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:209,Y:347}
POSITION {NAME:POS2,X:409,Y:347}
START 100
;*****************************************************
GENERATE 3,0,0,10 {NAME:GEN1,X:141,Y:351}
ASSIGN values, ["Device_Motor_Enabled"]
BRIDGE_READ {name:bridge1, trigger:on_read, VALUES:V$values}
ADVANCE 5,0 {TO:POS1}
if (D$N %2 == 0 )
ASSIGN values, { Device_Motor_Enabled: 0 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
else
ASSIGN values, { Device_Motor_Enabled: 1 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
endif
ADVANCE 5,0 {TO:POS2}
ENDGENERATE 1
;----------------------------------------------------------------------------
procedure on_write
move {name:Text1,TEXT:"AC1$ WRITE P$(PARAM_A)"}
TERMINATE_VE
endprocedure
;----------------------------------------------------------------------------
procedure on_read
move {name:Text2,TEXT:"AC1$ READ P$(PARAM_A) \n:: Valor: P$(PARAM_A.Device_Motor_Enabled)"}
TERMINATE_VE
endprocedure
;----------------------------------------------------------------------------
procedure bridge1_on_error
move {name:TextError,TEXT:"AC1$ P$PARAM_A"}
TERMINATE_VE
endprocedure
En este ejemplo vamos a ver todos los elementos del sistema en funcionamiento.
El modelo representa una línea de producción automatizada en la que los paquetes llegan uno a uno, son pesados en una báscula, y según su peso son desviados hacia un camino u otro mediante un pistón neumático.
Detección de entrada
Un sensor de puerta detecta la llegada de un nuevo paquete (simboliza un producto que entra en la zona).
Cada detección genera una nueva entidad en GPSS-Plus.
Control de flujo unitario
Un restroom actúa como liberador unitario, permitiendo que los paquetes pasen de uno en uno hacia la báscula.
El siguiente paquete solo avanza cuando el anterior ha terminado el pesaje.
Pesaje
Al llegar a la báscula, la entidad activa el sensor (Device_Scale_Trigger = 1).
Cuando el peso se estabiliza, el sistema asigna el valor leído a la entidad.
Clasificación
Si el peso supera un umbral (pesoLimite), se activa el pistón (Device_Piston_Trigger = 1) que desvía el paquete hacia la zona de pesados.
Si no, continúa recto hacia la zona de ligeros.
Sincronización con actuadores físicos
El pistón se retrae automáticamente.
GPSS-Plus se suscribe al estado del pistón (Device_Piston_IsExtended) para saber cuándo está listo para el siguiente ciclo.
Arquitectura del sistema:
[Sensor Puerta] -> genera entidad
↓
[Liberador Unitario]
↓
[Báscula] ←→ OPC-UA Device_Scale_Weight
↓
[Separador / Pistón]
↓
[Salida Ligeros] / [Salida Pesados]
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Restroom {NAME:LiberadorUnitario,X:155,Y:332}
Queuer {NAME:zonaBascula,X:163,Y:32}
Graphic {NAME:lineZonaBascula,Type:LINE,color:#FF0000, X1:82,Y1:64,X2:260,Y2:65}
Restroom {NAME:RestroomBascula,X:226,Y:110}
initial pesoLimite,8
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:733,Y:556
,SERVER:"wss://bridger.gpss-plus.com:3000"
,CLIENT:"opc.tcp://opcua.gpss-plus.com:4840"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:97,Y:437}
POSITION {NAME:POS2,X:95,Y:183}
POSITION {NAME:PosBascula,X:225,Y:199}
POSITION {NAME:PosSeparador,X:387,Y:201}
POSITION {NAME:PosPesados,X:513,Y:302}
POSITION {NAME:PosPesados2,X:594,Y:302}
POSITION {NAME:PosLigeros,X:511,Y:197}
POSITION {NAME:PosLigeros2,X:592,Y:197}
Graphic {NAME:Text1,Type:TEXT,X:167,Y:484,Text:"On OPEN"}
Graphic {NAME:TextError,Type:TEXT,X:424,Y:582,Text:"Error"}
Graphic {NAME:lineBascula,Type:LINE,color:#FF0000, X1:191,Y1:176,X2:263,Y2:176}
Graphic {NAME:txtBascula,Type:TEXT,X:226,Y:156,Text:"bascula"}
Graphic {NAME:lineSeparador,Type:LINE,color:#FF0000, X1:363,Y1:201,X2:432,Y2:201}
Graphic {NAME:txtSeparador,Type:TEXT,X:394,Y:143,Text:"separador"}
START 10
;*****************************************************
PROCEDURE PRE_RUN
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "Sensor_Door_IsOpen"
, TRIGGER: on_open
}
savevalue pesoActual,0
savevalue pesoAnterior,X$pesoActual
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "Device_Scale_Weight"
, TRIGGER: on_subscription_bascula
}
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "Device_Piston_IsExtended"
, TRIGGER: on_subscription_piston
}
ASSIGN values, { Device_Scale_Trigger: 0 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 0,0,0,0 {NAME:GEN1,X:91,Y:547}
ADVANCE 5,0 {TO:POS1,flow:1}
ADVANCE 5,0 {TO:LiberadorUnitario,flow:1}
if (R$(zonaBascula,in)>0)
rest LiberadorUnitario
endif
queue zonaBascula
ADVANCE 5,0 {TO:POS2,flow:1}
ADVANCE 5,0 {TO:PosBascula,flow:1}
ASSIGN values, { Device_Scale_Trigger: 1 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
rest RestroomBascula
depart zonaBascula
call liberarRestroomBascula
ADVANCE 2
mod {subtitle:"P$peso Kg"}
if (P$peso > X$pesoLimite)
mod {color:"red"}
else
mod {color:"green"}
endif
ASSIGN values, { Device_Scale_Trigger: 0 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
timeout moverPiston,4.3,P$peso
ADVANCE 5,0 {TO:PosSeparador,flow:1}
if (P$peso > X$pesoLimite)
ADVANCE 3,0 {TO:PosPesados}
ADVANCE 3,0 {TO:PosPesados2,flow:1}
else
ADVANCE 3,0 {TO:PosLigeros}
ADVANCE 3,0 {TO:PosLigeros2,flow:1}
endif
ENDGENERATE 1
;----------------------------------------------------------------------------
procedure on_open
move {name:Text1,TEXT:"DOOR P$(PARAM_A.VALUE)"}
if (P$(PARAM_A.VALUE.EXIST))
if (P$(PARAM_A.VALUE)==1)
NEW GEN1
endif
endif
TERMINATE_VE
endprocedure
procedure bridge1_on_error
move {name:TextError,TEXT:"P$PARAM_A"}
TERMINATE_VE
endprocedure
;-------------------------
procedure on_subscription_bascula
savevalue pesoActual,P$(PARAM_A.VALUE)
move {name:txtBascula,TEXT:"X$pesoActual Kg"}
if (ABS(X$pesoAnterior-X$pesoActual)<=0.1 && X$pesoActual>1)
FOREACH entidad, IN_RESOURCE, RestroomBascula
assign peso,round(X$pesoActual,1), P$entidad
ENDFOREACH
wake RestroomBascula
endif
savevalue pesoAnterior,X$pesoActual
TERMINATE_VE
endprocedure
;-------------------------
procedure on_write
TERMINATE_VE
endprocedure
procedure on_subscription_piston
if (P$(PARAM_A.VALUE)==1)
move {name:lineSeparador,Y2:220}
else
move {name:lineSeparador,Y2:200}
endif
move {name:txtSeparador,text:"Piston P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure liberarRestroomBascula
assign hecho,0
FOREACH entidad, IN_RESOURCE, zonaBascula
if (P$hecho==0)
wake RestroomBascula,P$entidad
endif
assign hecho,1
ENDFOREACH
endprocedure
procedure moverPiston
if (P$PARAM_A>X$pesoLimite)
ASSIGN values, { Device_Piston_Trigger: 1 }
BRIDGE_WRITE { name:bridge1, VALUES:V$values, TRIGGER:on_write }
assign tmp,"pesados"
else
assign tmp,"ligeros"
endif
; move {name:txtSeparador,text:"Piston P$(PARAM_A) Kg -- P$tmp"}
TERMINATE_VE
endprocedure
Para ejecutar los ejemplos más avanzados y conectar GPSS-Plus con el mundo real, será necesario instalar tu propio middleware.js y, opcionalmente, el dispositivo virtual OPC-UA y los firmwares de microcontroladores.
middleware.js : Versión 1.03
El middleware actúa como capa intermedia entre GPSS-Plus y los sensores o actuadores físicos.
Debe ejecutarse en un entorno Node.js con comunicación segura (HTTPS / WSS).
Instalación de certificados
Dependiendo de si lo ejecutarás en una red privada o en un dominio público, hay dos opciones:
Ideal para pruebas en red interna.
Ejemplo para mcert sobre Debian:
# Crea una entrada de DNS en tui dominio a tu IP local. Ejemplo:
ip11.midominio.com -> 192.168.1.11
# Instalar el paquete libnss3-tools (que contiene certutil)
sudo apt install libnss3-tools
# Descarga mkcert
wget https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-v1.4.4-linux-arm
mv mkcert-v1.4.4-linux-arm /usr/local/bin/mkcert
chmod +x /usr/local/bin/mkcert
mkcert -version (v1.4.4)
# instalar el certificado raiz
mkcert -install
# crea el certificado para tu subdominio particular de la entrada DNS creada:
mkcert ip11.tudomino.com 192.168.1.11
# Mueve los certificados a una ubicación estándar:
mkdir -p /etc/ssl/localcerts
mv ip11.tudominio.com+1.pem /etc/ssl/localcerts/
mv ip11.tudominio.com+1-key.pem /etc/ssl/localcerts/
chown root:root /etc/ssl/localcerts/*
chmod 600 /etc/ssl/localcerts/*-key.pem
#Para evitar errores de confianza, instala el archivo rootCA.pem (ubicado en ~/.local/share/mkcert)
#como “Entidad de certificación raíz de confianza” en tu sistema.
mkcert -CAROOT
/root/.local/share/mkcert (ruta probable de localicación del rectificado rootCA.pem)
En servidores accesibles desde Internet, utiliza certificados reales de, por ejemplo let´s Encrypt
Edita el archivo middleware.js
Descarga y edita el archivo middleware.js para incorporar las rutas de los certificados.
const host = process.env.SERVER_HOST || 'ipXXX.your.domain';
...
const certOptions = {
cert: fs.readFileSync("/etc/letsencrypt/live/PATH_CHAIN/fullchain.pem"),
key: fs.readFileSync("/etc/letsencrypt/live/PATH_PRIVKEY/privkey.pem")
};
OR
const certOptions = {
cert: fs.readFileSync("/etc/ssl/localcerts/ipXXX.your.domain.pem"),
key: fs.readFileSync("/etc/ssl/localcerts/ipXXX.your.domain-key.pem")
};
Ejecuta previniendo la instalación de las librerías necesarias:
npm install express ws cors aedes node-opcua mysql2 serialport mqtt npm install node middleware.js
La salida esperada es:
# node middleware.js [INFO] [MQTT_EMBEDDED] Embedded broker listening on port 1883 [INFO] Server listening on https://your.domain:3000
Una vez en ejecución, puedes configurarlo como un servicio (systemd, pm2, etc.).
OPC-UA virtual : Versión 1.0
Este servidor OPC-UA virtual simula un conjunto de sensores y actuadores, ideal para desarrollo y pruebas sin hardware físico o el acceso a los recursos locales.
Ejecuta previniendo la instalación de las librerías necesarias:
npm install npm modbus-serial node index.js
La salida esperada es:
# node index.js [-] RO: 1:Sensor_Motion_Detected [-] RO: 1:Sensor_Door_IsOpen [-] RO: 1:PIDControl_CurrentTemperature [+] RW: 1:PIDControl_Setpoint [-] RO: 1:PIDControl_PIDOutput [+] RW: 1:Device_Motor_Enabled [-] RO: 1:System_Battery_BatteryLevel [-] RO: 1:Device_Scale_Weight [+] RW: 1:Device_Scale_Trigger [-] RO: 1:Device_Piston_IsExtended [+] RW: 1:Device_Piston_Trigger OPC-UA running: opc.tcp://your.domain:4840
Firmware para ESP32 : Versión 1.0
Firmware básico para microcontroladores ESP32, diseñado para enviar temperatura y humedad mediante MQTT.
Firmware para ESP8266 : Versión 1.0
Firmware alternativo más sencillo, que permite:
El proceso de instalación es idéntico al del ESP32.
Solo cambia la configuración del tipo de placa y puerto en Arduino IDE.
Mientras que BRIDGE_READ y BRIDGE_WRITE se limitan a manejar variables de datos simples, BRIDGE_CALL permite a GPSS-Plus ejecutar métodos o funciones remotas definidas en el middleware o en dispositivos industriales.
Esta capacidad es fundamental para tareas que exceden el ámbito de la simulación pura, tales como:
Dado que estas llamadas ocurren a través de la red, la respuesta no es instantánea. Para evitar que la entidad continúe su camino antes de recibir el resultado, se utiliza el patrón Rest/Wake:
BRIDGE_CALL.rest esperaCall.trigger recibe la respuesta del servidor, identifica a la entidad mediante P$(PARAM_B) y la libera con un wake.Este mecanismo permite modelar con total realismo las latencias de red y el procesamiento asíncrono en sistemas industriales.
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
initial txt_error," "
Graphic {Type:TEXT,Name:Text1,X:295,Y:123}
Graphic {Type:TEXT,Name:Text2,X:295,Y:95}
Graphic {Type:TEXT,Name:Text3,X:295,Y:66}
Graphic {Type:TEXT,Name:Text4,X:295,Y:35}
Graphic {Type:TEXT,Name:TextError,X:192,Y:478,Text:"On error...."}
RestRoom {name:esperaCall,x:543,y:488}
Initial options,{user:"user", pass:"1234"}
BRIDGER {NAME:bridge1,X:542,Y:434
,SERVER:"wss://tu-dominio.com:3000"
,CLIENT:"opc.tcp://tu-dominio.com:4840"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:183,Y:300}
POSITION {NAME:POS2,X:422,Y:300}
POSITION {NAME:POS3,X:704,Y:300}
START 10
PROCEDURE PRE_RUN
; Suscripciones para monitorizar errores de red o sistema
BRIDGE_SUBSCRIPTION { NAME: bridge1, VARIABLE: "HTTP_Request_Error", TRIGGER: bridge1_on_error }
BRIDGE_SUBSCRIPTION { NAME: bridge1, VARIABLE: "Topics_CurrentTime", TRIGGER: on_clock }
; Lectura de un archivo en el sistema de archivos remoto (FS)
ASSIGN params, [{}]
BRIDGE_CALL {name:bridge1, trigger:on_read_file, params:V$params, method:"Methods_FS_Read"}
; Petición a una API externa vía HTTP
ASSIGN params, [{ "method": "GET", "params": { "name": "Antonio" } }]
BRIDGE_CALL { name:bridge1, trigger:on_http, params:V$params, method:"Methods_HTTP_Request" }
TERMINATE_VE
ENDPROCEDURE 1
GENERATE 3,0,0,10 {NAME:GEN1,X:59,Y:300}
ADVANCE 3,0 {TO:POS1}
; Llamada a un método de cálculo (Suma) en el servidor
ASSIGN params, [{ "Num1": (D$N), "Num2": (0) }]
BRIDGE_CALL {name:bridge1, trigger:on_call, params:V$params, method:"Methods_Do_Sum"}
; Sincronización asíncrona: la entidad espera la respuesta
rest esperaCall
mod {subtitle:"Suma: P$(suma)"}
ADVANCE 4,4 {TO:POS2}
ASSIGN params, [{ "Num1": (D$N), "Num2": (D$N) }]
BRIDGE_CALL {name:bridge1, trigger:on_call, params:V$params, method:"Methods_Do_Sum"}
rest esperaCall
mod {subtitle:"Suma2: P$(suma)"}
ADVANCE 4,4 {TO:POS3}
ENDGENERATE 1
; --- Procedimientos de Respuesta ---
procedure on_clock
move {name:Text2,TEXT:"Hora Servidor: P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_read_file
move {name:Text3,TEXT:"Contenido Archivo: P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_http
move {name:Text4,TEXT:"Respuesta HTTP: P$(PARAM_A.VALUE.message)"}
TERMINATE_VE
endprocedure
procedure on_call
move {name:Text1,TEXT:"Suma remota: P$(PARAM_A.sum) Entidad: P$(PARAM_B)"}
assign suma,P$(PARAM_A.sum),P$(PARAM_B)
; Despertamos a la entidad específica que hizo la llamada
wake esperaCall,-1,P$(PARAM_B)
TERMINATE_VE
endprocedure
procedure bridge1_on_error
savevalue txt_error,"X$txt_error\nP$(PARAM_A.VALUE)"
move {name:TextError,TEXT:"X$txt_error"}
TERMINATE_VE
endprocedureEl protocolo MQTT de comunicaciones es un estándar de los dispositivos para IoT.
Funciona por "topics", como las redes sociales. Un dispositivo como un termómetro envía la temperatura obtenida sin saber quién va a requerirla y leerla.
Por lo tanto se requerirá que un "broker MQTT" esté constantemente escuchando lo que los sensores envíen y estar dispuesto a transmitírselo a quien los necesite.
Un broker estándar es Mosquitto pero el middleware.js incorpora una labor muy parecida.
Así que volvemos a tener la misma arquitectura de sistema:
GPSS-Plus <-> (middleware.js + broker MQTT) <-> dispositivo
En este punto pasamos a programar y utilizar hardware extra.
No nos preocupemos demasiado, sí sería aconsejable tener ciertas nociones de programación en C pero en general es suficiente con pedir a nuestra IA favorita que nos construya aquello que queramos.
Vamos a programar un microcontrolador ESP8266 con WIFI que gestione:
Para ello, usaremos estos 5 elementos hardware y un software para guardar el firmware del ESP8266.
Uno sencillo es "Arduino IDE" que contiene todas las librerías necesarias.
Un apunte rápido del camino a seguir es:
Tras esto, hecho uno, hechos todos. Todos los dispositivos en general funcionan a través de microcontroladores ESP32 o ESP8266 y otros y se conectan de la misma manera.
En el ejemplo, podemos ver como realiza las 3 opciones de MQTT estándar:
Por lo que deberíamos ver los 3 datos de temperatura, humedad y luminosidad además del encendido y apagado del led.
Debe entenderse que la comprobación de WRITE (Respuesta "OK") es algo que comunica el BRIDGER en la capa de red y no el dispositivo al que solo puede uno subscribirse.
El browse no tiene contenido puesto que el broker no dispone de esa información.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
SYSTEM {TYPE:OPTIONS, REAL_TIME:1}
Initial options,{ }
BRIDGER {NAME:bridge1,X:733,Y:556
,SERVER:"wss://ip10.tidominio.com:3000"
,CLIENT:"mqtt://localhost"
,OPTIONS:V$options
,on_error:bridge1_on_error
}
POSITION {NAME:POS1,X:101,Y:378}
POSITION {NAME:POS2,X:143,Y:115}
Graphic {NAME:txtSubscribeL,Type:TEXT,X:394,Y:465,Text:"On SUBS"}
Graphic {NAME:txtSubscribeH,Type:TEXT,X:394,Y:435,Text:"On SUBS"}
Graphic {NAME:txtSubscribeT,Type:TEXT,X:394,Y:405,Text:"On SUBS"}
Graphic {NAME:txtSubscribeS,Type:TEXT,X:394,Y:375,Text:"On SUBS"}
Graphic {NAME:TextBrowse,Type:TEXT,X:116,Y:283,Text:"On Browse"}
Graphic {NAME:TextWrite,Type:TEXT,X:391,Y:59,Text:"On Write"}
Graphic {NAME:TextError,Type:TEXT,X:424,Y:582,Text:"Error"}
START 10
;*****************************************************
PROCEDURE PRE_RUN
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "devices/elprimero/sensor/humidity"
, TRIGGER: on_humidity
}
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "devices/elprimero/sensor/lux"
, TRIGGER: on_luz
}
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "devices/elprimero/sensor/temperature"
, TRIGGER: on_temperature
}
BRIDGE_SUBSCRIPTION { NAME: bridge1
, VARIABLE: "devices/elprimero/status"
, TRIGGER: on_status
}
; Sin salida
BRIDGE_BROWSE {name:bridge1, trigger:on_BROWSE }
TERMINATE_VE
ENDPROCEDURE
;*****************************************************
GENERATE 5,0 {NAME:GEN1,X:91,Y:547}
ADVANCE 5,0 {TO:POS1,flow:1}
if (D$N% 2== 1)
ASSIGN values, {"devices/elprimero/commands/led": "on"}
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
else
ASSIGN values, {"devices/elprimero/commands/led": "off"}
BRIDGE_WRITE { name:bridge1, VALUES:V$values ,TRIGGER:on_write}
endif
ADVANCE 5,0 {TO:POS2,flow:1}
ENDGENERATE 1
;----------------------------------------------------------------------------
procedure on_read
move {name:TextRead,TEXT:"WIFI: P$(PARAM_A)"}
TERMINATE_VE
endprocedure
procedure bridge1_on_error
move {name:TextError,TEXT:"P$PARAM_A"}
TERMINATE_VE
endprocedure
;-------------------------
procedure on_write
move {name:TextWrite,TEXT:"WRITE T:AC1$: P$(PARAM_A.devices/elprimero/commands/led)"}
TERMINATE_VE
endprocedure
procedure on_luz
move {name:txtSubscribeL,text:"Subs LUX P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_humidity
move {name:txtSubscribeH,text:"Subs HUM P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_temperature
move {name:txtSubscribeT,text:"Subs TEM P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_status
move {name:txtSubscribeS,text:"Subs STA P$(PARAM_A.VALUE)"}
TERMINATE_VE
endprocedure
procedure on_BROWSE
move {name:TextBrowse,text:"on_BROWSE P$(PARAM_A)"}
TERMINATE_VE
endprocedure
This document describes the virtual devices and sensors simulated by the Virtual OPC-UA Plant server.
Simulates a PID temperature control loop.
PIDControl_CurrentTemperature: Current temperature (read-only)PIDControl_Setpoint: Target temperature (read-write)PIDControl_PIDOutput: PID output signal (read-only)Simulates a motion detector.
Sensor_Motion_Detected: Boolean value indicating motion detected (read-only)Simulates a door open/close sensor.
Sensor_Door_IsOpen: Boolean value indicating if the door is open (read-write)Simulates an environmental humidity sensor.
Sensor_Humidity_Value: Humidity level in % (read-only)Simulates a proximity sensor.
Sensor_Proximity_Distance: Distance to object in cm (read-only)Simulates a light level sensor.
Sensor_Light_Lux: Light level in lux (read-only)Simulates a gas leakage detector.
Sensor_Gas_Concentration: Gas concentration in ppm (read-only)Simulates a vibration sensor.
Sensor_Vibration_Level: Vibration intensity (read-only)Simulates a smoke detector.
Sensor_Smoke_Detected: Boolean value indicating presence of smoke (read-only)Simulates a battery status monitor.
Sensor_Battery_Level: Battery level in % (read-only)Simulates network signal strength.
Sensor_Network_Strength: Signal strength from 0 to 100 (read-only)Indicates if the system is powered.
System_PowerStatus_IsPowered: Boolean value indicating power status (read-only)Note: Writable variables are intended for use with commands such as OPC_WRITE or OPC_SUBSCRIPTION.
SYSTEM {TYPE:PRE_RUN,TRIGGER:PRE_RUN}
OPC {NAME:opc1,X:100,Y:121,visible:1, TYPE:REAL,
CLIENT:"opc.tcp://192.168.1.10:4840"}
POSITION {NAME:POS1,X:251,Y:335}
POSITION {NAME:POS2,X:469,Y:341}
Graphic {NAME:Text1,Type:TEXT,X:425,Y:314,Text:"Entidad"}
Graphic {NAME:Text2,Type:TEXT,X:424,Y:124,Text:"qqqqqq"}
Graphic {NAME:Text3,Type:TEXT,X:425,Y:514,Text:"write"}
Graphic {NAME:Text4,Type:TEXT,X:115,Y:398,Text:"door"}
initial nAgente,0
START 500
;*****************************************************
PROCEDURE PRE_RUN
; timeout agente.main,0
OPC_SUBSCRIPTION { NAME: opc1
, VARIABLE: "PIDControl_CurrentTemperature"
, TRIGGER: on_suscription
}
OPC_SUBSCRIPTION { NAME: opc1
, VARIABLE: "Sensor_Door_IsOpen"
, TRIGGER: puerta_abierta
}
TERMINATE_VE
ENDPROCEDURE 1
;*****************************************************
GENERATE 0,0,0,0 {NAME:GEN1,X:43,Y:300}
;OPC_BROWSE { name:opc1, savevalue:variables }
if (D$N==1)
ASSIGN values, { PIDCONTROL_SETPOINT: 10 }
; OPC_WRITE { NAME: opc1, VALUES: V$values }
OPC_WRITE { NAME: opc1, VALUES: V$values ,async:1,trigger:on_write}
endif
;move {name:Text1,TEXT:"Entidad Resultado X$(variables)"}
OPC_READ {name:opc1, savevalue:temperatura }
;OPC_READ {name:opc1, async:1,trigger:on_results }
move {name:Text1,TEXT:"Resultado READ: X$(temperatura.Sensor_Proximity_DistanceInCm)"}
ADVANCE 20,0 {TO:pos1}
ENDGENERATE 1
procedure on_suscription
move {name:Text2,TEXT:"on_suscription AC1$:\n P$PARAM_A \N P$PARAM_B P$PARAM_C"}
TERMINATE_VE
endprocedure
procedure on_results
move {name:Text2,TEXT:"on_results AC1$:\n P$PARAM_A \N P$PARAM_B P$PARAM_C"}
TERMINATE_VE
endprocedure
procedure on_write
move {name:Text3,TEXT:"on_write AC1$:\n P$PARAM_A \N P$PARAM_B P$PARAM_C"}
TERMINATE_VE
endprocedure
procedure puerta_abierta
move {name:Text4,TEXT:"DOOR AC1$ ; P$PARAM_A"}
if (P$(PARAM_A.value)==1) {debug:1}
NEW GEN1
endif
TERMINATE_VE
endprocedure
;========== UI DECLARATIVO ==========
UI {TYPE: BUTTON, id:botonA,TEXT: "Lanzar Entidad A", LABEL: "Botón A", TRIGGER: lanzarA}
UI {TYPE: BUTTON, id:botonB, TEXT: "Lanzar Entidad B", LABEL: "Valores de fábrica", TRIGGER: lanzarB}
UI {TYPE: INPUT, ID: unInput, LABEL: "Nombre", VALUE: "Alice", TRIGGER: capturarInput}
UI {
TYPE: SLIDER, ID: unSlider, LABEL: "Velocidad",
VALUE: 1, MIN: 0.1, MAX: 2, STEP: 0.1,
TRIGGER: capturarInput
}
UI {TYPE: SELECT, id:unSelect,LABEL: "Modo", OPTIONS: "Normal,Avanzado,Turbo", TRIGGER: capturarInput}
UI {TYPE: CHECKBOX, id:unCheck,LABEL: "Activar turbo", VALUE: 1, TRIGGER: capturarInput}
UI {TYPE: RADIO, id:unRadio, LABEL: "Color", OPTIONS: "Rojo,Verde,Azul", TRIGGER: capturarInput}
;========== GRÁFICOS ==========
GRAPHIC {NAME:infoTexto, TYPE:TEXT, X:400, Y:100, TEXT:"Esperando..."}
GRAPHIC {NAME:infoDato, TYPE:TEXT, X:400, Y:130, TEXT:"Ningún valor aún"}
;========== POSICIONES Y ANIMACIÓN ==========
POSITION {NAME:ENTRADA, X:200, Y:220}
POSITION {NAME:SALIDA, X:500, Y:220}
START 100
;-----------------------------------------------------------------
GENERATE 0,0,0,0 {NAME:GEN_A, X:102,Y:161,ECOLOR:#0099ff}
ADVANCE 10,0 {TO:ENTRADA}
ADVANCE 10,0 {TO:SALIDA}
TERMINATE 1
GENERATE 0,0,0,0 {NAME:GEN_B, X:104,Y:406,ECOLOR:#ff9900}
ADVANCE 10,0 {TO:ENTRADA}
ADVANCE 10,0 {TO:SALIDA}
SET_UI unSlider, 1.5
SET_UI unInput, "Fin camino B"
SET_UI unSelect, "turbo"
SET_UI unRadio, "AZUL"
SET_UI unCheck, 1
TERMINATE 1
;========== PROCEDIMIENTOS ==========
PROCEDURE lanzarA
NEW GEN_A
TERMINATE
ENDPROCEDURE 1
PROCEDURE lanzarB
NEW GEN_B
TERMINATE
ENDPROCEDURE 1
PROCEDURE capturarInput
MOVE {NAME:infoTexto, TEXT:"ID = PP$A"}
MOVE {NAME:infoDato, TEXT:"VALUE = PP$B"}
; Si es el input del nombre, actualiza UI desde GPSS
IF "botonA",==,"PP$A"
SET_UI velSlider, 1.5
ENDIF
TERMINATE
ENDPROCEDURE 1
El concepto parte de la abstracción de un recurso más grande que una simple FACILITY o STORAGE.
Imagina que cada taller puede ser representado como una función gaussiana de tiempo de permanencia. En GPSS-Plus, si conocemos los resultados de configuraciones previas (ej. 5 empleados vs 8 empleados), no necesitamos repetir la simulación micro para un taller de 6 empleados.
GPSS-Plus toma la decisión interpolada de los resultados previsibles:
ADVANCE BF$(BFtalleres, 4, 2, 4)
Este bloque consultará la función de comportamiento más cercana, interpolará los valores y creará dinámicamente una nueva función adaptada.
Esta es la filosofía de las Behavior Functions: reutilizar conocimiento previo para simular infinitos escenarios nuevos a partir de la interpolación de datos.
En este primer ejemplo práctico, vamos a aplicar el concepto de Behavior Functions (BF$) de forma sencilla.
Hasta ahora, cuando necesitábamos modelar un tiempo aleatorio, usábamos funciones normales (FN$) previamente definidas.
Con las Behavior Functions, podemos interpolar automáticamente los parámetros adecuados a partir de unos pocos ejemplos, sin tener que definir funciones exactas para cada caso.
En este caso partimos del dos casos ya estudiados que hemos plasmado en las siguientes funciones de comportamiento que hemos llamado
"tramo1D":
GFunction {behavior: tramo1D, p0:10, b:20, Sigma1:2.5, Sigma2:2.5}
GFunction {behavior: tramo1D, p0:20, b:40, Sigma1:3.0, Sigma2:3.5}
Son dos funciones de distribución gaussianas, ambas con un único parámetro que las definió.
Para la primera P0=10 y la segunda P0=20.
Imaginemos que son, por ejemplo, el número de empleados en un taller. Con ese dato como parámetro, los datos recogidos en el sistema daban las variables de la función gaussiana b:20, Sigma1:2.5, Sigma2:2.5 y b:40, Sigma1:3.0, Sigma2:3.5 respectivamente.
Es decir:
P0=10 -> b:20, Sigma1:2.5, Sigma2:2.5
P0=20 -> b:40, Sigma1:3.0, Sigma2:3.5
Estas son nuestras Funciones de comportamiento.
Por supuesto , puede que se recojan más posibilidades que harán que el cálculo gane en eficacia.
Pues bien, ahora que tenemos como se comportan estos talleres, podremos predecir cómo se comportará cualquier otro del que conozcamos su parámetro configurador del comportamiento.
Su P0. Así, para el caso de P0=15, es inmediato pensar que, para una sola dimensión de interpolación (un parámetro de configuración equivale a una interpolación lineal) la salida de la función gaussiana resultante será exactamente la media de cada uno de los parámetros estudiados, puesto que 15 es la mitad entre 10 y 20.
Así: P0=15 -> b=30 , sigma1=2.75 , sigma2=3
Para el caso de P0=30 los resultados serán: P0= 30 -> b=60 , sigma1=3.5 , sigma2=4.5
Las Behavior Functions van a hacer este cálculo automáticamente.
Para este ejemplo, simularemos el recorrido de entidades a través de tres recursos:
El primer tramo utilizará una función tradicional (FN$), como hasta ahora.
Una gaussiana con B=20 se centrará en ese tiempo.
El segundo y tercer tramo utilizarán funciones generadas dinámicamente (BF$) a partir de un comportamiento interpolado.
Los resultados en el informe deberán ser curvas centradas en 20, 60 y 30 respectivamente.
Ahora podrás entender porqué, en los informes, tienes una aproximación de la fórmula de la función resultante del los datos estadísticos recogidos. Cuando hagas una simulación de un sistema complejo, estas fórmulas se convertirán en las funciones de comportamiento futuras.
SYSTEM {TYPE:OPTIONS,Speed:5}
Graphic {NAME:Text1,Type:TEXT,X:230,Y:496,Text:"FN$gName1: b=20 / sigma1=2.5 / sigma2=2.5"}
Graphic {NAME:Text2,Type:TEXT,X:349,Y:100,Text:"BF$(tramo1D 30): b=40 / sigma1=3.5 / sigma2=4.5 (interpolado)"}
Graphic {NAME:Text3,Type:TEXT,X:523,Y:397,Text:"BF$(tramo1D 15): b=30 / sigma1=2.75 / sigma2=3 (interpolado)"}
Facility {NAME:VENTANILLA1,X:233,Y:444,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:30}
Facility {NAME:VENTANILLA2,X:352,Y:152,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:30}
Facility {NAME:VENTANILLA3,X:523,Y:354,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:80,capacity:30}
POSITION {NAME:POS1,X:708,Y:268}
Function {behavior: tramo1D, type: GAUSS, p0:10, Name:gName1, a:1, b:20, Sigma1:2.5, Sigma2:2.5}
Function {behavior: tramo1D, type: GAUSS, p0:20, Name:gName2, a:1, b:40, Sigma1:3.0, Sigma2:3.5}
START 2000
;*****************************************************
GENERATE 2,0,0,0 {NAME:GEN1,X:56,Y:319}
ADVANCE 30,10 {TO:VENTANILLA1}
SEIZE VENTANILLA1
ADVANCE FN$gName1
;ADVANCE BF$(tramo1D,10)
RELEASE VENTANILLA1
ADVANCE 20,10 {TO:VENTANILLA2}
SEIZE VENTANILLA2
ADVANCE BF$(tramo1D,30)
RELEASE VENTANILLA2
ADVANCE 20,10 {TO:VENTANILLA3}
SEIZE VENTANILLA3
ADVANCE BF$(tramo1D,15)
RELEASE VENTANILLA3
ADVANCE 20,0 {TO:POS1}
TERMINATE 1
Damos un paso más: usamos dos parámetros de entrada para modelar situaciones más complejas (Longitud y Meteorología).
La interpolación se realiza mediante técnicas IDW (Inverse Distance Weighting). Esto permite que, dadas combinaciones intermedias de longitud y clima, GPSS-Plus genere de forma automática una función de tiempo adaptada.
Este ejemplo muestra cómo construir sistemas de simulación versátiles y realistas con muy poca información de base, simplemente interpolando el espacio entre casos conocidos.
SYSTEM {TYPE:OPTIONS,Speed:5}
QUEUER {NAME:TRAMO1,X:66,Y:350,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
QUEUER {NAME:TRAMO2,X:276,Y:554,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
QUEUER {NAME:TRAMO3,X:664,Y:360,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
POSITION {NAME:POS1,X:115,Y:92}
POSITION {NAME:POS2,X:146,Y:449}
POSITION {NAME:POS3,X:432,Y:506}
POSITION {NAME:POS4,X:756,Y:33}
Function {behavior: tramo2D, type:GAUSS, p0:50, p1:0, Name:gName1, a:1, b:40, Sigma1:3.0, Sigma2:3.0}
Function {behavior: tramo2D, type:GAUSS, p0:50, p1:1, Name:gName2, a:1, b:60, Sigma1:5.0, Sigma2:5.0}
Function {behavior: tramo2D, type:GAUSS, p0:100, p1:0, Name:gName3, a:1, b:80, Sigma1:5.0, Sigma2:5.0}
Function {behavior: tramo2D, type:GAUSS, p0:100, p1:1, Name:gName4, a:1, b:120, Sigma1:10.0, Sigma2:10.0}
START 2000
;*****************************************************
GENERATE 2,0,0,0 {NAME:GEN1,X:55,Y:41}
ADVANCE 30,10 {TO:POS1}
queue TRAMO1
ADVANCE BF$(tramo2D,60,0.5) {TO:POS2}
depart TRAMO1
queue TRAMO2
ADVANCE BF$(tramo2D,55,0.5) {TO:POS3}
depart TRAMO2
queue TRAMO3
ADVANCE BF$(tramo2D,90,0.5) {TO:POS4}
depart TRAMO3
TERMINATE 1
Las funciones de comportamiento definen tanto el comportamiento hacia el interior de la entidad como hacia el exterior.
Imaginemos un tramo de carretera o un taller, una cosa es el tiempo que tarden las entidades en procesarse y otra los eventos que se producen como accidentes o rotura de las máquinas. En general, éstas serán una función de Poisson.
Así que lo fácil para interpretar tanto las de entrada como las de salida es sintetizar todas ellas en un único BEHAVIOR PROCEDURE.
Vamos a observar un ejemplo de tramos de carretera en los que los vehículos tardarán un determinado tiempo en recorrerlo y eventualmente tendrán que ir a repostar. Usaremos el SAVEVALUE "tramos" para sintetizar los parámetros de cada tramo del 0 al 2. Designaremos un número para el origen, destino, la distancia y la meteorología de cada tramo.
Además de definir las BEHAVIOR FUNCTIONS tanto Gaussiana para el tiempo de trayecto dependiendo de la distancia y meteorología, añadimos una Poisson para definir el lambda respecto de la distancia que haga que las entidades entren a repostar. Recordemos que en una distribución de Poisson, un lambda de 1/10 indica que regularmente, uno de cada 10 vehículos repostarán.
El BEHAVIOR PROCEDURE se encarga de todo y solo recibe dos parámetros, origen y destino.
Obtiene los nombres de los recursos y parámetros basados en esos números. Calcula si es necesario repostar y cuánto tiempo va a tardar en recorrer el tramo. Las estadísticas finales permiten observar el tiempo total de recorrido por tramo (acumulado en los QUEUERs), así como el uso y ocupación de las gasolineras (vía R_BIN_*).
Gracias a la flexibilidad de los Behavior Procedures, podemos encapsular tanto la lógica de simulación como las variaciones estadísticas y los eventos adicionales en un único bloque modular.
Esto permite replicar fácilmente comportamientos realistas en sistemas más grandes como cadenas logísticas, rutas de transporte o procesos industriales con condiciones variables.
;SYSTEM {TYPE:OPTIONS,Speed:5}
initial tramos, [
{
origen:1,
destino:2,
Km:90,
Meteo:0.6
},
{
origen:2,
destino:3,
Km:100,
Meteo:0.8
},
{
origen:3,
destino:4,
Km:50,
Meteo:1.0
}
]
Graphic {NAME:Text_0,Type:TEXT,X:219,Y:178,Text:"Text_0"}
Graphic {NAME:Text_1,Type:TEXT,X:246,Y:454,Text:"Text_1"}
Graphic {NAME:Text_2,Type:TEXT,X:429,Y:354,Text:"Text_2"}
QUEUER {NAME:TRAMO_0,X:66,Y:350,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
QUEUER {NAME:TRAMO_1,X:276,Y:554,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
QUEUER {NAME:TRAMO_2,X:664,Y:360,E_BIN_start:0,E_BIN_SIZE:1,E_BIN_COUNT:180}
Facility {NAME:Gas_0,X:215,Y:125,capacity:10,R_BIN_start:0,R_BIN_SIZE:1,R_BIN_COUNT:10}
Facility {NAME:Gas_1,X:245,Y:406,capacity:10,R_BIN_start:0,R_BIN_SIZE:1,R_BIN_COUNT:10}
Facility {NAME:Gas_2,X:434,Y:405,capacity:10,R_BIN_start:0,R_BIN_SIZE:1,R_BIN_COUNT:10}
POSITION {NAME:POS_0,X:115,Y:92}
POSITION {NAME:POS_1,X:146,Y:449}
POSITION {NAME:POS_2,X:432,Y:506}
POSITION {NAME:POS_3,X:756,Y:33}
Function {behavior: bTramoTiempo, type:GAUSS, p0:50, p1:0, Name:gName1, a:1, b:40, Sigma1:3.0, Sigma2:3.0}
Function {behavior: bTramoTiempo, type:GAUSS, p0:50, p1:1, Name:gName2, a:1, b:60, Sigma1:5.0, Sigma2:5.0}
Function {behavior: bTramoTiempo, type:GAUSS, p0:100, p1:0, Name:gName3, a:1, b:80, Sigma1:5.0, Sigma2:5.0}
Function {behavior: bTramoTiempo, type:GAUSS, p0:100, p1:1, Name:gName4, a:1, b:120, Sigma1:10.0, Sigma2:10.0}
Function {behavior: bTramoGas, type:POISSON, p0:100, Name:fPoisson1, LAMBDA:1/8}
Function {behavior: bTramoGas, type:POISSON, p0:50, Name:fPoisson2, LAMBDA:1/20}
START 2000
;*****************************************************
PROCEDURE TRAMO_BEHAVIOR
assign nQueuer, "TRAMO_P$PARAM_A"
assign nText, "Text_P$PARAM_A"
assign nGas, "Gas_P$PARAM_A"
assign Km,X$(tramos.P$PARAM_A.Km)
assign Meteo,X$(tramos.P$PARAM_A.Meteo)
assign forceGas,BF$(bTramoGas,P$Km)
if (P$forceGas>=1)
move {name:P$nText,text:"GAS P$Km / P$Meteo"}
ADVANCE 10 {TO:P$nGas}
seize P$nGas
ADVANCE 30,0
release P$nGas
ADVANCE 10 {TO:"POS_P$PARAM_A"}
move {name:P$nText,text:" "}
endif
queue P$nQueuer
ADVANCE BF$(bTramoTiempo,P$Km,P$Meteo) {TO:"POS_P$PARAM_B"}
depart P$nQueuer
ENDPROCEDURE 1
;*****************************************************
GENERATE 2,0,0,0 {NAME:GEN1,X:55,Y:41,enumber:0}
ADVANCE 10,2 {TO:POS_0}
CALL TRAMO_BEHAVIOR,0,1
CALL TRAMO_BEHAVIOR,1,2
CALL TRAMO_BEHAVIOR,2,3
TERMINATE 1
Este proyecto no habría sido posible sin un sinfin de colaboradores. Mi más sincero agradecimiento a:
Antonio Sánchez y resto del equipo. Enero de 2026