Compare commits
5 commits
1ef5c20317
...
1b85b42f37
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b85b42f37 | |||
| 615b07d412 | |||
| 0dee1aad7d | |||
| 81ed77c0a1 | |||
| 44abf1fddc |
6 changed files with 161 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
result*
|
|
||||||
.direnv/
|
.direnv/
|
||||||
.envrc
|
.envrc
|
||||||
*.pdf
|
*.pdf
|
||||||
|
result
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,19 @@
|
||||||
== Literaturrecherche und Konzeption
|
== Literaturrecherche und Konzeption
|
||||||
|
|
||||||
Zunächst fand eine tiefere Einarbeitung in die existierende Literatur zu alternierenden Optimierungsverfahren statt.
|
Zunächst fand eine tiefere Einarbeitung in die existierende Literatur zu alternierenden Optimierungsverfahren statt.
|
||||||
Aufgrund der Nähe der Themen hat sich eine Recherche zu den "k-Means Clustering" und "Expectation-maximization" Algorithmen angeboten. Spannende Literatur zu diesen Themen wurde von Bezdek @bezdek_notes_2002 -- allgemein zu alternierenden Optimierungsverfahren und spezifischer von Do @do_what_2008 publiziert.
|
Aufgrund der Nähe der Themen hat sich eine Recherche zu den "k-Means Clustering" und "Expectation-maximization (EM)" Algorithmen angeboten. Spannende Literatur zu diesen Themen wurde von Bezdek @bezdek_notes_2002 -- allgemein zu alternierenden Optimierungsverfahren und spezifischer von Do @do_what_2008 publiziert.
|
||||||
|
|
||||||
|
==== Vergleiche der Algorithmen mit dem gestellten Problem
|
||||||
|
|
||||||
|
Sowohl die hier vorgestellten Methoden, als auch die "k-Means Clustering" und EM Algorithmen lösen ein Optimierungsproblem mittels eines iterativen Verfahrens.
|
||||||
|
Der EM-Algorithmus befasst sich mit der Schätzung von Parametern in statistischen Modellen, insbesondere wenn der vorgegebene Datensatz unvollständig ist, während der "k-Means" Algorithmus darauf abzielt, Daten in Cluster zu gruppieren. Besonderes bei letzterem ähnelt die Clusterbildung sehr dem hier gestellten Problem.
|
||||||
|
Ein entscheidender Unterschied ist, dass bei k-Means Datenpunkte nur einer vorgegebenen Anzahl an Clustern zugewiesen werden.
|
||||||
|
Eine Beschränkung, welche Datenpunkte in welches Cluster fallen, wird durch den Algorithmus nicht implementiert, wäre aber für eine Verbesserung der Eingangswerte vor der Quantisierung notwendig
|
||||||
|
|
||||||
|
|
||||||
Wenngleich diese Publikationen keinen direkten Weg zur Lösung der Problemstellung der Praxis bieten konnten, stellten sie ein gutes Grundverständnis für diese Art von Problem dar.
|
Wenngleich diese Publikationen keinen direkten Weg zur Lösung der Problemstellung der Praxis bieten konnten, stellten sie ein gutes Grundverständnis für diese Art von Problem dar.
|
||||||
|
|
||||||
|
|
||||||
// Hier noch Unterschiede und Gemeinsamkeiten der Literatur vs. Problem aufstellen in Stichpunkten
|
// Hier noch Unterschiede und Gemeinsamkeiten der Literatur vs. Problem aufstellen in Stichpunkten
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,7 +60,18 @@ Anschließend wurde die betragsmäßige Optimierung, welche in @bach_original_1
|
||||||
Als nächste Möglichkeit für den Multi-Bit Fall ist ein rekursiver Ansatz des Problems implementiert worden. Hierfür werden die Eingangswerte zunächst mit der betragsmäßigen Optimierung verarbeitet um so eine "optimale" Verteilung für den 1-bit Fall zu konstruieren. Anschließend wird die Verteilung in zwei symmetrische Unterverteilungen aufgeteilt, und jeweils deren Mittelwert bestimmt.
|
Als nächste Möglichkeit für den Multi-Bit Fall ist ein rekursiver Ansatz des Problems implementiert worden. Hierfür werden die Eingangswerte zunächst mit der betragsmäßigen Optimierung verarbeitet um so eine "optimale" Verteilung für den 1-bit Fall zu konstruieren. Anschließend wird die Verteilung in zwei symmetrische Unterverteilungen aufgeteilt, und jeweils deren Mittelwert bestimmt.
|
||||||
Anschließend werden für jede Summe der jeweiligen Unterverteilungen zusätzliche fraktionierte Gewichtungen auf die bereits bestehenden Gewichtungen aufaddiert.
|
Anschließend werden für jede Summe der jeweiligen Unterverteilungen zusätzliche fraktionierte Gewichtungen auf die bereits bestehenden Gewichtungen aufaddiert.
|
||||||
Basierend auf der Mittelwertbestimmung werden zusätzliche Grenzen definiert, anhand deren die aufkommenden neuen Summen mit fraktionierte Gewichtungen optimal gewählt werden.
|
Basierend auf der Mittelwertbestimmung werden zusätzliche Grenzen definiert, anhand deren die aufkommenden neuen Summen mit fraktionierte Gewichtungen optimal gewählt werden.
|
||||||
Basierend auf der Anzahl $m$ an Bits die aus einer Summe extrahiert werden sollen wird dieses Verfahren $m$-Mal mit allen entstehenden Unterverteilungen durchgeführt.
|
Basierend auf der Anzahl $m$ an Bits die aus einer Summe extrahiert werden sollen wird dieses Verfahren $m$-Mal mit allen entstehenden Unterverteilungen durchgeführt.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
include("../graphics/execution/recursive.typ"),
|
||||||
|
caption: "Darstellung des rekursiven Algorithmus"
|
||||||
|
)<fig:bach_recursive_algo>
|
||||||
|
|
||||||
|
@fig:bach_recursive_algo zeigt das grundsätzliche Schema für den Rekursiven Algorithmus auf.
|
||||||
|
Zu Beginn werden die anfänglichen Eingangswerte nach der Methode zur Betragsoptimierung verarbeitet.
|
||||||
|
Anschließend wird die Verteilung in zwei symmetrische Unterverteilungen aufgeteilt.
|
||||||
|
*n1* beschreibt das Skalar, welches in allen möglichen Kombinationen auf die Helperdaten der Linearkombinationen von *u1* addiert bzw. subtrahiert werden.
|
||||||
|
Um die nächsten beiden Unterverteilungen eines Knotens zu bestimmen, wird ermittelt, welche der neuen möglichen Linearkombinationen mit zusätzlichen Gewichtungen am weitesten Weg von einer
|
||||||
|
|
||||||
Ein erstes positives Ergebnis hier war die schnelle Konvergenz der Verteilung und die Gleichverteilung der quantisierten Symbole, da in jeden Grenzbereich möglichst gleich viele Summen gelegt worden sind. @fig:bach_recursive_dist zeigt das Ergebnis des rekursiven Ansatzes für die Quantisierung von 2 Bit.
|
Ein erstes positives Ergebnis hier war die schnelle Konvergenz der Verteilung und die Gleichverteilung der quantisierten Symbole, da in jeden Grenzbereich möglichst gleich viele Summen gelegt worden sind. @fig:bach_recursive_dist zeigt das Ergebnis des rekursiven Ansatzes für die Quantisierung von 2 Bit.
|
||||||
|
|
||||||
|
|
@ -60,19 +81,19 @@ Ein erstes positives Ergebnis hier war die schnelle Konvergenz der Verteilung un
|
||||||
caption: "Verteilung der Eingangswerte nach dem rekursiven Ansatz"
|
caption: "Verteilung der Eingangswerte nach dem rekursiven Ansatz"
|
||||||
)<fig:bach_recursive_dist>
|
)<fig:bach_recursive_dist>
|
||||||
|
|
||||||
Jedoch stellte sich nach der Analyse der verwendeten Hilfsdatenvektoren heraus, dass durch die Zuweisung der Hilfsdaten Informationen über den Schlüssel ableitbar sind.
|
Jedoch stellte sich nach der Analyse der verwendeten Helperdatenvektoren heraus, dass durch die Zuweisung der Helperdaten Informationen über den Schlüssel ableitbar sind.
|
||||||
|
|
||||||
#figure(
|
#figure(
|
||||||
image("../graphics/execution/helperdata_occurs.png", width: 70%),
|
image("../graphics/execution/helperdata_occurs.png", width: 70%),
|
||||||
caption: "Verteilung der Hilfsdatenvektoren für jedes Bitsymbol"
|
caption: "Verteilung der Helperdatenvektoren für jedes Bitsymbol"
|
||||||
)<fig:bach_recursive_hd_occurs>
|
)<fig:bach_recursive_hd_occurs>
|
||||||
|
|
||||||
Das Histogramm in @fig:bach_recursive_hd_occurs zeigt dieses Problem auf. Damit über die Hilfsdaten keine Informationen über den Schlüssel bekannt werden, muss jeder verwendete Hilfsdatenvektor von jedem Symbol gleich häufig verwendet werden. Mit diesem Ansatz werden von je zwei Symbolen jedoch nur vier von acht möglichen Hilfsdatenvektoren verwendet.
|
Das Histogramm in @fig:bach_recursive_hd_occurs zeigt dieses Problem auf. Damit über die Helperdaten keine Informationen über den Schlüssel bekannt werden, muss jeder verwendete Helperdatenvektor von jedem Symbol gleich häufig verwendet werden. Mit diesem Ansatz werden von je zwei Symbolen jedoch nur vier von acht möglichen Helperdatenvektoren verwendet.
|
||||||
|
|
||||||
=== Vorgabe des Codeworts
|
=== Vorgabe des Codeworts
|
||||||
|
|
||||||
Eine weitere getestete Methode bestand aus dem vorgeben des zu verwendeten Codewords bzw. Schlüssels.
|
Eine weitere getestete Methode bestand aus dem vorgeben des zu verwendeten Codewords bzw. Schlüssels.
|
||||||
Hierfür werden die Grenzen der Quantisiererfunktion über die kumulative Verteilungsfunktion der Eingangswerte bestimmt. Anschließend wird jene Summe mit Hilfsdaten gewählt, welche die Summe zu ihrem vorgegebenen Codewort quantisieren.
|
Hierfür werden die Grenzen der Quantisiererfunktion über die kumulative Verteilungsfunktion der Eingangswerte bestimmt. Anschließend wird jene Summe mit Helperdaten gewählt, welche die Summe zu ihrem vorgegebenen Codewort quantisieren.
|
||||||
|
|
||||||
#figure(
|
#figure(
|
||||||
image("../graphics/execution/given_codeword.png", width: 65%),
|
image("../graphics/execution/given_codeword.png", width: 65%),
|
||||||
|
|
@ -90,18 +111,22 @@ Im Detail wurde für eine große Menge an möglichen Grenzen die Distanzmaximier
|
||||||
Direkt im Anschluss wurde über Pearson's Chi-square Test die Gleichverteilung der Quantisierten Symbole überprüft und nach einem Maximum des Ergebnisses des Tests gesucht.
|
Direkt im Anschluss wurde über Pearson's Chi-square Test die Gleichverteilung der Quantisierten Symbole überprüft und nach einem Maximum des Ergebnisses des Tests gesucht.
|
||||||
@fig:bach_brute_force zeigt das Ergebnis der Verarbeitung dieser Grenzen für einen 3-bit Fall.
|
@fig:bach_brute_force zeigt das Ergebnis der Verarbeitung dieser Grenzen für einen 3-bit Fall.
|
||||||
|
|
||||||
Da diese Brute-Force Operation sehr rechenaufwändig ist, wurden die bereits in Julia implementierten Lösungen für parallel Computing eingesetzt und die Berechnung der idealen Grenzen auf einem Computer mit hoher Rechenkapazität ausgelagert.
|
Da diese Brute-Force Operation sehr rechenaufwendig ist, wurden die bereits in Julia implementierten Lösungen für parallel Computing eingesetzt und die Berechnung der idealen Grenzen auf einem Computer mit hoher Rechenkapazität ausgelagert.
|
||||||
|
|
||||||
|
Damit das parallele Rechnen eine signifikante Verbesserung in der Rechengeschwindigkeit erzielt, gab es einige Punkte zu beachten:
|
||||||
|
- Für einen festgelegten Datensatz ändern sich die möglichen gewichteten Summen nicht wären der Ausführung des Algorithmus', also können diese vorab berechnet und gespeichert werden.
|
||||||
|
- Das Verwenden der "pmap" Funktion zur parallelen Ausführung des Optimierungsalgorithmus hat den Rechenprozess um ca. $700"ms"$ verlangsamt. \
|
||||||
|
Eine effizientere Lösung besteht darin, bei der Ausführung des Julia Skripts die Anzahl an Threads vorzudefinieren und die jeweiligen Funktionen mit dem "\@everywhere" Flag zu markieren, damit sie von den verschiedenen Threads aufgerufen werden können.
|
||||||
#figure(
|
#figure(
|
||||||
image("../graphics/execution/brute-force.png", width: 80%),
|
image("../graphics/execution/brute-force.png", width: 80%),
|
||||||
caption: "Resultat nach Verwendung der durch den Brute-Force Ansatz gefundenen Grenzen"
|
caption: "Resultat nach Verwendung der durch den Brute-Force Ansatz gefundenen Grenzen"
|
||||||
)<fig:bach_brute_force>
|
)<fig:bach_brute_force>
|
||||||
|
|
||||||
Auch die Betrachtung des Histogramms der Hilfsdatenverteilung zeigt befriedigende Ergebnisse auf.
|
Auch die Betrachtung des Histogramms der Verteilung der Helperdaten zeigt befriedigende Ergebnisse auf.
|
||||||
Wie in @fig:bach_brute_force_occurs zu sehen ist, wird jeder Hilfsdatenvektor von jedem Symbol gleich häufig verwendet.
|
Wie in @fig:bach_brute_force_occurs zu sehen ist, wird jeder Helperdatenvektor von jedem Symbol gleich häufig verwendet.
|
||||||
|
|
||||||
#figure(
|
#figure(
|
||||||
image("../graphics/execution/brute-force-occurs.png", width: 80%),
|
image("../graphics/execution/brute-force-occurs.png", width: 75%),
|
||||||
caption: "Resultat nach Verwendung der durch den Brute-Force Ansatz gefundenen Grenzen"
|
caption: "Resultat nach Verwendung der durch den Brute-Force Ansatz gefundenen Grenzen"
|
||||||
)<fig:bach_brute_force_occurs>
|
)<fig:bach_brute_force_occurs>
|
||||||
|
|
||||||
|
|
|
||||||
7
content/results.typ
Normal file
7
content/results.typ
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
= Ergebnisse & Zusammenfassung
|
||||||
|
|
||||||
|
Insgesamt sind die meisten geplanten Ziele zum Erfolg der Ingenieurspraxis erreicht worden.
|
||||||
|
Da mit einer der getesteten Methoden zur Optimierung der Grenzen eine reproduzierbare Lösung gefunden wurde, stellen sich auch spannende zukünftige Projekte in diesem Gebiet auf.
|
||||||
|
Insgesamt könnte der Brute-Force Ansatz noch weiter optimiert werden, etwa mit einer iterativen Erhöhung der Genauigkeit der Grenzen um so auch bei sehr hohen Symbolbreiten genaue Grenzen bestimmen zu können.
|
||||||
|
Da die optimalen Grenzen für den 2- und 4-bit Fall mit einer mittelwertfreien Standardnormalverteilung bestimmt worden sind, lassen sich die hier bereits gefundenen Grenzen leicht auf weitere Normalverteilungen mit anderen Parametern übertragen. Außerdem besteht die Möglichkeit des Zusammenstellens einer Datenbank mit den numerisch optimalen Grenzen für dieses Problem für verschiedene Symbolbreiten.
|
||||||
|
Obwohl letztendlich keine vollständige Integration des portierten BCH-Codes stattgefunden hat, bietet die nun portierte Bibliothek eine solide Grundlage für zukünftige Integration auch in anderen Projekten.
|
||||||
64
flake.nix
64
flake.nix
|
|
@ -37,7 +37,7 @@
|
||||||
fontPaths = [
|
fontPaths = [
|
||||||
# Add paths to fonts here
|
# Add paths to fonts here
|
||||||
# "${pkgs.roboto}/share/fonts/truetype"
|
# "${pkgs.roboto}/share/fonts/truetype"
|
||||||
"template/resources"
|
"./template/resources"
|
||||||
];
|
];
|
||||||
|
|
||||||
virtualPaths = [
|
virtualPaths = [
|
||||||
|
|
@ -46,21 +46,79 @@
|
||||||
# dest = "icons";
|
# dest = "icons";
|
||||||
# src = "${inputs.font-awesome}/svgs/regular";
|
# src = "${inputs.font-awesome}/svgs/regular";
|
||||||
# }
|
# }
|
||||||
|
{
|
||||||
|
dest = "./graphics/";
|
||||||
|
src = ./graphics;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
dest = "./bibliography.bib";
|
||||||
|
src = ./bibliography.bib;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
dest = "./template/";
|
||||||
|
src = ./template;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
unstable_typstPackages = [
|
||||||
|
{
|
||||||
|
name = "glossarium";
|
||||||
|
version= "0.5.6";
|
||||||
|
hash = "sha256-kCU2Kyz9Ywtl9IxIgyUDsHIAbIpx2HL+YV2oMd0Be+Q=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "cetz-plot";
|
||||||
|
version = "0.1.1";
|
||||||
|
hash= "sha256-w5n7gCT6gDtxMvYjDhrwrPjT0b2M69whEUDByjTYDtQ=";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
name = "cetz";
|
||||||
|
version = "0.3.4";
|
||||||
|
hash = "sha256-5w3UYRUSdi4hCvAjrp9HslzrUw7BhgDdeCiDRHGvqd4=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "cetz";
|
||||||
|
version = "0.3.2";
|
||||||
|
hash = "sha256-3Abz+31Y61rZUnnKlXayqIsEYEOaD47BQPUMwm0i0xA=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "oxifmt";
|
||||||
|
version = "0.2.1";
|
||||||
|
hash = "sha256-8PNPa9TGFybMZ1uuJwb5ET0WGIInmIgg8h24BmdfxlU=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "codly";
|
||||||
|
version = "1.3.0";
|
||||||
|
hash = "sha256-WcqvySmSYpWW+TmZT7TgPFtbEHA+bP5ggKPll0B8fHk=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "codly-languages";
|
||||||
|
version = "0.1.1";
|
||||||
|
hash= "sha256-TSp3unFhn3NSaWhWYZb/i3rD4OolbNZNTdQeBxJ4Jfs=";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "fletcher";
|
||||||
|
version = "0.5.8";
|
||||||
|
hash = "sha256-kKVp5WN/EbHEz2GCTkr8i8DRiAdqlr4R7EW6drElgWk=";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# Compile a Typst project, *without* copying the result
|
# Compile a Typst project, *without* copying the result
|
||||||
# to the current directory
|
# to the current directory
|
||||||
build-drv = typixLib.buildTypstProject (commonArgs
|
build-drv = typixLib.buildTypstProject (commonArgs
|
||||||
// {
|
// {
|
||||||
inherit src;
|
inherit src unstable_typstPackages;
|
||||||
});
|
});
|
||||||
|
|
||||||
# Compile a Typst project, and then copy the result
|
# Compile a Typst project, and then copy the result
|
||||||
# to the current directory
|
# to the current directory
|
||||||
build-script = typixLib.buildTypstProjectLocal (commonArgs
|
build-script = typixLib.buildTypstProjectLocal (commonArgs
|
||||||
// {
|
// {
|
||||||
inherit src;
|
inherit src unstable_typstPackages;
|
||||||
});
|
});
|
||||||
|
|
||||||
# Watch a project and recompile on changes
|
# Watch a project and recompile on changes
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#import "@preview/cetz:0.3.4": canvas
|
#import "@preview/cetz:0.3.4": canvas
|
||||||
#import "@preview/cetz-plot:0.1.1": plot, chart
|
#import "@preview/cetz-plot:0.1.1": plot, chart
|
||||||
|
|
||||||
#let data = csv("z_distribution.csv")
|
#let data = csv("./z_distribution.csv")
|
||||||
#let data = data.map(value => value.map(v => float(v)))
|
#let data = data.map(value => value.map(v => float(v)))
|
||||||
|
|
||||||
#let line_style = (stroke: (paint: black, thickness: 2pt))
|
#let line_style = (stroke: (paint: black, thickness: 2pt))
|
||||||
|
|
|
||||||
56
graphics/execution/recursive.typ
Normal file
56
graphics/execution/recursive.typ
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
|
||||||
|
#import fletcher.shapes: pill
|
||||||
|
|
||||||
|
#diagram(
|
||||||
|
spacing: (-3mm, 8mm), // wide columns, narrow rows
|
||||||
|
node-stroke: 1pt, // outline node shapes
|
||||||
|
edge-stroke: 1pt, // make lines thicker
|
||||||
|
mark-scale: 60%, // make arrowheads smaller
|
||||||
|
node((0,0), [Start], name: <start>),
|
||||||
|
node((-4, 1), [u1], name: <u1>),
|
||||||
|
node((4, 1), [u2], name: <u2>),
|
||||||
|
|
||||||
|
node((-6, 2), [u11], name: <u11>),
|
||||||
|
node((-2, 2), [u12], name: <u12>),
|
||||||
|
node((2, 2), [u21], name: <u21>),
|
||||||
|
node((6, 2), [u22], name: <u22>),
|
||||||
|
|
||||||
|
node((-7, 3), [u111], name: <u111>),
|
||||||
|
node((-5, 3), [u112], name: <u112>),
|
||||||
|
node((-3, 3), [u121], name: <u121>),
|
||||||
|
node((-1, 3), [u122], name: <u122>),
|
||||||
|
node((1, 3), [u211], name: <u211>),
|
||||||
|
node((3, 3), [u212], name: <u212>),
|
||||||
|
node((5, 3), [u221], name: <u221>),
|
||||||
|
node((7, 3), [u222], name: <u222>),
|
||||||
|
|
||||||
|
node((-20mm, 33mm), [n1], shape: pill, name: <n1>),
|
||||||
|
node((-20mm, 18.5mm), [n2], shape: pill, name: <n2>),
|
||||||
|
node((-20mm, 4mm), [n3], shape: pill, name: <n3>),
|
||||||
|
|
||||||
|
edge(<n1>, <iter1>, "->"),
|
||||||
|
edge(<n2>, <iter2>, "->"),
|
||||||
|
edge(<n3>, <iter3>, "->"),
|
||||||
|
|
||||||
|
edge(<start>, <u1.north>, "->"),
|
||||||
|
edge(<start>, <u2.north>, "->"),
|
||||||
|
|
||||||
|
edge(<u1>, <u11.north>, "->"),
|
||||||
|
edge(<u1>, <u12.north>, "->"),
|
||||||
|
edge(<u2>, <u21.north>, "->"),
|
||||||
|
edge(<u2>, <u22.north>, "->"),
|
||||||
|
|
||||||
|
edge(<u11>, <u111.north>, "->"),
|
||||||
|
edge(<u11>, <u112.north>, "->"),
|
||||||
|
edge(<u12>, <u121.north>, "->"),
|
||||||
|
edge(<u12>, <u122.north>, "->"),
|
||||||
|
edge(<u21>, <u211.north>, "->"),
|
||||||
|
edge(<u21>, <u212.north>, "->"),
|
||||||
|
edge(<u22>, <u221.north>, "->"),
|
||||||
|
edge(<u22>, <u222.north>, "->"),
|
||||||
|
|
||||||
|
node(enclose: (<u1>, <u2>), stroke: aqua, fill: aqua.lighten(90%), name: <iter1>),
|
||||||
|
node(enclose: (<u11>, <u22>), stroke: teal, fill: teal.lighten(90%), name: <iter2>),
|
||||||
|
node(enclose: (<u111>, <u222>), stroke: eastern, fill: eastern.lighten(90%), name: <iter3>)
|
||||||
|
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue