Forum: Offtopic Bruch approximieren


von Walter T. (nicolas)


Lesenswert?

Hallo zusammen,

ich habe einen Bruch int32_t Z, int32_t N, bei dem Zähler und/oder 
Nenner 32 Bit breit sind. Der Zähler und Nenner sind schon teilerfremd, 
d.h. der Bruch ist maximal gekürzt, und normalisiert, d.h. der Nenner 
ist immer größer als 0.

Jetzt suche ich den Bruch int16_t z, int16_t n, bei dem Zähler und 
Nenner kleiner als 16 Bit (vorzeichenbehaftet) sind, der den Bruch Z/N 
bestmöglich approximiert.

Erster naiver Ansatz: n = INT16_MAX; aber es lässt sich für viele Brüche 
Z/N zeigen, dass es bessere Approximationen gibt.

Jetzt bin ich bei vollständiger Enumeration des Suchraums, d.h. ich gehe 
den für den Nenner alle Zahlen von 1 bis INT16_MAX durch, prüfe ob der 
zugehörige Zähler (und der Zähler plus 1) einen kleineren Fehler als die 
bisherige Approximation liefert.

Das Ganze muss nur dreimal beim Start meines STM32F446 laufen, die 
Laufzeit ist also nicht wahnsinnig kritisch, aber dominiert die 
Startzeit leider.

Gibt es einen intelligenteren Algorithmus?

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Lesenswert?

Euklidischer Algorithmus? Ich bin mir gerade nicht sicher, ob der immer 
das Optimum liefert, aber auf jeden Fall sehr schnell gute Näherungen.

von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Euklidischer Algorithmus?

Den kenne ich nur für die Ermittlung des größten gemeinsamen Teilers. 
Das ist auch tatsächlich ein vorab-Schritt, um das hier sicherzustellen:

Walter T. schrieb:
> Der Zähler und Nenner sind schon teilerfremd,
> d.h. der Bruch ist maximal gekürzt, und normalisiert, d.h. der Nenner
> ist immer größer als 0.

Gibt es da eine Erweiterung zum Approximieren?
(Ganzzahl-Arithmetik und Restklassenringe sind meine Achillesferse.)

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Lesenswert?

Ich hatte vergessen, auch das Stichwort "Kettenbruch" zu erwähnen. Diese
beiden Artikel helfen dir evtl. weiter:

  https://de.wikipedia.org/wiki/Kettenbruch#Beste_N%C3%A4herungen
  https://de.wikipedia.org/wiki/Euklidischer_Algorithmus#Euklidischer_Algorithmus_und_Kettenbruchzerlegung

von Peter D. (peda)


Lesenswert?

Du versuchst nun schon über sehr lange Zeit, haufenweise spezielle 
Würg-Arounds zu basteln, um float zu vermeiden. Das ist doch alles rein 
akademisch ohne jeden praktischen Nutzen. Hast Du wirklich soviel 
Langeweile?

Ich war früher auch mal militanter float Gegner, hab dann aber gemerkt, 
daß selbst ein AT89C51 @12MHz reichlich Power hat, damit der Benutzer 
nicht die geringsten Laufzeitunterschiede merkt.
Ich benutze inzwischen auch ausschließlich sprintf, sscanf, statt mir 
umständlich was selber zu basteln. Man braucht nur etwas 
Einarbeitungszeit in die Formatstrings, aber es lohnt sich. Man kann die 
Doku ja jederzeit im Web nachschlagen.

Walter T. schrieb:
> Das Ganze muss nur dreimal beim Start meines STM32F446 laufen, die
> Laufzeit ist also nicht wahnsinnig kritisch, aber dominiert die
> Startzeit leider.

Es gibt dafür 2 einfache Tricks:
1. Man startet die Mainloop sofort und läßt die Berechnung im 
Hintergrund laufen, z.B. ein Timerinterrupt führt kleine Häppchen davon 
aus.
2. Man läßt die Rechnung nur einmal laufen und speichert die Ergebnisse 
im EEPROM/Flash ab.

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
> Du versuchst nun schon über sehr lange Zeit, haufenweise spezielle
> Würg-Arounds zu basteln, um float zu vermeiden.

Float versuche ich nicht zu vermeiden. Float ist super. Mit float und 
double fühle ich mich zuhause.

Ich versuche double (möglichst, nicht komplett) zu vermeiden. Und an 
manchen Stellen ist es mir wichtig, dass der Fehler nicht anwächst. Da 
komme ich um Integer nicht herum.

Das Ergebnis wird als Bruch in Integer z/n benötigt. Wenn die Bestimmung 
einer möglichst exakten Approximation des Bruches Z/N über den 
Zwischenschritt einer Approximation in double geht, ist das absolut 
okay. Wird ja eh nur dreimal gerechnet. Mit fällt da nur auch kein 
besserer Algorithmus als vollständige Enumeration ein.

Peter D. schrieb:
> 2. Man läßt die Rechnung nur einmal laufen und speichert die Ergebnisse
> im EEPROM/Flash ab.

Kann man so machen. Ist eine Notlösung. Sie gefällt mir deshalb nicht 
besonders, weil ein sehr lokales Problem ( Z/N -> z/n ) zu einem 
verteilten Problem mit zahlreichen Abhängigkeit ausweitet. Wäre ich 
verzweifelt, würde ich das wohl so machen.

Peter D. schrieb:
> 1. Man startet die Mainloop sofort und läßt die Berechnung im
> Hintergrund laufen, z.B. ein Timerinterrupt führt kleine Häppchen davon
> aus.

