Marzo de 2016
Volumen 31, número 3
C#. Simulación de eventos discretos: un ejemplo de crecimiento poblacional
A lo largo de la historia, la capacidad de simular ha ayudado al desarrollo de varias ciencias. Los modelos médicos que simulan el cuerpo humano mejoran el estudio de la anatomía humana. Los juegos de ordenador de simulación como "World of Warcraft" recrean todo un mundo de fantasía y "Flight Simulator" ayuda a entrenar pilotos en tierra. Distintos programas de simulación exploran respuestas a ataques terroristas, enfermedades pandémicas y otras crisis posibles. Incluso los dinosaurios simulados de la película "Parque Jurásico" ofrecen una idea de las amplias aplicaciones de la simulación y su potencial.
La simulación es una técnica en la que un sistema o proceso de la vida real se emula mediante un modelo diseñado. El modelo encapsula todos los comportamientos y características del sistema; la simulación es la ejecución de este sistema en el tiempo. Existen varias fases para diseñar una simulación:
- La definición del sistema que se va a modelar, que implica estudiar el problema en cuestión, identificar las propiedades del entorno y especificar los objetos que se deben alcanzar.
- La formulación del modelo, que incluye la definición de todas sus variables y relaciones lógicas, y la creación de los diagramas de flujo necesarios.
- La definición de los datos que requerirá el modelo para generar el resultado deseado.
- La creación de una implementación informatizada del modelo.
- La comprobación respecto a si el modelo implementado cumple el diseño.
- La validación mediante comparación de que el simulador realmente representa el sistema real que se está simulando.
- La experimentación para generar datos deseados mediante el simulador.
- El análisis y la interpretación de resultados del simulador y la toma de decisiones basadas en dichos resultados.
- La documentación del modelo creado y el simulador como una herramienta.
Las simulaciones normalmente se componen de un proceso continuo o de eventos discretos. Por ejemplo, para simular un sistema de tiempo meteorológico, el seguimiento se realiza continuamente debido a que todos los elementos están cambiando constantemente. Por tanto, la variable de temperatura con respecto a la variable tiempo se representaría mediante una curva continua. Por contra, los despegues y aterrizajes de aviones se producen como momentos en el tiempo y, por tanto, una simulación puede considerar solamente esos momentos o eventos concretos y descartar todo lo demás. Este tipo de simulación se conoce como simulación de eventos discretos (DES) y es lo que trataré en este artículo.
Simulación de eventos discretos
La DES modela un sistema o un proceso como una secuencia ordenada de eventos individuales en el tiempo; es decir, desde el momento en que se produce un evento hasta el momento en que se produce el siguiente. Por lo tanto, en una DES, el tiempo es normalmente muy inferior al tiempo real.
Cuando se desarrolla una DES, existen seis elementos principales que se deben considerar:
Los objetos que representan elementos del sistema real. Tienen propiedades, están relacionados con eventos, consumen recursos y entran y salen de las colas a lo largo del tiempo. En el escenario de despegues y aterrizajes de aviones que mencioné anteriormente, los objetos serían aviones. En un sistema sanitario, los objetos podrían ser pacientes u órganos. En un sistema de almacén, los objetos serían los productos en stock. Se supone que los objetos deben interactuar entre ellos o con el sistema y se pueden crear en cualquier momento durante una simulación.
Las propiedades son características particulares de cada objeto (tamaño, tiempo de aterrizaje, sexo, precio, etc.) y se almacenan para determinar las respuestas a varios escenarios que podrían producirse en la simulación; estos valores se pueden modificar.
Los eventos son cosas que pueden suceder en el sistema, especialmente a los objetos, como el aterrizaje de un avión, la llegada de un producto a un almacén, la aparición de una enfermedad concreta, etc. Los eventos pueden producirse y volver a producirse en cualquier orden.
Los recursos son elementos que ofrecen servicios para los objetos (por ejemplo, una pista de aterrizaje de un aeropuerto, los compartimentos de almacenamiento de un almacén y los médicos de una clínica). Cuando un recurso se ocupa y un objeto lo necesita, el objeto debe hacer cola y esperar hasta que el recurso esté disponible.
Las colas son los conductos en los que se organizan los objetos para esperar a que se libere un recurso que esté actualmente ocupado. Las colas pueden tener una capacidad máxima y distintos enfoques de llamada: primero en entrar, primero en salir (FIFO); último en entrar, primero en salir (LIFO); o basarse en ciertos criterios o prioridades (progresión de enfermedad, consumo de combustible, etc.).
El tiempo (como sucede en la vida real) es esencial en la simulación. Para medir el tiempo, se inicia un reloj al comienzo de una simulación y después puede usarse para realizar un seguimiento de períodos de tiempo concretos (hora de salida o llegada, tiempo de transporte, tiempo transcurrido con síntomas determinados, etc.). Ese seguimiento es fundamental porque permite conocer cuándo debería producirse el siguiente evento.
Como la programación de la simulación puede complicarse, ha habido muchos intentos de crear lenguajes que reúnan todos los requisitos del paradigma de simulación para facilitar el desarrollo. Uno de esos lenguajes es SIMULA, una invención de los años 60 de Ole Johan y Kristen Nygaard que fue el primero en introducir el concepto de programación orientada a objetos (OOP), el paradigma de programación más extendido hoy en día. Actualmente, el enfoque se centra más en la creación de paquetes, marcos o bibliotecas que incorporen los que los programadores necesitan cuando crean una simulación. Estas bibliotecas están pensadas para que se las llame desde lenguajes cotidianos, como C#, C++, Java o Python.
En “A History of Discrete Event Simulation Programming Languages”, Nance propuso seis requisitos mínimos que cualquier lenguaje de programación debe cumplir (bit.ly/1ZnvkVn):
- Generación de números aleatorios.
- Transformadores de procesos, para permitir variables distintas de las variables aleatorias uniformes.
- Procesamiento de listas, para facilitar la creación, manipulación y eliminación de objetos.
- Análisis estadístico, para ofrecer un resumen descriptivo del comportamiento de los modelos.
- Generación de informes, para ayudar en la presentación de grandes conjuntos de datos y facilitar la toma de decisiones.
- Un mecanismo de flujo de tiempo
Todos estos requisitos se pueden cumplir en C#, por lo que en este artículo presentaré una simulación de eventos para un crecimiento poblacional desarrollada en C#.
DES para el crecimiento poblacional
El crecimiento poblacional es uno de los muchos aspectos que se consideran en el estudio de cómo cambian las poblaciones (animales y plantas) en el tiempo y el espacio, y cómo interactúan con su entorno. Las poblaciones son grupos de organismos de la misma especie que viven en el mismo momento, en ocasiones en el mismo espacio o delimitados por características particulares.
¿Por qué es importante estudiar el crecimiento poblacional? Un mejor entendimiento sobre la forma en que las poblaciones crecen o decrecen permite a los científicos tener la posibilidad de realizar mejores predicciones sobre los cambios en la conservación de la biodiversidad, el uso de recursos, el cambio climático, la contaminación, el sistema sanitario, las necesidades de transporte, etc. También ofrece información detallada sobre la forma en que los organismos interactúan entre ellos y con el entorno, un aspecto esencial cuando se considera si una población puede prosperar o decaer.
En este artículo, presentaré una DES para el crecimiento de una población. El objetivo es observar cómo evoluciona la población en el tiempo y obtener algunas estadísticas mediante el análisis de los resultados finales (tamaño de población, personas mayores, personas jóvenes, etc.). La población comenzará con m hombres y n mujeres, cada uno con una edad asociada. Claramente, m y n deben ser mayores que cero o la simulación no tendría sentido. Los objetos de este modelo son individuos.
Un individuo es capaz de comenzar una relación con otro individuo después de llegar a una edad que se distribuye según una función de Poisson con el parámetro λ = 18 años. (No es necesario comprender completamente las distribuciones normales, exponenciales o de Poisson en este momento; se explicarán en la siguiente sección).
Existe menos de un 50 % de probabilidades de que individuos de sexos opuestos solteros y aptos se relacionen entre ellos, lo cual solo ocurre cuando la diferencia de edad no es superior a 5 años. En la Figura 1 se muestra un ejemplo de la probabilidad de que una relación entre dos individuos termine.
Figura 1. Probabilidad de que una relación termine
Promedio de edad | Probabilidad |
14-20 | 0,7 |
21-28 | 0,5 |
29+ | 0,2 |
Los individuos que estén en una relación pueden tener un hijo después de un tiempo que se distribuye según una función exponencial con el parámetro λ= 8 años.
Una mujer puede quedarse embarazada si tiene una edad que siga una distribución normal (en forma de campana) con los parámetros µ = 28 y σ2= 8 años. Todas las mujeres tienen un número de hijos que se distribuye según una función normal con los parámetros µ = 2 y σ2= 6 años. (El parámetro µ representa el promedio de edad, mientras que σ2es la medida de la variabilidad de edad).
Todos los individuos tienen una esperanza de vida que se distribuye según una función de Poisson con el parámetro λ= 70 años, donde λ representa el promedio de esperanza de vida.
En la descripción anterior, es posible identificar varios eventos:
- Iniciar una relación.
- Finalizar una relación.
- Quedarse embarazada.
- Tener un hijo.
- Morir.
Todos los eventos se acompañan de una variable aleatoria discreta que determina el momento en que se producirá el evento.
Distribuciones de probabilidad
Una variable aleatoria discreta es aquella cuyo conjunto de valores es finito o infinito numerable. Es decir, los valores se pueden enumerar como una secuencia finita o infinita de valores 1, 2, 3, ... Para una variable aleatoria discreta, su distribución de probabilidad es cualquier gráfica, tabla o fórmula que asigne una probabilidad a cada valor posible. La suma de todas las probabilidades debe ser 1 y cada probabilidad individual debe estar entre 0 y 1. Por ejemplo, cuando se lanza un dado perfecto (en el que todas las caras tienen igual probabilidad), la variable aleatoria discreta X representa los resultados posibles que tendrá la distribución de probabilidad X(1) = 1/6, X(2) = 1/6, …, X(6) = 1/6. Todas las caras tienen la misma probabilidad, por lo que la probabilidad asignada de cada una de ellas es 1/6.
Los parámetros l y µ indican la media (valor esperado) en sus distribuciones correspondientes. La media representa el valor que toma la variable aleatoria en promedio. En otras palabras, es la suma E=[(cada posible resultado) × (probabilidad de ese resultado)], donde E denota la media. En el caso del dado, la media sería E = 1/6 + 2/6 + 3/6 + 4/6 + 5/6 + 6/6 = 3,5. Observe que el resultado 3,5 está en realidad a medio camino entre todos los posibles valores que el dado puede tomar; es el valor esperado cuando el dado se lanza un número elevado de veces.
El parámetro σ2 indica la varianza de la distribución. La varianza representa la dispersión de los posibles valores de la variable aleatoria y nunca es negativa. Las varianzas pequeñas (cercanas a 0) suelen indicar que los valores están próximos los unos a los otros y a la media; las varianzas elevadas (cercanas a 1) indican que existe una gran distancia entre los valores y con respecto a la media.
Poisson es una distribución discreta que expresa probabilidades en cuanto al número de eventos por unidad de tiempo (consulte la Figura 2). Normalmente se aplica cuando la probabilidad de un evento es pequeña y el número de oportunidades de que ocurra es elevado. El número de erratas en un libro, los clientes que llegan a un centro de negocios, los coches que llegan a semáforos y las muertes al año de un grupo de edad determinado son ejemplos de aplicaciones de la distribución de Poisson.
Figura 2. Distribución de Poisson, parámetro λ = 18
Una distribución exponencial expresa el tiempo entre los eventos de un proceso de Poisson (consulte la Figura 3). Por ejemplo, si está trabajando con un proceso de Poisson que describe el número de clientes que llegan a un centro de negocios durante un tiempo determinado, puede que le interese una variable aleatoria que indique cuánto tiempo ha transcurrido desde que llegó el primer cliente. Una función exponencial puede servir para este propósito. También se puede aplicar para procesos físicos; por ejemplo, para representar la vida de partículas, donde λ indicaría la velocidad a la que decae la partícula.
Figura 3. Distribución exponencial, parámetro λ = 8
La distribución normal describe las probabilidades que suelen encontrarse en torno a un valor central, sin una inclinación a izquierda o derecha, como se muestra en la Figura 4. Las distribuciones normales son simétricas y tienen curvas de densidad con forma de campana y un único pico en la media. El cincuenta por ciento de la distribución se queda a la izquierda de la media y el otro cincuenta a la derecha. La desviación estándar indica la extensión y el contorno de la curva con forma de campana; cuanto más pequeña es la desviación estándar, más concentrados están los datos. La media y la desviación estándar deben indicarse como parámetros de la distribución normal. Muchos fenómenos naturales siguen en gran medida una distribución normal: la presión sanguínea, la altura de las personas, los errores de medida y muchos otros.
Figura 4. Distribución normal, parámetros µ = 28 y σ2 = 8 años
Ahora, mostraré cómo implementar la DES propuesta en un lenguaje popular, sofisticado y elegante como C#.
Implementación
Para desarrollar esta simulación, aprovecharé todos los beneficios del paradigma OOP. La idea es obtener un código lo más legible posible. Las aplicaciones científicas suelen ser complejas y difíciles de entender, por lo que es esencial intentar que sean lo más claras posible para que otros puedan comprender el código. Desarrollaré la aplicación como una aplicación de consola en C#, con la estructura que se muestra en la Figura 5.
Figura 5. La estructura de la aplicación de simulación
Una ruta de acceso lógica es esencial; observe como la estructura del espacio de nombres mantiene su propio sentido común: Simulation.DES.PopulationGrowth.
La carpeta Events contiene un archivo Event.cs, que define un tipo de enumeración que representa todos los eventos posibles de la simulación:
namespace DES.PopulationGrowth.Events
{
public enum Event
{
Capable Engaging,
Birth EngageDisengage,
GetPregnant,
ChildrenCount,
TimeChildren,
Die
}
}
La carpeta Objects contiene todas las clases relacionadas con los individuos; son las partes del código que más se beneficiarán de OOP. Existen tres clases relacionadas con los individuos: Individual, Male y Female. La primera es una clase abstracta y las otras heredan de ella; es decir, los elementos male y female son elementos individual. La mayoría del código se encuentra en la clase Individual. En la Figura 6 se muestran sus propiedades y métodos.
Figura 6. Propiedades y métodos de la clase Individual
Aquí se muestra una descripción de todas las propiedades:
- Age: edad del individuo.
- Couple: tiene un valor nulo si está soltero; en caso contrario, su pareja (otro elemento Individual).
- Engaged: verdadero si el individuo está en una relación, falso en caso contrario.
- LifeTime: edad a la que muere el individuo.
- RelationAge: edad en la que el individuo puede comenzar una relación.
- TimeChildren: tiempo en la simulación (no edad) en la que el individuo puede tener hijos.
Y a continuación se muestra una descripción de sus métodos:
- Disengage: finaliza la relación entre dos individuos.
- EndRelation: determina si la relación debería finalizar, según la tabla de probabilidades de la Figura 1.
- FindPartner: busca una pareja disponible para un individuo.
- SuitablePartner: determina si un individuo es apto para iniciar una relación (diferencia de edad y sexo opuesto).
- SuitableRelation: determina si un individuo puede iniciar una relación; es decir, si está soltero y tiene edad para iniciarla.
- ToString: Override para representar información de individuos como una cadena.
La clase comienza definiendo todas las propiedades y después el constructor, que simplemente establece la edad:
public abstract class Individual
{
public int Age { get; set; }
public int RelationAge{ get; set; }
public int LifeTime{ get; set; }
public double TimeChildren{ get; set; }
public Individual Couple { get; set; }
protected Individual(int age)
{
Age = age;
}
Los métodos SuitableRelation y SuitablePartner y la propiedad Engaged son simplemente pruebas lógicas, muy sencillas:
public bool SuitableRelation()
{
return Age >= RelationAge&& Couple == null;
}
public bool SuitablePartner(Individual individual)
{
return ((individual is Male && this is Female) ||
(individual is Female && this is Male)) &&Math.Abs(individual.Age - Age) <= 5;
}
public bool Engaged
{
get { return Couple != null; }
}
El método Disengage termina una relación al establecer el campo Couple como nulo en la pareja y después en el individuo. También establece su tiempo para tener hijos a 0 porque ya no están juntos.
public void Disengage()
{
Couple.Couple = null;
Couple = null;
TimeChildren = 0;
}
El método EndRelation es básicamente una traducción de la tabla de probabilidades para determinar la posibilidad de que una relación termine. Utiliza una variable aleatoria uniforme, que genera un valor aleatorio en el intervalo [0, 1] equivalente a generar un porcentaje de aceptación. El diccionario de distribuciones se crea en la clase simulation (que se describe brevemente) y contiene pares (evento, distribución de probabilidades), por lo que asocian todos los eventos con su distribución:
public bool EndRelation(Dictionary<Event, IDistribution> distributions)
{
var sample =
((ContinuousUniform) distributions[Event.BirthEngageDisengage]).Sample();
if (Age >= 14 && Age <= 20 && sample <= 0.7)
return true;
if (Age >= 21 && Age <= 28 && sample <= 0.5)
return true;
if (Age >= 29 && sample <= 0.2)
return true;
return false;
}
El método FindPartner, que se muestra en la Figura 7, busca una pareja disponible para la instancia de individual. Recibe como entrada la lista de la población, el tiempo actual de la simulación y el diccionario de distribuciones.
Figura 7. El método FindPartner
public void FindPartner(IEnumerable<Individual> population, int currentTime,
Dictionary<Event, IDistribution> distributions)
{
foreach (var candidate in population)
if (SuitablePartner(candidate) &&
candidate.SuitableRelation() &&
((ContinuousUniform) distributions[Event.BirthEngageDisengage]).Sample() <= 0.5)
{
// Relate them
candidate.Couple = this;
Couple = candidate;
// Set time for having child
var childTime = ((Exponential) distributions[Event.TimeChildren]).Sample()*100;
// They can have children on the simulated year: 'currentTime + childTime'.
candidate.TimeChildren = currentTime + childTime;
TimeChildren = currentTime + childTime;
break;
}
}
Por último, el método ToString define la representación de cadena de un individuo:
public override string ToString()
{
Return string.Format("Age: {0} Lifetime {1}", Age, LifeTime);
}
La clase Male es muy simple:
public class Male: Individual
{
public Male(int age) : base(age)
{
}
public override string ToString()
{
return base.ToString() + " Male";
}
}
La clase Female es algo más complicada porque incluye un tratamiento para el embarazo, el alumbramiento, etc. La definición de la clase comienza con la declaración de todas las propiedades y el constructor:
public class Female :Individual
{
public bool IsPregnant{ get; set; }
public double PregnancyAge{ get; set; }
public double ChildrenCount{ get; set; }
public Female(int age) : base(age)
{
}
}
Estas son las propiedades de la clase Female:
- IsPregnant: determina si la mujer está embarazada.
- PregnancyAge: determina la edad a la que una mujer se puede quedar embarazada.
- ChildrenCount: indica el número de hijos que dará a luz.
Y a continuación se muestran los métodos que contiene:
- SuitablePregnancy: determina si la mujer puede quedarse embarazada.
- GiveBirth: indica una mujer que da a luz, con lo que se agrega un nuevo individuo a la población.
- ToString:override: se usa para representar información de la mujer como una cadena.
SuitablePregnancy es un método de prueba que da como resultado verdadero cuando la instancia de woman cumple todas las condiciones para quedarse embarazada:
public bool SuitablePregnancy(intcurrentTime)
{
return Age >= PregnancyAge && currentTime <= TimeChildren && ChildrenCount > 0;
}
El método GiveBirth, que se muestra en la Figura 8, es el código que agrega e inicializa nuevos individuos en la población.
Figura 8. El método GiveBirth
public Individual GiveBirth(Dictionary<Event, IDistribution> distributions, int currentTime)
{
var sample =
((ContinuousUniform) distributions[Event.BirthEngageDisengage]).Sample();
var child = sample > 0.5 ? (Individual) newMale(0) : newFemale(0);
// One less child to give birth to
ChildrenCount--;
child.LifeTime = ((Poisson)distributions[Event.Die]).Sample();
child.RelationAge = ((Poisson)distributions[Event.CapableEngaging]).Sample();
if (child is Female)
{
(child as Female).PregnancyAge =
((Normal)distributions[Event.GetPregnant]).Sample();
(child as Female).ChildrenCount =
((Normal)distributions[Event.ChildrenCount]).Sample();
}
if (Engaged && ChildrenCount> 0)
{
TimeChildren =
currentTime + ((Exponential)distributions[Event.TimeChildren]).Sample();
Couple.TimeChildren = TimeChildren;
}
else
TimeChildren = 0;
IsPregnant = false;
return child;
}
Se genera una muestra uniforme para determinar primero si el nuevo individuo será hombre o mujer, cada uno de ellos con una probabilidad del 50 por ciento (0,5). El valor de ChildrenCount disminuye en 1, lo que indica que esta mujer tiene un hijo menos al que dar a luz. El resto del código está relacionado con la inicialización de individuos y el restablecimiento de algunas variables.
El método ToString cambia la forma en que los elementos Female se representan como cadenas:
public override string ToString()
{
return base.ToString() + " Female";
}
Como todas las funciones relacionadas con los individuos se colocaron en clases independientes, la clase Simulation ahora es mucho más sencilla. Las propiedades y el constructor se encuentran al principio del código:
public class Simulation
{
public List<Individual> Population { get; set; }
public int Time { get; set; }
private int _currentTime;
private readonly Dictionary<Event, IDistribution> _distributions ;
Las propiedades y variables son autodescriptivas y algunas de describieron previamente. El elemento Time representa el tiempo (en años) que durará la simulación y _currentTime representa el año actual de la simulación. En este caso, el constructor (que se muestra en la Figura 9) es más complicado porque incluye la iniciación de variables aleatorias para cada individuo.
Figura 9. El constructor de la clase Simulation
public Simulation(IEnumerable<Individual> population, int time)
{
Population = new List<Individual>(population);
Time = time;
_distributions = new Dictionary<Event, IDistribution>
{
{ Event.CapableEngaging, new Poisson(18) },
{ Event.BirthEngageDisengage, new ContinuousUniform() },
{ Event.GetPregnant, new Normal(28, 8) },
{ Event.ChildrenCount, new Normal(2, 6) },
{ Event.TimeChildren, new Exponential(8) },
{ Event.Die, new Poisson(70) },
};
foreach (var individual in Population)
{
// LifeTime
individual.LifeTime = ((Poisson) _distributions[Event.Die]).Sample();
// Ready to start having relations
individual.RelationAge = ((Poisson)_distributions[Event.CapableEngaging]).Sample();
// Pregnancy Age (only women)
if (individual is Female)
{
(individual as Female).PregnancyAge = ((Normal) _distributions[Event.GetPregnant]).Sample();
(individual as Female).ChildrenCount = ((Normal)_distributions[Event.ChildrenCount]).Sample();
}
}
}
Finalmente, en el método Execute, que se muestra en la Figura 10, se produce toda la lógica de simulación.
Figura 10. El método Execute
public void Execute()
{
while (_currentTime< Time)
{
// Check what happens to every individual this year
for (vari = 0; i<Population.Count; i++)
{
var individual = Population[i];
// Event -> Birth
if (individual is Female&& (individual as Female).IsPregnant)
Population.Add((individual as Female).GiveBirth(_distributions,
_currentTime));
// Event -> Check whether someone starts a relationship this year
if (individual.SuitableRelation())
individual.FindPartner(Population, _currentTime, _distributions);
// Events where having an engaged individual represents a prerequisite
if (individual.Engaged)
{
// Event -> Check whether arelationship ends this year
if (individual.EndRelation(_distributions))
individual.Disengage();
// Event -> Check whether a couple can have a child now
if (individual is Female &&
(individual as Female).SuitablePregnancy(_currentTime))
(individual as Female).IsPregnant = true;
}
// Event -> Check whether someone dies this year
if (individual.Age.Equals(individual.LifeTime))
{
// Case: Individual in relationship (break relation)
if (individual.Engaged)
individual.Disengage();
Population.RemoveAt(i);
}
individual.Age++;
_currentTime++;
}
}
Las iteraciones del bucle exterior representan los años que transcurren en la simulación. El bucle interior recorre los eventos que pueden ocurrirle a esos individuos en ese año concreto.
Para ver cómo evoluciona la población en el tiempo, configuro una nueva aplicación de consola, como se muestra en la Figura 11.
Figura 11. Visualización de la población en el tiempo
static void main()
{
var population = new List<Individual>
{
new Male(2),
new Female(2),
new Male(3),
new Female(4),
new Male(5),
new Female(3)
};
var sim = new Simulation(population, 1000);
sim.Execute();
// Print population after simulation
foreach (var individual in sim.Population)
Console.WriteLine("Individual {0}", individual);
Console.ReadLine();
}
En la Figura 12 se muestra la población después de 1000 años.
Figura 12. Población después de 1000 años
En este artículo he desarrollado una simulación de eventos discretos para ver cómo evoluciona una población en el tiempo. El enfoque orientado a objetos ha resultado ser muy útil para obtener código legible y conciso que los lectores pueden probar y mejorar si fuera necesario.
Arnaldo Pérez Castaño* es un científico de la computación que vive en Cuba. Es el autor de una serie de libros sobre programación: "JavaScript Fácil", "HTML y CSS Fácil" y "Python Fácil" (Marcombo S.A.). Es experto en Visual Basic, C#, .NET Framework e inteligencia artificial, y ofrece sus servicios como free lance a través de nubelo.com. Entre sus pasiones figuran el cine y la música. Puede ponerse en contacto con él en <arnaldo.skywalker@gmail.com.>*
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey