Freigeben über



März 2016

Band 31, Nummer 3

C# – Diskrete ereignisorientierte Simulation: Ein Beispiel zum Bevölkerungswachstum

Von Arnaldo Pérez Pérez

Rückblickend hat die Möglichkeit der Simulation die Entwicklung zahlreicher Wissenschaften gefördert. Medizinische Modelle zur Simulation des menschlichen Körpers verbessern die Erforschung der menschlichen Anatomie. In Computersimulationsspielen wie „World of Warcraft“ wird eine Fantasiewelt neu erschaffen, und „Flight Simulator“ hilft Piloten bei der Schulung am Boden. In diversen Simulationsprogrammen werden Reaktionen auf Terroranschläge, Pandemien und andere mögliche Krisen untersucht. Selbst die simulierten Dinosaurier im Film „Jurassic Park“ deuten auf die umfassende Nutzung von Simulationen und ihres Potenzials hin.

Die Simulation ist eine Methode, bei der reale Systeme oder Prozesse mithilfe eines entworfenen Modells emuliert werden. Das Modell kapselt sämtliche Merkmale und Verhalten des Systems. Die Simulation ist die Ausführung dieses Systems über einen Zeitraum. Beim Entwurf einer Simulation gibt es mehrere Phasen:

  • Definieren des zu modellierenden Systems, wozu das Untersuchen des vorliegenden Problems zählt, Bestimmen der Eigenschaften der Umgebung und Festlegen der zu erreichenden Ziele.
  • Entwerfen des Modells, wozu das Bestimmen aller seiner Variablen und ihrer logischen Beziehungen gehört, und Erstellen der nötigen Flussdiagramme.
  • Das Definieren des Datenmodells erfordert das Generieren des gewünschten Ergebnisses.
  • Erstellen einer computerisierten Implementierung des Modells.
  • Überprüfen, ob das implementierte Modell dem Entwurf entspricht.
  • Überprüfen mittels Vergleich, ob der Simulator tatsächlich das simulierte reale System abbildet.
  • Experimentieren zum Erzeugen der gewünschten Daten mithilfe des Simulators.
  • Analysieren und Interpretieren der Ergebnisse des Simulators und Treffen von Entscheidungen basierend auf diesen Ergebnissen.
  • Dokumentieren des erstellten Modells und des Simulators als Hilfsmittel.

Simulationen erfassen im Allgemeinen entweder einen laufenden Prozess oder diskrete Ereignisse. Zum Simulieren eines Wettersystems erfolgt die Verfolgung beispielsweise fortlaufend, da sich alle Elemente ständig ändern. Demzufolge wird die Temperaturvariable im Vergleich mit der Zeitvariablen meist als kontinuierliche Kurve dargestellt. Im Gegensatz dazu erfolgen Starts und Landungen von Flugzeugen zu bestimmten Zeitpunkten, weshalb eine Simulation nur genau diese Momente bzw. Ereignisse berücksichtigen muss und alle anderen verwerfen kann. Diese Art der Simulation wird als „Diskrete ereignisorientierte Simulation“ (DES) bezeichnet und in diesem Artikel erörtert.

Diskrete ereignisorientierte Simulation

Die DES modelliert ein System oder einen Prozess als geordnete Sequenz einzelner Ereignisse über einen Zeitraum, und zwar vom Zeitpunkt des ersten Ereignisses bis zum Zeitpunkt des nächsten Ereignisses. Deshalb ist bei einer DES-Simulation die Zeit meist wesentlich kürzer als die Echtzeit.

Beim Entwickeln einer DES sind sechs Hauptelemente zu berücksichtigen:

