"Wasser": Ein Mehrdimensionales Benchmark Problem


Dies ist ein kurzes Essay das eigentlich in die Dokumentation von PGAPack gehört, aber ich habe es nicht geschafft, plotly Grafiken in ein github README.rst file einzubinden, nichtmal mit der Proxy-Seite raw.githack.com (das ist eine Seite die html und javascript files mit dem richtigen Content-Type ausliefert so dass man sie im Web-Browser anzeigen kann, github liefert diese Seiten mit dem Content-Type text/plain aus). Github weigert sich, einen iframe in einer github Seite zu rendern.

Das "Wasser Ressourcenplanungs-Problem" [RTS01] ist ein technisches Problem das als Benchmark für das Testen von Optimierungssalgorithmen mit mehreren Zielfunktionen in der Literatur verwendet wird, unter andererem in Klassikern wie dem NSGA-II Artikel [DPAM02]. Das Problem besteht aus sieben Randbedingungen und einer fünfdimensionalen Zielfunktion.

Ich nutze dieses Problem seit einiger Zeit als Test meiner NSGA-II Implementierung in PGAPack. Es ist im Verzeichnis examples/nsgaii und kann (nach compilieren) mit dem Test-Problem Index 12 aufgerufen werden:

examples/nsgaii/optimize 12

Die Ausgabe des Programms kann auch in der folgenden Datei gefunden werden:

test/nsgaii_optimize_12.data

Zum Testen liefere ich auch das Script crowdingplot zum Plotten von mehrdimensionalen Optimierungsproblemen mit. Es findet sich im selben Directory wie der Code für das Beispiel.

Das "Wasser" Problem hat eine starke Korrelation zwischen den Zielfunktionen 0 und 2 (was auch von Singh et. al. [SIR11] publiziert wurde), wir sehen das wenn wir die beiden Zielfunktionen gegeneinander plotten mit:

crowdingplot -x=0 -y=2 test/nsgaii_optimize_12.data

Der obige Aufruf von crowdingplot nutzt das matplotlib Backend, wenn das mit dem plotly Backend angezeigt werden soll (wo wie in der obigen Grafik) kann die -S Option verwendet werden.

Was wir in dieser Grafik sehen ist dass wir bei einer besseren Bewertung für die Zielfunktion 0 auch eine bessere Bewertung für die Zielfunktion 2 bekommen und umgekehrt, d.h., die Zielfunktionen widersprechen sich nicht. Ausserdem ist die Zielfunktion 3 redundant [SIR11] mit den anderen Zielfunktionen und man kann sie beim Plotten der Zielfunktionen weglassen. So kann eine fünfdimensionale Pareto-Front zu einer dreidimensionalen reduziert werden und mit crowdingplot in drei Dimensionen angezeigt werden, mit sehr ähnlichen Resultaten wenn man die Achsen auf gleiche Werte skaliert:

crowdingplot -3 -x=0 -y=1 -z=4 test/nsgaii_optimize_12.data
crowdingplot -3 -x=2 -y=1 -z=4 test/nsgaii_optimize_12.data

Da bedeutet einerseits dass das Beispiel nicht wirklich fünfdimensional ist – andererseits ist es nett, die Daten zu visualisieren was in fünf Dimensionen wohl nicht so einfach wäre.

[DPAM02]

Kalyanmoy Deb, Amrit Pratap, Sameer Agarwal, and T. Meyarivan. A fast and elitist multiobjective genetic algorithm: NSGA-II. IEEE Transactions on Evolutionary Computation, 6(2):182–197, April 2002.

[RTS01]

Tapabrata Ray, Kang Tai, and Kin Chye Seow. Multiobjective design optimization by an evolutionary algorithm. Engineering Optimization, 33(4):399–424, 2001.

[SIR11] (1,2)

Hemant Kumar Singh, Amitay Isaacs, and Tapabrata Ray. A pareto corner search evolutionary algorithm and dimensionality reduction in many-objective optimization problems. IEEE Transactions on Evolutionary Computation, 15(4):539–556, August 2011.

Neue Messungen zur Nicht-Dominanz Sortierung


Dies ist eine Aktualisierung zu einem früheren Beitrag [Sch25] zur Implementierung von Nicht-Dominanz Sortierung in PGAPack. Nicht-Dominanz Sortierung ist eine zentrale Komponente von Optimierungsalgorithmen mit mehreren Zielfunktionen.

Im früheren Beitrag hatte ich nicht erwähnt für wie viele Generation der Test lief: Die Anzahl der Generationen im letzten und in diesem Test war 200. Im letzten Test war der Code ohne Optimierung compiliert. Die folgenden Messungen wurden mit der Option -O3 gemacht, der besten die gcc anbietet. Ausserdem war mir aufgefallen, dass meine Implementierung des DTLZ-1 tests [DTLZ05] quadratisch in der Anzahl der Zielfunktionen ist und ich befürchtete dass dies die Tests beeinflussen könnte. Es stellte sich aber heraus dass dies nicht der Fall war, der Overhead für 20 Zielfunktionen ist immer noch zu klein um bemerkbar zu sein.

Zusätzlich zu den ersten Tests messe ich jetzt – neben der Gesamtzeit – auch die Zeit nur für die Nicht-Dominanz Sortierung. Im folgenden gibt es also zwei Typen von Grafiken: Wo im Titel "Overall" steht ist die Zeit für die Zielfunktion und was der Algorithmus sonst noch an Verwaltung tut inkludiert (Gesamtzeit). Die anderen welche mit "Non-dominated" anfangen messen nur die Zeit für die Nicht-Dominanz Sortierung.

Also sehen wir uns erstmal die Gesamt-Zeiten an, verglichen mit meinem letzten Beitrag [Sch25] diesmal mit eingeschalteter Optimierung.

Ich habe hier auch die absolute Differenz zwischen den Messungen geplottet. Die absolute Differenz wenn die Gesamtzeit gemessen wird verglichen wenn nur die Zeit für die Nicht-Dominanz Sortierung gemessen wird ist bis auf ein paar Millisekunden gleich (wie man erwarten würde). Wir sehen dass die absoluten Differenzen schneller als linear wachsen, was man erwartet da die Laufzeit des alten Algorithmus quadratisch mit der Populationsgröße wächst und damit das kleinere Wachstum des neuen Algorithmus überdeckt.

Schließlich zeigt die folgende Grafik die Beschleunigungen die wir bekommen. Ähnlich wie Jensen [Jen03] sind die Zeiten für den neuen Algorithmus bei großer Anzahl von Zielfunktionen und niedriger Anzahl von Individuen etwas größer. Die absolute Differenz ist aber sehr klein wie man in der vorangegangenen Grafik nur sehen kann wenn man zu den ersten zwei Messungen mit Populationsgröße 100 und 200 zoomt. Die absolute Differenz ist unter 30ms für 200 Generationen. Ich werde also keinen Spezialfall für kleine Populationsgrößen implementieren – dies wäre einerseits schwierig weil es von der CPU-Geschwindigkeit abhängt wie man im letzten Blog-Beitrag sehen kann [Sch25] wo wir (bei abgeschalteter Optimierung) keine Verlangsamung mit dem neuen Algorithmus sehen. Ausserdem braucht man für viele Zielfunktionen typischerweise größere Populationen um die Pareto-Front abzudecken [Jen03].

Die folgenden Grafiken enthalten die Messungen wo nur die Zeit für die Nicht-Dominanz Sortierung gemessen wurde. Wenn man die Beschleunigung vergleicht (in der vorhergehenden Grafik als auch am Ende der folgenden Grafiken), besonders wenn man die Kurven für 2, 3 und 5 Zielfunktionen ausschaltet (das ist möglich wennn man in der Legende auf der rechten Seite der Grafik klickt), sieht man dass die Zuwächse sub-linear sind. Bei der Berechnung der Kurve teilen wir hier etwas das quadratisch wächst durch etwas was etwas stärker als linear wächst. Der Bruch ist dann sub-linear wie erwartet. Dabei sollte die Wirksamkeit des neuen Algorithmus nicht unterschätzen: Wie oben gezeigt wachsen die absoluten Differenzen stärker als linear.

Das Script das zur Messung verwendet wurde, das Script zum Plotten und die gemessenen Daten (und die alten gemessenen Daten) mache ich verfügbar falls das jemand nachvollziehen möchte. Das Beispiel ist in PGAPack im Verzeichnis examples/nsgaii aber die Version mit dem neuen Nicht-Dominanz Sortierungs-Algorithmus und der Instrumentierung zur Zeitmessung im Beispiel ist noch nicht released während ich das hier schreibe.

[DTLZ05]

Kalyanmoy Deb, Lothar Thiele, Marco Laumanns, and Eckart Zitzler. Scalable test problems for evolutionary multiobjective optimization. In Ajith Abraham, Lakhmi Jain, and Robert Goldberg, editors, Evolutionary Multiobjective Optimization, Theoretical Advances and Applications, Advanced Information and Knowledge Processing, pages 105–145. Springer, 2005.

[Jen03] (1,2)

Mikkel T. Jensen. Reducing the run-time complexity of multiobjective EAs: The NSGA-II and other algorithms. IEEE Transactions on Evolutionary Computation, 7(5):503–515, October 2003.

[Sch25] (1,2,3)

Ralf Schlatterbeck. Nicht-Dominanz Sortierung bei Optimierung mit mehreren Zielfunktionen. Blog post, Open Source Consulting, Juni 2025.

Nicht-Dominanz Sortierung bei Optimierung mit mehreren Zielfunktionen


Einige Leser hier wissen dass ich PGApack, ein Paket für Genetische Algorithmen, zusammen mit PGApy einer Python Bibliothek pflege (maintaine). PGAPack implementiert Optimierung mit mehreren Zielfunktionen (multi-objective optimization) mit NSGA-II [DPAM02] und NSGA-III [DJ14], [JD14].

Mehrere Zielfunktionen heisst dass wir mehr als ein Zielfunktion (auch Fitness-Funktion genannt) haben. Typischerweise widersprechen sich die Ziele – wenn eine Lösung in einer Zielfunktion besser ist wird, wird sie typischerweise in einer anderen Lösung schlechter – wir müssen uns also überlegen was besser im Kontext von mehreren Zielfunktionen bedeutet. Eine Lösung \(A\) ist strikt besser als eine andere Lösung \(B\) – wir sprechen davon dass Lösung \(A\) die Lösung \(B\) dominiert – wenn \(A\) besser oder gleich \(B\) in allen Zielfunktionen ist und strikt besser in mindestens einer davon.

Weil wir keine einfache Fitness für Lösungen haben, etablieren Algorithmen für mehrere Zielfunktionen typischerweise eine Rangfolge (ranking) der Lösungen nach Dominanz. Lösungen die nicht von einer anderen Lösung dominiert werden bekommen den Rang 0. Dann entfernt man alle Lösungen mit Rang 0. Alle Lösungen die dann nicht dominiert sind bekommen Rang 1 usw. Diese Prozedur heisst typischerweise Nicht-Dominanz Sortierung (non-dominated sorting). Der Rang ist dann eine der Komponenten für eine neue Fitness-Funktion.

Der NSGA-II Artikel [DPAM02] implementiert diese Sortierung indem jede Lösung gegen jede andere Lösung der Population auf Dominanz verglichen wird. Dann werden nicht-dominierte Lösungen eine nach der anderen entfernt und die Ränge zugewiesen wie oben angedeutet. Dieser Algorithmus zur Sortierung hat eine Laufzeit die mit der Anzahl der Lösungen quadratisch ansteigt. In der Informatik schreiben wir mit der O-Notation (auch Landau Symbole) Aussagen über die Laufzeit auf. Der quadratische Aufwand wird als \(O(n^2)\) notiert, wobei \(n\) die Anzahl der Lösungen ist.

2003 hat Jensen [Jen03] einen Algorithmus publiziert der die Nicht-Dominanz-Sortierung mit einem Aufwand von \(O\left(n\cdot(\log n)^{m-1}\right)\) durchführen kann wobei \(m\) die Anzahl der Zielfunktionen (und \(n\) wieder die Populationsgröße) ist. Jensen's Algorithmus wurde zehn Jahre später nochmal geändert publiziert weil das Original nicht mit gleichen Werten der Zielfunktionen umgehen konnte. Jensen schreibt zwar das sei einfach, offensichtlich nicht für andere [FGP13]. Ein Jahr später gab es dann nochmal eine kleinere Modifikation um formal zu beweisen dass die Laufzeit-Komplexität wirklich der obigen Formel folgt [BS14].

Ich habe diesen Algorithmus jetzt implementiert und neue gegen alte Version verglichen. Als Benchmark verwende ich dennn DTLZ-1 Algorithmus [DTLZ05] wie Jensen im Original-Artikel [Jen03]. Ähnlich wie Jensen habe ich die Zeiten bis zu einer Populationsgröße von 2000 gemessen. Auch ich bekomme die größte Beschleunigung für eine niedrige Anzahl von Zielfunktionen. In den folgenden Grafiken ist die Y-Achse in Sekunden CPU-Zeit während die X-Achse die Populationsgröße beschreibt. Es gibt drei Grafiken für 2, 10 und 20 Zielfunktionen. Der Aufwand steigt mit der Anzahl der Zielfunktionen, die anderen (nicht gezeigten) Grafiken sind sehr ähnlich. Die Grafiken haben lineare Achsen, nicht wie bei Jensen doppelt logarithmisch. Alle Messungen beinhalten die Gesamtzeit die der Algorithmus braucht, nicht nur die Nicht-Dominanz-Sortierung, also z.B. auch die Zeit um die Evaluierungsfunktion zu berechnen. Diese Zeiten habe ich nicht separat gemessen.

Die folgende Grafik zeigt die erreichten Beschleunigungen. Bei Jensen waren für hohe Anzahl von Zielfunktionen die Zeiten für den neuen Algorithmus leicht größer. Diesen Effekt sehe ich nicht, vermutlich weil auch die Rechner die letzten 20 Jahre etwas schneller wurden (und damit der Grund-Overhead kleiner ist). Interessant ist, dass beginnend mit etwa 5 Zielfunktionen sich die Beschleunigung mit der Anzahl der Zielfunktionen nicht mehr ändert (das sind natürlich die relativen Werte im Vergleich zum \(O(n^2)\) Algorithmus). Dies passiert vermutlich deshalb weil beide Algorithmen keine weiteren Zielfunktionen vergleichen wenn etabliert ist dass zwei Lösungen einander nicht dominieren: Das ist der Fall wenn eine Lösung bei einer Zielfunktion einen höheren Wert hat während sie bei einer anderen Zielfunktion einen niedrigeren Wert hat. Weil nach einigen Optimierungsschritten die meisten Lösungen nicht-dominiert sind kann anscheinend die Nicht-Dominanz schnell berechnet werden ohne alle Zielfunktionen zu vergleichen.