Das ginge notfalls auch. Beim Systemstart arbeitet man langsam mit dem 
exakten Bruch Z/N und schaltet dann um auf die Näherung, sobald sie 
vorliegt, und aktiviert erst dann die Zusatzfunktionalität, die durch 
die freiwerdende Rechenleistung ermöglich wird. Oder lebt eben am Anfang 
mit einer etwas zäheren Bedienbarkeit.

Aber auch das sind hauptsächlich erst einmal mehr Abhängigkeiten für ein 
sehr lokales Problem.

Peter D. schrieb:
> Das ist doch alles rein akademisch ohne jeden praktischen Nutzen.

Das ist möglich. Aber der Vorteil an einem reinen Hobbyprojekt ist, dass 
ich meinen Zeitaufwand gegenüber niemandem rechtfertigen muss, sondern 
mir einfach jeweils die interessanteresten Teilaspekte aussuchen kann.

Yalu X. schrieb:
> Ich hatte vergessen, auch das Stichwort "Kettenbruch" zu erwähnen. Diese
> beiden Artikel helfen dir evtl. weiter:

Danke! Ich denke, dass muss ich sowohl mal ausgeschrieben als auch 
einmal programmiert haben, um das komplett verstanden zu haben. Mit 
Kettenbrüchen hatte ich bislang noch keine Berührpunkte.

Nachtrag: Auf den ersten Blick sieht das sogar sehr gut aus. Da kann ich 
mir die "Vorkonditionierung" (größter gemeinsamer Teiler) sogar sparen 
und direkt über die Kettenbruchzerlegung gehen. Aber das ist etwas für 
später. Die Kaffeepause ist um.

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Lesenswert?

Walter T. schrieb:
> Wird ja eh nur dreimal gerechnet. Mit fällt da nur auch kein
> besserer Algorithmus als vollständige Enumeration ein.

Von welcher Laufzeit sprechen wir hier? Die 3·2¹⁶ Schleifendurchläufe
gehen doch auf dem STM32F446 (der ja keine lahme Ente ist) sicher so
schnell, dass der Anwender kaum etwas davon mitbekommt.

von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Von welcher Laufzeit sprechen wir hier?

Bitte nicht hauen! Ich weiß es noch nicht. Ich habe es noch nicht 
ausprobiert. Bei vollständiger Enumeration treibt mich eher eine Phobie.

von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Von welcher Laufzeit sprechen wir hier?

Von ca. 28 Millisekunden. (28 Sekunden bei 1000 Durchläufen mit 
Zufallszahlen ohne Abbruch bei Fehler=0). Also schnell genug. Das heißt 
jede Verbesserung durch einen anderen Algorithmus ist rein 
Interessehalber.

von Yalu X. (yalu) (Moderator)


Angehängte Dateien:

Lesenswert?

Walter T. schrieb:
> Das heißt jede Verbesserung durch einen anderen Algorithmus ist rein
> Interessehalber.

Hab's mal mit den Kettenbrüchen versucht, aber nur an wenigen Beispielen
getestet. Der Code könnte also noch Bugs, evtl. sogar grobe Denkfehler
enthalten.

Der Rechenaufwand steigt logarithmisch mit dem Nennerlimit. Bei einem
Nennerlimit von 65536 werden maximal 25 Schleifendurchläufe benötigt.

von Walter T. (nicolas)


Angehängte Dateien:

Lesenswert?

Yalu X. schrieb:
> Hab's mal mit den Kettenbrüchen versucht, aber nur an wenigen Beispielen
> getestet.

Danke für's Probieren! Ich führe mir das direkt mal zu Gemüte. Ehrlich 
gesagt ist mir der Denkansatz auch viel wichtiger als eine korrekte 
Implementierung.

Wie ich ja schon oben schrieb, ist dieser Teil der Mathematik definitiv 
nicht meine Stärke.

Ich habe mal versucht, die Vorgehensweise bei Kettenbruchzerlegung und 
einer daraus zu bestimmenden Näherung zu verstehen, komme aber auf 
keinen grünen Zweig.

a) Ich mache eine Kettenbruchzerlegung des Bruchs Z/N. Da das rational 
ist, wird es auf jeden Fall terminieren, weil irgendwann der Rest 0 ist. 
Ich habe dann eine vorher nicht bestimmbare Anzahl an Quotienten q_0 bis 
q_n und zwei Reste r_n und r_n+1. Alle anderen Reste benötige ich für 
meinen Kettenbruch nicht. Wenn ich das richtig sehe, muss ich alle q_i 
und die letzten beiden r_i speichern, um den ursprünglichen Bruch exakt 
als Kettenbruch darzustellen.

b) Wenn ich das alles ausmultipliziere, habe ich wieder den kompletten 
Bruch wie vorher. Also muss ich wohl irgendwann früher abbrechen. Aber 
woher weiß ich, wann? Ich nehme an, wenn der letzte Rest r_i in die 
gewünschte Größenordnung kommt, weil das ja später der Hauptnenner ist.

Dann aber:

c) Wenn Z und N teilerfremd ist, sollte der Algorithmus doch automatisch 
im dritten Schritt terminieren, oder habe ich da einen Denkfehler? Aber 
wenn ich ohnehin nur drei Schritte habe - was kann ich dann weglassen, 
ob gröber zu werden (respektive einen kleineren Nenner zu bekommen)?

Vielleicht klären sich Teile daraus aus dem Quelltext. Danke für den 
Ansatz!

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Walter T. schrieb:
> 1 bis INT16_MAX

Du brauchst nur den Bereich int16_MAX/2 bis INT16_MAX und nur für die 
größere Zahl durchlaufen. Ersparnis: 50%

von Walter T. (nicolas)


Lesenswert?

A. S. schrieb:
> Du brauchst nur den Bereich int16_MAX/2 bis INT16_MAX [...] durchlaufen.