Objekte stellen Elemente des realen Systems dar. Sie haben Eigenschaften, stehen in Beziehung zu Ereignissen, nutzen Ressourcen und werden über einen Zeitraum Warteschlangen hinzugefügt und aus diesen entfernt. Beim zuvor erwähnten Szenario mit startenden und landenden Flugzeugen sind diese die Objekte. Bei einem System für das Gesundheitswesen können Patienten oder Organe Objekte sein. Bei einem Lagersystem sind die Produkte auf Lager die Objekte. Objekte sollen miteinander oder mit dem System interagieren und können jederzeit während einer Simulation erstellt werden.

Eigenschaften sind besondere Merkmale der einzelnen Objekte (Größe, Landezeit, Geschlecht, Preis usw.), die gespeichert werden, um Reaktionen auf verschiedene Szenarien zu bestimmen, die in der Simulation erfolgen können. Diese Werte sind änderbar.

Ereignisse sind Dinge, die im System erfolgen, insbesondere bei Objekten, wie z. B. das Landen eines Flugzeugs, der Eingang eines Produkts im Lager, das Auftreten einer bestimmte Krankheit usw. Ereignisse können in beliebiger Reihenfolge eintreten und sich wiederholen.

Ressourcen sind Elemente, die Objekten Dienste bereitstellen (z. B. die Landebahn an einem Flughafen, die Lagerräume in einem Lager und Ärzte in einem Krankenhaus). Wenn eine Ressource belegt ist und ein Objekt sie braucht, muss sich das Objekt in eine Warteschlange einreihen und warten, bis die Ressource verfügbar ist.

Warteschlangen sind Kanäle, in denen Objekte organisiert werden, um auf die Freigabe einer derzeit belegten Ressource zu warten. Warteschlangen können eine Maximalkapazität haben und verschiedene Aufrufansätze aufweisen: FIFO (First-in-first-out), LIFO (Last-in-first-out) oder basierend auf bestimmten Kriterien oder einer Priorität (Krankheitsverlauf, Kraftstoffverbrauch o. ä.).

Zeit (d. h. deren Ablauf im wirklichen Leben) ist für die Simulation essenziell. Zum Messen der Zeit wird eine Uhr zu Beginn einer Simulation gestartet, die zum Verfolgen bestimmter Zeiträume genutzt werden kann (Abfahrt- und Ankunftszeit, Transportdauer, Zeitraum mit bestimmten Symptomen usw.). Diese Verfolgung ist wesentlich, da Sie dadurch wissen können, wann das nächste Ereignis eintreten sollte.

Da Simulationsprogramme kompliziert sein können, wurde vielfach versucht, Sprachen zu entwickeln, die alle Anforderungen des Simulationsparadigmas enthalten, um die Entwicklung zu erleichtern. Eine davon ist die in den 1960er Jahren von Ole-Johan Dahl und Kristen Nygaard entwickelte Sprache SIMULA, die als Erste mit dem Konzept der objektorientierten Programmierung (OOP) aufwartete, dem heute führenden Programmiermodell. Mittlerweile liegt der Fokus mehr auf dem Erstellen von Paketen, Frameworks oder Bibliotheken, die das enthalten, was Programmierer beim Erstellen einer Simulation benötigen. Diese Bibliotheken können in herkömmlichen Programmiersprachen wie C#, C++, Java oder Python aufgerufen werden.

In „A History of Discrete Event Simulation Programming Languages“ gibt Richard E. Nance sechs Mindestanforderungen vor, die eine DES-Programmiersprache erfüllen sollte (bit.ly/1ZnvkVn):

  • Zufallszahlengenerierung.
  • Prozesstransformationen, um andere Variablen als die gleichverteilten Zufallsvariablen zu erlauben.
  • Listenverarbeitung zum Erleichtern der Erstellung, Bearbeitung und Löschung von Objekten.
  • Statistische Analysen zum Bereitstellen einer beschreibenden Zusammenfassung des Modellverhaltens.
  • Berichtserzeugung zum Unterstützen der Darstellung großer Datenbestände und Vereinfachen der Entscheidungsfindung.
  • Zeitablaufmechanismus.