[BS14]

Maxim Buzdalov and Anatoly Shalyto. A provably asymptotically fast version of the generalized Jensen algorithm for non-dominated sorting. In Thomas Bartz-Beielstein, Jürgen Branke, Bogdan Filipič, and Jim Smith, editors, Parallel Problem Solving from Nature – PPSN XIII, volume 8672 of Lecture Notes in Computer Science, pages 528–537. Ljubljana, Slovenia, September 2014.

[DJ14]

Kalyanmoy Deb and Himanshu Jain. An evolutionary many-objective optimization algorithm using reference-point-based nondominated sorting approach, part I: Solving problems with box constraints. IEEE Transactions on Evolutionary Computation, 18(4):577–601, August 2014.

[DPAM02] (1,2)

Kalyanmoy Deb, Amrit Pratap, Sameer Agarwal, and T. Meyarivan. A fast and elitist multiobjective genetic algorithm: NSGA-II. IEEE Transactions on Evolutionary Computation, 6(2):182–197, April 2002.

[DTLZ05]

Kalyanmoy Deb, Lothar Thiele, Marco Laumanns, and Eckart Zitzler. Scalable test problems for evolutionary multiobjective optimization. In Ajith Abraham, Lakhmi Jain, and Robert Goldberg, editors, Evolutionary Multiobjective Optimization, Theoretical Advances and Applications, Advanced Information and Knowledge Processing, pages 105–145. Springer, 2005.

[FGP13]

Félix-Antoine Fortin, Simon Grenier, and Marc Parizeau. Generalizing the improved run-time complexity algorithm for non-dominated sorting. In Christian Blum, editor, Genetic and Evolutionary Computation GECCO 2013, pages 615–622. Amsterdam, The Netherlands, July 2013.

[JD14]

Himanshu Jain and Kalyanmoy Deb. An evolutionary many-objective optimization algorithm using reference-point-based nondominated sorting approach, part II: Handling constraints and extending to an adaptive approach. IEEE Transactions on Evolutionary Computation, 18(4):602–622, August 2014.

[Jen03] (1,2)

Mikkel T. Jensen. Reducing the run-time complexity of multiobjective EAs: The NSGA-II and other algorithms. IEEE Transactions on Evolutionary Computation, 7(5):503–515, October 2003.

Dynamisches DNS mit Bind funktioniert nicht mehr


Im Jahr 2021 hatte ich ein dynamisches DNS-Service mit dem ISC Bind DNS-Server aufgesetzt und das Setup in einem Blog Artikel gepostet [1].

Vor kurzem (in der zweiten Hälfte von 2024) haben die DNS Updates plötzlich nicht mehr funktioniert. In der Konfiguration hatte ich das Public Key Verfahren namens SIG(0) verwendet. Dieses wird schon lange in Bind unterstützt und es ist auch dokumentiert dass es funktioniertin der letzten Version.

Plötzlich wurden alles DNS Update Anfragen mit einem REFUSED Status zurückgewiesen. Selbst mit aufgedrehtem Debugging mit -d 15 bekam ich keine weiteren Informationen, auch nicht wenn alle Logs auf Standard Error umgeleitet wurden mit der Option -g.

Es stellte sich heraus dass das Public Key Verfahren SIG(0) wegen eines "Denial of Service" Angriffsvektors, der in CVE-2024-1975 dokumentiert ist, entfernt worden war. DNS updates mit TSIG keys funktionieren noch. Leider sah es so aus als ob das nicht dokumentiert ist und es kamen auch keine sinnvollen Logmeldungen, daher habe ich einen Bug report gemacht.

Wie sich dann herausstellte hätte ich in die Dokumentation von 9.18 – die Version die von Debian bookworm paketiert ist – schauen müssen. Diese dokumentiert dass SIG(0) entfernt wurde wegen CVE-2024-1975.

Es sieht so aus dass spätere Versionen von Bind (z.B. 9.20.6 und höher) wieder SIG(0) unterstützen, mit einem Quota-Mechanismus der einen Denial of Service verhindert. Aber für frühere Versionen besteht der "Fix" darin, SIG(0) komplett zu deaktivieren.

Modellieren einer Drahtantenne mit Isolation


[Update 2024-08-20: Korrektur der Formel zu L und äquivalentem Radius]

[Update 2024-09-19: Korrektur Literaturangabe Artikel von Stearns, Ergänzung zum Antenna Book [5]]

Für mein Antennen-Modellierungsprogramm pymininec (das eine Reimplementierung des original Mininec Basic-Codes darstellt) habe ich mich damit beschäftigt, wie man isolierte Drähte in Antennen modellieren kann. Ich hatte Roy Lewallen, W7EL, den Autor von EZNEC gefragt was er in EZNEC verwendet und einen Hinweis auf ein Paper von J. H. Richmond [1] erhalten, das das erste Paper war, welches einen Algorithmus für isolierte Drähte vorgeschlagen und auch implementiert hat. Der Algorithmus wurde von Jerry McCormack in seiner Masterarbeit [2] implementiert und später in einem Report [3] (dieser ist fast identisch mit der Masterarbeit bis auf neueren Fortran Code und einen anderen Autor) wiederveröffentlicht. Der original Fortran Code ist nicht nur in den obigen Reporten abgedruckt, sondern auch heute noch verfügbar auf Ray L. Cross' Seite zum "Antenna Scatterers Analysis Program" ASAP. Die Subroutine "DSHELL" implementiert den Algorithmus für isolierte Drähte. Mit einigen Modifikationen konnte ich diesen Code auch heute noch ausführen. Großer Dank gebührt an dieser Stelle Roy, W7EL der mir diese Infos zugänglich machte.

Um meine Implementierung in pymininec gegen gemessene Daten zu testen habe ich ein Paper von David Lamensdorf [4] ausgegraben wo der Effekt von Isolation wirklich mal gemessen wurde. Weil zu dieser Zeit der Einfluss von Isolation nur mit einem unendlichen Zylinder theoretisch modelliert werden konnte verwendete das Experiment Isolation die über das Ende des Drahtes hinausragte bis keine Änderung der gemessenen Parameter mehr beobachtet wurde.

Experimente mit den Formeln von Richmond [1] zeigten mir dass die resultierende Impedanzmatrix schlecht konditioniert ist: Für kurze Drahtsegmente erzeugen die Formeln sehr große absolute Werte die zur Hauptdiagonale addiert und von den beiden Nebendiagonalen (die Matrix ist symmetrisch, die beiden Nebendiagonalen sind für die obere und untere Dreiecksmatrix ident) subtrahiert werden. Weil diese Werte während der Matrix-Inversion subtrahiert werden, kommt es zu katastrophalen Auslöschungseffekten, was die Ergebnisse dominiert.

Also habe ich weiter nachgeforscht und bin auf ein Paper [5] und eine Präsentation [6] von Steve Stearns, K6OIK gestoßen, die Formeln für die verteilte Induktivität und einen äquivalenten Radius angibt mit welchen sich Isolation modellieren lässt. Seine Präsentation hat auch eine gute Übersicht über unterschiedliche Ansätze zur Modellierung von Isolation mit Programmen die dies nicht unterstützen (wie die original NEC-2 Implementierung oder das original Mininec):

\begin{align*} a_e &= a \left(\frac{b}{a}\right)^{\left(1- \frac{1}{\varepsilon_r}\right)} \\ L &= \frac{\mu_0}{2\pi}\left(1-\frac{1}{\varepsilon_r} \right)\log\left(\frac{b}{a}\right) \\ \sigma_e &= \sigma\left(\frac{a}{b}\right)^{2\left(1-\frac{1}{\varepsilon_r}\right)} \\ \end{align*}

Hier ist \(a\) der Original-Radius des Drahtes, \(b\) der Radius mit Isolation, \(\sigma\) ist die Elektrische Leitfähigkeit des Drahtes, \(\varepsilon_r\) ist die relative Permittivität der Isolation, \(a_e\) ist der äquivalente Radius und \(\sigma_e\) die äquivalente Permittivität die verwendet wird um den Effekt des veränderten Radius bei der Berechnung des Skin-Effekts aufzuheben. Die Induktivität \(L\) ist die Induktivität pro Länge des isolierten Drahtes (oder Drahtsegments).

Dann habe ich ein Paper gesucht das die Formeln von Steve Stearns bestätigt und habe ein noch älteres Paper von Tai Tsun Wu [7] gefunden das exakt die gleichen Formeln für den äquivalenten Radius \(a_e\) und für die verteilte Impedanz \(L\) angibt.

Die Formel für die äquivalente Permittivität wird nur verwendet um den Effekt des äquivalenten Radius auf die Berechnung des Skin Effekts aufzuheben. In einem früheren Blog Post [8] hatte ich eine Formel für den Skin Effekt angegeben. In dieser Formel taucht die Elektrische Leitfähigkeit \(\sigma\) als ihr Kehrwert \(\rho\) auf, der spezifische Widerstand. Die Quadratwurzel aus \(\sigma\) wird immer als Produkt mit dem Radius verwendet. Daher können wir den Effekt der Radius-Änderung durch Multiplikation von \(\sigma\) mit dem Kehrbruch des Faktors für den äquivalenten Radius multiplizieren. Wir bekommen:

\begin{align*} \sqrt{\sigma}a &= \sqrt{\sigma_e}a\left(\frac{b}{a}\right)^{\left (1-\frac{1}{\varepsilon_r}\right)} \\ \frac{\sigma_e}{\sigma} &= \frac{1}{\left(\frac{b}{a}\right)^{2\left (1-\frac{1}{\varepsilon_r}\right)}} = \left(\frac{a}{b}\right)^{2\left(1-\frac{1}{\varepsilon_r}\right)} \\ \end{align*}

In pymininec brauche ich allerdings den äquivalenten Radius bei der Skin Effekt Berechnung nicht zu korrigieren da ich dort einfach den Original-Radius verwenden kann.

In den folgenden drei Grafiken vergleiche ich diverse Implementierungen von isolierten Drähten mit den Messungen von Lamensdorf [4]. Der Draht hat einen Durchmesser (nicht Radius) von 0.25 Zoll bei einer Frequenz von 600MHz. Ich verwende nur die Messungen mit \(\varepsilon_r = 3.2\), die höheren Permittivitäten sind in der Praxis unrealistisch hoch und die Ergebnisse dürften durch das unrealistische experimentelle Setup abweichen. Ich plotte den Real- (elektrische Leitfähigkeit, auch Konduktivität) und Imaginärteil (Suszeptanz) der Admittanz (Kehrbruch der Impedanz). Die drei Grafiken sind:

  • Ohne Isolation

  • Durchmesser des Drahtes mit Isolation 0.375 Zoll (Isolation 0.125 Zoll)

  • Durchmesser des Drahtes mit Isolation 0.5 Zoll (Isolation 0.25 Zoll)

Die unterschiedlichen Kurven in den Plots sind:

  • ASAP mit zwei, vier, acht und 16 segmenten für eine Dipolhälfte, die Kurven sind mit 'AS02', 'AS04', 'AS08' and 'AS16' markiert, es werden nicht alle Kurven in jedem Plot gezeigt.

  • 4NEC2 das nach Stearns [6] die Formel von Cebik [9] implementiert, diese Kurven sind mit '4N2' markiert

  • NEC-2 mit Wu's [7] Formel mit äquivalentem Radius und verteilter Induktivität sowie mit Stearns [6] Formel für das äquivalente \(\sigma\) markiert als 'NEC'

  • Meine pymininec Implementierung verwendet Wu's [7] Formel mit äquivalentem Radius und verteilter Induktivität (aber nicht die \(\sigma\) Korrektur weil ich für die Skin-Effekt Berechnung den Original-Radius verwenden kann), markiert als 'mini'

  • Die Messungen von Lamensdorf [4] sind die blauen und orangen Blasen (jeweils für den Real- und Imaginärteil)

  • In den Grafiken mit Isolation zeige ich auch die Resultate von EZNEC, die Kurven sind ausgegraut und können in der Legende auf der rechten Seite eingeschaltet werden

Die Formeln von Cebik [9] und Wu [7] sind sehr ähnlich, die Kurven '4N2' und 'NEC' sind fast identisch.

Die EZNEC Kurven und einige der ASAP Kurven sind ausgegraut, weil sie numerische Probleme aufgrund der schlecht konditionierte Impedanzmatrix durch die Formeln von Richmond zeigen. Es ist interessant zu sehen, dass EZNEC auch numerische Probleme bei kleinen Segmenten hat. Die ausgegrauten Kurven können mit Klick in die Legende auf der rechten Seite eingeschaltet werden.

Die Segmentierung in allen Beispielen sind 21 Segmente für einen Draht. Das bedeutet recht kurze Segmente für die kleineren Drahtlängen. Dies triggert eine Warnung zu kurzen Segmenten in EZNEC für die ersten drei Punkte (bis zu einschließlich \(\frac{4.2}{32} = 0.06558125\) Wellenlängen) aber nicht für den klar numerisch ungültigen 6. Wert in der ersten Grafik mit Isolation und für die numerisch instabilen Werte in der zweiten Grafik mit Isolation.

Es sei darauf hingewiesen dass die Messungen von Lamensdorf [4] an Monopol-Antennen durchgeführt wurden. Ich simuliere Dipole. Die Impedanz eines Monopols ist die Hälfte von der eines Dipols, daher multipliziere ich die Admittanz mit dem Faktor zwei. Alle Admittanzen sind mit milli-Siemens, früher auch milli-mhos (wobei mho ohm rückwärts ist) mit dem Symbol ℧ zur Zeit des Lamensdorf [4] Papers.

Plotting Antenna Pattern


Ich bin der Autor eines Antennen-Plot-Programms namens plot-antenna. Es ist in Python geschrieben und hat Backends für Matplotlib und Plotly. Während ich das hier schreibe kann die Version auf pypi noch nicht unterschiedliche Frequenz-Auflösungen für Gewinn und Impedanz-Plots. Das Matplotlib Backend ist besser für die Erzeugung von Grafiken für gedruckte Dokumentation während Plotly sich eignet, interaktive Grafiken für das Web zu erzeugen. Die Doku von plot-antenna enthält einige Matplotlib Beispiele daher gehe ich hier mehr ins Detail was das Plotly Backend betrifft.