Warum? Warum kann die perfekte Näherung nicht zwischen 1 und INT16_MAX/2 
liegen? (Oder vielleicht auch: Wenn die perfekte Näherung zwische 1 und 
INT16_MAX/2 liegt - woher kann ich sicher sein, dass sie in der anderen 
Hälfte ein zweites Mal vorhanden ist?)

Nachtrag: Hat sich geklärt. Ich lasse die Frage mal aus didaktischen 
Gründen stehen.

Für jede Zahl in der unteren Hälfte gibt es die doppelte in der oberen 
Hälfte. Da Z und N aber teilerfremd sind, brauche ich erst gar nicht zu 
kürzen versuchen.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Walter T. schrieb:
> A. S. schrieb:
>> Du brauchst nur den Bereich int16_MAX/2 bis INT16_MAX [...] durchlaufen.
>
> Warum? Warum kann die perfekte Näherung nicht zwischen 1 und INT16_MAX/2
> liegen?

Kann sie. Sie liegt dann aber "auch" im oberen Bereich.

Du kannst es noch weiter eingrenzen: Bei Zahlen > 1 interessiert nur 
fraktionale Teil. Beispiel: Bei 3,745  und Nenner n ist der Zähler immer 
3n+z1. z1/n kannst Du somit getrennt suchen. für z1 bleibt aber nur ein 
Zahlenbereich von 0.. INT16_MAX/3,745 Und davon wieder nur die obere 
Hälfte.

von Walter T. (nicolas)


Lesenswert?

A. S. schrieb:
> [...] und nur für die größere Zahl durchlaufen.

Daran knobele ich noch.

Ich mache am Anfang der Einfachheit halber alles vorzeichenlos. Wenn ich 
n durchlaufe, habe ich als Näherung für z zur Auswahl:
z = Z * n/N;
z = Z * n/N + 1;

Meine Fehlerbedingung ist:
err = Z*n - z*N (oder umgekehrt, falls z*N > Z*n).

Warum kann es nur der größere Wert sein?

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

A. S. schrieb:
> für z1 bleibt aber nur ein
> Zahlenbereich von 0.. INT16_MAX/3,745 Und davon wieder nur die obere
> Hälfte.

Sorry, das ist falsch.

von Walter T. (nicolas)


Angehängte Dateien:

Lesenswert?

Yalu X. schrieb:
> Der Code könnte also noch Bugs, evtl. sogar grobe Denkfehler
> enthalten.

Für Z = 40973 und N = 29147 kommen z = -24563 und n = 29147 heraus. Mit 
vollständiger Enumeration komme ich auf z = 9067, n = 6450. Auffällig an 
dem Beispiel ist, dass Zähler und Nenner recht klein sind. Zuerst 
vermutete ich, dass das an der Abbruchbedingung liegt, die nur den 
Nenner sicherstellt. Aber tauscht man Zähler und Nenner entsprechend 
nach dem Größeren, werden einfach die Eingangswerte durchgereicht. Ich 
habe aber noch nicht verstanden, wie die Abbruchbedingung Optimalität 
sicherstellen soll, deswegen suche ich noch.

Die vollständigen Enumeration ohne die oben vorgeschlagenen 
Optimierungen ist recht primitiv (siehe Anhang). Mit den Optimierungen 
ändert sich meßbar aber nicht spürbar etwas.

Außerdem interessiert mich der Ansatz über die Kettenbruchzerlegung 
jetzt. Hier zieht Gewitter auf, also die beste Gelegenheit für mich, 
noch einmal mit Stift und Papier das Ganze anzugehen.

: Bearbeitet durch User
von Egon D. (Gast)


Lesenswert?

Walter T. schrieb:

> Für Z = 40973 und N = 29147 [...]

Die ersten Glieder der Entwicklung von 40973/29147
in einen regulären Kettenbrauch lauten
[1;2,2,6,1,1,2,1,12,1,1,...]; zum Weiterrechnen
hatte ich keine Lust mehr.


> Mit vollständiger Enumeration komme ich auf
> z = 9067, n = 6450.

Das ist in der Tat der 11. Näherungsbruch; er
ergibt sich genau aus der oben angegebenen
Teilfolge.


> Auffällig an dem Beispiel ist, dass Zähler und Nenner
> recht klein sind.

Das haben Kettenbruch-Näherungen so an sich.
Die Näherung 355/113 für Pi stimmt auf sieben geltende
Ziffern.

von Walter T. (nicolas)


Lesenswert?

Walter T. schrieb:
> Ich habe dann eine vorher nicht bestimmbare Anzahl an Quotienten q_0 bis
> q_n und zwei Reste r_n und r_n+1.

Wenn ich weiter drüber nachdenke: Dass ich die Ordnung des Kettenbruchs 
nicht a priori absehen kann, ist ja überhaupt kein Problem. Der 
Euklid-Algorithmus braucht ja nicht lange. Da ist das gleiche Problem 
bei jedem sprintf() schlimmer.

: Bearbeitet durch User
von Thorsten M. (pappkamerad)


Lesenswert?

Weiß nicht, ob schon genant wurde, wie wärs mit Shiften?

Also Zähler und Nenner so lange nach links shiften bis das MSB entweder 
des Zählers oder Nenners 1 ist. Dann die jeweiligen höherwertigen 16bit 
kopieren, das sollte die beste Näherung sein. Will man noch den 
kleinstmöglichen Zähler und Nenner dann nochmal beide nach rechts 
shiften bis eine 1 im LSB steht.

Shiften ist ja eine Multiplikation (Division) mit 2. Zähler und Nenner 
gleichzeitig shiften kürzt sich weg, ist also eine Erweiterung des 
Bruchs.

Von der Rechenzeit käme man mit max 16 Linksshifts und anschließend max 
16 Rechtsshifts hin.

von Walter T. (nicolas)


Lesenswert?

