Ein Überblick über die Erstellung eines System Dynamics Modells unter Verwendung des BPTK-Py-Frameworks
System Dynamics-Simulationen interaktiv in Jupyter mit Python erstellen
Wir lieben es, Computermodelle zu erstellen und unsere Lieblingsumgebungen für diese Art von explorativer analytischer Arbeit sind Jupyter und Python.
Um die Computational-Modeling zu erleichtern, entwickeln wir ein einfaches Framework, das derzeit System Dynamics und Agentbasierte Modeling unterstützt.
Seitdem haben wir einige neue Funktionen erstellt, mit denen Sie System Dynamics Modelle und agentenbasierte Modelle interaktiv in
Jupyter mit
Python erstellen können. Um die Modellerstellung so einfach wie möglich zu gestalten, haben wir eine einfache, domänenspezifische Sprache (DSL) erstellt, die sowohl System Dynamics als auch agentenbasierte Modellierung unterstützt und einen Großteil der zugrunde liegenden Komplexität von Computational-Models verbirgt.
Mit dieser Sprache können Sie nicht nur System Dynamics Modelle und agentenbasierte Modelle erstellen, sondern Sie können sogar beide mischen, um "hybride" Simulationsmodelle zu erstellen.
Eine solche DSL zu haben, ist aus mehreren Gründen nützlich:
Erstellen Sie Modelle interaktiv in Jupyter, wodurch der Modellierungsprozess sehr effektiv wird.
Python-Anfänger können sich auf die Modellierung konzentrieren, ohne dass sie viel über Python wissen müssen.
Python-Experten können ihre Modelle mit anderen analytischen Frameworks mischen, z. B. mit Machine-Learning-Toolkits.
Natürlich fügt sich die neue Funktionalität problemlos in den Rest des
BPTK-Py-Frameworks ein, sodass Sie die Vorteile aller High-Level-Szenario-Management- und Diagramm-Funktionen, die Teil des Frameworks sind, nutzen können.
In diesem Beitrag konzentriere ich mich auf die Erstellung eines System Dynamics Modells unter Verwendung des Frameworks, ich werde in einem späteren Beitrag einen Blick auf die agentenbasierte Modellierung werfen.
Der Beitrag ist auch als Jupyter-Notebook in unserem BPTK-Py-Tutorial verfügbar, das
hier zum Download bereitsteht.
Ein einfaches Modell zur Demonstration der Bibliothek
Zur Veranschaulichung der DSL werden wir das einfache Projektmanagementmodell aufbauen, das wir in unserem
Einführung in System Dynamics vorgestellt haben.
Das Projektmanagementmodell ist wirklich einfach und enthält nur ein paar Bestände, Flüsse und Konverter, wie Sie im folgenden Diagramm sehen können:
Selbst wenn Sie also das Modell nicht kennen, sollten Sie diesem Beitrag sehr leicht folgen können.
Um loszulegen, müssen wir zunächst die Bibliothek und insbesondere die SD Funktionsbibliothek in unser Notebook importieren.
from BPTK_Py import Model
from BPTK_Py import sd_functions as sd
Die SD-Funktionsbibliothek enthält die Funktionen und Operatoren, die zur Definition von Modellgleichungen benötigt werden (dies sind die Built-Ins, die Sie aus Ihren visuellen Modellierungsumgebungen wie Stella oder Vensim kennen). Da die Bibliothek Funktionen wie
min
und
max
enthält, die auch Teil der Python-Standardbibliothek sind, importieren wir die SD-Funktionsbibliothek mit dem Präfix sd, um Namenskonflikte zu vermeiden.
Als nächstes erstellen wir ein Modell mit der Klasse
Model
. Unser Modell wird alle unsere Modellelemente wie Bestände, Flüsse, Konverter und Konstanten enthalten.
Model(starttime=0,stoptime=120,dt=1,name='SimpleProjectManagament')
Das Erstellen von Modellelementen ist wirklich einfach:
openTasks = model.stock("openTasks")
Wie Sie sehen können, besteht unsere Konvention darin, die für die Modellelemente die Camel-Case-Namenskonvention zu verwenden und für Python Variablen für die gleichnamigen Elemente zu erstellen.
Sobald ein Modellelement auf diese Weise definiert wurde, brauchen wir uns nur noch auf die Python Variable zu beziehen und müssen nicht mehr auf das Modellelement verweisen (d. h. wir können
openTasks
in unseren Gleichungen verwenden, im Gegensatz zur Verwendung von
model.stock("openTasks")
). Dies spart eine Menge Tipparbeit.
Lassen Sie uns nun auch die anderen Modellelemente und Variablen definieren, damit wir uns anschließend auf die Gleichungen konzentrieren können:
closedTasks = model.stock("closedTasks")
staff = model.stock("staff")
completionRate = model.flow("completionRate")
currentTime = model.converter("currentTime")
remainingTime = model.converter("remainingTime")
schedulePressure = model.converter("schedulePressure")
productivity = model.converter("productivity")
deadline = model.constant("deadline")
effortPerTask = model.constant("effortPerTask")
initialStaff = model.constant("initialStaff")
initialOpenTasks = model.constant("initialOpenTasks")
Beachten Sie, dass wir in unseren Modellen zwischen Konstanten und Konvertern unterscheiden - dies ist aus Sicht von System Dynamics nicht unbedingt notwendig, erleichtert aber die Überprüfung des Modells auf Fehler.
Nun wollen wir unsere Bestände initialisieren - dazu müssen wir nur die Eigenschaft
initial_value
der Bestände setzen. Diese Eigenschaft kann entweder eine numerische Konstante oder ein konstantes Element sein.
closedTasks.initial_value = 0
staff.initial_value = initialStaff
openTasks.initial_value = initialOpenTasks
Die Definition der Modellgleichungen ist wirklich einfach: Jede Modellvariable hat eine Gleichungseigenschaft, die Gleichung selbst wird ähnlich wie in einer visuellen Modellierungsumgebung geschrieben, wobei die anderen Modellvariablen nach Bedarf verwendet werden.
Die Definition von Konstanten ist besonders einfach:
deadline.equation = 100
effortPerTask.equation = 1
initialStaff.equation = 1
initialOpenTasks.equation = 100
Die Variable
currentTime
verfolgt die Simulationszeit, die von der Funktion time in der SD-Funktionsbibliothek erfasst wird.
currentTime.equation=sd.time()
Die
remainingTime
ist gleich der Differenz zwischen der
deadline
und der
currentTime
:
remainingTime.equation = deadline - currentTime
Sie sehen also, dank der DSL ist das Definieren von Gleichungen sehr intuitiv!
Die Gleichungen für die Bestände sind ebenfalls sehr einfach - sie enthalten nur die Einflüsse (mit einem positiven Zeichen) und die Abflüsse (mit einem negativen Zeichen).
Ein kurzer Blick auf das obige Diagramm zeigt uns, dass die
openTasks
nur einen Abfluss (definiert durch die
completionRate
) und die
closedTasks
nur einen Einfluss (ebenfalls definiert durch die
completionRate
) haben:
openTasks.equation = -completionRate
closedTasks.equation = completionRate
Der
schedulePressure
ist ein dimensionsloses Verhältnis, das den erforderlichen Aufwand zur Erledigung aller verbleibenden offenen Aufgaben mit der verbleibenden Arbeitskapazität vergleicht.
Wir verwenden die Funktionen
min
und
max
aus der SD-Funktionsbibliothek, um sicherzustellen, dass keine Division durch Null erfolgt und dass der Zeitplandruck auf 2,5 begrenzt wird:
schedulePressure.equation = sd.min(
(openTasks*effortPerTask)/
(staff*sd.max(remainingTime,1)),
2.5
)
Wir definieren die Produktivität in unserem Modell über eine nicht lineare Beziehung (abhängig vom Zeitplandruck). Wir erfassen diese Beziehung in einer Look-up-Tabelle, die wir in der
points
Eigenschaft des Modells speichern (unter Verwendung einer Python Liste):
model.points["productivity"] = [
[0,0.4],
[0.25,0.444],
[0.5,0.506],
[0.75,0.594],
[1,1],
[1.25,1.119],
[1.5,1.1625],
[1.75,1.2125],
[2,1.2375],
[2.25,1.245],
[2.5,1.25]
]
Wir können die Look-up-Tabelle einfach darstellen, um zu sehen, ob sie die richtige Form hat:
model.plot_lookup("productivity")
Die Produktivitätsgleichung wird dann über eine Lookup-Funktion definiert - in unserem Fall hängt die
productivity
nichtlinear von
schedulePressure
ab, wie in der Lookup-Tabelle definiert:
productivity.equation = sd.lookup(schedulePressure,"productivity")
Die letzte Gleichung, die wir definieren müssen, ist die
completionRate
- die Fertigstellungsrate ist definiert durch die Anzahl der am Projekt arbeitenden Mitarbeiter geteilt durch den Aufwand pro Aufgabe. Dies multiplizieren wir dann mit der (durchschnittlichen) Produktivität der Mitarbeiter. Die Fertigstellungsrate darf nie größer sein als die Anzahl der
openTasks
, daher schränken wir sie mit der Funktion
min
ein.
completionRate.equation = sd.max(0,
sd.min(openTasks,
staff*(productivity/effortPerTask)
)
)
Nachdem wir nun alle notwendigen Gleichungen definiert haben, sind wir bereit, das Modell auszuführen. Am einfachsten ist es, eine Modellvariable zu einem bestimmten Zeitschritt auszuwerten - dieser Ansatz ist besonders nützlich, wenn Sie das Modell interaktiv erstellen (z. B. in einem Jupyter-Notebook) und Zwischenergebnisse testen.
closedTasks(80), closedTasks(100), closedTasks(120)
(80.0, 100.0, 100.0)
Lassen Sie uns mit verschiedenen Einstellungen für die Frist spielen:
deadline.equation = 120
closedTasks(80), closedTasks(100), closedTasks(120)
(63.33020661244643, 81.06644489208418, 99.99777243819346)
deadline.equation=80
closedTasks(80), closedTasks(100), closedTasks(120)
(92.6853060260874, 100.00000000000004, 100.00000000000004)
Natürlich können wir die Variablen auch direkt in einem Diagramm darstellen, indem wir die
plot()
Methode des Elements verwenden.
Jetzt, da wir ein Modell haben, können wir das leistungsfähige Szenario-Management nutzen, die in das BPTK-Py-Framework eingebaut ist.
import BPTK_Py
bptk = BPTK_Py.bptk()
Dann richten wir einen Szenario-Manager mit einem Python Dictionary ein. Der Szenario-Manager identifiziert die Basiskonstanten des Modells:
scenario_manager = {
"smSimpleProjectManagementDSL":{
"model": model,
"base_constants": {
"deadline": 100,
"initialStaff": 1,
"effortPerTask": 1,
"initialOpenTasks": 100,
},
"base_points":{
"productivity": [
[0,0.4],
[0.25,0.444],
[0.5,0.506],
[0.75,0.594],
[1,1],
[1.25,1.119],
[1.5,1.1625],
[1.75,1.2125],
[2,1.2375],
[2.25,1.245],
[2.5,1.25]
]
}
}
}
Der Szenario-Manager muss wie folgt registriert werden:
bptk.register_scenario_manager(scenario_manager)
Sobald wir dies haben, können wir (ein oder mehrere) Szenarien wie folgt definieren und registrieren:
bptk.register_scenarios(
scenarios =
{
"scenario80": {
"constants": {
"initialOpenTasks": 80
}
}
}
,
scenario_manager="smSimpleProjectManagementDSL")
Wir können dann das Szenario wie folgt darstellen:
bptk.plot_scenarios(
scenarios="scenario80",
scenario_managers="smSimpleProjectManagementDSL",
equations="openTasks")
Lassen Sie uns ein paar weitere Szenarien registrieren:
bptk.register_scenarios(
scenarios =
{
"scenario100": {
},
"scenario120": {
"constants": {
"initialOpenTasks" : 120
}
}
},
scenario_manager="smSimpleProjectManagementDSL")
scenario100
entspricht den Basiseinstellungen, daher müssen wir keine Einstellungen dafür definieren.
Jetzt können wir die Szenarien leicht vergleichen:
bptk.plot_scenarios(
scenarios="scenario80,scenario100,scenario120",
scenario_managers="smSimpleProjectManagementDSL",
equations="openTasks",
series_names={
"smSimpleProjectManagementDSL_scenario80_openTasks":"scenario80",
"smSimpleProjectManagementDSL_scenario100_openTasks":"scenario100",
"smSimpleProjectManagementDSL_scenario120_openTasks":"scenario120"
}
)
Damit ist unser kurzer Rundgang durch die SD DSL innerhalb des Business Prototyping Toolkits abgeschlossen. Das BPTK-Framework ist unter der MIT-Lizenz auf
PyPi verfügbar, sodass Sie es sofort einsetzen können.
Sie können ein Tutorial (das diesen Blogbeitrag als Jupyter-Notebook enthält) auf der
BPTK-Produkt-Homepage herunterladen.
Das Tutorial zeigt auch einige fortgeschrittenere Techniken, insbesondere auch, wie Sie die SD DSL in Python nutzen können, ohne Jupyter zu verwenden.
Fazit
In diesem Beitrag wurde eine einfache domänenspezifische Sprache für System Dynamics vorgestellt, die in Python implementiert ist. Mit ihr können Sie System Dynamics in Python erstellen und die interaktive Modellierung in Jupyter unterstützen.
Das Erstellen von System Dynamics Modellen direkt in Python ist besonders nützlich, wenn Sie Ihre SD Modelle mit eigenen SD Funktionen erweitern oder Ihre Modelle mit anderen Computermodellen wie agentenbasierten Modellen oder mathematischen Modellen kombinieren möchten.