Vor kurzem habe ich Unterstützung für das "Antenna Scatterers Analysis Program" ASAP eingebaut. Das ist ein Antennen-Simulationsprogramm aus einer Zeit vor dem Fortran-77 Standard aus dem Jahr 1974. Es ist heute noch interessant weil es eine Formulierung der Momentenmethode (method of moments) verwendet, die sich sowohl von NEC [3] als auch Mininec [4] unterscheidet. Und ASAP hat zuerst eine Simulation von isolierten Drähten in einer Antenne unterstützt.

Im Folgenden verwende ich eine 3-Elemente Yagi-Uda Antenne die sich in der Beispiel-Sektion von ASAP findet. Die verwendete Eingabedatei exportiert Gewinn-Daten für 280 MHz bis 305 MHz in 5 MHz Schritten. Zusätzlich exportiert es Impedanzwerte in 1 MHz Schritten.

Für die Erstellung von Gewinn-Grafiken unterstützt plot-antenna unterschiedliche Skalierungsmethoden. Der Standard ist ARRL Skalierung [5] die ich auch im folgenden Beispiel verwende.

Im 3D-Plot kann man die Frequenz auf der rechten Seite in der Legende der Grafik umschalten. Ein Klick auf eine Frequenz ändert das Display auf die Gewinn-Grafik für diese Frequenz. Zusätzlich kann man beim Drüberfahren mit der Maus die Gewinnwerte an unterschiedlichen Positionen sehen. Es ist möglich mit dem Maus-Rad zu zoomen und den Plot durch klicken und ziehen zu drehen.

Natürlich kann plot-antenna auch konventionelle 2D-Plots für Azimuth und Elevation erzeugen:

Im 2D-Display kann man wieder in der Legende auf der rechten Seite die Plots für verschiedene Frequenzen einblenden, im 2D-Plot auch mehrere gleichzeitig. Damit ist es einfacher, Gewinnwerte zu vergleichen als im 3D-Plot wo immer nur der Gewinn für eine Frequenz gleichzeitig sichtbar ist. Ähnlich wie im 3D-Plot kann man die Gewinnwerte beim Drüberfahren mit der Maus sehen. Es ist auch hier möglich zu zoomen (obwohl das in einem Polarplot nicht sehr sinnvoll ist) und die Anzeige mit den "Home" Knopf im Menu auf die Standardansicht zurückzusetzen.

Oft ist es nützlich das Stehwellenverhältnis (voltage standing wave ratio, VSWR) und die Antennen-Impedanz anzeigen zu lassen. Das geht mit dem VSWR-Plot. In der folgenden Grafik habe ich die Anzeige der Impedanz mit der Option --swr-show-impedance eingeschaltet. Die vertikale grüne Linie zeigt die Position des minimalen Stehwellenverhältnisses an. Auch hier sehen wir die geplotteten Werte beim Drüberfahren mit der Maus. Wir können heranzoomen indem wir ein Rechteck mit der Maus selektieren oder mit dem +/- Knöpfen im Menu. Auch hier kann die Standardansicht mit dem "Home" Knopf wieder hergestellt werden.

Nicht zuletzt kann die Antennen-Geometrie angezeigt werden. Dies ist wieder ein 3D-Plot wo wir die Position der Antennen-Elemente beim Drüberfahren mit der Maus sehen können. In der Legende auf der rechten Seite kann man z.B. die Sichtbarkeit des Speisepunkts (in Orange) ausschalten. Andere Antennen können z.B. zusätzlich eine Ground-Plane (Boden) anzeigen oder Impedanz-Lasten auf der Antenne. Die Geometrie-Ansicht ist sehr nützlich um zu checken ob eine Antenne korrekt modelliert wurde (z.B. ob der Speisepunkt an der korrekten Position sitzt).

Das Programm plot-antenna unterstützt derzeit Ausgabe von ASAP, vom Original Mininec (das in Basic geschrieben ist), von meiner Python-Version pymininec, und der Ausgabe von NEC-2. Das Format wird automatisch erkannt. Zusätzlich kann mit dem Begleitprogramm plot-eznec der Gewinn einer mit EZNEC simulierten Antenne angezeigt werden. Während ich das hier schreibe gibt es eine Version die auch VSWR-Plots für EZNEC Daten anzeigen kann indem die beim Plotten von VSWR-Daten mit EZNEC erzeugte Datei LastZ.txt verwendet wird.

Skin Effekt Belag und Impedanz: Update


In meinem letzten Post [1] habe ich mich gefragt, warum meine Implementierung von Skin Effekt Belag (Drähte mit einem endlichen Widerstand) nicht mit den Resultaten von NEC-2 übereinstimmten. Es stellt sich heraus dass ich falsch lag in der Annahme dass Skin-Effekt Belag nur eine rein ohmsche Komponente hat, durch den Skin Effekt ergibt sich in Wirklichkeit eine relativ große reaktive Komponente.

Hinweis: Dieser Artikel enthält ziemlich viel Mathematik, vielleicht möchten Sie ja ans Ende scrollen und checken wie die neue Implementierung in pymininec sich verglichen mit meinem letzten Artikel verhält wo nur ein rein ohmscher Widerstand angenommen wurde.

Der englische Wikipedia Artikel zum skin effect enthält die korrekte Formel:

\begin{equation*} \newcommand{\Int}{{\mathrm\scriptscriptstyle int}} \newcommand{\ber}{\mathop{\mathrm{ber}}\nolimits} \newcommand{\bei}{\mathop{\mathrm{bei}}\nolimits} \end{equation*}
\begin{equation*} Z_\Int = \frac{k\rho}{2\pi r}\frac{J_0 (kr)}{J_1 (kr)} \end{equation*}

wobei

\begin{equation*} k = \sqrt{\frac{-j\omega\mu}{\rho}} \end{equation*}

und \(r\) ist der Draht-Radius, \(J_v\) sind die Bessel-Funktionen der ersten Gattung mit der Ordnung \(v\). \(Z_\Int\) ist die Impedanz pro Längeneinheit des Drahtes.

NEC-2 verwendet eine andere Formel [2] (S.75):

\begin{equation*} Z_i = \frac{j\Delta_i}{a_i} \sqrt{\frac{\omega\mu}{2\pi\sigma}} \left[\frac{\ber (q) + j\bei (q)} {\ber^\prime(q)+ j\bei^\prime(q)}\right] \end{equation*}

mit

\begin{equation*} q = (\omega\mu\sigma)^{1/2} a_i \end{equation*}

und \(a_i\) ist der Draht-Radius, \(\Delta_i\) die Länge des Drahtsegments, \(\sigma\) die Elektrische Leitfähigkeit des Drahtes und \(\ber\) und \(\bei\) die Kelvin-Funktionen. Im folgenden werde ich wieder \(r\) für den Draht-Radius, und nicht \(a_i\) verwenden. Nachdem \(\ber^\prime\) die Ableitung von \(\ber\) ist, können wir \(\ber\) als \(\ber_0\) (Ordnung 0) und \(\ber^\prime\) als \(\ber_1\) (Ordnung 1) schreiben und analog für \(\bei\).