Thorsten M. schrieb:
> Weiß nicht, ob schon genant wurde, wie wärs mit Shiften?

Walter T. schrieb:
> Erster naiver Ansatz: n = INT16_MAX; aber es lässt sich für viele Brüche
> Z/N zeigen, dass es bessere Approximationen gibt.

Z = 40973 und N = 29147 ist die oben genannte Näherung z.B. besser als 
10^-8.

: Bearbeitet durch User
von Egon D. (Gast)


Lesenswert?

Walter T. schrieb:

> Wenn ich weiter drüber nachdenke: Dass ich die Ordnung
> des Kettenbruchs nicht a priori absehen kann, ist ja
> überhaupt kein Problem. Der Euklid-Algorithmus braucht
> ja nicht lange.

Soweit ich weiss, ist die reelle Zahl mit der am langsamsten
konvergierenden Kettenbruchentwicklung der Goldene Schnitt.
Dessen Entwicklung lautet [1;1,1,1,1,1....].

Für eine vorgegebene Genauigkeit braucht jede andere Zahl
somit höchstens gleich viele Schritte.

Zu allem Überfluss sind Zähler und Nenner dieser Entwicklung
aufeinanderfolgende Elemente der Fibonacci-Folge, und
auffälligerweise ist gerade das 25. Element dieser Folge
das erste, das größer als 2^16 ist.

von Walter T. (nicolas)


Angehängte Dateien:

Lesenswert?

Nachtrag: Der Ansatz über den Kettenbruch ist goldrichtig.

Der angehängte Quelltext ist alles andere als optimiert, aber er schafft 
das Zehnfache von dem, was oben mit vollständiger Enumeration in 28 
Sekunden gemacht wurde in einer Zeit, die ich nicht mit der Stoppuhr 
erfassen kann. Er liefert insbesondere auch in allen getesteten Fällen 
das gleiche Ergebnis wie die vollständige Enumeration.

Wahrscheinlich ist noch der eine oder andere Bug drin, aber den bekommt 
man noch plattgebügelt. Insbesondere wird es vermutlich sinnvoll sein, 
den Stack, den er sich genehmigen darf, zu begrenzen.

Danke nochmal insbesondere an Yalu für den richtigen Tipp und danke an 
die anderen für die ebenfalls durchgängig brauchbaren Beiträge!

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Lesenswert?

Walter T. schrieb:
> Für Z = 40973 und N = 29147 kommen z = -24563 und n = 29147 heraus.

Ich habe vergessen, auch den Zähler in die Abbruchbedingung mit
aufzunehmen. Der Algorithmus liefert deswegen 40973/29147 als beste
Näherung. Du hast dann wohl die 40973 in ein int16_t konvertiert, was
wegen des Überlaufs -24563 ergibt.

Ich muss mal schauen, wie ich das korrigieren kann (aber heute nicht
mehr). Die Abbruchbedingung selber ist nicht das Problem, aber bei der
Bestimmung der besten Nebennäherung kann es passieren, dass der Zähler
trotzdem größer als das Limit wird.

> Mit vollständiger Enumeration komme ich auf z = 9067, n = 6450.
> Auffällig an dem Beispiel ist, dass Zähler und Nenner recht klein
> sind.

Dein Algorithmus bestimmt das, was hier als "beste Näherung 2. Art"
bezeichnet wird:

  https://de.wikipedia.org/wiki/Kettenbruch#Zwei_M%C3%B6glichkeiten_bester_N%C3%A4herung

Dabei wird für die Bewertung der Lösungskandidaten nicht – wie bei der
besten Näherung 1. Art – der absolute Fehler, sondern das Produkt aus
absolutem Fehler und dem Nenner des Kandidaten herangezogen. Dadurch
werden Näherungen mit kleinem Nenner deutlich bevorzugt.

> Außerdem interessiert mich der Ansatz über die Kettenbruchzerlegung
> jetzt. Hier zieht Gewitter auf, also die beste Gelegenheit für mich,
> noch einmal mit Stift und Papier das Ganze anzugehen.

Der Trick bei der Sache ist es, den endlichen Kettenbruch nicht als
Verkettung nichtlinearer Funktionen f auf ℚ mit f(x) = q_i + 1/x,
sondern als Verkettung linearer Funktionen auf ℕ×ℕ mit g((z, n)) =
(q_i·z + n, z) aufzufassen. Solche Funktionen können als Matrix
dargestellt werden, wodurch aus der Verkettung der Funktionen ein
leichter handzuhabendes Produkt aus 2×2-Matrizen wird:

  https://de.wikipedia.org/wiki/Kettenbruch#Matrixdarstellung

Dies ermöglicht es, den Wert von [q_0;q_1...q_(n+1)] rekursiv aus dem
Wert von [q_0;q_1...q_(n)] und q_(n+1) zu berechnen. Damit hat man mit
jedem q_i, das der Euklidische Algorithmus generiert, sofort auch den
Zähler und Nenner des bis zu diesem q_i ausgewerteten Kettenbruchs.

Ohne diesen Trick muss man alle Koeffizienten in einer Liste speichern
und sich für die Berechnung des Kettenbruchs jedesmal in einer Schleife
vom letzten bis zum ersten Koeffizienten durchhangeln. Das geht
natürlich auch, braucht aber mehr Rechenzeit und ist umständlicher zu
programmieren.

: Bearbeitet durch Moderator
von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Dabei wird für die Bewertung der Lösungskandidaten nicht – wie bei der
> besten Näherung 1. Art – der absolute Fehler, sondern das Produkt aus
> absolutem Fehler und dem Nenner des Kandidaten herangezogen. Dadurch
> werden Näherungen mit kleinem Nenner deutlich bevorzugt.