Diese Anforderungen lassen sich alle mit C# erfüllen, weshalb ich in diesem Artikel eine in C# entwickelte diskrete ereignisorientierte Simulation des Bevölkerungswachstum präsentiere.

DES für Populations- bzw. Bevölkerungswachstum

Populationswachstum ist einer der zahlreichen Aspekte, die bei der Forschung berücksichtigt werden, wie Populationen (Tiere und Pflanzen) sich mit der Zeit in einem Raum ändern und mit ihrer Umgebung interagieren. Populationen sind Gruppen von Organismen derselben Art, die zur gleichen Zeit leben, mitunter im selben Raum oder gemäß besonderer Merkmale voneinander getrennt.

Warum ist die Erforschung des Populationswachstums wichtig? Wenn Wissenschaftler besser verstehen, wie Populationen wachsen oder schrumpfen, lassen sich bessere Vorhersagen zu Änderungen treffen, z. B. Erhalt der Biodiversität, Ressourcenverbrauch, Klimaänderungen, Verschmutzung, Gesundheitsschutz, Transportanforderungen usw. Außerdem lassen sich Einblicke gewinnen, wie Organismen miteinander und mit der Umwelt interagieren, was ein wichtiger Aspekt ist, wenn geprüft wird, ob eine Population gedeiht oder zurückgeht.

In diesem Artikel stelle ich eine DES für das Wachstum einer Bevölkerung vor. Ziel ist das Beobachten, wie sich die Bevölkerung mit der Zeit entwickelt, und die Erfassung von Statistiken durch Analyse der Endergebnisse (Bevölkerungsgröße, alte Menschen, junge Menschen usw.). Die Bevölkerung beginnt mit „m“ männlichen und „n“ weiblichen Personen, denen jeweils ein Alter zugeordnet ist. „m“ und „n“ müssen selbstredend größer als null sein, damit die Simulation überhaupt sinnvoll ist. Die Objekte in diesem Modell sind Personen.

Eine Person kann eine Beziehung mit einer anderen Person nach Erreichen eines Alters eingehen, das mithilfe einer Poisson-Funktion mit dem Parameter λ = 18 Jahre verteilt wird. (Poisson-, Normal- und Exponentialverteilung werden im nächsten Abschnitt ausführlich erläutert.)

Es besteht eine Wahrscheinlichkeit von unter 50 %, dass sich alleinstehende und geeignete Personen unterschiedlichen Geschlechts aufeinander einlassen, und selbst das tritt nur ein, wenn der Altersunterschied nicht größer als 5 Jahre ist. Abbildung 1 zeigt ein Beispiel der Wahrscheinlichkeit des Endes einer Beziehung zwischen zwei Personen.

Abbildung 1: Wahrscheinlichkeit eines Beziehungsendes

Durchschnittsalter Wahrscheinlichkeit
14-20 0,7
21-28 0,5
29+ 0,2

Personen in einer Beziehung können ein Kind nach einem Zeitraum bekommen, der sich gemäß einer Exponentialfunktion mit dem Parameter λ = 8 Jahre verteilt.

Eine Frau kann in einem Alter schwanger werden, das einer (glockenförmigen) Normalverteilungsfunktion mit den Parametern µ = 28 und σ folgt.2= 8 Jahre. Jede Frau hat eine Anzahl von Kindern, die sich anhand einer Normalverteilungsfunktion mit den Parametern µ = 2 und σ verteilt.2= 6 Jahre. (Der Parameter µ stellt das Durchschnittsalter dar, während σ2ein Maß der Altersschwankung ist.)

Jede Person hat eine Lebenserwartung, die sich gemäß einer Poisson-Funktion mit dem Parameter λ = 70 verteilt, wobei λ die durchschnittliche Lebenserwartung darstellt.

In der vorherigen Beschreibung lassen sich mehrere Ereignisse ausmachen:

  • Eine Beziehung eingehen.
  • Eine Beziehung beenden.
  • Schwanger werden.
  • Ein Kind bekommen.
  • Sterben.