Im folgenden bin ich etwas ungenau in der Verwendung der magnetischen Permeabilität im Freiraum \(\mu_0\) vs. der Permeabilität eines Drahtes \(\mu\), für nicht ferromagnetische Drähte sind diese (fast) identisch: \(\mu = \mu_r * \mu_0\) mit \(\mu_r\approx 1\) für nicht ferromagnetische Materialien.

Der NEC-2 code verwendet die Fortran Funktion ZINT mit zwei Parametern, der Leitfähigkeit multipliziert mit der Wellenlänge \(\lambda\) (Parameter SIGL) und der Radius geteilt durch die Wellenlänge \(\lambda\) (Parameter ROLAM). Neben der Approximation der Kelvin-Funktionen (der Term mit dem Bruch aus Kelvin-Funktionen ist BR1), berechnet ZINT:

X = SQRT (TPCMU * SIGL) * ROLAM
ZINT = FJ * SQRT (CMOTP / SIGL) * BR1 / ROLAM

mit TPCMU = 2368.705, CMOTP=60.0, und FJ ist \(j\). Das berechnete X wird der Berechnung der Kelvin-Funktionen übergeben mit BR1 als Resultat des Bruch-Terms. Das berechnete ZINT enthält nicht die Segment-Länge, ist also äquivalent mit der Wikipedia Definition in pro Längeneinheit. Mit den Reverse-Engineerten magischen Zahlen

CMOTP \(\approx \frac{\mu_0 c}{2\pi}\)

TPCMU \(\approx 2\pi c\mu_0\)

(\(c\) ist die Lichtgeschwindigkeit) bekommen wir für X aus dem Code (was wir jetzt \(q\) nennen um mit der obigen Formel aus [2] konsistent zu sein):

\begin{align*} q &= \frac{r}{\lambda}\sqrt{2368.705\sigma\lambda} \\ &= r\sqrt{\frac{2368.705\sigma}{\lambda}} \\ &\approx r\sqrt{\frac{2\pi c\mu_0\sigma}{\lambda}} \\ &= r\sqrt{\frac{2\pi c\mu_0\sigma f}{c}} \\ &= r\sqrt{\frac{2\pi \mu_0\sigma \omega}{2\pi}} \\ &= r\sqrt{\omega\mu_0\sigma} \\ \end{align*}

Was mit der obigen Definition von \(q\) in [2] übereinstimmt.

Für \(Z_\Int\) der Fortran-Implementierung bekommen wir:

\begin{align*} Z_\Int &= \frac{j}{r} \sqrt{\frac{\mu_0 c}{2\pi\sigma\lambda}} \left[\frac{\ber_0 (q) + j\bei_0 (q)} {\ber_1(q)+ j\bei_1(q)}\right] \\ &= \frac{j}{r} \sqrt{\frac{\mu_0 c f}{2\pi\sigma c}} \left[\frac{\ber_0 (q) + j\bei_0 (q)} {\ber_1(q)+ j\bei_1(q)}\right] \\ &= \frac{j}{r} \sqrt{\frac{\mu_0 f}{2\pi\sigma}} \left[\frac{\ber_0 (q) + j\bei_0 (q)} {\ber_1(q)+ j\bei_1(q)}\right] \\ &= \frac{j}{r} \sqrt{\frac{\mu_0 \omega}{2\pi 2\pi\sigma}} \left[\frac{\ber_0 (q) + j\bei_0 (q)} {\ber_1(q)+ j\bei_1(q)}\right] \\ &= \frac{j}{2\pi r} \sqrt{\frac{\mu_0 \omega}{\sigma}} \left[\frac{\ber_0 (q) + j\bei_0 (q)} {\ber_1(q)+ j\bei_1(q)}\right] \\ \end{align*}

Diese Formel stimmt NICHT mit der Formel für \(Z_i\) aus [2] weiter oben überein. Es sieht so aus als ob die Formel aus [2] (S. 75) die Frequenz \(f\) statt der Kreisfrequenz \(\omega\) unter der Wurzel haben sollte. Die aus der NEC-2 Implementierung abgeleitete Formel ist aber konsistent mit der obigen Formel aus Wikipedia.

Der Term vor dem Bruch mit den Kelvin-Funktionen unterscheidet sich um einen Faktor von der Wikipedia Formel (Ich lasse \(\Delta_i\) weg und habe so eine Formel pro Längeneinheit wie in Wikipedia). Ausserdem gilt \(\rho=1/\sigma\):

Aus dem Code abgeleitet:

\begin{equation*} \frac{j}{2\pi r}\sqrt{\frac{\omega\mu}{\sigma}} \end{equation*}

Aus Wikipedia:

\begin{equation*} \frac{k\rho}{2\pi r} = \sqrt{\frac{-j\omega\mu}{\rho}}\frac{\rho}{2\pi r} = \frac{1}{2\pi r}\sqrt{-j\omega\mu\rho} = \frac{1}{2\pi r}\sqrt{\frac{-j\omega\mu}{\sigma}} \end{equation*}

Das ist ein Faktor von

\begin{equation*} \frac{\sqrt{-j}}{j} = -\frac{1+j}{\sqrt{2}} \end{equation*}

weil \(\sqrt{-j} = \frac{1-j}{\sqrt{2}}\)

Für die Kelvin-Funktionen gilt die folgende Äquivalenz zu den Bessel Funktionen [3] (10.61, Kelvin Functions):

\begin{equation*} \ber_v x + j \bei_v x = J_v \left(xe^{3\pi j/4}\right) = e^{v\pi j} J_v \left(xe^{-\pi j / 4}\right) \end{equation*}

Mit den Spezialfällen der Ordnung 0 und 1:

\begin{align*} \ber_0 x + j \bei_0 x &= e^{0} J_0 \left(xe^{-\pi j / 4}\right) \\ &= J_0 \left(\frac{1-j}{\sqrt{2}}x\right) \\ \ber_1 x + j \bei_1 x &= e^{j\pi} J_1 \left(xe^{-\pi j / 4}\right) \\ &= -J_1 \left(\frac{1-j}{\sqrt{2}}x\right) \\ \end{align*}

Die Wikipedia Formel übergibt \(kr\) an die Bessel-Funktionen:

\begin{equation*} kr = r\sqrt{\frac{-j\omega\mu}{\rho}} = r\sqrt{-j\omega\mu\sigma} \end{equation*}

während NEC-2 folgendes übergibt:

\begin{equation*} q = r\sqrt{\omega\mu\sigma} \end{equation*}

Diese unterscheiden sich um den Faktor \(\sqrt{-j}\). Das bedeutet wir übergeben einen Faktor um \(\sqrt{2}\) zu groß an die Bessel-Funktionen. Daraus ergibt sich wegen der Symmetrie der Bessel Funktionen und er Tatsache dass die Funktionen im Nenner die Ableitung der Funktionen im Zähler sind, dass das Ergebnis um einen Faktor von \(\sqrt{2}\) zu groß ist was den Faktor im ersten Term ausgleicht.

Nach der Implementierung der neuen Formel in pymininec verhält sich der reaktive Teil der Impedanz ähnlich wie bei NEC-2 und der ohmsche Widerstand ist noch näher an den Ergebnissen von NEC als vorher.