Stimmt. Gestern sah das noch nach der geschicktesten Wahl aus. Aber ich 
will ja eigentlich den relativen Fehler einer Operation A*z/n 
minimieren, nicht den relativen Fehler z/n. Dafür muss ich natürlich den 
absoluten Fehler von z/n minimieren.

Yalu X. schrieb:
> Der Trick bei der Sache ist es, den endlichen Kettenbruch nicht als
> Verkettung nichtlinearer Funktionen f auf ℚ mit f(x) = q_i + 1/x,
> sondern als Verkettung linearer Funktionen auf ℕ×ℕ mit g((z, n)) =
> (q_i·z + n, z) aufzufassen. Solche Funktionen können als Matrix
> dargestellt werden, wodurch aus der Verkettung der Funktionen ein
> leichter handzuhabendes Produkt aus 2×2-Matrizen wird:
>
>   https://de.wikipedia.org/wiki/Kettenbruch#Matrixdarstellung
>
> Dies ermöglicht es, den Wert von [q_0;q_1...q_(n+1)] rekursiv aus dem
> Wert von [q_0;q_1...q_(n)] und q_(n+1) zu berechnen.

Endlich mal jemand, der es so erklärt, dass ich das auch verstehe. 
Scherz beiseite: Das war jetzt wirklich die richtige Erklärung zum 
richtigen Zeitpunkt. Gestern habe ich die Matrixdarstellung wegen der 
schieren Menge an Neuem als "exotischer Nebenpfad" abgetan.

Yalu X. schrieb:
> Ohne diesen Trick muss man alle Koeffizienten in einer Liste speichern
> und [...]

Was ich noch nicht ergründen konnte: Der Ansatz besteht darin, beim 
"Zurückgewinnen" des ursprünglichen Bruchs die Koeffizientenliste 
irgendwann abzubrechen. (Oder besser: Später anzufangen, wenn die Liste 
rückwärts wieder aufgedröselt wird). Das entspricht ja, ein Restglied 
Null zu setzen.

Bei anderen Approximationen kann es ein besseres Ergebnis liefern, ein 
Restglied auf 1 zu setzen. Habe ich das richtig verstanden, dass ein 
frühzeitiger Abbruch immer ein tendenziell zu großes Ergebnis liefert 
(nach dem 0-ten Glied), weil der Nenner immer schwerer wird?

Edit: Nee, das ist Blödsinn. Der Nenner wird abwechselnd schwerer und 
leichter. Es wird ja ständig der Kehrwert gebildet. Das dauert noch, bis 
ich zu dem Konstrukt Intuition entwickele.

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Semi-OT: Kennt jemand von euch ein gutes erstsemestertaugliches 
Mathematik-Lehrbuch, in dem dieses und ähnliche Themengebiete behandelt 
werden?

Mein erstes Semester ist zwar sehr lange her, aber dieser Themenkreis 
wurde absolut überhaupt nicht angeschnitten.