Jedes Ereignis wird von einer diskreten Zufallsvariablen begleitet, die den Moment bestimmt, in dem das Ereignis eintritt.

Wahrscheinlichkeitsverteilungen

Eine diskrete Zufallsvariable hat eine Menge von Werten, die endlich oder abzählbar unendlich ist. Die Werte können also als eine endliche oder unendliche Folge von Werten 1, 2, 3 … aufgeführt werden. Die Wahrscheinlichkeitsverteilung einer diskreten Zufallsvariablen ist ein Graph, eine Tabelle oder Formel, der/die jedem möglichen Wert eine Wahrscheinlichkeit zuweist. Die Summe aller Wahrscheinlichkeiten muss 1 sein, und jede einzelne Wahrscheinlichkeit muss im Bereich von 0 bis 1 liegen. Wenn ich beispielsweise einen ordnungsgemäß hergestellten Würfel (dessen Seiten alle gleich wahrscheinlich sind) werfe, hat die diskrete Zufallsvariable X, die die möglichen Ergebnisse darstellt, die Wahrscheinlichkeitsverteilung X(1) = 1/6, X(2) = 1/6, …, X(6) = 1/6. Alle Seiten sind gleich wahrscheinlich, weshalb die jeweils zugeordnete Wahrscheinlichkeit 1/6 ist.

Die Parameter l und µ geben das arithmetische Mittel (den erwarteten Wert) in ihren entsprechenden Verteilungen an. Das arithmetische Mittel stellt den Wert dar, den die Zufallsvariable durchschnittlich verwendet. In anderen Worten handelt es sich um die Summe E=[(jedes mögliche Ergebnis) × (Wahrscheinlichkeit dieses Ergebnisses)], wobei E das arithmetische Mittel darstellt. Im Fall des Würfels ist das arithmetische Mittel E = 1/6 + 2/6 + 3/6 + 4/6 + 5/6 + 6/6 = 3,5. Beachten Sie, dass das Ergebnis 3,5 die Hälfte aller möglichen Werte ist, die der Würfel aufweisen kann, d. h. dies ist der erwartete Wert, wenn der Würfel sehr oft geworfen wird.

Der Parameter σ2 gibt die Abweichung der Verteilung an. Die Abweichung stellt die Verteilung möglicher Werte der Zufallsvariablen dar und ist stets nicht negativ. Kleine Abweichungen (nahe 0) bedeuten meist, dass die Werte nahe beieinander und am arithmetischen Mittel sind. Große Abweichungen (nahe 1) weisen auf eine große Entfernung zwischen den Werten und vom arithmetischen Mittel hin.

Die Poisson-Verteilung ist eine diskrete Verteilung, die Wahrscheinlichkeiten hinsichtlich der Anzahl von Ereignissen pro Zeiteinheit ausdrückt (siehe Abbildung 2). Sie kommt meist zum Einsatz, wenn die Wahrscheinlichkeit eines Ereignisses gering und die Anzahl der Chancen für sein Eintreten hoch ist. Die Anzahl von Druckfehlern in einem Buch, der Kunden, die in einem Einkaufscenter eintreffen, von Autos, die an Ampeln zum Stehen kommen, und Todesfälle pro Jahr in einer bestimmten Altersgruppe sind allesamt Beispiel für die Anwendung der Poisson-Verteilung.

Poisson-Verteilung, Parameter λ = 18
Abbildung 2: Poisson-Verteilung, Parameter λ = 18

Eine Exponentialverteilung drückt die Zeit zwischen Ereignissen in einem Poisson-Prozess aus (siehe Abbildung 3). Wenn Sie es beispielsweise mit einem Poisson-Prozess zu tun haben, der die Anzahl der Kunden beschreibt, die während eines bestimmten Zeitraums in einem Einkaufscenter eintreffen, sind Sie ggf. an einer Zufallsvariablen interessiert, die angibt, wie Zeit verstrichen ist, bevor der erste Kunde eintrifft. Eine Exponentialverteilung eignet sich hierfür. Sie kann auch auf physikalische Prozesse angewendet werden, um beispielsweise die Lebensdauer von Partikeln darzustellen, wobei λ die Rate angibt, mit der das Partikel altert.