Auch wenn man den Imaginärteil (die Reaktanz) plottet sind die Resultate jetzt wie erwartet. Die Werte von MMANA sind immer noch überraschend, eine mögliche Ursache ist dass ich MMANA unter WINE in Linux verwendet habe, die ohmschen Werte der Impedanz die von MMANA berechnet wurden schauen allerdings korrekt aus.

Skin Effekt Belag und Impedanz


Vor kurzem habe ich für pymininec, meine Re-Implementierung des Mininec Antennen-Modellierungs-Codes Skin Effekt Belag (Drähte mit einem endlichen Widerstand) implementiert. Das Original-Mininec hatte dieses Feature nicht.

Um die Implementierung zu verifizieren entschied ich einer uralten Publikation von R. P. Haviland, W4MB [1] zu folgen wo er unter anderem den Effekt von Draht-Widerstand auf die Speisepunkt-Impedanz von Antennen untersucht. Er hat zwei Grafiken wo der Real- und Imaginärteil der Speisepunkt-Impedanz eines Dipols gegen das Verhältnis von Länge zu Durchmesser des Drahtes geplottet wird. Er erwähnt zwar Mininec im Text aber bei genauerer Analyse ist es nicht ganz klar ob für die Plots der Speisepunkt-Impedanz wirklich Mininec oder etwas anderes verwendet wurde. Ich habe versucht, seine Grafiken mit PyNEC, einem Python wrapper für eine C++ Implementierung von NEC-2, und meiner pymininec Implementierung von Mininec zu reproduzieren. Die Resultate von Haviland haben die gleiche Tendenz wie die folgenden NEC Resultate aber sind nicht gleich.

Die X-Achse dieser Grafiken ist logarithmisch und die Drähte werden nach rechts dicker (die X-Achse ist das Verhältnis von Länge zu Durchmesser des Drahtes). In allen Fällen vergleichen wir Drähte ohne Widerstand mit Kupferdrähten. "Resistance" ist der (ohmsche) Widerstand (Realteil der Impedanz) und "Reactance" ist der Blindwiderstand (Reaktanz), der Imaginärteil der Impedanz.

Der ohmsche Widerstand ist nicht überraschend: Der Realteil der Antennen-Impedanz steigt mit dem Draht-Widerstand (weil der Draht dünner wird).

Der interessante Teil ist der Imaginärteil (die Reaktanz) der Antenne. Das NEC-Resultat besagt dass die Reaktanz mit dem Widerstand des Drahtes steigt (nach links) während meine pymininec Implementierung ein leichtes Sinken der Reaktanz vorraussagt.

Um das genauer zu untersuchen habe ich mich entschlossen den spezifischen Widerstand des Drahtes ohne Modifikation des Durchmessers zu ändern um zu zeigen dass der Effekt unabhängig vom Drahtdurchmesser ist. Der Radius ist im Folgenden 66µm bei einer Wellenlänge von 1m, \(\frac{l}{d}\approx 7576\).

Zusätzlich zu PyNEC und pymininec habe ich auch Resultate für MMANA-GAL und EZNEC in die Grafik aufgenommen. MMANA basiert ursprünglich auf Mininec. Die EZNEC Resultate stimmen ziemlich genau mit PyNEC überein (beides verwendet den gleichen Modellierungscode, einmal in Fortran, einmal in C++) aber MMANA hält die Reaktanz komplett konstant bei \(42.63\Omega\) über alle spezifischen Widerstände des Drahtes. Die interessantesten Werte des spezifischen Widerstands sind ganz links die ersten drei großen Punkte, der erste ist ein idealer Draht (spezifischer Widerstand 0) gefolgt von Kupfer und Alu.

Ich habe derzeit keine Ahnung welche Resultate der physischen Wirklichkeit am nächsten kommen.

Schwebung


Musiker kennen den Effekt beim Stimmen von Instrumenten: Wenn zwei Instrumente nicht ganz den selben Ton spielen klingt das als ob es eine periodische Variation der Lautstärke gibt. Die Periode wird größer je besser die beiden Instrumente gestimmt sind und stoppt wenn beide den selben Ton spielen. Wir nennen diesen Effekt "Schwebung".

Funkamateure kennen den "umgekehrten" Fall dieses Effekts von der Amplitudenmodulation einer Radio-Frequenz mit einer Sinusschwingung im Audio-Bereich: Das Resultat sind zwei Radio Frequenzen, eine um die Audio-Frequenz niedriger als die Modulationsfrequenz, eine um die Audio Frequenz höher.

Amplitudenmodulation ist eine einfache Multiplikation: Wir multiplizieren die Radiofrequenz mit dem Audio, wir haben:

$$m(t) = A(t) \cdot\cos(2\pi f t)$$

Wobei $t$ die Zeit ist, $m(t)$ das amplitudenmodulierte Signal, $\cos(2\pi f_r t)$ ist das Radiofrequenz-Signal mit der Frequenz $f_r$ und $A(t)$ ist das Audiosignal. Für eine einfache Audio Sinusschwingung bekommen wir:

$$m(t) = \cos(2\pi f_a t)\cdot\cos(2\pi f_r t)$$

wobei $f_a$ die Audio-Frequenz ist.

Wir möchten jetzt zeigen, dass beide Effekte das gleiche Ding sind. Wir brauchen keine Radiofrequenzen, wir können das gleiche einfach im Audio-Spektrum tun.

Aus Wikipedia bekommen wir eine trigonometrische Identität über Produkte von Winkelfunktionen, daraus nehmen wir die für zwei Cosinus Funktionen:

$$\displaystyle\cos\theta \cos\phi = \frac{\cos(\theta-\phi) + \cos(\theta+\phi)}{2}$$

Wenn wir die auf unser Modulation-Beispiel anwenden bekommen wir:

$$\displaystyle \begin{align} \ \cos(2\pi f_a t) \cos(2\pi f_r t) & = \frac{\cos(2\pi f_r t-2\pi f_a t) + \cos(2\pi f_r t+2\pi f_a t)}{2} \\ & = \frac{\cos(2\pi(f_r-f_a)t)) + \cos(2\pi(f_r+f_a)t)}{2} \\ \end{align} $$

Die Multiplikation von zwei Cosinus-Schwingungen mit unterschiedlichen Frequenzen resultiert in der Summe von zwei Cosinus-Schwingungen, eine davon die Differenz der beiden ursprünglichen Frequenzen, die andere die Summe.

Egal wie wir das modulierte Signal erzeugen, durch Multiplikation von zwei Signalen oder durch Addition von zwei Signalen, wir bekommen das selbe Resultat.

Im folgenden Beispiel multiplizieren wir ein 3Hz Signal (die Schwebungsfrequenz) mit einem 440Hz Signal (Das ist die Note "A" in der Musik). Wir bekommen wie erwartet ein moduliertes Signal das die Lautstärke mit der Schwebungsfrequenz ändert.

In [1]:
import numpy as np
# Drei Sekunden mit 18000 Abtastungen
t = np.linspace (0, 3, 18000)
# Produkt von einer 3Hz Schwingung und einem 440Hz Ton
y = np.cos (2*np.pi*3*t) * np.cos (2*np.pi*440*t)
# Wir plotten nur 1/9 der drei Sekunden = 1/3s = eine Periode des 3Hz Signals
limit = int (2000)
In [2]:
import plotly.graph_objs as go
import plotly.io as pio
from plotly_fix_fn import setup_plotly
setup_plotly ('content/2024/05/plotly', '11-de')
show_opt = dict (include_plotlyjs = 'directory')