(Es gab letztens einen passenden XKCD dazu: https://xkcd.com/2501/ )

: Bearbeitet durch User
von Pandur S. (jetztnicht)


Lesenswert?

Der Ansatz ist richtig, koennte mit etwas mehr Mathe abgekuerzt werden.

Es gibt verschiedene Bezeichnung, verschiedene Ansaetze. Mathematische 
Methoden der Physik, Mathematik fuer Informatiker, usw. Benoetigen aber 
eine Aufbau.
Ich wuerd mich erst mal mit dem ersten Ansatz, den Teilen durch 2^n, 
bedeutet Schieben, zufrieden geben. Und mir ueberlegen, weshalb ich mehr 
Aufloesung und Genauigkeit benoetige. Wieviele Bit hat mein ADC, welche 
Genauigkeit hat er ?
Regelungen rechne ich zB nie in zB Grad Celsius, sondern in ADC 
Einheiten. Ich lasse auf ADC Endwert regeln, micht auf Grad Celsius.
Alternativ mit mehr Stellen rechnen.
Wenn die Rechenzeit zu lange dauert, zB in einem Loop die periodischen 
Aufgaben zu stark verzoegert, die Rechnung in einer Zustandsmaschine, zB 
ein Digit pro Ausfuehrung rechnen. Das kann man machen, um 
Anzeigegroessen, welche hoechtens 3 Mal pro Sekunge beoetigt werden, zu 
rechnen, wenn der Loop schneller als 10ms sein muss, und die Rechnung 
30ms dauert.

von Walter T. (nicolas)


Lesenswert?

Pandur S. schrieb:
> Ich wuerd mich erst mal mit dem ersten Ansatz, den Teilen durch 2^n,
> bedeutet Schieben, zufrieden geben.

Das ist okay. Kannst Du so machen.

Ich bin gerade in die komplette Gegenrichtung unterwegs. Ich bewege mich 
von einer Genauigkeitsdiskussion in jedem Einzelschritt hin zu einer 
überschaubaren Anzahl von Operationen mit definierter Genauigkeit und 
bekannter Geschwindigkeit.

Viel Quelltext kann man dadurch gar nicht mal löschen, aber viele Zeilen 
Kommentar und viele Assertions. Das ist insbesondere sehr praktisch, 
weil das sehr anstrengender Kommentar ist - und leider auch nicht sehr 
nachhaltig. Immer wenn die Reihenfolge in der Bearbeitungskette geändert 
wird, muss alles angepasst werden.

Aber lassen wir die Motivation außen vor. Mit der Implementierung bin 
ich ja schon in trockenen Tüchern. Es gibt keinen Grund, auf eine 
schlechtere zu wechseln. Nur noch auf eine bessere.

Außerdem ist dieser Abstecher in die Zahlentheorie schon für sich 
interessant. Selbst wenn ich keinen Takt gewonnen hätte, hätte sich das 
gelohnt. Selbst wenn sich morgen herausstellte, dass es ein 
__builtin_reduce_fraction() gibt, das alles in sieben Takten erledigt, 
hätte sich das gelohnt.

: Bearbeitet durch User
von Alexander S. (alesi)


Lesenswert?

Walter T. schrieb:
> Kennt jemand von euch ein gutes erstsemestertaugliches
> Mathematik-Lehrbuch, in dem dieses und ähnliche Themengebiete behandelt
> werden?

Die ersten beiden Bücher aus meinem Regal, die ich dazu gefunden habe 
sind:

Karl Strubecker, Einführung in die Höhere Mathematik, Band I., 
Oldenbourg Verlag, 1966
27. Kettenbrüche              S. 77
28. Anwendungen               S. 81
29. Unendliche Kettenbrüche   S. 85
30. Periodische Kettenbrüche  S. 88
und
Harald Scheid, Zahlentheorie, BI-Wissenschaftsverlag, 1994
1.8 Kettenbrüche              S. 46
1.9 Periodische Kettenbrüche  S. 56

Nachtrag: Neben einer Quelle von 1966 habe ich auch etwas moderneres:

http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/cfINTRO.html

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Alexander S. schrieb:
> Karl Strubecker, Einführung in die Höhere Mathematik, Band I.,

Alexander S. schrieb:
> Harald Scheid, Zahlentheorie, BI-Wissenschaftsverlag, 1994

Bei beiden sieht das Inhaltsverzeichnis brauchbar aus, bei beiden kann 
ich wahrscheinlich im Laufe der übernächsten Woche einen Blick 
hineinwerfen. Danke für die beiden Tipps!

von Alexander S. (alesi)


Lesenswert?

Hier noch eine dritte Quelle:

Mangoldt, Knopp, Höhere Mathematik 1, Hirzel Verlag, 1990
64. Endliche regelmäßige Kettenbrüche    S. 144
65. Näherungsbrüche                      S. 147
66. Beispiele                            S. 149

von Peter D. (peda)


Lesenswert?

Man sollte auch berücksichtigen, daß viele 32Bitter keine HW-Division 
können oder die Division mehr Takte benötigt als die Multiplikation.
Es kann daher gut sein, daß eine float Multiplikation mit dem Kehrwert 
sogar schneller ist als die Ganzzahl Division bzw. kaum langsamer. Der 
ganze Aufwand kann also umsonst sein.

Ich habe viele Regelkreise, die einen großen Dynamikbereich benötigen, 
z.B. eine Elektronenquelle für 10nA..1mA. Da würde ich mir einen 
abbrechen, wenn ich auf float verzichten wollte. Die Teilchenquellen 
werden über den Filamentstrom geregelt, d.h. die Regelung muß eh träge 
sein, damit sie nicht schwingt (dominanter Pol).

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
> Ich habe viele Regelkreise, die einen großen Dynamikbereich benötigen,

In einem Regelkreis hast Du einen Feedback-Zweig. In einem Kreis mit 
Zustandsrückführung können sich Diskretisierungsfehler nicht 
akkumulieren. Da ist float perfekt.

Das Gegenteil ist z.B. eine Streckenmessung über Pulszählung. Da läufst 
Du mit float irgendwann in eine Sättigung und es gilt z.B. 16777216 + 1 
+ 5 != 16777216 + 5 + 1. Ich stelle mir das übrigens absolut brutal vor, 
soetwas zu debuggen, wenn man das nicht vorher weiß. Zumal bei vielen 
Längeneinheiten es ja durchaus ein wenig dauert, bis man bei solchen 
Zahlen angelangt ist.  (Eintrag im Bugtracker: "Wenn man sehr langsam 
fährt, zählt er nicht mehr hoch! Tritt alle vier Wochen auf!")

Dass float für vieles gut ist heisst nicht, dass es für alles gut ist.

Aber wir sind hier ja eh im Off-Topic-Forum. Hier dürfen auch 
Zahlenspielereien rein, denen jegliche praktische Anwendung fehlt. 
Lassen wir die Anwendung beiseite. Das Problem an sich ist interessant 
genug.

Edit: Bevor jemand Unbedarftes (was ich keinem Mitdiskutanten 
unterstelle, aber man stößt ja auch beim Googlen auf Threads) beim Lesen 
das obige Beispiel falsch versteht: Das ist kein Epsilon-Problem beim 
Vergleich. Es kommen immer ganze Zahlen heraus. Nur eben 
unterschiedliche je nach Operations-Reihenfolge.

: Bearbeitet durch User
Beitrag #6816523 wurde vom Autor gelöscht.
von Walter T. (nicolas)


Lesenswert?

Ich habe oben etwas falsch korrigiert und nun ist meine Zeit abgelaufen. 
(Der Zeilenumbruch macht mich kirre!)
Gemeint ist:
"Die Ausdrücke
 16777216 + (5 + 1) und
 16777216 + 5 + 1
liefern unterschiedliche Werte mit 32-Bit-Float."


Zurück zum Thema: Habe eben in der Badewanne noch einmal die 
Neben-Näherungsbrüche angeschaut. Mit einem Tag Abstand ist das doch gar 
nicht so kompliziert. Ich versuche das erst einmal selbst - also bitte 
nicht vor Montag vorsagen, Yalu!

Edit: Hat sich erledigt. Wenn man kein Problem damit hat, jeweils zwei 
Brüche zwischenzuspeichern, ist das wirklich geradeheraus. Danke für das 
Stichwort! Ohne wäre ich nie darauf gekommen.

An der Genauigkeit macht das nicht viel. Ich habe tatsächlich gerade 
noch kein Beispiel gefunden, wo es bei der anschließenden Multiplikation 
mit einem großen 64-Bit-Integer eine komplette Stelle Unterschied macht. 
Außer bei einem Beispiel, wo der Wert des Bruchs sehr groß ist und der 
Nebenbruch einfach den größeren Zähler hat, bevor abgebrochen wird.

Aber es ist schön, es probiert zu haben und es klappt.

Quelltext kommt, sobald ich ihn schön finde.

Edit2: Kann man sich auf die Eigenschaft verlassen, dass die Hauptbrüche 
immer abwechselnd über und unter dem realen Wert sind?

Edit3: Klar, muss ich ja. Sonst wären die Nebenbrüche nicht dichter 
dran.

Edit4: Willkommen in meiner Lerngruppe!

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Angehängte Dateien:

Lesenswert?

Yalu X. schrieb:
> Ich habe vergessen, auch den Zähler in die Abbruchbedingung mit
> aufzunehmen.

Das habe ich jetzt nachgeholt.

Außerdem habe ich den Vergleich der Fehler der am Ende des Algorithmus
entstehenden beiden Lösungskandidaten von double auf uint64_t
umgestellt, da ich befürchte, dass die Auflösung von double (52 Bit) in
einigen Fällen nicht ausreichend ist. Ich müsste dafür zwar noch ein
Beispiel konstruieren, wo dies relevant wird, aber mit der neuen Methode
ist man auf jeden Fall auf der sicheren Seite.

Schließlich habe ich noch einen Wrapper für vorzeichenbehaftete Brüche
hinzugefügt.

Übrigens solltest du (falls du es noch nicht getan hast) in deinem
approx2.c in Zeile 79
1
  for( size_t ii = 1; ii<nEl; ii++ )

das < durch ein <= ersetzen, sonst wird in vielen Fällen nicht die
beste, sondern nur die zweitbeste Näherung gefunden.

Könntest du vielleicht noch etwas dazu sagen, wozu du die rationale
Näherung brauchst? Du schriebst oben zwar etwas von Pulszählung,
Streckenmessung und akkumulierenden Fehlern. Aber so ganz verstehe ich
den Zusammenhang mit der rationale Näherung nicht.

Einige hier vermuten ja, dass du damit einfach nur Zahlenwerte skalieren
möchtest, und da wäre die FP-Multiplikation auf dem STM32F446 ganz klar
schneller als die Integer-Multiplikation und -Division. Aber es gibt
natürlich auch Fälle, wo die FP-Arithmetik überhaupt nicht weiterhilft,
bspw. bei der Parametrierung eines Frequenzteiles mit rationalem
Teilungsfaktor.

Walter T. schrieb:
> Edit2: Kann man sich auf die Eigenschaft verlassen, dass die Hauptbrüche
> immer abwechselnd über und unter dem realen Wert sind?

Bei der Sorte von Kettenbrüchen, um die es hier geht (also mit
ausschließlich positiven Koeffizienten), ja.

von Walter T. (nicolas)


Lesenswert?

Guten Morgen,

danke, dass Du Deine Verfeinerung mit uns teilst! Sie ist auf jeden Fall 
schon einmal schneller als meine. Ganz habe ich sie noch nicht 
verstanden. Aber das ist etwas für heute abend. Ich hatte noch keinen 
Kaffee.


Etwas Off-Topic:
1
  
2
if (zv < 0) {
3
    negative = true;
4
    zv = -(uint32_t)zv;
5
}
Ist das erlaubt?

von Pandur S. (jetztnicht)


Lesenswert?

Bezueglich float und integer. Ma sollte sich vergewaertigen, dass Int32 
einen dynamischen Bereich von 9 digits bringt, waehrend es bei float32 
nur 6 digits sind. Ich mache daher meine Regelungen welchen auf 24 Bit 
Wandlern beruhen auf Int32, mit float32 haette ich schon zu Beginn weg 
verloren. float32 hat nur 24bit  Mantisse.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Walter T. schrieb:
> if (zv < 0) {
>     negative = true;
>     zv = -(uint32_t)zv;
> }
> Ist das erlaubt?

Es ist nicht nur erlaubt, sondern auch unabhängig von der internen
Darstellung von Integer-Zahlen und damit portabel.

Die Konvertierung einer negativen Zahl in einen Unsigned-Typ ist in
Ordnung:

"6.3.1.3 Signed and unsigned integers

When a value with integer type is converted to another integer type
other than _Bool, if the value can be represented by the new type, it is
unchanged.

Otherwise, if the new type is unsigned, the value is converted by
repeatedly adding or subtracting one more than the maximum value that
can be represented in the new type until the value is in the range of
the new type."


Der Vorzeichenwechsel mit dem "-"-Operator ist ebenfalls in Ordnung:

6.2.5 Types

...

A computation involving unsigned operands can never overflow, because a
result that cannot be represented by the resulting unsigned integer type
is reduced modulo the number that is one greater than the largest value
that can be represented by the resulting type."


Wichtig ist, dass zuerst in unsigned konvertiert und dann erst das
Vorzeichen geändert wird. Andersherum führt -zv für zv=INT32_MIN zu
undefined Behavior:

"5.5 Expressions

...

If an exceptional condition occurs during the evaluation of an
expression (that is, if the result is not mathematically defined or not
in the range of representable values for its type), the behavior is
undefined."

von Alexander S. (alesi)


Lesenswert?

Egon D. schrieb:
> Die ersten Glieder der Entwicklung von 40973/29147
> in einen regulären Kettenbrauch lauten
> [1;2,2,6,1,1,2,1,12,1,1,...]; zum Weiterrechnen
> hatte ich keine Lust mehr.

Du warst fast fertig :-)
40973/29147 = [1;2,2,6,1,1,2,1,12,1,1,4]

http://www.arndt-bruenner.de/mathe/scripts/bruchrechnung1.htm#kettenbruch

von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Es ist nicht nur erlaubt, sondern auch unabhängig von der internen
> Darstellung von Integer-Zahlen und damit portabel.

Gut zu wissen. Das Idiom kannte ich noch nicht, aber spart einen 
Vergleich (für den Sonderfall INT_MIN) gegenüber meinem bisherigen 
Vorgehen.

Yalu X. schrieb:
> Übrigens solltest du (falls du es noch nicht getan hast) in deinem
> approx2.c in Zeile 79
>   for( size_t ii = 1; ii<nEl; ii++ )
>
> das < durch ein <= ersetzen, sonst wird in vielen Fällen nicht die
> beste, sondern nur die zweitbeste Näherung gefunden.

Hoppla, da hatte ich wohl einen Zaunpfahl vor dem Kopf. Ich hätte von 2 
bis nEl testen sollen.

Pandur S. schrieb:
> Ich mache daher meine Regelungen welchen auf 24 Bit Wandlern beruhen auf Int32

Um den Dynamikbereich beneiden Dich viele. Dann schwäche ich meine obige 
Aussage und füge das Wort "oft" hinzu.


--

Was mir noch etwas Kopfzerbrechen bereitet: Als "historische" Begründung 
für die Kettenbruchentwicklung wird das Bestreben Huygens angeführt, den 
Bruch 77708431/2640858 (Verhältnis der Umlaufbahnen von Erde und Saturn) 
durch das praktikablere Teilungsverhältnis 206/7 zu approximieren.

Das Problem: Nur ein absolut Wahnsinniger würde ohne Not ein 
Zähnezahlenverhältnis von 412/14 als einstufiges Getriebe realisieren, 
zumal sich Erde und Saturn ja im gleichen Umlaufsinn bewegen. 
Kettenbrüche zu erfinden, um ein Planetarium zu bauen, lasse ich mal 
nicht als Indiz für den Wahnsinn gelten.

Also brauchte er mindestens ein zweistufiges Getriebe. Aber das würde 
bedeuten, dass er in Wirklichkeit noch einen Freiheitsgrad hatte, um die 
Genauigkeit zu steigern:

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

Yalu X. schrieb:
> Walter T. schrieb:
>> if (zv < 0) {
>>     negative = true;
>>     zv = -(uint32_t)zv;
>> }
>> Ist das erlaubt?
>
> Es ist nicht nur erlaubt, sondern auch unabhängig von der internen
> Darstellung von Integer-Zahlen und damit portabel.

Hier ist zv wohl int32_t. Und dann ist zwar -(uint32_t)zv zulässig, aber 
die anschliessende Zuweisung an int32_t wird im Fall von INT32_MIN 
interessant. Denn das ist zunächst eine positive Zahl ausserhalb des 
zulässigen Wertebereichs von zv.
1
uint32_t u_zv = zv;
2
if (zv < 0) {
3
  negative = true;
4
  u_zv = -(uint32_t)zv;
5
}

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Angehängte Dateien:

Lesenswert?

(prx) A. K. schrieb:
> aber
> die anschliessende Zuweisung an int32_t wird im Fall von INT32_MIN
> interessant.

Danke für den Hinweis und die Korrektur.

Ich habe mich wohl zu sehr darauf konzentriert, die rechte Seite der
Zuweisung richtig zu machen, so dass ich ganz vergessen habe, dass auf
der linken Seite noch der falsche Typ steht.

Die korrigierte Version des Programms habe ich angehängt.

von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> Das Gegenteil ist z.B. eine Streckenmessung über Pulszählung. Da läufst
> Du mit float irgendwann in eine Sättigung

Pulse zählt man natürlich immer als Ganzahl und nicht als float. Und 
ganz zum Schluß zur Anzeige rechnet man erst in die jeweilige Maßeinheit 
um. Da kann man dann auch nationale Vorlieben berücksichtigen.

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
> Walter T. schrieb:
>> Das Gegenteil ist z.B. eine Streckenmessung über Pulszählung. Da läufst
>> Du mit float irgendwann in eine Sättigung
>
> Pulse zählt man natürlich immer als Ganzahl und nicht als float. Und
> ganz zum Schluß zur Anzeige rechnet man erst in die jeweilige Maßeinheit
> um. Da kann man dann auch nationale Vorlieben berücksichtigen.

Meine Rede. Und wenn Du jetzt noch der Meinung bist, dass sich ein 
64-Bit-Integer mit einer rationalen Zahl viel komplikationsärmer als mit 
einem float umrechnen lässt, sind wir auf einem gemeinsamen Nenner.

von Pandur S. (jetztnicht)


Lesenswert?

Allenfalls kann man sich den Bresenham Algorithmus ueberlegen. Vom Punkt 
(Z,N) nach (0,0) eine Linie ziehen, und schauen, wo sie am naechsten an 
anderen Punkten vorbei geht.

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Jetzt bin ich mal dazu gekommen, approx3a.c detailliert durchzugehen.

Schade. Ich sehe kein Verbesserungspotenzial. Vielleicht bis auf die 
Winzigkeit, dass ich mir noch q != 0 als Rückgabewert gegönnt habe. Wenn 
ich nicht absolut daneben liege, lässt sich so ja direkt ohne 
Mehraufwand unterscheiden, ob der Bruch approximiert oder nur reduziert 
wurde.

(Wieso ist jetzt eigentlich der Cast im Wrapper weg? Ich dachte immer, 
signed integer overflow und damit -INT_MIN sei undefined behaviour.)

Ich habe das gerade mal versucht, auf 64-Bit-Integer zu bekommen, und 
der Vergleich am Schluß benötigt fast die gleiche Rechenzeit wie die 
Hauptaufgabe. :-)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.