Exponentialverteilung, Parameter λ = 8
Abbildung 3: Exponentialverteilung, Parameter λ = 8

Die Normalverteilung beschreibt Wahrscheinlichkeiten im Umfeld eines zentralen Werts ohne Abweichung nach links oder rechts (siehe Abbildung 4). Normalverteilungen sind symmetrisch und haben glockenförmige Dichtekurven mit einer einzelnen Spitze beim arithmetischen Mittel. 50 % der Verteilung befinden sich links vom arithmetischen Mittel und 50 % rechts davon. Die Standardabweichung gibt den Umfang der Glockenkurve an – je kleiner die Standardabweichung, desto konzentrierter die Daten. Sowohl das arithmetische Mittel als auch die Standardabweichung müssen als Parameter für die Normalverteilung angegeben werden. Viele Naturphänomene halten sich eng an eine Normalverteilung: Blutdruck, Körpergröße, Messfehler u.v.m.

Normalverteilung, Parameter µ = 28 und σ2 = 8 Jahre
Abbildung 4: Normalverteilung, Parameter µ = 28 undσ2 = 8 Jahre

Als Nächstes zeige ich, wie die vorgeschlagene DES in einer beliebten, durchdachten und eleganten Sprache wie C# implementiert wird.

 

Implementierung

Zum Entwickeln dieser Simulation nutze ich alle Vorteile des OOP-Modells. Ziel ist Code, der so lesbar wie möglich ist. Wissenschaftliche Anwendungen sind meist komplex und schwierig zu verstehen. Deshalb ist es sehr wichtig, sie so klar wie möglich zu gestalten, damit andere Ihren Code verstehen können. Ich entwickle die Anwendung als Konsolenanwendung in C# mit der in Abbildung 5 gezeigten Struktur.

Die Struktur der Simulationsanwendung
Abbildung 5: Die Struktur der Simulationsanwendung

Ein logischer Pfad ist sehr wichtig. Beachten Sie, wie die Namespacestruktur ihre Übersichtlichkeit behält: Simulation.DES.PopulationGrowth.

Der Ordner „Events“ enthält die Datei „Event.cs“, die einen „enum“-Typ definiert, der alle möglichen Ereignisse in der Simulation darstellt:

namespace DES.PopulationGrowth.Events
{
public enum Event
  {
Capable Engaging,
Birth EngageDisengage,
GetPregnant,
ChildrenCount,
TimeChildren,
    Die
  }
}

Der Ordner „Objects“ enthält alle personenbezogenen Klassen. Dies sind die Teile des Codes, die am meisten von der OOP profitieren. Es gibt drei personenbezogene Klassen: „Individual“, „Male“ und „Female“. Die erste ist eine abstrakte Klasse, von der die anderen erben, was heißt, dass Männer und Frauen zu „Individual“ gehören. Die meiste Programmierung erfolgt in der „Individual“-Klasse. Abbildung 6 zeigt ihre Eigenschaften und Methoden.

Eigenschaften und Methoden der „Individual“-Klasse
Abbildung 6: Eigenschaften und Methoden der „Individual“-Klasse

Es folgt eine Beschreibung der einzelnen Eigenschaften:

  • Age: Alter der Person.
  • Couple: „Null“, wenn die Person alleinstehend ist, andernfalls ihr Partner, eine weitere Person.
  • Engaged: „True“, wenn die Person eine Beziehung hat, andernfalls „false“.
  • LifeTime: Alter, in dem die Person stirbt.
  • RelationAge: Alter, in dem die Person eine Beziehung eingehen kann.
  • TimeChildren: Zeit in der Simulation (nicht das Alter), zu der die Person Kinder bekommen kann.