fig = go.Figure ()
fig.add_trace (go.Scatter (x = t [:limit], y = y [:limit]))
fig.update_layout (title = "Moduliertes Signal", xaxis_title = "Zeit (s)", yaxis_title = "Amplitude")
fig.show (** show_opt)

Nun machen wir eine Fourier-Transformation des Resultats um das Frequenz-Spektrum zu bekommen. Wir sehen dass das Resultat der Multiplikation aus zwei Frequenzen besteht: Ein 3Hz unter 440Hz, eine um 3Hz darüber.

In [3]:
from numpy.fft import rfft, fftfreq
fft   = rfft (y, norm = 'forward')
freqs = fftfreq (len (y), d = 3/18000)
fig = go.Figure ()
fig.add_trace (go.Scatter (x = freqs [:5000], y = np.abs (fft [:5000])))
fig.update_layout (title = "Frequenz-Spektrum", xaxis_title = "Frequenz (Hz)")
fig.update_xaxes (range = [430, 450])
fig.show (** show_opt)

Wir können das selbe Resultat erzielen wenn wir einfach die beiden Frequenzen addieren (und das Ganze durch zwei teilen damit wir die selbe Amplitute haben)

In [4]:
y = 0.5 * (np.cos (2*np.pi*(440-3)*t) + np.cos (2*np.pi*(440+3)*t))
fig = go.Figure ()
fig.add_trace (go.Scatter (x = t [:limit], y = y [:limit]))
fig.update_layout (title = "Moduliertes Signal", xaxis_title = "Zeit (s)", yaxis_title = "Amplitude")
fig.show (** show_opt)

Schließlich verifizieren wir dass dieses Vorgehen das selbe Audiospektrum erzeugt wie vorher die Multiplikation, wir machen eine weitere Fourier-Transformation des neuen Signals:

In [5]:
fft   = rfft (y, norm='forward')
freqs = fftfreq (len (y), d = 3/18000)
fig = go.Figure ()
fig.add_trace (go.Scatter (x = freqs [:5000], y = np.abs (fft [:5000])))
fig.update_layout (title = "Frequenz-Spektrum", xaxis_title = "Frequenz (Hz)")
fig.update_xaxes (range = [430, 450])
fig.show (** show_opt)

Was wir also beim Stimmen von Instrumenten hören ist keine Illusion: Die Addition von zwei Schwingungen mit unterschiedlichen Frequenzen ist das selbe wie die Modulation eines Tons mit einer Schwebungsfrequenz.

Chipselect Aussetzer beim Schreiben auf SPI NOR des IMX-28


Für einen Kunden pflege ich eine Linux Umgebung (inclusive Kernel) für ein kundenspezifischen Board das den IMX-28 ARM Prozessor verwendet. Für den User Space verwende ich ELBE, das Embedded Linux Build Environment das es erlaubt, das User Space Dateisystem aus eine Debian Linux Release zu bauen. Bei der Migration von einer früheren Debian Version zum derzeitigen stabilen Release mit dem Code-Namen "Bookworm" brauchte es ein Kernel Upgrade weil Systemd einige Dinge benötigt die nicht in früheren Kernels enthalten sind.

Nach dem Upgrade von Kernel 4.14 auf 6.1 (beide eine longterm Support Version) funktionierte unser SPI NOR Flash nicht mehr. Das JFFS2 Dateisystem produzierte Fehlermeldungen die nahelegten dass wir ein Problem beim Schreiben in das NOR Flash hatten (Einige der Fehlermeldungen zeigten dass eine 0 erwartet aber 0xFF gelesen wurde was zum Schluss führte dass diese Bytes nicht geschrieben wurden).

Nach Überprüfung der Taktrate des SPI Busses beim Schreiben des Flash mit der Beobachtung dass sich diese nicht geändert hatte schaute ich mir die Signale des SPI Busses an.

Bild Aussetzer im Chip Select

Aussetzer im Chip Select bei der Übertragung

Bild Aussetzer im Chip Select höhere zeitliche Auflösung

Aussetzer im Chip Select bei der Übertragung: Höhere zeitliche Auflösung

In den Oszilloscop Bildern (Blau ist Chip Select, Gelb ist der SPI Takt) sieht man dass es einen kurzen Aussetzer im Chip Select gibt. Das hat den Effekt dass das "Page Program" Kommando an das NOR Flash frühzeitig beendet wird. Aus dem Bild mit der höheren zeitlichen Auflösung sieht man dass dieser Aussetzer mitten in einem Bit der Übertragung stattfindet. Die exakte Position variiert leicht von Übertragung zu Übertragung (Der Aussetzer trifft nicht jedesmal das selbe Bit aber er trifft das selbe Byte).

Weiter Experimente zeigten dass das Problem nur bei Übertragungen mit direktem Speicherzugriff (DMA) auftritt, nicht wenn kein DMA (sondern programmierter Input-Output, PIO) verwendet wurde. Der SPI Treiber optimiert Übertragungen: Nur längere Übertragungen werden mit DMA durchgeführt, kürzere Übertragungen verwenden PIO weil der DMA Overhead dann den Zeit-Gewinn überwiegt. Bei genauerer Inspektion der DMA Implementierung für den IMX-28 in drivers/dma/mxs-dma.c stellte sich heraus dass es eine Änderung bei den Flags des DMA Transfers gab, die alte Version hat:

if (flags & DMA_CTRL_ACK)
        ccw->bits |= CCW_WAIT4END;

Die neue Version hat:

if (flags & MXS_DMA_CTRL_WAIT4END)
        ccw->bits |= CCW_WAIT4END;

Das Bit CCW_WAIT4END (Übersetzt aus der IMX-28 Prozessor Referenz): "Der Wert eins gibt an dass der Kanal auf das Ende des Kommandosignalsvom APBH Device zum DMA wartet bevor ein neues DMA Kommando gestartet wird". Dieses Flag wird für den DMA Transfer zum NOR Flash gesetzt. Eine Besonderheit des IMX-28 ist dass auch Register Einstellungen über DMA übertragen werden können. Vor dem eigentlichen Speichertransfer finden ein Register Transfer statt der das IGNORE_CRC Bit im Prozessor SSP Register setzt. Aus der Prozessor Referenz zum IGNORE_CRC Bit (übersetzt): "Im SPI/SSI Modus: Wenn auf 1 gesetzt, setze den Chip Select (SSn) Pin zurück nachdem das Kommando ausgeführt wurde".

Was also passiert wenn das CCW_WAIT4END nicht gesetzt wird, ist dass der Prozessor das Chip-Select zu früh ausschaltet.

Die DMA Übertragung die vom SPI Treiber in drivers/spi/spi-mxs.c initiiert wird setzt das neue MXS_DMA_CTRL_WAIT4END nicht, so dass das flag CCW_WAIT4END für die Übertragung nicht gesetzt wird.

Der Fix ist damit offensichtlich, wir müssen nur dieses Flag bei der Initiierung der DMA-Übertragung im SPI Treiber in drivers/spi/spi-mxs.c setzen.