Ruprecht-Karls Universität Heidelberg Fakultät für Mathematik und Informatik Lehrstuhl Technische Simulation Bachelor-Arbeit Implementierung eines effizienten Parameter-Vererbungsalgorithmus zur Konfiguration des Softwarepakets NeuGen Sergei Wolf Matrikelnummer: 2241758 Betreut durch Prof. Dr. Gabriel Wittum und Dr. Jens P. Eberhard 5. Dezember 2007 Erklärung Hiermit versichere ich, dass ich diese Bachelor-Arbeit selbstständig verfasst und nur die angegebenen Quellen und Hilfsmittel verwendet habe. .................................................... Datum: 5. Dezember 2007 Danksagung Ich danke Herrn Prof. Dr. Gabriel Wittum und Herrn Dr. Jens P. Eberhard für die Vergabe des interessanten Themas. Besonderen Dank schulde ich Alexander Wanner und Simone Eberhard für zahllose kritische Kommentare und Verbesserungsvorschläge. Abstract The computer-based generation, computation and post-processing of neuronal networks in 3D is an important research field of the neurosciences today. In order to make progress in this field, the detailed reconstruction and analysis of neuronal nets is an indispensable instrument. For this purpose the software NeuGen has been developed and is utilized in the presented study. NeuGen is written in C++ and Java, and it provides algorithms for the generation of detailed neurons as well as neuronal networks in a three-dimensional cortical column. A configuration editor can read, handle and modify the necessary parameters for the configuration of the geometry. For an efficient inheritance of these parameters for the different neuron classes, a faster method is in order. The presented thesis deals with the development, implementation and run-time analysis of a new algorithm. The algorithm uses so-called dependence graphs, and the resulting run-time is of linear order. Zusammenfassung Die computergestützte Erzeugung, Berechnung und Verarbeitung neuronaler Netze in drei Raumdimensionen (3D) ist ein wichtiges Teilgebiet der Neurowissenschaften heutzutage. Um in diesem Gebiet die Forschung voranzutreiben, ist die detailgetreue Rekonstruktion und Analyse neuronaler Netze ein unverzichtbares Hilfsmittel. Das zu diesem Zweck entwickelte Softwarepaket NeuGen ist Gegenstand dieser Bachelor-Arbeit. NeuGen ist in C++ und Java geschrieben und enthält Algorithmen zur Erzeugung detaillierter Nervenzellen sowie neuronaler Netze in einer kortikalen Kolumne in 3D. Die zur Konfiguration erforderlichen Geometrieparameter werden über einen Konfigurationseditor eingegeben bzw. eingelesen und modifiziert. Zur effizienten Vererbung dieser Parameter auf die einzelnen Neuronen-Klassen ist ein schnellerer Algorithmus notwendig geworden. Diese Arbeit beschäftigt sich mit der Entwicklung, Implementierung und Laufzeitanalyse eines solchen Algorithmus. Der neue Algorithmus verwendet sogenannte Abhängigkeitsgraphen und zeigt insgesamt ein lineares Laufzeitverhalten. Inhaltsverzeichnis 1 2 Einleitung 1 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Grundlagen 5 2.1 Neurobiologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.1 Das Gehirn . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.2 Die Nervenzelle . . . . . . . . . . . . . . . . . . . . . . . . . 8 NeuGen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.1 Neuronenklassen . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.2 Parameterdateien . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.2.3 Vererbungsalgorithmus . . . . . . . . . . . . . . . . . . . . . . 15 2.2.4 Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2.5 Visualisierung . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2.6 Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Graphentheorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.2 2.3 3 Vererbung der Geometrieparameter 21 3.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.2 Grundbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 3.3 Definition von Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.3.1 Beziehung erster Ordnung . . . . . . . . . . . . . . . . . . . . 24 3.3.2 Beziehung n-ter Ordnung . . . . . . . . . . . . . . . . . . . . . 25 3.3.3 Reihenfolge der Vererbung . . . . . . . . . . . . . . . . . . . . 26 Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.4 i Inhaltsverzeichnis 3.5 3.6 3.7 4 Traversierung von Bäumen . . . . . . . . . . . . . . . . . . . . 28 3.4.2 Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . 31 Default-Recursive-Inheritance-Vererbungsalgorithmus . . . . . . . . . 34 3.5.1 Anhängen von Knoten . . . . . . . . . . . . . . . . . . . . . . 34 3.5.2 Vervollständigen von Knoten . . . . . . . . . . . . . . . . . . . 36 3.5.3 Vererben von Knoten . . . . . . . . . . . . . . . . . . . . . . . 39 3.5.4 Umkehrung der Vererbung . . . . . . . . . . . . . . . . . . . . 41 3.5.5 Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . 42 3.5.6 Kosten der Funktionen . . . . . . . . . . . . . . . . . . . . . . 43 Default-Inheritance-Vererbungsalgorithmus . . . . . . . . . . . . . . . 47 3.6.1 Programmablauf . . . . . . . . . . . . . . . . . . . . . . . . . 47 3.6.2 Bestimmung von Siblingketten . . . . . . . . . . . . . . . . . . 48 3.6.3 Bestimmung und Ergänzung von abhängigen Teilbäumen . . . . 50 3.6.4 Wurzel-Siblingketten . . . . . . . . . . . . . . . . . . . . . . . 53 3.6.5 Teilbäume für die Bestimmung der zweiten Ordnung . . . . . . 56 3.6.6 Vererbung von Blättern . . . . . . . . . . . . . . . . . . . . . . 58 3.6.7 Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . 61 3.6.8 Kosten der Funktionen . . . . . . . . . . . . . . . . . . . . . . 61 Simplified-Inheritance-Vererbungsalgorithmus . . . . . . . . . . . . . . 64 3.7.1 Parameterwerte aktualisieren . . . . . . . . . . . . . . . . . . . 64 3.7.2 Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . 66 3.7.3 Kosten der Funktion . . . . . . . . . . . . . . . . . . . . . . . 67 Evaluierung 68 4.1 Vergleich der Vererbungsalgorithmen . . . . . . . . . . . . . . . . . . 68 4.1.1 Default-Recursive-Inheritance-Vererbungsalgorithmus . . . . . 70 4.1.2 Default-Inheritance-Vererbungsalgorithmus . . . . . . . . . . . 72 4.1.3 Simplified-Inheritance-Vererbungsalgorithmus . . . . . . . . . 73 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.2 5 3.4.1 Zusammenfassung 76 Abbildungsverzeichnis 78 ii Inhaltsverzeichnis Algorithmenverzeichnis 81 Literaturverzeichnis 82 iii 1 Einleitung Während Sie diese Zeilen lesen, ist es schon aktiv: Ihr Gehirn. Die Informationsverarbeitung im Gehirn zu verstehen und zu simulieren, ist eines der Forschungsziele der Neuroinformatik. Dazu werden geometrische Modelle von Nervenzellen und ihrer Vernetzung benötigt. Auf solchen Modellen kann dann die Signalausbreitung in einem neuronalen Netz berechnet werden. Aufgrund technischer Beschränkungen ist die mikroskopische Aufnahme und Rekonstruktion von biologischen neuronalen Netzwerken nicht möglich. Daher wurde das Softwarepaket NeuGen entwickelt [4, Eberhard]. NeuGen erzeugt realistische Morphologien verschiedener Klassen von Neuronen und synaptisch verbundener neuronaler Netzwerke in drei Raumdimensionen. Auf solchen synthetischen Netzen kann dann die Signalausbreitung simuliert werden. Damit die Nervenzellen den in der Natur gefundenen möglichst gut entsprechen, werden Geometrieparameter für die Generierung benötigt. Beispiele für solche Geometrieparameter sind der Radius des Zellkörpers, sowie Gesamtlänge und Anzahl der Verzweigungen des Dendritenbaumes. Diese Parameter werden aus experimentellen Daten extrahiert. Die Geometrieparameter werden zur Konfiguration von NeuGen verwendet. Sie können mithilfe eines Konfigurationseditors eingestellt werden. Da viele der Parameter für die unterschiedlichen Nervenzellarten identisch sind, müssen sie nicht für jede Zelle einzeln festgelegt, sondern können vererbt werden. Vererbung bedeutet, dass ein Wert eines Parameters eines Neurons für bestimmte andere Neuronen unverändert übernommen werden kann. Dieses Vorgehen spart Speicherplatz und Bearbeitungszeit. Auch in natürlichen neuronalen Netzen werden bei verschiedenen Neuronen übereinstimmende Parameter gefunden. 1 1 Einleitung Der bisher in NeuGen implementierte Algorithmus für die Vererbung neuronaler Morphologieparameter ist rekursiv. Da die Laufzeit exponentiell anwächst, ist dieser Algorithmus für große Parameteranzahlen nicht gut geeignet. Dies ist besonders dann ein Problem, wenn viele Geometrieparameter gleichzeitig im Konfigurationseditor geändert werden. In dieser Bachelor-Arbeit wird der bisherige Vererbungsalgorithmus analysiert. Darauf aufbauend wird dann ein neuer, schnellerer Algorithmus für die Vererbung von Geometrieparametern entwickelt und in NeuGen implementiert. Die Analyse des neuen Algorithmus und ein Vergleich mit dem bisherigen wird ebenfalls für verschiedene Testfälle durchgeführt. Es wird gezeigt, dass der neue Algorithmus eine effizientere Generierung von synthetischen neuronalen Netzen in drei Raumdimensionen ermöglicht. Damit leistet die vorliegende Arbeit einen kleinen Beitrag zum Fortschritt der Neuroinformatik. 1.1 Motivation Um einen Parameter für ein Standard-Netz im Konfigurationseditor zu editieren, benötigt der bisherige Recursive-Tree-Inheritance-Vererbungsalgorithmus (RTI), der in C++ geschrieben ist, ca. 3.7 Sekunden. NeuGen wurde in C++ und die zugehörige graphische Benutzeroberfläche in Java implementiert. Aus Kompatibilitätsgründen wurde ein Programmierwerkzeug SWIG (Simplified Wrapper and Interface Generator) eingesetzt, um die graphische Benutzeroberfläche und den RTI-Vererbungsalgorithmus zu verbinden. Die Gründe für die hohe Zeit sind die exponentielle Laufzeit des RTIVererbungsalgorithmus und der Zugriff des Codes auf eine andere Programmiersprache mithilfe der generierten Funktionen von SWIG. Darauf wird im Detail im Kapitel 4 nochmals eingegangen. Im ersten Schritt wurde der RTI-Vererbungsalgorithmus und die für die Vererbung benötigten Klassen in Java implementiert. Infolgedessen ist die Zeit von 3.7 auf 0.5 Sekunden gesunken, gemessen auf einem Intel Pentium 4 mit einer Taktfrequenz von 3.00 GHz und 1 GB Arbeitsspeicher. Den Algorithmus auf der Java-Seite bezeichnen wir 2 1.1 Motivation mit Default-Recursive-Inheritance-Vererbungsalgorithmus (DRI). Der DRI-Vererbungsalgorithmus verwendet als Datenstruktur für die Geometrieparameter einen Baum. Entlang des Baumes werden Knoten ermittelt, die von anderen Knoten den Inhalt erben sollen. Die Knoten mit den zu vererbenden Geometrieparametern werden in bestimmte Knoten kopiert und als vererbt markiert. Wird ein Wert im Konfigurationseditor geändert, müssen die Knoten, die von anderen erben, wieder bestimmt werden. Für Bäume mit wenigen vererbten Geometrieparametern, ist DRI im Allgemeinen schnell, im anderen Fall, bei vielen vererbten Geometrieparametern ist DRI langsam, da es dazu kommen kann, dass der Baum mehrmals durchlaufen werden muss. DRI hat im schlechtesten Fall exponentielle Laufzeit. Ein verbesserter Default-Inheritance-Vererbungsalgorithmus (DI) wird entwickelt, der eine geringere Laufzeit aufweist. Es wird ein Abhängigkeitsgraph aufgebaut, in dem alle Informationen über abhängige Knoten und Erblasser zu jedem Knoten enthalten sind. Der Abhängigkeitsgraph wird für die Vererbung von Knoten verwendet. Für die graphische Benutzeroberfläche entwickeln wir einen vereinfachten SimplifiedInheritance-Algorithmus (SI). Dieser Algorithmus verwendet den oben erwähnten Abhängigkeitsgraphen. Im neuen Algorithmus müssen nur bestimmte Knoten durchlaufen werden. Im schlimmsten Fall muss der ganze Graph durchlaufen werden, um abhängige Knoten, die auf vererbt gesetzt sind, mit dem Inhalt von deren Erblasser zu überschreiben. Danach werden die Algorithmen DI und SI für die Parameterdatei Param.neu mittels Tests mit DRI verglichen. Dabei werden zufällige Werte in gleiche Parameterbäume eingetragen und die Vererbung mit verschiedenen Algorithmen ausgeführt. Zum Schluss vergleichen wir, ob die Algorithmen gleiche Bäume erzeugen. 3 1 Einleitung 1.2 Aufbau der Arbeit Diese Bachelorarbeit besteht aus fünf Teilen. Kapitel 2 beschäftigt sich mit den Grundlagen. Dabei werden zunächst die biologischen Grundlagen der Nervenzelle und der Kolumne, danach die Komponenten der Software NeuGen beschrieben. Anschließend werden die notwendigen Definitionen aus der Graphentheorie gegeben. Kapitel 3 stellt die Implementierung der neuen Vererbungsalgorithmen vor. Im darauf folgenden Kapitel 4 werden Algorithmen zum Testen der neuen Vererbungsalgorithmen vorgestellt. Kapitel 5 schließt mit einer Zusammenfassung. 4 2 Grundlagen Bevor wir uns näher mit den Details des Vererbungsmechanismus beschäftigen, ist es notwendig, einige grundlegende Begriffe zu klären, die zum Verständnis der Arbeit erforderlich sind. 2.1 Neurobiologie 2.1.1 Das Gehirn Das menschliche Gehirn besteht im Durchschnitt aus etwa 100 Milliarden Nervenzellen, die synaptisch miteinander verbunden sind, wiegt 1,35 Kilogramm und ist eines der größten Organe des menschlichen Körpers [1, Neil A. Campbell]. Es lässt sich in Vorderhirn (Prosencephalon) und Hirnstamm (Truncus cerebri) unterteilen. • Das Vorderhirn umfasst die Regionen Großhirn (Telencephalon) und Zwischenhirn (Diencephalon). Das Großhirn ist in eine linke und eine rechte Großhirnhemisphäre unterteilt. • Als Hirnstamm bezeichnet man die Regionen Mittelhirn (Mesencephalon) und Rautenhirn (Rhombencephalon). Das Rautenhirn besteht aus drei Untereinheiten Brücke (Pons), verlängertes Rückenmarkt (Medulla oblongata) und Kleinhirn (Cerebellum). 5 2 Grundlagen Abbildung 2.1: Schemazeichnung der wichtigsten Gehirnstrukturen aus [6, Kandel] Die Großhirnrinde (Cortex) ist der größte Teil des Gehirns mit einer Oberfläche von etwa 0,5 Quadratmeter. Der Cortex ist aus verschiedenen Schichten aufgebaut. Der Neocortex (Isocortex) wird in sechs übereinanderliegende Schichten (Layer) unterteilt [2, Anja Schierloh]. Eine kortikale Kolumne (s. Abbildung 2.2) oder Säule (Column) ist eine funktionale Verarbeitungseinheit, die alle Schichten des Cortex umfasst. Die Nervenzellen einer Kolumne sind untereinander sehr stark verbunden und bilden ein lokales Netzwerk (neuronal network). Eine Kolumne besteht aus mehreren horizontal verbundenen Minikolumnen (minicolumn), die sich durch die Schichten II-IV erstrecken. Die säulenartige Anordnung der Nervenzellen ist ein grundlegendes Strukturprinzip der Großhirnrinde. Abbildung 2.2 zeigt synaptische Konnektivität innerhalb einer kortikalen Kolumne. Die Dreiecke stellen Pyramidenzellen, Kreise die Sternzellen dar. Rote Bereiche zeigen erregende (exzitatorische) Synapsen, blaue Bereiche hemmende (inhibitorische) Synapsen, Pfeile die Weiterleitungsrichtung der Signale. 6 2.1 Neurobiologie • Schicht I (Molekularschicht) enthält keine Nervenzellen und besteht aus Fortsätzen von Pyramidenzellen. • In Schicht II/III sind kleine Pyramidenzellen vorhanden. Schicht II bezeichnet die äußere Körnerschicht und Schicht III die äußere Pyramidenschicht. • Schicht IV (innere Körnerschicht) ist hauptsächlich aus Nicht-Pyramidenzellen aufgebaut. • Schicht V (innere Pyramidenschicht) weist große Pyramidenzellen auf. • Schicht VI (multiforme Schicht) setzt sich aus kleineren Pyramidenzellen Pyramidenzellen zusammen. und Nicht- Abbildung 2.2: Kolumne nach [4, Eberhard] 7 2 Grundlagen 2.1.2 Die Nervenzelle Die Neuronen (gr. neuron, Nerv) sind die kleinsten Einheiten des Gehirns. Ihre Aufgabe ist die Aufnahme, Verarbeitung und Weitergabe von Reizen und Informationen. Eine Nervenzelle (s. Abbildung 2.3) besteht aus einem Zellkörper (Soma) und den davon ausgehenden Fortsätzen, den Dendriten und dem Axon. Abbildung 2.3: Aufbau einer Nervenzelle aus [1] • Das Soma (gr. Körper) enthält den Zellkern (Nucleus) mit der genetischen Information. Im Soma findet der Stoffwechsel der Zelle statt. • Die Dendriten (gr. dendron, Baum) sind kurze, dünne verzweigte Fortsätze, die Informationen von anderen Neuronen empfangen und in Richtung des Zellkörpers leiten. 8 2.1 Neurobiologie • Das Axon (gr. Achse) ist ein langer Fortsatz, über den elektrische Signale vom Soma zu anderen Nervenzellen weitergeleitet werden. Der Ursprungsort des Axons heißt Axonhügel. Die elektrischen Signale werden als Aktionspotentiale bezeichnet. • Zwischen den Nervenzellen gibt es Verbindungsstellen. Diese Kontaktpunkte nennt man Synapsen. Sie ermöglichen die Informationsübermittlung von einer Nervenzelle auf die andere. Es existieren verschiedene Typen von Nervenzellen. Sie werden nach Form, wie der Anzahl der vorhandenen Fortsätze, Größe und Funktion unterschieden. Die Nervenzellen können in zwei Hauptklassen eingeteilt werden: die Pyramiden- und NichtpyramidenZellen. Pyramidenzellen haben einen pyramidenförmigen Zellkörper und ein langes Axon. Von der Spitze des Somas entspringt der apikale Dendrit. An der Grundfläche des Somas befinden sich die basalen Dendriten. In dieser Arbeit werden wir uns mit fünf unterschiedlichen Neuronentypen befassen: den L2/3-Pyramidenzellen, den L4-Sternzellen, den L4-Spiny-Stellate-Zellen, den L5APyramidenzellen und den L5B-Pyramidenzellen. 9 2 Grundlagen 2.2 NeuGen NeuGen1 ist ein Softwarepaket zur Generierung von realistischen kortikalen Neuronen und neuronalen Netzen in 3D. Zur Generierung verwendet NeuGen Geometrieparameter, die die Struktur und andere Eigenschaften von verschiedenen Nervenzelltypen beschreiben. Ein Vererbungsmechanismus vererbt alle oder einige Geometrieparameter von einem Basis-Neuron auf die einzelnen Neuronen-Klassen. Anhand der vererbten Geometrieparameter können detaillierte Nervenzellen generiert werden. Eine schematische Darstellung der Neuronen- und Neuronennetz-Erzeugung von NeuGen zeigt Abbildung 2.4. Zunächst werden die Geometrieparameter eingelesen und durch einen Vererbungsalgorithmus verändert. Mit den vererbten Geometrieparametern als Eingabe wird die Erzeugung von Neuronen und neuronalen Netzen gestartet. Daten importieren Parameterdatei Parameter Vererbungs- Parameter algorithmus editieren Konfigurationseditor Vererbte Geometrieparameter NeuronenGenerierung Daten exportieren hoc-Datei dx-Datei Abbildung 2.4: Schematische Darstellung von NeuGen 1 http://www.neugen.org/ 10 3D-Visualisierung 2.2 NeuGen 2.2.1 Neuronenklassen NeuGen unterstützt fünf verschiedene kortikale Neuronentypen, L4-Spiny-Stellate-Zellen, L2/3Pyramidenzellen, L5A-Pyramidenzellen, L5BPyramidenzellen und L4-Star-Pyramidenzellen. Neuron axon ... subclasses L4Stellate In den Abbildungen 2.6-2.10 sind verschieaxon dene Nervenzelltypen zu sehen. Blaue Struk... turen stellen das Axon, rote den Zellkörper synapse pyramidal und gelbe die Dendriten dar. In Abbildung 2.5 axon ist die Neuronen-Klassen-Hierarchie in Neu... Gen vereinfacht dargestellt. Die Zellen vom subclasses L23pyramidal Typ L4Stellate, Pyramidal, Starpyramidal eraxon ben Geometrieparameter direkt von der Klasse ... Neuron. L23pyramidal und L5pyramidal erben synapse L5pyramidal von pyramidal, die selbst Geometrieparameter axon von der Klasse Neuron erbt. L5Apyramidal und ... L5Bpyramidal erben von der Klasse L5pyramidal. subclasses L5Apyramidal axon ... synapse L5Bpyramidal axon ... synapse synapse starpyramidal axon ... synapse soma synapse Abbildung 2.5: NeuronenklassenAbbildung 2.6: L4-Star-Pyramidenzellen Hierarchie 11 2 Grundlagen Abbildung 2.7: L4-Spiny-Stellate-Zelle Abbildung 2.8: L2/3-Pyramidenzellen Abbildung 2.9: L5A-Pyramidenzellen Abbildung 2.10: L5B-Pyramidenzellen 12 2.2 NeuGen 2.2.2 Parameterdateien Die Konfiguration von NeuGen umfasst Geometrieparameter der einzelnen Neuronenklassen, die in XML-Dateiformat gespeichert werden. Eine XML-Datei lässt sich als Baum darstellen und besteht aus Elementen, markiert durch Tags, die einen Knoten repräsentieren. Die Knoten besitzen einen Tag-Namen und eine Menge von Attributen mit Wertzuweisungen. Attribute liefern Zusatzinformationen über einen Knoten. Die Blätter des Baumes bilden den Inhalt (character data). Diese sind als Knoten in den Parameterdateien string, double, integer, object mit einem Attribut key definiert. Die object-Knoten haben ein zusätzliches Attribut classdescriptor und können Kinderknoten besitzen. Im folgenden ist eine Beispiel dargestellt: Das äußerste Element <object> <object key=" dendrite " classdescriptor= " " > <object key=" gen_0 " classdescriptor=" " > <object key=" branch_angle " classdescriptor=" " > <real key=" max " >60</real> </object> <object key=" siblings " classdescriptor=" siblings " > <object key=" siblings " classdescriptor=" " > </object> </object> </object> </object> mit dem Attribut-Name key und Attribut-Wert dendrite umfasst den gesamten Inhalt und hat ein Kindknoten mit Attribut-Wert gen_0. Knoten gen_0 besitzt zwei Knoten mit den Attribut-Werten branch_angle und siblings. Der Knoten branch_angle hat ein Element real mit dem Inhalt 60. Der Attribut-Name classdescriptor mit Attribut-Wert sibling wird vom Vererbungsalgorithmus verwendet. Diese Geometrieparameter beschreiben detailliert die morphologische Struktur eines Neurons. In Abbildung 2.11 ist ein vereinfachter Parameterbaum bestehend aus dem Basisneuron und der Neuronenklasse L4Stellate dargestellt. Es ist erkennbar, dass die beiden Klassen eine etwa gleiche Parameterstruktur besitzen. Die Klasse L4Stellate kann gleichnamige Parameter vom Basisneuron erben. Die vererbbaren Parameter werden nicht in der Datei gespeichert, um möglichst übersichtliche und leicht erweiterbare Parameterdateien zu erhalten. 13 2 Grundlagen L4Stellate axon len_param z: 375 nbranch: 26.0 nparts: 90 rad max: 0.5 min: 0.1 dendrite gen_0 branch_angle max: 60 min: 30 len_param x: 75 y: 0 z: 0 nbranch_param: 4 nparts_density: 0.25 siblings siblings branch_angle max: 60 min: 30 len_param x: 50 y: 0 z: 0 nbranch_param: 2 nparts_density: 0.25 siblings ... rad max: 1.5 min: 0.25 deviaton x: 1.0 y: 1.0 z: 1.0 napiden: 0 nden: 7 soma rad: 5 synapse rad: 1.0 Abbildung 2.11: Parameterdatei 14 max 10 Sibling-Generationen max 10 Sibling-Generationen Neuron axon len_param z: 400 nbranch: 10 nparts: 100 rad max: 0.5 min: 0.1 dendrite gen_0 branch_angle max: 60 min: 30 len_param x: 20 y: 0 z: 0 nbranch_param: 3 nparts_density: 0.25 siblings siblings branch_angle max: 60 min: 30 len_param x: 10 y: 0 z: 0 nbranch_param: 3 nparts_density: 0.25 siblings ... rad max: 1.5 min: 0.25 deviaton x: 1.0 y: 1.0 z: 1.0 napiden: 0 nden: 11 subclasses L4Stellate ... pyramidal ... starpyramidal ... soma rad: 5 synapse rad: 1 2.2 NeuGen 2.2.3 Vererbungsalgorithmus Die Aufgabe des Vererbungsalgorithmus besteht darin, die Geometrieparameter entlang der Neuronen-Klassenhierarchie zu vererben. Der Algorithmus prüft für jede Klasse in der Hierarchie, ob alle notwendigen Geometrieparameter vorhanden sind. Die Neuronenklassen besitzen die Parameter für das Axon, die Dendriten, das Soma und die Synapse. Beispielsweise kann man deren Radius in µ m im Konfigurationseditor einstellen. Parameter, die nicht explizit gesetzt sind, erben von ihrer Oberklasse. In Abbildung 2.11 kann L4Stellate von der Oberklasse Neuron die Werte der gleichnamigen Parameter erben. Parameter können innerhalb derselben Klasse vererbt werden, z. B. enthält der Dendrit die Parameter für die Fortsätze des dendritischen Baumes. Wenn ein Parameter des ersten Fortsatzes (gen_0) des dendritischen Baumes explizit gesetzt ist, dann können die Parameter der nachfolgenden Fortsätze (siblings) die Parameter von ihren vorhergehenden Fortsätzen erben. Wenn Parameter des ersten Fortsatzes von ihrer Oberklasse erben, dann erben auch die nachfolgenden Fortsätzen von ihrer Oberklasse. 2.2.4 Konfiguration NeuGen verfügt über einen graphischen Konfigurationseditor (s. Abbildung 2.12). Mithilfe des Konfigurationseditors lassen sich Geometrieparameter einer Parameterdatei einstellen. Die Funktionen des Konfigurationseditors sind das Laden und das Speichern der Dateien, sowie das Editieren der Parameter. Für das Speichern und Laden von Parameterdateien in Form von XML-Dateien wird die libxml-Bibliothek2 verwendet. Nach dem Laden einer Parameterdatei, wird ein Baum aufgebaut, auf den der Vererbungsalgorithmus angewendet wird, um die Parameter entlang der Neuronenklassen zu vererben. Zusätzlich wird eine Datei mit Kommentaren zu den Parametern geladen. Auch die Kommentare werden vererbt. Vor dem Speichern einer Parameterdatei werden die vererbten Parameter aus dem Baum entfernt. Über die Baumansicht auf der linken Seite des Konfigurationseditors können die ein2 http://xmlsoft.org/ 15 2 Grundlagen zelnen Parameter per Mausklick ausgewählt werden. Die Parameter vom Typ object enthalten andere Parameter. Für Parameter vom Typ string, double und integer können Werte in einen Eingabedialog eingegeben werden. Eine Funktion bestimmt Parameter, die von ihrer Oberklasse oder innerhalb der eigenen Klasse erben können. Es ist möglich diese Parameter auf vererbt (set inherited), oder auf nicht vererbt (set uninherited) zu setzen. Falls falsche Werte wie z. B. eine Fließkommazahl anstelle einer ganzen Zahl eingegeben werden, wird eine Fehlermeldung ausgegeben. Nach einer fehlerfreien Änderung eines Parameters werden seine abhängigen Parameter, die auf vererbt gesetzt sind, in eigener Klasse und in den Unterklassen aktualisiert. Das bedeutet, dass durch eine einzige Änderung viele Änderungen bewirkt werden. Der rechte Teil zeigt die Erklärungen zu den einzelnen Parametern. Die Zuordnung zwischen den Elementen der Parameterdateien und den dazu gehörigen Erklärungen der Kommentardateien, findet per Mausklick statt. Abbildung 2.12: Konfigurationseditor (links ist der Baum der Parameter, rechts ist die Erklärung) 16 2.2 NeuGen 2.2.5 Visualisierung Die erzeugten Netze können schließlich mit Hilfe von Java3D 3 in NeuGen visualisiert werden. NeuGen kann die Geometrie für die Software OpenDX 4 zur Visualisierung von neuronalen Netzen in 3D exportieren. 2.2.6 Simulation NeuGen erzeugt die Neuronengeometrie vernetzter dreidimensionaler Zellen in einem Netzwerk und ein elektrophysiologisches Modell zur Simulation. Für die Simulation der Signalverarbeitung auf den künstlichen neuronalen Netzen kann NeuGen hoc-Dateien für Software NEURON 5 generieren. Die hoc-Datei mit der erzeugten Neuronengeometrie und dem detaillierten Neuronenmodell kann in NEURON eingelesen und eine Simulation durchgeführt werden. 3 4 5 http://java.sun.com/products/java-media/3D/ http://www.opendx.org/ http://www.neuron.yale.edu/neuron/ 17 2 Grundlagen 2.3 Graphentheorie Wir verwenden im folgenden Eigenschaften und Begriffe im Zusammenhang mit Graphen, die nach [5, Ottmann u.Widmayer] wie folgt definiert sind: • Ein gerichteter Graph G = (V, E) (englisch: directed graph) besteht aus einer Menge V von Knoten und (englisch: vertices) und einer Menge E = V × V von Kanten (englisch: edges). • Ein Paar (v, v0 ) mit v, v0 ∈ V bezeichnet eine Kante e, v heißt Anfangs- und v0 Endknoten. • Ein Weg (englisch: path) von v nach v0 ist eine endliche Folge von Kanten 0 p = (v0 , v1 ), . . . , (vm−1 , vm ) mit v = v0 , v = vm und (vi , vi+1 ) ∈ E für 0 < i < m. m ist die Länge des Weges. v0 ist direkter Vorgänger von v1 , v1 ist direkter Nachfolger von v0 . v1 heißt Vorgänger von vi , vi heißt Nachfolger von v1 für i > 1. • Ein Weg p heißt Zyklus, wenn er geschlossen ist, also wenn vm = v0 gilt. • Der Eingangsgrad (englisch: indegree) indeg(v) eines Knotens v ist die Anzahl seiner direkten Vorgänger v0 , also indeg(v) = |{v0 |(v0 , v) ∈ E}|. Der Ausgangsgrad (englisch: outdegree) outdeg(v) ist die Anzahl der direkten Nachfolger v0 von v0 , also outdeg(v) = |{v0 |(v, v0 ) ∈ E}|. • Ein Baum t (englisch: tree) ist ein gerichteter Graph, wenn t keine Zyklen enthält und es existiert ein Knoten v0 mit indeg(v0 ) = 0, für alle anderen Knoten v gilt indeg(v) = 1. • Der Nachfolger v von einem Knoten v0 heißt Sohn, oder Kindknoten. • Der Vorgänger v von einem Knoten v0 heißt Vater, oder Elternknoten. 18 2.3 Graphentheorie • Ein Knoten ohne Vater heißt die Wurzel des Baumes. • Ein Knoten mit outdeg(v) = 0 heißt Blatt, andere Knoten, die weder Blätter noch Wurzel sind nennt man innere Knoten. • Zwei Knoten v und v0 , die Kinder desselben Vaters sind, heißen Geschwister, oder Nachbarknoten. • Ein n-ärer Baum ist ein Baum, bei dem jeder Knoten höchstens n Nachfolger hat. Für n = 2 heißt der Baum ein Binärbaum. • Seien t1 , . . . ,tm Bäume, man bekommt einen weiteren Baum, indem die Wurzeln von t1 , . . .,tm zu Söhnen einer neuen Wurzel r (englisch: root) werden. ti (r) heißt i-ter Teilbaum neuer Wurzel r (s. Abbildung 2.13). • Die Höhe h (englisch: height)) eines Baumes ist die maximale Länge des Weges von der Wurzel zu einem Blatt. Die Höhe eines leeren Baums, der aus keinem Knoten besteht ist h = 0. In nicht leeren Bäume ist h = 1 + max{h(t1 ), . . ., h(tm)}. • Die Tiefe d eines Knotens ist die Länge des Weges von der Wurzel zu dem Knoten. Die Knoten mit derselben Tiefe gehören zum selben Niveau l (englisch: layer). Wir werden innere Knoten eines Baumes als Kreise, Blätter als Quadrate und Kanten als Pfeile zwischen zwei Knoten darstellen (s. Abbildung 2.14). 19 2 Grundlagen Wurzel t1 t2 ... tm Abbildung 2.13: Teilbäume Wurzel innerer Knoten Blatt Abbildung 2.14: Darstellung eines Baumes 20 3 Vererbung der Geometrieparameter In diesem Kapitel untersuchen wir die Vererbungsalgorithmen. Zuerst besprechen wir, warum die Vererbung der Geometrieparameter notwendig ist. Danach werden die notwendigen Begriffe erklärt. Im Abschnitt 3.4 beschreiben wir die vorhandenen Datenstrukturen. Als Nächstes betrachten wir den bisher in NeuGen eingesetzten Vererbungsalgorithmus DRI. Danach wird der neue Algorithmus DI vorgestellt, der den Abhängigkeitsgraph erzeugt und die Knoten vererbt. Am Ende dieses Kapitels betrachten wir den im Konfigurationseditor eingesetzten Vererbungsalgorithmus SI, der den erstellten Abhängigkeitsgraphen von DI verwendet, um die Parameterwerte zu vererben. 3.1 Motivation Die zur Konfiguration erforderlichen Geometrieparameter können über einen Konfigurationseditor eingegeben werden. Der Vererbungsalgorithmus sorgt dafür, dass nicht explizit gesetzte Parameterwerte von ihren Oberklassen geerbt werden. Die gesetzten Parameterwerte sind nach dem Editieren in der Konfigurationsdatei gespeichert und die vererbten Geometrieparameter treten in der Konfigurationsdatei nicht auf. Mehrfach vorhandene Daten in derselben Konfigurationsdatei würden dazu führen, dass neuere Informationen an den verschiedenen Stellen eingetragen werden müssten. Das führt dazu, dass die Konfigurationsdatei sehr groß und unübersichtlich wird. Durch den Algorithmus bleibt die Konfigurationsdatei kompakt, neue Daten können schnell hinzugefügt, oder entfernt werden. Die Geometrieparameter besitzen Kommentare, die in einer Kommentardatei gespei- 21 3 Vererbung der Geometrieparameter chert werden. Auch die Kommentare zu den Geometrieparametern werden mithilfe des Vererbungsalgorithmus vererbt. Im Folgenden führen wir die benötigten Konzepte, die für die Vererbung wichtig sind, ein. 3.2 Grundbegriffe • Ein Knoten v speichert einen Schlüssel k und einen Vererbungszustand, der angibt, ob v den Inhalt von einem anderen Knoten geerbt hat. • Das i-te Kind eines Knotens v bezeichnen wir mit vi für i > 0. • Ein innerer Knoten v kann eine Typbeschreibung besitzen. Die Typbeschreibung kann Alias oder Sibling sein. • Ein Knoten v mit der Typbeschreibung Sibling heißt Sibling. Ein Sibling hat mindestens einen Vorgänger und einen inneren Knoten als Nachfolger. • Ein Knoten v mit der Typbeschreibung Alias heißt Alias. Ein Alias besitzt mindestens einen Vorgänger und Nachfolger. Der Vorgänger eines Alias ist kein Kind eines Sibling. • Sei ein Weg p(v, v0 ) von v nach v0 durch eine Folge (v0 , . . . , vm ) von Knoten mit v0 = v, vm = v0 und deren Schlüsselmenge S(v, v0 ) = {k0 , . . ., km } gegeben, dann bezeichnet S(v, v0) den Schlüsselpfad von v nach v0 . Seien S(v, v0 ), S1 (v, v0 ) und S2 (v, v0 ) Teilmengen von S(v, v0 ) für die gilt: S(v, v0 ) enthält keine Schlüssel der Siblings und deren direkten Nachfolger. S1 (v, v0 ) enthält keine Schlüssel der Alias Knoten. S2 (v, v0 ) enthält keine Schlüssel der Nachfolger von Alias. 22 3.2 Grundbegriffe • Ein Knoten w der von einem anderen Knoten v den Inhalt erben kann, heißt abhängiger Knoten. Der Knoten v, von dem der Inhalt geerbt wird, heißt Erblasser. • Gegeben sei ein Weg p von v nach v0 0 p = (v0 , v1 ), . . ., (vm−1 , vm ) mit v = v0 , v = vm und (vi , vi+1 ) ∈ E für 0 < i < m. Für i gerade ist vi ein Vorgänger bzw. Nachfolger von Sibling und für i ungerade ist vi ein Sibling. Dann bezeichnen wir die Folge von Knoten (v0 , . . .vm ) als Siblingkette. In Abbildung 3.1 veranschaulichen wir den Sibling graphisch durch eine Raute und den Alias durch ein Dreieck. Die gepunkteten Verbindungspfeile skizzieren einen Schlüsselpfad vom einen Knoten zum anderen Knoten. Der Schlüssel k von v, w und deren Schlüsselpfade S(r, v), S(r, w) sind gleich. Im nächsten Unterkapitel wird diese Eigenschaft beschrieben. r Sibling S(r,w) p(r,w) b b b S(r,v) p(r,v) b b w k b b b b v k Abbildung 3.1: Sibling, Siblingkette und Alias 23 3 Vererbung der Geometrieparameter 3.3 Definition von Vererbung In diesem Abschnitt definieren wir die Vererbung von Knoten. 3.3.1 Beziehung erster Ordnung Seien v und w Knoten mit einem minimalen gemeinsamen Teilbaum t(r) und r, v, w sind keine Siblinge und keine Aliase. w ist ein indirekter Nachfolger von einem Sibling. Wenn folgendes gilt: 1. S1 (r, v) = S2 (r, w), 2. |p(r, v)| < |p(r, w)|, 3. es existiert kein w0 mit 1, 2 und |p(r, w0 )| < |p(r, w)|. dann gibt es eine Beziehung erster Ordnung zwischen v und w. Zur Beschreibung von Beziehungen zwischen Knoten verwenden wir den Pfeil →. Wir schreiben für die Be1 ziehung erster Ordnung zwischen v und w: v − → w. Das bedeutet, dass w von v erben kann. r S1 (r,v) p(r,v) k v S2 (r,w) p(r,w) w k Abbildung 3.2: Beziehung erster Ordnung 24 3.3 Definition von Vererbung Abbildung 3.2 zeigt ein Beispiel für die Beziehung erster Ordnung zwischen den Knoten v und w ohne Alias im Teilbaum t(r). a) b) r r S1 (r,v) p(r,v) S1 (r,v) p(r,v) k v S2 (r,w) p(r,w) k S2 (r,w) p(r,w) k v w k w Abbildung 3.3: Beziehung erster Ordnung mit Alias im Teilbaum t(r) In Abbildung 3.3 ist die Beziehung erster Ordnung zwischen den Knoten v und w mit Alias im t(r) dargestellt. Im Beispiel a) wird die Teilmenge S2 (r, w) bestimmt, indem die Schlüssel der direkten Nachfolger w von Alias aus S2 (r, w) entfernt werden. Im Beispiel b) wird S1 (r, v) bestimmt, indem die Schlüssel von Alias aus S1 (r, v) entfernt werden. 3.3.2 Beziehung n-ter Ordnung Gegeben seinen die Knoten v und w. Falls es zwei minimale Teilbäume t1 (r1 ), t2 (r2 ) gibt, wobei r1 , r2 keine Siblinge oder Aliases sind, sowie v aus t1(r1 ), w aus t2 (r2 ) ist, und wenn folgende Eigenschaften gelten: 1. S1 (r1 , v) = S2 (r2 , w), 2. |S1 (r1 , v)| = |S2 (r2 , w)|, 25 3 Vererbung der Geometrieparameter 1 3. es existiert eine Beziehung r1 − → r2 , 1 4. es existiert keine Beziehung v − → w, 1 5. es existiert ein v0 mit v0 − → w. 2 dann existiert eine Beziehung zweiter Ordnung zwischen v und w: v − → w. Die Bezien hung n-ter Ordnung v → − w wird analog definiert wie die Beziehung zweiter Ordnung. r r1 k S1 (r, v) r2 k v k v0 S2 (r2 , w) w k Abbildung 3.4: Beziehungen zweiter Ordnung In Abbildung 3.4 ist ein Beispiel für die Beziehung zweiter Ordnung zwischen den Knoten v und w dargestellt. Zwischen v0 und w existiert eine Beziehung erster Ordnung. 3.3.3 Reihenfolge der Vererbung Wir unterscheiden drei Fälle, wann ein Knoten w von einem anderen Knoten v erbt. Sei n eine Beziehung v → − w gegeben, w ist auf vererbt gesetzt und es gebe kein v(1) , n(1) < n n(1) mit v(1) −−→ w, von dem w erbt. 26 3.3 Definition von Vererbung Fall 1: Sei v nicht auf vererbt gesetzt, dann erbt w von v. n(2) Fall 2: Sei v auf vererbt gesetzt und es existiert kein Knoten v(2) , n(2) > n mit v(2) −−→ w, dann erbt w von v. Fall 3: Sei v auf vererbt gesetzt und es gilt: Fall 1 Fall 2 v Fall 3 i) v n v n w n w w >n Abbildung 3.5: Reihenfolge bei der Vererbung (Fall 1-3 i) n(3) i) es existiert kein v(3) , n(3) mit v(3) −−→ v für n(3) > n, dann erbt w von v. n(3) ii) es existiert ein v(3) , n(3) mit v(3) −−→ v für n(3) > n und a) falls v von einem Knoten u über eine Ordnung > n erbt, dann erbt w von einem Knoten u0 über eine Ordnung > n. b) falls v von einem Knoten u über eine Ordnung ≤ n erbt, dann erbt w von v. a) b) ≤n >n v >n n >n v n w >n w Abbildung 3.6: Reihenfolge bei der Vererbung (Fall 3 ii) 27 3 Vererbung der Geometrieparameter 3.4 Datenstrukturen Listen und Bäume sind die wichtigsten Datenstrukturen der Informatik. Die Liste ist eine endliche Folge von Daten-Elementen. Der Nachteil von Listen ist, dass der Suchaufwand proportional zur Anzahl der gespeicherten Elemente ist (lineare Suchzeit O(n)), d. h. jedes Element muss bei einer erfolglosen Suche betrachtet werden. Die Suchwege lassen sich durch baumartige Datensturkturen verkürzen. Bevor wir die Implementierungen für die Knoten betrachten, beschäftigen wir uns mit der Frage, wie man den Baum durchläuft. 3.4.1 Traversierung von Bäumen Die Traversierung (Durchquerung) eines Baumes bedeutet, alle seine Knoten systematisch aufzusuchen, um an den Knoten spezielle Operationen durchführen zu können. In Bäumen gibt es verschiedene Reihenfolgen, wie die Knoten besucht werden können (aus [3, Robert Sedgewick]), (s. Abbildung 3.7): • Preorder: wir besuchen den Knoten, dann alle Teilbäume die sich links vom jeweiligen Vaterknoten, dann alle Teilbäume die sich rechts davon befinden. • Inorder (bei Binärbäumen): wir besuchen den linken Teilbaum, dann den Knoten und dann den rechten Teilbaum. • Postorder: wir besuchen alle Teilbäume die sich links vom jeweiligen Vaterknoten, dann alle Teilbäume die sich rechts davon befinden und dann den Knoten selbst. • Levelorder: wir besuchen den Knoten, dann alle Wurzel der Teilbäume die sich links von ihm, dann alle Wurzel der Teilbäume die sich rechts von ihm befinden, dann die nächste Ebene. Der Algorithmus für Postorder-Traversierung kann wie folgt programmiert werden: 28 3.4 Datenstrukturen Preorder: 1 1, 2, 3, 4, 5, 6, 7, 8, 9 Inorder: 3, 2, 4, 1, 7, 6, 8, 5, 9 Postorder: 5 2 3, 4, 2, 7, 8, 6, 9, 5, 1 Levelorder: 1, 2, 5, 3, 4, 6, 9, 7, 8 3 4 9 6 7 8 Abbildung 3.7: Traversieren von Bäumen Algorithm 3.1 postorder_traverse(v) Input: v if v == null then 2: return v 3: if v is leaf then 1: return null 5: for i = 1 to outdeg(v) do 6: vi ← v.getChild(i) 7: postorder_traverse(vi ) 4: 8: doSomething(v) 29 3 Vererbung der Geometrieparameter Die Funktion 3.1 bekommt als Argument den Knoten eines Baumes und traversiert zunächst die Unterbäume eines Knotens, bevor der Knoten selbst von der Funktion doSomething bearbeitet wird. Wenn wir den Aufruf von doSomething vor die for-Schleife mit dem rekursiven Aufruf verschieben, bekommen wir die Preorder-Traversierung. Es fällt auf, dass wir im Baum in die Tiefe gehen und als erstes den Knoten unten links bearbeiten. Deswegen werden alle Verfahren außer levelorder als Tiefensuche (engl. depth first search) bezeichnet. Der Zeitaufwand bei der Tiefensuche ist O(n) für einen Baum mit n Knoten, da jeder Knoten mindestens einmal besucht wird. Im schlimmsten Fall (entarteter Baum) sind insgesamt n − 1 Aufrufe von postorder_traverse(vi ) für den Durchlauf notwendig. Durch die Verwendung einer Liste können wir die Rekursion vermeiden (s. Funktion 3.2). Algorithm 3.2 levelorder_traverse(v) Input: v 1: LinkedList q q.addFirst(v) 3: while q.size() > 0 do 4: v ← q.getLast() 2: 5: 6: 7: 8: 9: 10: q.removeLast() doSomething(v) for k ← 0 to outdeg(v) do vi ← v.getChild(i) if v! = null && v != leaf then q.addFirst(vi ) Der Baum wird in Levelorder-Reihenfolge durchlaufen, indem wir eine FIFO-Datenstruktur benutzen (engl. first in first out (als erster rein, als erster raus)). Die Knoten werden am Anfang der Liste eingefügt und in der while-Schleife der letzte Knoten geholt. Die Knoten des Baumes werden von oben nach unten und von links nach rechts besucht. Hier gehen wir nicht zuerst in die Tiefe, sondern in die Breite (engl. breadth first search). Die Laufzeit ist O(n), da jeder Knoten einmal besucht wird. 30 3.4 Datenstrukturen 3.4.2 Implementierungen Die Daten der Parameterdatei werden in einer Baumstruktur gespeichert. Jeder Knoten enthält elementare Daten wie Knotenname, Knotenwert und eventuelle Kinderknoten. Ein DefaultXMLObject-Objekt ist eine Wurzel oder ein innerer Knoten, der eine Liste von Instanzen der Klasse DefaultXMLNode enthält. Ein DefaultXMLNode-Objekt kann ein innerer Knoten oder ein Blatt sein. Für die Liste benutzen wir die Java-Klasse ArrayList. Eine Übersicht der für die Knoten verwendeten Klassen ist in Abbildung 3.8 zu sehen. In der Abbildung ist ein Klassendiagramm mit den Klassen, ihren Attributen und Methoden dargestellt. Die Vererbungsbeziehung zwischen einer allgemeineren Klasse (Oberklasse) und einer spezialisierten Subklasse (Unterklasse) wird durch eine Linie mit einem Dreieck repräsentiert. Die Spitze des Dreiecks zeigt auf die Oberklasse. Dieselbe Darstellung wird für die Schnittstellenvererbung benutzt, wenn Oberklasse und Unterklasse eine Schnittstelle (Interface) ist. Wenn eine Schnittstelle von einer normalen Klasse implementiert wird, dann wird die Linie gestrichelt gezeichnet. bildet die Basisschnittstelle und dient dazu, den Zugriff auf ein Knotenobjekt festzulegen. Die Schnittstelle enthält folgende Methoden. getParent() liefert den Elternknoten, getName() den Schlüssel, isA() den Typ, isInherited() den VererXMLNodeInterface bungsstatus; setInherited() trägt den Status als vererbt, setUninherited() als nicht vererbt ein. DefaultXMLNode implementiert die Schnittstelle XMLNodeInterface. Die Klasse DefaultXMLNode repräsentiert ein Knotenobjekt. Ein Knoten besitzt einen Schlüssel, einen Elternknoten und einen Vererbungsstatus. Die Klasse enthält zusätzlich die Methode getPath(), die den Pfad von der Wurzel bis zum Knoten liefert. sind abgeleitete Klassen, sie erben alle Eigenschaften von der Vaterklasse DefaultXMLNode. Die Klassen repräsentieren die Blatt- DefaultXMLString, -Real, -Bool, -Int 31 3 Vererbung der Geometrieparameter Abbildung 3.8: Übersicht der verwendeten Klassen 32 3.4 Datenstrukturen objekte. Sie unterscheiden sich von DefaultXMLNode dadurch, dass sie zusätzlich einen Wert und einen Typ für ein Blatt speichern. Die Klasse enthält zusätzlich die Methode convert(), die verwendet wird, um ein DefaultXMLNode in ein -String, -Real, -Bool oder -Int umzuwandeln. Sie überprüft, zu welcher Klasse ein Objekt gehört. ist abgeleitet von XMLNodeInterface, die Schnittstelle erbt alle Methodendefinitionen der Basisschnittstelle. Sie definiert folgende Methoden. addChild() fügt einen neuen Kindknoten an das Ende der Liste an, removeChild() entfernt XMLObjectInterface einen beliebigen Knoten, getChild(i) ermöglicht indizierten Zugriff auf die Kinder, getChildrenCount() ermittelt die Anzahl der Kinder, hasUninheritedChildren() kann abfragen, ob ein Teilbau vererbte Kinder besitzt, addDependants() fügt für einen Knoten einen abhängigen Knoten in eine Menge ein, getAllDependants() liefert alle abhängigen Knoten, getObjectClassName() ermittelt den Klassennamen eines Knotens und getSupreme() liefert den Erblasser. DefaultXMLObject implementiert die Schnittstelle XMLObjectInterface. Die Klasse repräsentiert die inneren Knoten. Ein innerer Knoten speichert seine Kinder in einer ArrayList children. Die Knoten von denen geerbt wird und die erben werden, werden als eine sortierte Menge von geordneten Paaren gespeichert. Für die Menge benutzen wir die Java-Klasse TreeMap, die alle Schlüssel in einem Binärbaum sortiert. Diese Datenstruktur verbindet einen Schlüssel mit einem Wert, vergleichbar beispielsweise mit einem Telefonbuch. Ein Name ist ein Schlüssel und die Nummer der Wert. In unserem Fall wird in die TreeMap die Ordnung (Schlüssel) und die Menge der abhängigen Knoten (Wert) eingetragen. Die Menge der abhängigen Knoten wird in der Java-Klasse HashSet, die keine Duplikate enthält, gespeichert. Die Erblasser werden direkt mit der Ordnung in die TreeMap eingetragen. Die Klasse enthält folgende Methoden. printAllDependants(), die Ausgabe aller Knoten, die von einem Knoten erben könnten, unterstützt uns beim Debuggen, convert() kann abfragen, ob ein Knoten ein innerer Knoten ist, getRoot() liefert die Wurzel. 33 3 Vererbung der Geometrieparameter 3.5 Default-Recursive-InheritanceVererbungsalgorithmus In diesem Kapitel wird der bisher in NeuGen implementierte Default-RecursiveInheritance-Vererbungsalgorithmus (DRI) beschrieben. Bevor wir die Funktion für das Vererben von Knoten betrachten, betrachten wir zunächst die Funktionen für das Anhängen und Vervollständigen von Knoten: copyAppend und complete. 3.5.1 Anhängen von Knoten Mit Hilfe der rekursiven Funktion 3.3 copyAppend wird eine Kopie eines Knotens erzeugt und an einen anderen Knoten angehängt. Die Funktion bekommt zwei Knoten v und w der Teilbäume t(v) und t(w) als Argumente. Beide Teilbäume werden mittels der rekursiven Tiefensuche in Preorder-Reihenfolge durchlaufen und es wird nach den Schlüsseln der Nachfolger von v in w gesucht. Wenn ein Schlüssel nicht gefunden wird, dann erzeugen wir eine Kopie des gesuchten Knotens und hängen sie an den entsprechenden Vaterknoten an. Dabei werden folgende Fälle unterschieden: v ist ein Alias, v ist ein Blatt, v ist ein innerer Knoten und kein Alias. (s. Zeilennummern in der Funktion 3.3) 1-9: Fall 1 (s. Abbildung 3.9): v ist ein Alias, dann werden alle seine Kinder vi durchlaufen. Wir suchen einen Knoten w(k) von w mit demselben Schlüssel k wie vi . Die Funktion wird rekursiv für alle vi und w aufgerufen, um vi an w anzuhängen, wenn w(k) nicht gefunden wird. Andernfalls wird die rekursive Funktion complete (s. 3.4) aufgerufen. In dieser Funktion prüfen wir, ob alle Knoten von t(vi ) in t(w) enthalten sind, um fehlende Knoten durch den Aufruf von copyAppend anzuhängen. 10-11: Fall 2 (s. Abbildung 3.10a): v ist ein Blatt, dann erzeugen wir eine Kopie von v und hängen sie an w. 34 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus 12-16: Fall 3 (s. Abbildung 3.10b): v ist ein innerer Knoten und kein Alias, dann wird ein Knoten v0 mit dem Schlüssel und Inhalt von v erzeugt und an w angehängt. Danach wird die Funktion rekursiv für alle vi und v0 aufgerufen, um beide Knoten zu bearbeiten. Algorithm 3.3 copyAppend(v, w) Input: v and w 1: if v is Alias then // Fall 1 2: for i = 1 to outdeg(v) do 3: vi ← v.getChild(i) 4: 5: 6: k ← vi .getKey() w(k) ← w.getChild(k) if w(k) == null then copyAppend(vi , w) 8: else 9: complete(vi , w(k)) 10: else if v is leaf then // Fall 2 7: new Leaf(v.getKey(), w, v.getValue()) 12: else if v is inner node then // Fall 3 13: v0 ← new Node(v.getKey(), w, v.getType()) 14: for i = 1 to outdeg(v) do 11: 15: 16: vi ← v.getChild(i) // hänge vi an v0 copyAppend(vi , v0 ) v w 1 2 3 4 w =⇒ w 1 3 =⇒ 1 2 3 4 Abbildung 3.9: Anhängen von Knoten (Fall 1) 35 3 Vererbung der Geometrieparameter Abbildung 3.9 veranschaulicht die Arbeitsweise von copyAppend. Der Alias v wird nicht an w angehängt, stattdessen sucht die Funktion nach den Schlüsseln der Nachfolger von v in w. Wenn kein Schlüssel gefunden wird, dann wird der gesuchte Knoten erzeugt und an t(w) angehängt. v 1 w =⇒ w v 1 w =⇒ w 1 (a) Fall 2 1 (b) Fall 3 Abbildung 3.10: Anhängen von Knoten (Fall 2 und Fall 3) 3.5.2 Vervollständigen von Knoten Die rekursive Funktion 3.4 complete sucht nach den Schlüsseln eines Teilbaumes in einem anderen Teilbaum. Dabei werden die Teilbäume mithilfe der rekursiven Tiefensuche in Preorder-Reihenfolge durchlaufen. Wird ein Schlüssel nicht gefunden, dann wird durch Aufruf der Funktion copyAppend eine Kopie des gesuchten Knotens erzeugt und angehängt. Die Funktion complete bekommt zwei Knoten v und w als Argumente. Die beiden Knoten werden verglichen und es ergeben sich folgende Fallunterscheidungen: 1-2: Fall 1: w ist ein Blatt, dann terminiert der Aufruf. 3-6: Fall 2 (s. Abbildung 3.11): w ist ein Alias, dann werden alle seine Kinder vervollständigt, d. h. v wird mit wi verglichen. 7-21: Fall 3 (s. Abbildung 3.12): w ist eine innerer Knoten und kein Alias, dann wird getestet, ob ein Nachfolger vi ein Alias ist. Ist das der Fall, dann wird geprüft, ob die Schlüssel der Kinder von Alias in w enthalten sind. Ansonsten wird nach 36 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus den Knoten w(k) mit den gleichen Schlüsseln wie vi in w gesucht. Die Funktion copyAppend wird aufgerufen, um eine Kopie von vi zu erzeugen und sie an w anzuhängen, wenn der Schlüssel nicht gefunden wird. Wenn der gesuchte Knoten existiert und kein Alias ist, dann wird vi mit w(k) verglichen. Falls w(k) ein Alias ist, dann wird vi mit allen Nachfolgern des Alias verglichen. Algorithm 3.4 complete(v, w) Input: v and w 1: if w is leaf then // Fall 1 2: return 3: if w is Alias then // Fall 2 for i = 1 to outdeg(w) do 5: wi ← w.getChild(i) 6: complete(v, wi ) 7: else // Fall 3 4: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: for i = 1 to outdeg(v) do vi ← v.getChild(i) if vi is Alias then complete(vi , w); continue k ← v.getKey() w(k) ← w.getChild(k) if w(k) == null then copyAppend(vi , w) else if !w(k) is Alias then complete(vi , w(k)) else for j = 1 to outdeg(w(k)) do w j ← w(k).getChild( j) complete(vi , w j ) Die Abbildung 3.11 zeigt ein Beispiel, wie der Teilbaum t(w), mit w ist ein Alias, vervollständigt wird. Das Verfahren sucht in den Kindern von Alias nach den Knoten mit den Schlüsseln 2 und 3. Durch den Aufruf von copyAppend werden Kopien der Knoten mit den Schlüsseln 2, 3 für alle Nachfolger von Alias erzeugt und angehängt. 37 3 Vererbung der Geometrieparameter w v 1 2 3 4 5 w 1 w 1 =⇒ =⇒ 2 3 2 3 2 3 4 5 4 5 4 5 Abbildung 3.11: Vervollständigen von Knoten (Fall 2) v 1 w 1 w 1 =⇒ 2 2 3 3 Abbildung 3.12: Vervollständigen von Knoten (Fall 3) 38 1 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus 3.5.3 Vererben von Knoten Sehen wir uns die rekursive Funktion 3.5 processNode für das Vererben von Knoten in einem Baum an. Der Baum wird mithilfe der rekursiven Tiefensuche in PostorderReihenfolge durchlaufen. Die Funktion bekommt einen Knoten v als Argument. 1-14: Wenn v kein Blatt ist, dann werden alle seine Kinder vi besucht und geprüft, ob einer von ihnen ein Sibling ist. Die Funkion processNode wird für vi aufgerufen, wenn er kein Sibling ist, ansonsten für die Kinder des Siblings. Wenn vi ein Sibling ist, wird s = vi initialisiert, um anschließend testen zu können, ob ein Sibling gefunden wurde. 15-27: Wird ein Sibling s gefunden, dann werden alle seine Nachbarknoten vi und alle seine Kinder s j durchlaufen. Es wird nach dem Kind s(k) mit dem gleichen Schlüssel k wie vi in s j gesucht. Wenn der Schlüssel nicht gefunden wird, dann wird die rekursive Funktion copyAppend aufgerufen und die Knoten vi , s j übergeben. Ansonsten, wenn der Schlüssel gefunden wird, wird die rekursive Funktion complete aufgerufen und die Knoten vi , s(k) übergeben. 28-30: Zum Schluss wird processNode auf alle Nachfolger des Siblings angewendet. Im Falle einer Siblingkette können die Knoten von oben nach unten vererbt werden. Wenn ein Knoten vererbt wird, dann muss dieser auch in die Unterbäume vererbt werden. Deswegen wird die Funktion für die Kinder des Siblings nochmal aufgerufen. In Abbildung 3.13 ist die Arbeitsweise von processNode an einem Beispiel veranschaulicht. Die Knoten v1 und v2 werden an die Kinder des Siblings vererbt. Die Funktion copyAppend erzeugt die zu vererbenden Knoten und hängt sie an die Teilbäume t(s1) und t(s2) an. Die Knoten von v2 werden an die Kinder des Alias angehängt. Die nachfolgende rekursive Funktion processNode implementiert das Verfahren. 39 3 Vererbung der Geometrieparameter Algorithm 3.5 processNode(v) Input: v 1: if v == null then return v 3: if v is leaf then 4: return null 2: s ← null 6: for i = 1 to outdeg(v) do 7: vi ← v.getChild(i) 8: if vi is Sibling then 5: 9: 10: 11: 12: s ← vi for j = 1 to outdeg(s) do s j ← s.getChild( j) processNode(s j ) else 14: processNode(vi ) 15: if s! = null then // Sibling gefunden 13: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 40 for i = 1 to outdeg(v) do vi ← v.getChild(i) if v(i) is Sibling then continue for j = 1 to outdeg(s) do s j ← s.getChild( j) k ← v.getKey() s(k) ← s j .getChild(k) if s(k) == null then copyAppend(vi , s j ) else complete(v(i), s(k)) for i = 1 to outdeg(s) do // Vererbung auf Teilbäume von Sibling si ← s.getChild(i) processNode(si ) return v 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus s1 v v1 1 3 v2 2 4 5 s1 s2 ⇒ 1 s s2 3 2 4 5 ⇒ 1 3 2 2 4 5 5 Abbildung 3.13: Vererben von Knoten 3.5.4 Umkehrung der Vererbung Die Funktion 3.6 für die Umkehrung der Vererbung reverseProcess wird verwendet, um vererbte Knoten eines Baumes zu entfernen. Dabei wird der Baum mithilfe der rekursiven Tiefensuche in Postorder-Reihenfolge durchlaufen. Ein innerer Knoten wird entfernt, wenn er keine Kinder hat, und er kein Nachfolger eines Alias oder Sibling ist. 1-3: Zuerst prüfen wir, ob der Knoten v ein Blatt oder ein innerer Knoten ist. Wenn der Knoten ein Blatt und vererbt ist, dann wird er entfernt. 4-13: Falls v ein innerer Knoten ist, dann rufen wir die Funktion rekursiv für alle seine Kinder auf. Wir löschen v, wenn er keine Kinder hat und kein Nachfolger eines Alias oder Sibling ist. 41 3 Vererbung der Geometrieparameter Algorithm 3.6 reverseProcess(v) Input: v 1: if v is leaf && v.isInherited() then v.getParent().removeChild(v) 3: return null 4: if v is inner node then 2: 5: 6: 7: 8: 9: 10: 11: 12: 13: for i = 0 to outdeg(v) do vi = v.getChild(i) reverseProcess(vi ) if outdeg(v) = 0 then w = v.getParent() if w! = root && !w is Alias && !w is Sibling then w.removeChild(v) return null return v 3.5.5 Implementierungen Abbildung 3.14: Klasse DefaultRecursiveInheritance 42 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus Die Klasse DefaultRecursiveInheritance (s. Abbildung 3.14) wurde in Java implementiert. Sie dient der Vererbung von Knoten und enthält folgende Methoden. process bekommt einen fertigen Baum und ruft die Methode processNode auf, um die Knoten zu vererben, isAlias und isSibling fragen ab, ob ein Knoten ein Sibling oder Alias ist, copyAppend erzeugt eine Kopie von einem Knoten und hängt sie an einen anderen Knoten an. Die Methode complete vervollständigt fehlende Knoten eines Knotens mit dem entsprechenden Knoten eines anderen Knotens. Die Methode reverseProcess entfernt alle vererbten Knoten des Baumes. 3.5.6 Kosten der Funktionen Die Funktion complete wird auf zwei Teilbäume t(v), t(w) eines n-ären Baumes mit N inneren Knoten angewendet. Die Laufzeit ist linear, Vervollständigen von Knoten weil jeder Knoten aus t(w) genau einmal besucht wird. Im schlimmsten Fall können für jeden inneren Knoten aus t(v) mehrere Aliase in t(w) existieren. In Abbildung 3.15 sind die Teilbäume t(v) und t(w) dargestellt. w v 1 2 1 3 2 3 2 3 Abbildung 3.15: Aufwand für complete Die Anzahl der Knoten des Niveaus l mit l ≥ 1 von t(v) bezeichen wir mit pl und von t(w) mit ql . Um den Teilbaum t(w) in dem Niveau l zu vervollständigen, wird die 43 3 Vererbung der Geometrieparameter Funktion pl · ml , wobei ml = 1 pl (q2l−1 + q2l ), mal aufgerufen. p 1 · m1 + p 2 · m2 + · · · + p l · ml = q 1 + q 2 + · · · + q l h = ∑ ql l=1 = O(N) In einem n-ären Baum mit Höhe h gibt es auf dem Niveau l maximal nl−1 Knoten. Die Funktion benötigt für zwei Teilbäume h ∑n l=1 l−1 h (n2l−2 + n2l−1 ) = ∑ (n2l−2 + n2l−1 ) · l−1 n l=1 Aufrufe. Zum Beispiel benötigen wir für den Baum aus Abbildung 3.15 2 ∑ (22l−2 + 22l−1) = 3 + 12 = 15 l=1 Aufrufe von complete. Davon sind drei Aufrufe für den Schlüssel 1 und jeweils 6 für die Schlüssel 2 und 3. Das Ergebnis ist die Anzahl der Knoten von t(w). Die Laufzeit wird dominiert von der Anzahl N der Knoten des gesamten Baumes. Der Aufwand ist von der Größenordnung O(N). Vererben von Knoten Die Laufzeit zum Ausführen der Funktion processNode, hängt von der Anzahl n der Siblinge des Baumes ab. Im schlimmsten Fall ist der Baum mit N Knoten eine Siblingkette mit maximal n = N−1 2 Siblingen. In Abbildung 3.16 ist eine Siblingkette mit der Anzahl der Funktionsaufrufe dargestellt. Die Siblingkette wird in Postorder-Reihenfolge traversiert. Wenn ein Sibling gefunden wird, dann wird die Funktion für die Kinder des Siblings (Schlüssel 2,3,4) einmal aufgerufen. Dann können die notwendigen Anweisungen ausgeführt werden, wie das Anhängen oder das Vervollständigen von Knoten. Im letzten Schritt wird die Funktion wieder für die Kinder des Siblings aufgerufen, um die Knoten, die zunächst oben vererbt werden, nach unten zu vererben. 44 3.5 Default-Recursive-Inheritance-Vererbungsalgorithmus 1 1 2 2 4 3 8 4 Abbildung 3.16: Aufwand für processNode 1 + 2 + 4 + . . . = 20 + 21 + · · · + 2n n = ∑ 2i i=0 = 1 − 2n+1 1−2 = 2n+1 − 1 = O(2 N+1 2 − 1) = O(2N ) Im schlimmsten Fall ist der Gesamtaufwand von der Größenordnung O(2N · N), wenn die Funkion copyAppend oder complete aufgerufen wird. Die Laufzeit zum Ausführen der Funktion copyAppend für einen n-ären Baum mit N Knoten ist im schlechtesten Fall von der Größenordnung Anhängen von Knoten O(N), wenn ein Teilbaum t1 mit N −3 Knoten erzeugt und angehängt wird (s. Abbildung 3.17). 45 3 Vererbung der Geometrieparameter b =⇒ b t1 t1 b 0 t1 Abbildung 3.17: Aufwand für copyAppend Die Laufzeit zum Ausführen der Funktion reverseProcess ist O(N), da der Baum, um vererbte Knoten zu löschen, in Postorder-Reihenfolge durchlaufen wird. Umkehrung der Vererbung Die Gesamtlaufzeit des DRI-Vererbungsalgorithmus ist von der Laufzeit der Funktion processNode abhängig. Folglich ist die Gesamtlaufzeit des DRI-Vererbungsalgorithmus O(2N · N). 46 3.6 Default-Inheritance-Vererbungsalgorithmus 3.6 Default-Inheritance-Vererbungsalgorithmus In diesem Kapitel wird ein neuer Default-Inheritance-Vererbungsalgorithmus (DI) für die Vererbung von Knoten beschrieben. Der DRI-Vererbungsalgorithmus speichert keine Informationen über die abhängigen Knoten und hat eine exponentielle Laufzeit O(2N · N). Der DI-Vererbungsalgorithmus erzeugt einen Abhängigkeitsgraphen, indem alle Beziehungen erster und zweiter Ordnung zwischen den Knoten gespeichert werden, und hat eine lineare Laufzeit. 3.6.1 Programmablauf Der DI-Vererbungsalgorithmus wird in folgenden Schritten durchgeführt (s. Tabelle 3.1). Zuerst bestimmen wir alle Siblingketten eines Baumes, um in den Siblingketten die Siblingketten bestimmen Teilbäume ergänzen und die Beziehungen erster Ordnung speichern Wurzel-Siblingketten bestimmen Teilbäume für die Beziehungen zweiter Ordnung bestimmen Teilbäume ergänzen und die Beziehungen zweiter Ordnung speichern Blätter vererben Tabelle 3.1: Programmablauf Beziehungen erster Ordnung zwischen den Knoten zu dokumentieren. Dann vererben wir innere Knoten in den Teilbäumen der Siblingketten und speichern die Beziehungen erster Ordnung zwischen den Knoten. Als Nächstes bestimmen wir die Siblingketten, die nicht von anderen Siblingketten abhängen. Danach bestimmen wir Teilbäume für die Beziehung zweiter Ordnung. Anschließend speichern wir die Beziehungen zweiter Ordnung. Zum Schluss vererben wir die Blätter. Auf Details wird in folgenden Abschnitten eingegangen. 47 3 Vererbung der Geometrieparameter 3.6.2 Bestimmung von Siblingketten Die Funktion 3.7 für die Bestimmung von Siblingketten getDependanceTrees durchläuft den Baum in Level-Order-Reihenfolge. Zur Implementierung der Traversierung verwenden wir eine Warteschlange (Queue) q. Dabei suchen wir nach Siblingketten und fügen sie in die Warteschlange sibChainQueue ein. Die zweite Warteschlange q2 wird verwendet, um die Siblingketten zu durchlaufen und die Beziehungen erster Ordnung zwischen dem Vorgänger und den Nachfolgern des Siblings zu speichern. Für die Warteschlangen q und q2 wird die Java-Klasse LinkedList und für sibChainQueue ArrayList verwendet. 1-3: Nachdem die Warteschlange initialisiert ist, wird die Wurzel des Baumes am Anfang der Warteschlange eingefügt. Solange noch Knoten in der Warteschlange liegen, wird der letzte Knoten aus der Warteschlange geholt. 4-7: Um einen Knoten nicht mehrmals zu bearbeiten, wird geprüft, ob er abhängige Knoten hat. Falls der Knoten keine abhängigen Knoten hat, wird die zweite Warteschlange initialisiert und der Knoten hinzugefügt. Solange noch Knoten in der zweiten Warteschlange sind, wird die nachfolgende Anweisung durchgeführt. 9-17: Der letzte Knoten wird aus der zweiten Warteschlange geholt und entfernt. Wenn ein Sibling s gefunden wird, dann wird die Beziehung erster Ordnung zwischen dem Vorgänger und den Nachfolgern von s gespeichert. Die Kinder des Siblings werden in die zweite Wartschlange eingefügt, um die Siblingkette zu durchlaufen. Falls ein Knoten der Anfang einer Siblingkette ist, dann wird er in die Warteschlange sibChainQueue hinzugefügt. Um die Teilbäume der Siblingketten zu besuchen, werden die Knoten in die erste Warteschlange hinzugefügt. 18-21: Um den Baum in Level-Order-Reihenfolge zu traversieren, werden die Nachfolger des Knotens, die keine Siblinge sind, in die Warteschlange q eingefügt. 48 3.6 Default-Inheritance-Vererbungsalgorithmus Algorithm 3.7 getDependanceTrees(r, sibChainQueue) 1: Queue q; q.addFirst(r) // Liste zum Traversieren des Baumes 2: while q.size() > 0 do 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: v ← q.getLast(); q.removeLast() H ← v.getAllDependants() // Menge der abhängigen Knoten if H.size() = 0 then // verhindert Duplikate in der Queue sibChainQueue Queue q2; q2.addFirst(v) // Liste zum Traversieren der Siblingketten while q2.size() > 0 do w ← q2.getLast(); q2.removeLast() if w! = v then // füge nicht besuchte Knoten in q ein q.addFirst(w) s ← getSibChild(w) if s! = null then for i = 1 to outdeg(s) do si ← s.getChild(i); q2.addFirst(si ) 1 w.addDepentants(1, si ) // speichere w − → si in H von w if w is SiblingChain then // w ist eine Siblingkette sibChainQueue.addFirst(w) // Siblingketten des Baumes for i = 1 to outdeg(v) do vi ← v.getChild(i) if !vi is Sibling then q.addFirst(vi ) 49 3 Vererbung der Geometrieparameter 1 1 1 Abbildung 3.18: Beziehungen erster Ordnung zwischen dem Vorgänger und den Nachfolgern des Siblings 3.6.3 Bestimmung und Ergänzung von abhängigen Teilbäumen Im vorherigen Verfahren getDependanceTrees werden alle Knoten die, der Anfang einer Siblingkette sind, in die Warteschlange sibChainQueue eingefügt und die Beziehung erster Ordnung zwischen dem Vorgänger und den Nachfolgern des Siblings gespeichert. Für die Vererbung ist es notwendig, die Beziehungen erster und zweiter Ordnung für die Knoten in den Teilbäumen der Siblingketten zu speichern, um alle Abhängigkeiten zu dokumentieren. Die Funktion 3.8 getDependancies erhält zwei Parameter, die Ordnung n und die Warteschlange sibChainQueue mit den Siblingketten. 1-10: Zuerst wird die Warteschlange zum Traversieren der Siblingketten und deren Teilbäume initialisiert, dann werden die Siblingketten, die in sibChainQueue liegen, durchlaufen. Danach wird die Menge der abhängigen Knoten des aktuellen Knoten ermittelt. Falls die Kinder des Knotens keine Siblinge und keine Blätter sind, werden sie zur childrenQueue zum Speichern der Beziehung und zur q zum Traversieren der Teilbäume hinzugefügt. 11-15: Hier wird die Menge der abhängigen Knoten durchlaufen. Für jeden abhängigen Knoten werden die Kinder des Erblassers durchlaufen. 50 3.6 Default-Inheritance-Vererbungsalgorithmus 16-25: Fall 1: Falls das Kind v j des Erblassers v ein Alias ist und der abhängige Knoten w kein Kind w(k) mit dem gleichen Schlüssel wie v j hat, werden die Kinder des Alias erzeugt und in w angehängt. Dann wird die Funktion getDependanceTrees(w(k), sibChainQueue) aufgerufen. Wenn w(k) eine Siblingkette ist, wird sie zur sibChainQueue hinzugefügt und die Beziehungen erster Ordnung in ihr gespeichert. Wenn v j eine Siblingkette ist, wird w(k) zur sibChainQueue hinzugefügt. Danach werden die Beziehungen der Ordnung n zwischen den Knoten gespeichert. 26-34: Fall 2: Ansonsten, wenn w(k) nicht existiert, wird v j kopiert und an w angehängt. Fall 3: Falls w(k) existiert und kein Alias ist, wird die Beziehung zwischen den Knoten gespeichert. Fall 4: Ist w(k) ein Alias, so wird die Beziehung zwischen dem Kind des Erblassers und den Kindern des Alias gespeichert. Fall 1 n v w n v n =⇒ v j vj w 1 2 n Fall 2 1 2 1 2 3 4 3 4 n v w n v vj 1 =⇒ v j 1 2 2 n w 1 Abbildung 3.19: Anhängen von Knoten und Speichern von Beziehungen (Fall 1 und Fall 2) 51 3 Vererbung der Geometrieparameter Algorithm 3.8 genDependancies(n, sibChainQueue) 1: Queue q, childrenQueue for i = 1 to sibChainQueue.size() do // Siblingketten 3: p ← sibChainQueue.get(i); q.addFirst(p) 4: while q.size() > 0 do // Teilbäume der Siblingkette 2: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: v ← q.getLast(); q.removeLast() H ← v.getAllDependants(n) // Abhängige Knoten n-ter Ordnung for i = 1 to outdeg(v) do vi ← v.getChild(i) if !vi is leaf && !is Sibling then q.addFirst(vi ); childrenQueue.addFirst(vi ) for all nodes in H do // Abhängige Knoten von v w ← H.next(); q.addFirst(w) for j = 0 to childrenQueue.size() do // Kinder von v v j ← childrenQueue.get( j); k ← v j .getKey(); w(k) ← w.getChild(k) if v j is Alias && w(k) == null then // 1.Fall for i = 1 to outdeg(v j ) do v ji ← v j .getChild(i); k ← v ji .getKey(); w(k) ← w.getChild(k) if w(k) == null then // hänge v ji an w(k) copyAppend(v ji , w) getDependanceTrees(w(k), sibChainQueue) if v ji is SiblingChain then 20: 21: 22: 23: sibChainQueue.add(w(k)) n → w(k) in H von w v ji .addDepentants(n, w(k)) // speichere v ji − 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 52 else if w(k) == null then // Fall 2 . . . // wie 20-25 mit v ji ← v j else if !w(k) is Alias then // Fall 3 . . . // wie 23-25 mit v ji ← v j else // Fall 4 for l = 1 to outdeg(w(k)) do w(k)l ← w(k).getChild(l); . . . // wie 23-25 mit v ji ← v j und wk ← w(k)l 3.6 Default-Inheritance-Vererbungsalgorithmus Fall 3 n v vj 1 w 1 2 n v w n =⇒ v j 1 1 2 Fall 4 n v vj 1 2 w 1 n v =⇒ v j 1 2 w n 1 n Abbildung 3.20: Anhängen von Knoten und Speichern von Beziehungen (Fall 3 und Fall 4) 3.6.4 Wurzel-Siblingketten Um die Beziehungen zweiter Ordnung zu dokumentieren, ist es notwendig, die Siblingketten zu bestimmen, die nicht von anderen Knoten abhängen. Ein Knoten v sei die n Wurzel einer Siblingkette. Wenn es für v kein v0 − → v gibt, heißt v Wurzel-Siblingkette. Als Erstes erzeugen wir mithilfe der Funktion 3.9 getDependanceChains zwei Maps. Die Schlüsselmenge der Maps ist die Menge aller Siblingketten. Die Datenmenge der ersten Map (dependanceTreeMap) ist die Menge der abhängigen Siblingketten und die der zweiten Map (innerDependancies) die Menge von restlichen abhängigen Knoten, die kein Anfang einer Siblingketten sind. 1-7: Zuerst kopieren wir alle Siblingketten in die Menge sibChainSet. Dann durchlaufen wir alle Siblingketten v. Für jedes v durchlaufen wir seine abhängigen Knoten w. 8-11: Falls w in der Menge der Siblingketten enthalten ist, fügen wir v (Erblasser) in die Schlüsselmenge und w (abhängige Siblingkette) in die Datenmenge von de- 53 3 Vererbung der Geometrieparameter pendanceTreeMap ein. Ansonsten, wenn w keine Siblingkette ist, fügen wir v (Schlüssel) und w (Wert) in innerDependancies ein. Algorithm 3.9 getDependanceChains(sibChainQueue) 1: dependanceTreeMap, innerDependancies sibChainSet ← sibChainQueue 3: for i = 1 to sibChainQueue.size() do // Siblingketten 4: v ← sibChainQueue.get(i) 5: H ← v.getAllDependants(1) // Abhängige Knoten erster Ordnung 2: 6: 7: 8: 9: 10: 11: for all nodes in H do // Abhängige Knoten von v w ← H.next() if sibChainSet.contains(w) then dependanceTreeMap.put(v, w) else innerDependancies.put(v, w) Zum Auffinden der Wurzel-Siblingketten verwenden wir dependanceTreeMap. Die Funktion 3.10 getRootCandidate bestimmt eine Teilmenge von Schlüsseln aus der Schlüsselmenge von dependanceTreeMap, die nicht in ihrer Datenmenge vorkommen. 1-4: Wir kopieren die Schlüsselmenge von dependanceTreeMap in zwei Mengen: rootCandidateSet und keySet. Dann durchlaufen wir alle Knoten v von keySet. 5-8: Für jedes v holen wir seine Datenmenge von dependanceTreeMap. Dann durchlaufen wir alle Knoten w aus der Datenmenge und entfernen w (abhängige Siblingketten) aus rootCandidateSet. Als Ergebnis erhalten wir die Menge rootCandidateSet, die Wurzel-Siblingketten enthält. In Abbildung 3.21 ist der Abhängigkeitsgraph mit Beziehungen erster und zweiter Ordnung zwischen den Knoten dargestellt. Die Knoten v und w sind zwei WurzelSiblingketten. 54 3.6 Default-Inheritance-Vererbungsalgorithmus Algorithm 3.10 getRootCandidate(dependanceTreeMap) 1: rootCandidateSet ← dependanceTreeMap.keySet() // Schlüsselmenge der Map 2: keySet ← dependanceTreeMap.keySet() for all nodes in keySet do 4: v ← keySet.next() 5: depSet ← dependanceTreeMap.get(v) // Datenmenge von v 3: 6: 7: 8: for all nodes in depSet do w ← depSet.next() rootCandidateSet.remove(w) // entferne w aus der Menge 1 v 1 1 w k 1 1 2 1 2 1 2 1 2 k 1 1 k Abbildung 3.21: Abhängigkeitsgraph mit den Wurzel-Siblingketten v und w 55 3 Vererbung der Geometrieparameter 3.6.5 Teilbäume für die Bestimmung der zweiten Ordnung Als Nächstes bestimmen wir die Teilbäume für die Beziehungen zweiter Ordnung. Die Funktion 3.11 createSecondOrderTrees geht von der Menge der Wurzel-Siblingketten aus. Die Funktion kann wie folgt beschrieben werden: 1-8: Hier durchlaufen wir alle Wurzel-Siblingketten und fügen sie in die Warteschlange q ein. Für jede Siblingkette sibChain aus q durchlaufen wir alle ihre abhängigen Siblingketten depSibChain. Die depSibChain fügen wir in q ein. Das Paar (sibChain, depSibChain) fügen wir in die zweite Warteschlange q2 ein. 11-15: Solange q2 Paare von Knoten enthält, holen wir das Paar mit den Knoten v und seinem abhängigen Knoten w. Für v durchlaufen wir alle seine restlichen abhängigen Knoten v0 aus der Menge innerDepSetv. 20-26: Nun durchlaufen wir alle restlichen abhängigen Knoten w0 von w. Wenn w0 den gleichen Schlüssel hat wie v0 , speichern wir die Beziehung zweiter Ordnung zwi2 schen den beiden Knoten. Wenn für v0 kein u mit der Eigenschaft u − → v0 existiert, fügen wir v0 in die Menge secondOrderTrees ein. Um die Teilbäume zu durchlaufen, fügen wir das Paar(v0 , w0 ) in q2 ein. 27-29: Wenn wir keinen Knoten mit dem Schlüssel von v0 finden, verlängern wir die abhängige Siblingkette, d. h., wir erzeugen einen Sibling und hängen ihn an w an. Dann wird ein Knoten w0 mit dem Inhalt von v0 erzeugt und an den Sibling 2 angehängt. Danach speichern wir die Beziehung zweiter Ordnung v0 − → w0 . Wenn 2 es keinen Knoten u mit der Eigenschaft u − → v0 gibt, fügen wir v0 in die Menge secondOrderTrees. Anschließend fügen wir das Paar(v0 , w0 ) in q2 ein. In Abbildung 3.22 ist ein Baum mit den Siblingketten v und w dargestellt. Die Siblingkette mit der Wurzel v ist eine Wurzel-Siblingkette. Die Knoten mit den Schlüsseln 2 und 3 bezeichen wir als Teilbäume zweiter Ordnung. 56 3.6 Default-Inheritance-Vererbungsalgorithmus Algorithm 3.11 createSecondOrderTrees(dependanceTreeMap, innerDependancies, rootCandidateSet, order=1) 1: for all nodes in rootCandidateSet do // Wurzel-Siblingketten 2: p ← rootCandidateSet.next() 3: Queue q, q2; q.addFirst(p) 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: while q.size() > 0 do sibChain ← q.getLast(); q.removeLast() dependanceSet ← dependanceTreeMap.get(sibChain) for all nodes in dependanceSet do // Abhängige Siblingketten depSibChain ← dependanceSet.next() q.addFirst(depSibChain) q2.addFirst(Pair(sibChain, depSibChain)) while q2.size() > 0 do pair ← q2.getLast(); q2.removeLast() v ← pair.first w ← pair.second innerDepSetv ← innerDependancies.get(v) for all nodes in innerDepSetv do // Abhängige von v 0 v ← innerDepSetv .next() innerDepSetw ← innerDependancies.get(w) found = false for all nodes in innerDepSetw do // Abhängige von w w0 ← innerDepSetw .next() 0 if w0 .getKey().equals(v .getKey()) then 0 0 v .addDepentants(order + 1, w ) 0 secondOrderTrees.add(v ) 0 0 q2 .addFirst(Pair(v , w )) found = true if !found then w0 ← completeDepChain(v0 , w) // verlängere abhängige Siblingkette . . . // wie 23-25 57 3 Vererbung der Geometrieparameter r r =⇒ v v 1 1 1 1 1 2 1 1 1 w 1 2 1 3 1 2 1 1 w 2 3 1 2 2 1 3 Abbildung 3.22: Teilbäume der zweiten Ordnung 3.6.6 Vererbung von Blättern Als Nächstes wollen wir die Blätter innerhalb eines Baumes vererben. Dabei wird der Baum in Level-Order-Reihenfolge traversiert. Die Funktion 3.12 inherit bekommt die Wurzel des Baumes und die Ordnung als Argumente. 1-13: Zuerst fügen wir die Wurzel r des Baumes in die Warteschlange q ein. Solange noch Knoten in q liegen, wird der letzte Knoten v geholt. Um alle Knoten einmal zu besuchen, durchlaufen wir alle Kinder vi des aktuellen Knotens und fügen sie in q ein. Für alle vi durchlaufen wir alle abhängigen Knoten w von v. Dabei prüfen wir, ob w ein Kind w(k) mit demselben Schlüssel wie vi hat. 14-20: Falls wir den Knoten w(k) nicht finden, betrachten wir folgende Fälle: Fall 1 (s. Abbildung 3.23): Falls vi nicht auf vererbt gesetzt ist, oder w keine Beziehung höherer Ordnung zu einem anderen Knoten hat, erzeugen wir mithilfe 58 3.6 Default-Inheritance-Vererbungsalgorithmus von complete eine Kopie von vi und hängen sie an w an. Fall 2 (s. Abbildung 3.24): Wenn vi auf vererbt gesetzt ist und v kein Anfang einer abhängigen Siblingkette ist, erzeugen wir eine Kopie von vi und hängen sie an w an. Algorithm 3.12 inherit(r, order) 1: Queue q; q.addFirst(r) 2: while q.size() > 0 do 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: v ← q.getLast(); q.removeLast() for i = 0 to outdeg(v) do vi ← v.getChild(i) if !vi is leaf then q.addFirst(vi ) else H ← v.getDependants(order) for all nodes in H do w ← H.next() k ← vi .getKey() w(k) ← w.getChild(k) if w(k) == null then if !vi .isInherited() || w.getSupreme(order+1) == null then // Fall 1 w(k) ← complete(vi , w) else if !isTreeRoot(v) then // Fall 2 w(k) ← complete(vi , w) In Abbildung 3.25 ist ein Fall dargestellt, wann ein Blatt nicht vererbt wird. Der Knoten w hat eine Beziehung höherer Ordnung n zu einem anderen Knoten u. Wenn folgendes 1 2 gilt: v − → w und u − → w, wird v j nicht an w vererbt. 59 3 Vererbung der Geometrieparameter Fall 1 v n w ⇒ vj 1 v n vj 1 w 1 Abbildung 3.23: Vererbung von Blättern (Fall 1) Fall 2 ≤n v ≤n n w ⇒ vj 1 v n vj 1 w 1 Abbildung 3.24: Vererbung von Blättern (Fall 2) Sonst ≤n v vj 1 ≤n >n n w ; v >n n vj 1 Abbildung 3.25: Keine Vererbung von Blättern 60 w 1 3.6 Default-Inheritance-Vererbungsalgorithmus 3.6.7 Implementierungen Die Klasse DefaultInheritance (s. Abbildung 3.26) enthält folgende Methoden. getDependanceTrees bestimmt alle Siblingketten eines Baumes und speichert Beziehungen zwischen dem Vorgänger und den Nachfolgern des Siblings. genDependancies speichert Beziehungen erster und zweiter Ordnung und vererbt innere Knoten. getDependanceChains bestimmt zwei Maps. Die Datenmenge der ersten Map enthält alle abhängigen Siblingketten, die der zweiten Map die restlichen von einer Siblingkette abhängigen Knoten. Die Methode getRootCandidate ermittelt die Menge der Wurzel-Siblingketten. createSecondOrderTrees bestimmt Teilbäume für die Beziehungen zweiter Ordnung. inherit vererbt die Blätter. Abbildung 3.26: Klasse DefaultInheritance 3.6.8 Kosten der Funktionen Die Funktionen werden auf einen n-ären Baum mit N Knoten angewendet. Die Laufzeit der Funktion getDependanceTrees ist O(N). Der Baum wird in Level-Order-Reihenfolge durchlaufen. Dabei kommt jeder Bestimmung von Siblingketten 61 3 Vererbung der Geometrieparameter Knoten höchstens einmal in den Warteschlangen q und q2 vor. Bestimmung und Ergänzung von abhängigen Teilbäumen Die Laufzeit der Funk- tion getDependanceTrees ist O(N). Jeder Knoten wird höchstens einmal in die Warteschlange q eingefügt, um die Beziehungen zwischen den Knoten zu speichern und innere Knoten zu vererben. Der Laufzeit der Funktion getDependanceChains ist von der Größenordnung O(N). Es werden alle Siblingketten und deren abhängige Knoten durchWurzel-Siblingketten laufen. Für jeden abhängigen Knoten wird geprüft, ob er in der Menge rootCandidateSet ist. Der Aufwand ist Nsib · Nab · c < N · c, wobei Nsib die Anzahl der Siblingketten, Nab die Anzahl der abhängigen Knoten und c der Suchaufwand in der Menge ist. Dabei wird jeder abhängige Knoten höchstens einmal besucht. Für die Menge verwenden wir die Java-Klasse HashSet. Die Laufzeit dieser Klasse zum Suchen ist konstant. In der Anwendung sind Nsib und Nab klein. Die Funktion getRootCandidate hat eine Laufzeit von O(N). Es werden alle Siblingketten und deren abhängige Knoten aus der Datenmenge von dependanceTreeMap durchlaufen. Die abhängigen Knoten, die in der Menge rootCandidateSet sind, werden gelöscht. Die Laufzeit zum Löschen eines Knotens ist konstant. Teilbäume für die Bestimmung zweiter Ordnung Die Laufzeit der Funktion crea- teSecondOrderTrees ist O(N). Es werden alle Wurzel-Siblingketten und ihre abhängigen Siblingketten durchlaufen, um Teilbäume mit Beziehung zweiter Ordnung zu dokumentieren. Dabei werden die Knoten der abhängigen Siblingkette höchstens einmal besucht. Vererbung von Blättern Die Laufzeit der Funktion inherit ist O(N). Um die Blätter zu vererben, wird der Baum in der Level-Order-Reihenfolge traversiert. Jeder innere Knoten wird höchstens einmal in die Warteschlange eingefügt. 62 3.6 Default-Inheritance-Vererbungsalgorithmus Aufgrund der Laufzeiten der Funktionen von der Größenordnung O(N) ist die Gesamtlaufzeit des Default-Inheritance-Vererbungsalgorithmus O(N). 63 3 Vererbung der Geometrieparameter 3.7 Simplified-Inheritance-Vererbungsalgorithmus In diesem Kapitel beschreiben wir den Simplified-Inheritance-Vererbungsalgorithmus (SI) für den Konfigurationseditor von NeuGen. Der SI-Vererbungsalgorithmus verwendet den erzeugten Abhängigkeitsgraphen des DI-Vererbungsalgorithmus, um die Abhängigkeiten gezielt abzulaufen und die Parameterwerte zu vererben. 3.7.1 Parameterwerte aktualisieren Wenn ein Parameter im Konfigurationseditor verändert wird, ist es notwendig, alle seine abhängigen Parameter im Abhängigkeitsgraphen zu aktualisieren. Wird ein Parameter auf vererbt gesetzt, wird der Wert von seinem Erblasser übernommen und die abhängigen Parameter aktualisiert. Die Funktion 3.13 updateAllInheritedValues stellt diese Funktionalität bereit. Die Funktion, die ein Blatt als Argument erhält, kann wie folgt beschrieben werden: 1-10: Zuerst initialisieren wir die Warteschlange q und fügen den Vater des Blattes p in q ein. Solange q Knoten enthält, holen wir den letzten Knoten v und entfernen ihn aus q. Dann holen wir ein Kind v(k) von v, das den gleichen Schlüssel wie der Knoten p besitzt. Wenn v(k) auf vererbt gesetzt ist, ermitteln wir den Erblasser u(k) und kopieren anschließend mithilfe der Funktion setInheritedValue den Wert von u(k) in v(k). 11-20: Hier durchlaufen wir für die erste und zweite Ordnung alle abhängigen Knoten v0 von v. Dann prüfen wir, ob das Kind v0 (k) mit dem gleichen Schlüssel wie v(k) auf vererbt gesetzt ist. Wenn v0 (k) auf vererbt gesetzt und nicht in der Menge procSet enthalten ist, fügen wir v0 in q ein. Zum Schluss wird v0 in procSet eingefügt, um den Knoten nicht mehrmals in die Warteschlange zu legen. In Abbildung 3.27 ist ein Beispiel dargestellt. Die Blätter des Baumes haben den gleichen Schlüssel. Das Blatt v(1) von v mit dem Wert 10 (value) wird auf vererbt gesetzt 64 3.7 Simplified-Inheritance-Vererbungsalgorithmus Algorithm 3.13 updateAllInheritedValues(p) 1: Queue q 2: q.addFirst((p.getParent()) 3: while q.size() > 0 do 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: v ← q.getLast(); q.removeLast() k ← p.getKey() v(k) ← v.getChild(k) if v(k).isInherited() then // wenn der Knoten vererbt ist u ← v.getSupreme() u(k) ← u.getChild(v(k).getKey()) setInheritedValue(u(k), v(k)) // kopiere den Wert von u(k) in v(k) order ← 2 for i = 1 to order do Hv ← v.getDependants(i) for all nodes in Hv do // die abhängigen Knoten von v v0 ← Hv .next() k ← v(k).getKey() v0 (k) ← v0 .getChild(k) if v0 (k).isInherited() && !procSet.contains(v0) then q.addFirst(v0 ) procSet.add(v0 ) 65 3 Vererbung der Geometrieparameter =⇒ u value = 5 1 u value = 5 1 v v value = 10 1 value = 5 v0 value = 10 1 v0 1 value = 5 1 Abbildung 3.27: Vererbung von Parameterwerten und erbt anschließend den Wert 5 von dem Blatt u(1) des Erblassers u. Danach wird das auf vererbt gesetzte Blatt v0 (1) des abhängigen Knotens v0 aktualisiert. 3.7.2 Implementierungen Abbildung 3.28: Klasse SimplifiedInheritance Die Klasse SimplifiedInheritance (s. Abbildung 3.28) enthält folgende Methoden: getHeritage ermittelt den Erblasser eines Knotens. setInheritedValue kopiert den Wert des Erblassers in einen abhängigen Knoten. getInheritedValue liefert für einen Knoten den Wert des Erblassers. updateAllInheritedValues aktualisiert die abhängigen Knoten eines Knotens. 66 3.7 Simplified-Inheritance-Vererbungsalgorithmus 3.7.3 Kosten der Funktion Parameterwerte aktualisieren Die Laufzeit der Funktion updateAllInheritedValues ist O(N). Im ungünstigsten Fall wird der ganze Baum mit N Knoten einmal durchlaufen. Für die Menge verwenden wir die Java-Klasse HashSet. Die Laufzeit zum Suchen eines Knotens in der Menge procSet ist konstant. Folglich ist die Gesamtlaufzeit des SI-Vererbungsalgorithmus O(N). 67 4 Evaluierung In diesem Kapitel werden die im vorherigen Kapitel beschriebenen Vererbungsalgorithmen Default-Inheritance und Simplified-Inheritance mittels Tests überprüft. Zur Evaluierung wurde eine Testumgebung in NeuGen implementiert, in der die Geometrieparameter der Parameterdatei verändert werden können, um danach die Ergebnisse der verschiedenen Vererbungsalgorithmen zu vergleichen. Zusätzlich wurde ein Algorithmus zum Erzeugen eines beliebigen Baumes programmiert, um die Laufzeit der Vererbungsalgorithmen für große Bäume zu messen. 4.1 Vergleich der Vererbungsalgorithmen Wir vergleichen die drei Vererbungsalgorithmen anhand von zwei Kriterien: Gleichheit der Parameterbäume nach der Vererbung und Laufzeit. In Tabelle 4.1 sind die Laufzeiten für die Vererbung von Geometrieparameter der Parameterdatei Param.neu mit 3622 Knoten, durchgeführt auf einem Intel Pentium 4 mit einer Taktfrequenz von 3.00 GHz und 1 GB Arbeitsspeicher, zusammengefasst. Für den Test wurde jeweils ein Parameter zufällig geändert und dann der Vererbungsalgorithmus angewandt. Insgesamt wurden so 2306 Parameter nacheinander bearbeitet, davon bekamen 847 einen zufälligen Wert, 1147 wurden auf vererbt und 312 auf nicht vererbt gesetzt. Der Eintrag für die Parameteranzahl 1 ist aus dem Eintrag für 2306 Parameter als Mittelwert berechnet. In Tabelle 4.1 sind zum Vergleich auch die Laufzeiten der bisherigen Vererbungsalgorithmen Recursive-Tree- 68 4.1 Vergleich der Vererbungsalgorithmen Inheritance und Default-Recursive-Inheritance. Es zeigt sich, dass die Vererbung mit dem Simplified-Inheritance-Vererbungsalgorithmus weniger Zeit in Anspruch nimmt als mit dem Default-Inheritance-Vererbungsalgorithmus. Dies war zu erwarten, da der Simplified-Inheritance-Vererbungsalgorithmus den erzeugten Abhängigkeitsgraphen des Default-Inheritance-Vererbungsalgorithmus verwendet. Wegen der exponentiellen Laufzeit benötigen der bisherige Recursive-Tree-Inheritance- und der DefaultRecursive-Inheritance-Vererbungsalgorithmus die meiste Zeit. Parameteranzahl Zeit in Sekunden neue Verfahren bisherige Verfahren Default- Simplified- Default-Recursive- Recursive-Tree- Inheritance Inheritance Inheritance Inheritance 2306 250 0.1 1128 8532 1 0.1 4.2E-5 0.49 3.7 Tabelle 4.1: Laufzeiten für die Vererbungsalgorithmen Der Recursive-Tree-Inheritance-Vererbungsalgorithmus wurde in C++ und die graphische Benutzeroberfläche in Java implementiert. Um den C++-Code unter Java nutzen zu können, wird das Programmierwerkzeug SWIG1 (Simplified Wrapper and Interface Generator) verwendet. SWIG generiert eine Art Hülle (Wrapper-Code) um den vorhandenen C++-Code, welche dem Java-Code den Zugriff auf den C++-Code ermöglicht. Durch den vielfachen Wechsel zwischen den verschiedenen Programmiersprachen können Probleme entstehen. Der Grund für die hohe Laufzeit des Recursive-TreeInheritance-Vererbungsalgorithmus ist das Übersetzen von C++-Klassen und Methoden nach Java mit SWIG. Die Laufzeit des Default-Recursive-Inheritance-Vererbungsalgorithmus auf der Java-Seite ist besser, als die Laufzeit des Recursive-TreeInheritance-Vererbungsalgorithmus mit dem Zugriff auf den C++-Code mithilfe von SWIG. 1 http://www.swig.org 69 4 Evaluierung Um die Ergebnisse der Vererbungsalgorithmen zu prüfen, wurde in jedem Schritt der editierte Baum mit den Geometrieparametern der Parameterdatei Param.neu kopiert. Auf die Bäume wurden jeweils verschiedene Vererbungsalgorithmen angewendet. Danach wurden die Bäume durchlaufen und die Knoten miteinander verglichen. Der Default-Inheritance- und der Simplified-Inheritance-Vererbungsalgorithmus liefern den gleichen Baum, wie der bisher in NeuGen eingesetzte Default-Recursive-InheritanceVerer-bungsalgorithmus. Im Folgenden wollen wir die Laufzeiten der Vererbungsalgorithmen betrachten. Dazu wurde ein Baum mit Siblingketten mit Beziehungen erster und zweiter Ordnung erzeugt und in jedem Schritt die Siblingketten vergrößert. Auf die Bäume wurde die Vererbung angewendet und die Laufzeit gemessen. 4.1.1 Default-Recursive-Inheritance-Vererbungsalgorithmus In diesem Abschnitt wollen wir uns die Laufzeit des Default-Recursive-InheritanceVererbungsalgorithmus ansehen. Dazu verlängern wir die Siblingketten und vererben dann die Knoten im Baum. In Abbildung 4.1 sehen wir, dass der Anteil der Siblinge erster oder zweiter Ordnung an der Gesamtanzahl der Siblinge einen Einfluss auf die Laufzeit hat. Wenn eine der beiden Ordnungen überwiegt, nähert sich die Struktur des Baumes der des in Kapitel 3.5 beschriebenen schlimmsten Falles an. In diesem Fall besitzt der Baum eine große Tiefe. Wenn der Anteil der Siblinge erster oder zweiter Ordnung etwa gleich ist, ist die Struktur des Baumes breiter. In diesem Fall sind weniger Durchläufe nötig. Wie wir in den Abbildungen 4.1 und 4.2 sehen, ist die gemessene Laufzeit des DefaultRecursive-Inheritance-Vererbungsalgorithmus für Bäume mit einer Gesamtanzahl von unter 100 Siblingen fast gleich. Ab etwa 100-110 Siblingen steigt die Kurve deutlich an. Bei einer großen Anzahl von Siblingen zeigt sich, dass die Laufzeit exponentiell wächst. Dies stimmt mit dem im Kapitel 3.5 berechneten Aufwand von der Größenordnung O(2N · N) überein. 70 4.1 Vergleich der Vererbungsalgorithmen Abbildung 4.1: Entwicklung der Laufzeit von Default-Recursive-Inheritance für Bäume mit bis zu 13 × 13 Siblingen DefaultRecursiveInheritance 14.000 Process performance 12.000 Zeit t [sec] 10.000 8.000 6.000 4.000 2.000 0.000 0 20 40 60 80 100 Siblinge 120 140 160 180 200 Abbildung 4.2: Entwicklung der Laufzeit von Default-Recursive-Inheritance für die Gesamtanzahl der Siblinge 71 4 Evaluierung 4.1.2 Default-Inheritance-Vererbungsalgorithmus Aus den Abbildungen 4.3 und 4.4 kann man entnehmen, dass der Default-InheritanceVererbungsalgorithmus eine bessere Laufzeit als der Default-Recursive-InheritanceVererbungsalgorithmus hat. Auch sehen wir in Abbildung 4.3, dass der Anteil der Ordnungen im Gegensatz zum vorher beschriebenen Algorithmus DRI keinen starken Einfluss auf die Laufzeit hat. Weiter erkennt man, dass die gemessene Laufzeit sich der theoretischen Laufzeit von O(N) für eine große Anzahl von Siblingen annähert. Dass die Kurve in Abbildung 4.4 nicht linear ist, können wir dadurch erklären, dass der CacheSpeicher für kleinere Bäume effizienter genutzt werden kann, was die Ausführungszeit verkürzt. Für große Bäume wird der Cache-Speicher nicht effizient verwendet und hat daher keinen Einfluss auf die Laufzeit. Außerdem verwenden wir im Vererbungsalgorithmus Datenstrukturen, die im Laufe des Algorithmus ihre Größe dynamisch ändern. Dadurch müssen die Daten umkopiert werden, was zu einer höheren Laufzeit führt. Die Peaks sind Artefakte des Betriebssystems, die das Ergebnis nicht beeinflussen. Abbildung 4.3: Entwicklung der Laufzeit von Default-Inheritance für Bäume mit bis zu 50 × 50 Siblingen (Peaks sind Artefakte, die das Ergebnis nicht beeinflussen) 72 4.1 Vergleich der Vererbungsalgorithmen DefaultInheritance 4.500 Process performance 4.000 3.500 Zeit t [sec] 3.000 2.500 2.000 1.500 1.000 0.500 0.000 0 500 1000 1500 2000 2500 Siblinge Abbildung 4.4: Entwicklung der Laufzeit von Default-Inheritance für die Gesamtanzahl der Siblinge 4.1.3 Simplified-Inheritance-Vererbungsalgorithmus Wir wollen in diesem Abschnitt die Laufzeit des Simplified-Inheritance-Vererbungsalgorithmus ansehen, der den erzeugten Abhängigkeitsgraphen des Default-InheritanceVererbungsalgorithmus verwendet. Wenn wir uns die Abbildungen 4.5 und 4.6 ansehen, stellen wir fest, dass die Laufzeit besser ist, als die des Default-Inheritance-Vererbungsalgorithmus und wie bei diesem nicht von der Struktur des Baumes abhängt. Dies können wir dadurch erklären, dass der Abhängigkeitsgraph nur einmal durchlaufen werden muss, um die Knoten zu vererben. Die Peaks sind Artefakte des Betriebssystems, die das Ergebnis nicht beeinflussen. Aufgrund der kurzen Laufzeiten sind mehr Peaks zu sehen, als in Abbildung 4.3. 73 4 Evaluierung Abbildung 4.5: Entwicklung der Laufzeit von Simplified-Inheritance für Bäume mit bis zu 100 × 100 Siblingen (Peaks sind Artefakte des Betriebssystems, die das Ergebnis nicht beeinflussen) SimplifiedInheritance 0.250 Process performance 0.200 Zeit t [sec] 0.150 0.100 0.050 0.000 0 1000 2000 3000 4000 5000 Siblinge 6000 7000 8000 9000 10000 Abbildung 4.6: Entwicklung der Laufzeit von Simplified-Inheritance für die Gesamtanzahl der Siblinge 74 4.2 Zusammenfassung 4.2 Zusammenfassung Es zeigt sich, dass der Default-Inheritance-Vererbungsalgorithmus und der für die grafische Benutzeroberfläche entwickelte Simplified-Inheritance-Vererbungsalgorithmus eine bessere Laufzeit als der bisher in NeuGen implementierte Default-RecursiveInheritance-Vererbungsalgorithmus aufweisen. Bereits ab 100 Siblingen benötigt der Default-Inherit-ance-Vererbungsalgorithmus deutlich mehr Zeit für die Vererbung. Bei den Tests liefern alle Vererbungsalgorithmen gleiche Parameterbäume für die gleiche Parameterdatei Param.neu. 75 5 Zusammenfassung In dieser Bachelor-Arbeit wurde der bisher in NeuGen implementierte Default-Recursive-Inheritance-Vererbungsalgorithmus betrachtet und ein schnellerer Default-Inheritance-Vererbungsalgorithmus vorgestellt. Für die grafische Benutzeroberfläche wurde der Simplified-Inheritance-Vererbungsalgorithmus beschrieben, der den Abhängigkeitsgraphen des Default-Inheritance-Vererbungsalgorithmus mit den gespeicherten Beziehungen erster und zweiter Ordnung zwischen den Knoten verwendet. Der betrachtete rekursive DRI-Vererbungsalgorithmus ist auch für die Vererbung mit Beziehungen höherer Ordnung verwendbar. Der Nachteil ist seine exponentielle Laufzeit. Außerdem werden keine Beziehungen zwischen den Knoten gespeichert, weswegen es nicht möglich ist, den Wert des Erblassers und der abhängigen Knoten zu bestimmen. Für jeden editierten Knoten im Konfigurationseditor muss der Vererbungsalgorithmus auf dem ganzen Baum ausgeführt werden. Um die Algorithmen zu testen, wurde eine Testumgebung implementiert, in der die Ergebnisse des bisher in NeuGen eingesetzten DRI-Vererbungsalgorithmus mit den Ergebnissen der neuen Algorithmen verglichen werden. Dabei liefern die Vererbungsalgorithmen gleiche Resultate, d. h. sie liefern für gleiche Parameterdateien gleiche Parameterbäume. Zusätzlich wurde die Laufzeit der Algorithmen für große Parameterbäume gemessen. Die Vorteile der beiden Vererbungsalgorithmen DI und SI liegen in den kurzen Ausführungszeiten. Vor allem aber sind sie für große Parameterdateien geeignet. Mithilfe des Abhängigkeitsgraphen kann der Wert des Erblassers und der abhängigen Knoten festgestellt werden. Die beiden Vererbungsalgorithmen sind nur für die Vererbung mit den 76 Beziehungen erster und zweiter Ordnung geeignet. Eine Anwendung der Vererbung mit den Beziehungen höherer Ordnungen wäre für den Benutzer im Konfigurationseditor unübersichtlich. Die Vererbung von Geometrieparametern bildet die in der Natur gefundenen Zusammenhänge zwischen den einzelnen Nervenzellklassen in sinnvoller Weise ab. Vor allem aber muss sie schnell und platzsparend sein. Beide Kriterien werden von der vorliegenden Arbeit erfüllt. Durch die Implementierung des vorgestellten neuen Algorithmus in das Softwarepaket NeuGen ist die effizientere Generierung von größeren synthetischen neuronalen Netzen ermöglicht worden. Diese Arbeit ist somit ein weiterer kleiner Schritt auf dem Weg zur Modellierung des Gehirns. 77 Abbildungsverzeichnis 78 2.1 Schemazeichnung der wichtigsten Gehirnstrukturen aus [6, Kandel] . . 6 2.2 Kolumne nach [4, Eberhard] . . . . . . . . . . . . . . . . . . . . . . . 7 2.3 Aufbau einer Nervenzelle aus [1] . . . . . . . . . . . . . . . . . . . . . 8 2.4 Schematische Darstellung von NeuGen . . . . . . . . . . . . . . . . . . 10 2.5 Neuronenklassen-Hierarchie . . . . . . . . . . . . . . . . . . . . . . . 11 2.6 L4-Star-Pyramidenzellen . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.7 L4-Spiny-Stellate-Zelle . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.8 L2/3-Pyramidenzellen . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.9 L5A-Pyramidenzellen . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.10 L5B-Pyramidenzellen . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.11 Parameterdatei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.12 Konfigurationseditor (links ist der Baum der Parameter, rechts ist die Erklärung) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.13 Teilbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.14 Darstellung eines Baumes . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.1 Sibling, Siblingkette und Alias . . . . . . . . . . . . . . . . . . . . . . 23 3.2 Beziehung erster Ordnung . . . . . . . . . . . . . . . . . . . . . . . . 24 3.3 Beziehung erster Ordnung mit Alias im Teilbaum t(r) . . . . . . . . . . 25 3.4 Beziehungen zweiter Ordnung . . . . . . . . . . . . . . . . . . . . . . 26 3.5 Reihenfolge bei der Vererbung (Fall 1-3 i) . . . . . . . . . . . . . . . . 27 3.6 Reihenfolge bei der Vererbung (Fall 3 ii) . . . . . . . . . . . . . . . . . 27 3.7 Traversieren von Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.8 Übersicht der verwendeten Klassen . . . . . . . . . . . . . . . . . . . . 32 3.9 Anhängen von Knoten (Fall 1) . . . . . . . . . . . . . . . . . . . . . . 35 Abbildungsverzeichnis 3.10 Anhängen von Knoten (Fall 2 und Fall 3) . . . . . . . . . . . . . . . . 36 3.11 Vervollständigen von Knoten (Fall 2) . . . . . . . . . . . . . . . . . . . 38 3.12 Vervollständigen von Knoten (Fall 3) . . . . . . . . . . . . . . . . . . . 38 3.13 Vererben von Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.14 Klasse DefaultRecursiveInheritance . . . . . . . . . . . . . . . . . . . 42 3.15 Aufwand für complete . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.16 Aufwand für processNode . . . . . . . . . . . . . . . . . . . . . . . . 45 3.17 Aufwand für copyAppend . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.18 Beziehungen erster Ordnung zwischen dem Vorgänger und den Nachfolgern des Siblings . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 3.19 Anhängen von Knoten und Speichern von Beziehungen (Fall 1 und Fall 2) 51 3.20 Anhängen von Knoten und Speichern von Beziehungen (Fall 3 und Fall 4) 53 3.21 Abhängigkeitsgraph mit den Wurzel-Siblingketten v und w . . . . . . . 55 3.22 Teilbäume der zweiten Ordnung . . . . . . . . . . . . . . . . . . . . . 58 3.23 Vererbung von Blättern (Fall 1) . . . . . . . . . . . . . . . . . . . . . . 60 3.24 Vererbung von Blättern (Fall 2) . . . . . . . . . . . . . . . . . . . . . . 60 3.25 Keine Vererbung von Blättern . . . . . . . . . . . . . . . . . . . . . . 60 3.26 Klasse DefaultInheritance . . . . . . . . . . . . . . . . . . . . . . . . 61 3.27 Vererbung von Parameterwerten . . . . . . . . . . . . . . . . . . . . . 66 3.28 Klasse SimplifiedInheritance . . . . . . . . . . . . . . . . . . . . . . . 66 4.1 Entwicklung der Laufzeit von Default-Recursive-Inheritance für Bäume mit bis zu 13 × 13 Siblingen . . . . . . . . . . . . . . . . . . . . . 4.2 Entwicklung der Laufzeit von Default-Recursive-Inheritance für die Gesamtanzahl der Siblinge . . . . . . . . . . . . . . . . . . . . . . . . 4.3 72 Entwicklung der Laufzeit von Default-Inheritance für die Gesamtanzahl der Siblinge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5 71 Entwicklung der Laufzeit von Default-Inheritance für Bäume mit bis zu 50 × 50 Siblingen (Peaks sind Artefakte, die das Ergebnis nicht beeinflussen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4 71 73 Entwicklung der Laufzeit von Simplified-Inheritance für Bäume mit bis zu 100 × 100 Siblingen (Peaks sind Artefakte des Betriebssystems, die das Ergebnis nicht beeinflussen) . . . . . . . . . . . . . . . . . . . . . 74 79 Abbildungsverzeichnis 4.6 80 Entwicklung der Laufzeit von Simplified-Inheritance für die Gesamtanzahl der Siblinge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Algorithmenverzeichnis 3.1 3.2 3.3 postorder_traverse(v) . . . . . . . . . . . . . . . . . . . . . . . . . . . levelorder_traverse(v) . . . . . . . . . . . . . . . . . . . . . . . . . . . copyAppend(v, w) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 30 35 3.4 3.5 3.6 complete(v, w) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . processNode(v) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . reverseProcess(v) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 40 42 3.7 3.8 3.9 3.10 getDependanceTrees(r, sibChainQueue) genDependancies(n, sibChainQueue) . . getDependanceChains(sibChainQueue) getRootCandidate(dependanceTreeMap) 49 52 54 55 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.11 createSecondOrderTrees(dependanceTreeMap, innerDependancies, rootCandidateSet, order=1) . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.12 inherit(r, order) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3.13 updateAllInheritedValues(p) . . . . . . . . . . . . . . . . . . . . . . . 65 81 Literaturverzeichnis [1] Neil A. Campbell. Biologie. Spektrum Akademischer Verlag, 1 edition, 1997. [2] Anja Schierloh. Neuronale netzwerke und deren plastizität im barrel-kortex der ratte. http://deposit.ddb.de/cgi-bin/dokserv?idn=97305395x, 2003. [3] Robert Sedgewick. Algorithmen in C++. Pearson Studium, 3 edition, 2002. [4] J. P. Eberhard, A. Wanner und G. Wittum. Neugen: A tool for the generation of realistic morphology of cortical neurons and neural networks in 3d. http://dx.doi.org/10.1016/j.neucom.2006.01.028, 2006. [5] Thomas Ottmann und Peter Widmayer. Algorithmen und Datenstrukturen. Spektrum Akademischer Verlag, 4 edition, 2002. [6] Eric R. Kandel, James H. Schwarz und Thomas M. Jessell. Neurowissenschaften Eine Einführung. Spektrum Akademischer Verlag, 1 edition, 1996. 82