Es folgt eine Beschreibung ihrer Methoden:

  • Disengage: Beendet die Beziehung zwischen zwei Personen.
  • EndRelation: Bestimmt, ob die Beziehung enden soll, gemäß der Wahrscheinlichkeitstabelle in Abbildung 1.
  • FindPartner: Sucht einen verfügbaren Partner für eine Person.
  • SuitablePartner: Bestimmt, ob eine Person geeignet ist, eine Beziehung einzugehen (Altersunterschied und Andersgeschlechtlichkeit).
  • SuitableRelation: Bestimmt, ob eine Person eine Beziehung eingehen kann, d. h. alleinstehend und im entsprechenden Alter ist.
  • ToString: „Override“, um Informationen zu einer Person als Zeichenfolge darzustellen.

Die Klasse beginnt mit dem Definieren aller Eigenschaften und später des Konstruktors, der lediglich das Alter festlegt:

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;
}

Die Methoden „SuitableRelation“ und „SuitablePartner“ sowie die „Engaged“-Eigenschaft sind bloß einfache logische Tests:

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; }
}

Die „Disengage“-Methode löst eine Beziehung auf, indem das Feld „Couple“ auf 0 für das Paar und später für die Person festgelegt wird. Auch ihre Zeit, ein Kind zu bekommen, wird auf 0 festgelegt, da die Beziehung nicht mehr besteht.

public void Disengage()
{
Couple.Couple = null;
Couple = null;
TimeChildren = 0;
}

Die „EndRelation“-Methode ist im Wesentlichen eine Übersetzung der Wahrscheinlichkeitstabelle zum Bestimmen der Chance eines Beziehungsendes. Sie verwendet eine gleichwertige Zufallsvariable, die einen Zufallswert im Bereich [0, 1] entsprechend dem Generieren eines Zustimmungsprozentsatzes erzeugt. Das Wörterbuch „distributions“ wird in der „simulation“-Klasse erstellt (die in Kürze beschrieben wird) und enthält Paare (Ereignis, Wahrscheinlichkeitsverteilung), womit jedes Ereignis mit seiner Verteilung verknüpft wird:

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;
}

Die „FindPartner“-Methode (siehe Abbildung 7) findet einen verfügbaren Partner für die Instanz „individual“. Sie erhält als Eingabe die Liste „population“, die aktuelle Zeit der Simulation und das Wörterbuch „distributions“.

Abbildung 7: Die „FindPartner“-Methode

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;
}
}

Zuletzt definiert die „ToString“-Methode die Zeichenfolgendarstellung einer Person:

public override string ToString()
{
Return string.Format("Age: {0} Lifetime {1}", Age, LifeTime);
}

Die „Male“-Klasse ist sehr einfach:

public class Male: Individual
{
public Male(int age) : base(age)
{
}
public override string ToString()
{
return base.ToString() + " Male";
}
}

Die „Female“-Klasse ist ein wenig komplizierter, da sie Faktoren wie Schwangerschaft, Geburt usw. berücksichtigen muss. Die Klassendefinition beginnt mit dem Deklarieren aller Eigenschaften und des Konstruktors:

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)
{
}
}

Es folgen die Eigenschaften der „Female“-Klasse:

  • IsPregnant: Bestimmt, ob diese Frau schwanger ist.
  • PregnancyAge: Bestimmt das Alter, in dem die Frau schwanger werden kann.
  • ChildrenCount: Gibt die Anzahl zu gebärender Kinder an.

Hier die Methoden dieser Klasse:

  • SuitablePregnancy: Bestimmt, ob diese Frau schwanger werden kann.
  • GiveBirth: Gibt eine gebärende Frau an, die der Population eine neue Person hinzufügt.
  • ToString:override: Dient zum Darstellen der Informationen zu einer Frau als Zeichenfolge.

„SuitablePregnancy“ ist eine Testmethode, die „true“ zurückgibt, wenn die Instanz „Frau“ alle Bedingungen für eine Schwangerschaft erfüllt:

public bool SuitablePregnancy(intcurrentTime)
  {
return Age >= PregnancyAge && currentTime <= TimeChildren && ChildrenCount > 0;
}

Die „GiveBirth“-Methode (siehe Abbildung 8) enthält den Code zum Hinzufügen und Initialisieren neuer Personen in der Population.

Abbildung 8: Die „GiveBirth“-Methode

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;
  }

Eine gleichwertige Stichprobe wird generiert, um zuerst zu bestimmen, ob die neue Person weiblich oder männlich sein wird, mit je einer Wahrscheinlichkeit von 50 % (0,5). Der Wert von „ChildrenCount“ wird um 1 gesenkt, um anzugeben, dass diese Frau ein Kind weniger zu gebären aufweist. Der restliche Code dient zum Initialisieren von Personen und Zurücksetzen einiger Variablen.

Die „ToString“-Methode ändert die Weise, in der Frauen als Zeichenfolgen dargestellt werden:

public override string ToString()
  {
return base.ToString() + " Female";
}

Da alle Funktionen in Bezug auf Personen in getrennten Klassen platziert wurden, ist die „Simulation“-Klasse nun wesentlich einfacher. Eigenschaften und Konstruktor befinden sich am Anfang des Codes:

public class Simulation
  {
public List<Individual> Population { get; set; }
public int Time { get; set; }
private int _currentTime;
private readonly Dictionary<Event, IDistribution> _distributions ;

Die Eigenschaften und Variablen erklären sich von selbst, wobei einige zuvor beschrieben wurden. „Time“ stellt die Dauer der Simulation (in Jahren) und „_currentTime“ das aktuelle Jahr der Simulation dar. Der Konstruktor in diesem Fall (siehe Abbildung 9) ist komplizierter, da er die Initialisierung der Zufallsvariablen für jede Person vorsieht.

Abbildung 9: Der Konstruktor der „Simulation“-Klasse

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();
}
}
}

In der „Execute“-Methode (siehe Abbildung 10) erfolgt schließlich sämtliche Simulationslogik.

Abbildung 10: Die „Execute“-Methode

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++;
}
}

Iterationen in der äußeren Schleife stellen Jahre dar, die in der Simulation ablaufen. Die innere Schleife durchläuft Ereignisse, die für diese Personen im jeweiligen Jahr eintreten können.

Um zu prüfen, wie sich die Bevölkerung mit der Zeit entwickelt, habe ich eine neue Konsolenanwendung eingerichtet (siehe Abbildung 11).

Abbildung 11: Anzeigen der Bevölkerung im Zeitverlauf

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();
}

Abbildung 12 zeigt die Bevölkerung nach 1.000 Jahren.

Bevölkerung nach 1.000 Jahren
Abbildung 12: Bevölkerung nach 1.000 Jahren

In diesem Artikel habe ich eine diskrete ereignisorientierte Simulation entwickelt, um zu prüfen, wie sich eine Bevölkerung mit der Zeit entwickelt. Der objektorientierte Ansatz erwies sich als sehr hilfreich für das Erzielen eines lesbaren, kompakten Codes, den Leser bei Bedarf ausprobieren und verbessern können.


Arnaldo Pérez Castaño* ist ein auf Kuba lebender Informatiker. Er ist Autor einer Reihe von Büchern zur Programmierung: „JavaScript Fácil“, „HTML y CSS Fácil“ und „Python Fácil“ (Marcombo S.A). Er ist Fachmann für Visual Basic, C#, .NET Framework und künstliche Intelligenz und bietet seine Dienste auf nubelo.com als Freiberufler an. Das Kino und die Musik zählen zu seinen Leidenschaften. Sie erreichen ihn unter <arnaldo.skywalker@gmail.com.>*

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: James McCaffrey