Mein Projekt: Formelparser und Funktionsplotter

  • VB.NET
  • .NET (FX) 4.0

Es gibt 59 Antworten in diesem Thema. Der letzte Beitrag () ist von Carbonunit.

    Richtig, erstmal mach ich das nur für mich, will dabei natürlich sinnvollen Code bauen, der auch unter anderen Bedingungen nicht sofort auf die Nase fällt. Aber eine Vermarktung oder Weitergabe ist vorerst nicht geplant.
    Also ich werde da mal ein bisschen herumprobieren. Erstmal die Daten eines Graphen berechnen und in einer Tabelle speichern, das kann man ja immer brauchen. Dann aus dieser Tabelle im Paint-Event zeichnen, ausprobieren wie das mit der Backgound-Image-Eigenschaft funktioniert, wenn ich zuerst in eine Datei zeichne und die dann lade oder eben das MS-Chart-Control nehmen, wobei das meiner Faulheit wohl am ehesten entgegen kommt. Eigentlich will ich die Zeichnung machen und gestalten, aber nicht dauernd den technischen Kram, dass die auch nicht plötzlich wieder verschwindet, richtig skaliert beim Resize etc. Wenn sich darum jemand im Hintergrund kümmert, ist mir das schon recht.

    Carbonunit schrieb:

    ...aber nicht dauernd den technischen Kram, dass die auch nicht plötzlich wieder verschwindet, richtig skaliert beim Resize etc. Wenn sich darum jemand im Hintergrund kümmert, ist mir das schon recht.
    dann nimm Ms-Chart.
    Das ist ein Standard, und ist äusserst leistungsfähig.

    Wenn du allerdings dich in OwnerDrawing-Grundlagen vertiefen willst, dann bastel halt selbst was. Ist auch schön, sich mit Matrix und GraphicsPath auszukennen.
    Macht aber Arbeit, und wird immer nur ein Spezialfall sein (und evtl. sogar punktuelle Vorteile gegenüber MsChart rausholen können).

    Ach guck - hier hab ich sogar ein': activevb.de/cgi-bin/tippupload…4/Ownerdrawn_ChartControl

    Es zeichnet nicht...

    Hallo, ich will zunächst die drei Möglichkeiten, also Paint-Event, in Datei zeichnen und als Backgroundimage laden und MS-Chart Control mal ausprobieren. Nur dafür müßte es wenigstens irgendwas zeichnen. Das tuts aber nicht. Im Folgenden der Code, von dem ich denke, dass er fürs Zeichnen im Paint-Event relevant ist. Sicher fehlt noch irgendwas, nur ich weiß nicht was.

    Dieser Code wird aufgerufen, wenn der Menuepunkt Funtion in neuem Fenster zeichnen gewählt wird:

    VB.NET-Quellcode

    1. Private Sub mnuDrawNewWindow_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuDrawNewWindow.Click
    2. Dim blnGetOn As Boolean = False
    3. Dim frmNewChild As frmDraw
    4. blnGetOn = OpenScaleEdit() ' Dialog zur Definition des Koordinatenkreuzes
    5. If blnGetOn Then
    6. ' Neues Fenster anzeigen
    7. frmNewChild = AddNewWindow(gobjFunctions.GetName)
    8. With frmNewChild
    9. ' Koordinatenkreuz festlegen und Zeichnen
    10. .DefineXAxis(frmDefineScale.X_AxisStart, frmDefineScale.X_AxisEnd, frmDefineScale.X_AxisTicks)
    11. .DefineYAxis(frmDefineScale.Y_AxisStart, frmDefineScale.Y_AxisEnd, frmDefineScale.Y_AxisTicks)
    12. .SetScale() ' Maßstabsfaktoren berechnen
    13. .DrawScale() ' Koordinazenkreuz zeichnen
    14. blnGetOn = .CalcNewGraph() ' Graphen berechnen
    15. End With
    16. gobjFunctions.ShowFunction()
    17. End If
    18. End Sub

    frmDraw ist eine Form ohne Rahmen, die als eiziges Steuerelement eine Picturebox hat (picDraw), welche die komplette Fläche der Form einnimmt und auf der eigentlich die Zeichnung zu sehen sein soll.

    Dieser Code berechnet und zeichnet das Koordinatenkreuz:

    VB.NET-Quellcode

    1. Friend Sub DrawScale()
    2. Dim ptStart_XAxis As System.Drawing.Point
    3. Dim ptEnd_XAxis As System.Drawing.Point
    4. Dim ptStart_YAxis As System.Drawing.Point
    5. Dim ptEnd_YAxis As System.Drawing.Point
    6. Dim penScale As System.Drawing.Pen = Pens.Black
    7. Dim intXAxisY As Integer = 0
    8. Dim intYAxisX As Integer
    9. ' Wo liegt die X-Achse?
    10. With mtypAxisX
    11. If CBool(.dblStart < 0.0) And CBool(.dblEnd > 0.0) Then ' Von Minus bis Plus
    12. ' Y-Achse irgendwo in der Mitte
    13. intYAxisX = Math.Abs(CInt(.dblStart * mdblFactorX))
    14. ElseIf CBool(.dblStart < 0.0) And CBool(.dblEnd <= 0.0) Then ' Nur negagtive Seite
    15. ' Y-Achse liegt ganz rechts
    16. intYAxisX = picDraw.Width
    17. ElseIf CBool(.dblStart >= 0.0) And CBool(.dblEnd > 0.0) Then ' Nur positive Seite
    18. ' Y-Achse liegt ganz links
    19. intYAxisX = 0
    20. End If
    21. End With
    22. ' Wo liegt die Y-Achse?
    23. With mtypAxisY
    24. If CBool(.dblStart < 0.0) And CBool(.dblEnd > 0.0) Then ' Von Minus bis Plus
    25. ' X-Achse irgendwo in der Mitte
    26. intXAxisY = Math.Abs(CInt(.dblStart * mdblFactorY))
    27. ElseIf CBool(.dblStart < 0.0) And CBool(.dblEnd <= 0.0) Then ' Nur negagtive Seite
    28. ' X-Achse liegt ganz oben
    29. intXAxisY = 0
    30. ElseIf CBool(.dblStart >= 0.0) And CBool(.dblEnd > 0.0) Then ' Nur positive Seite
    31. ' X-Achse liegt ganz unten
    32. intXAxisY = picDraw.Height
    33. End If
    34. End With
    35. ' X-Achse definieren
    36. ptStart_XAxis.X = 0
    37. ptStart_XAxis.Y = intXAxisY
    38. ptEnd_XAxis.X = picDraw.Width
    39. ptEnd_XAxis.Y = intXAxisY
    40. ' Y-Achse definieren
    41. ptStart_YAxis.X = intYAxisX
    42. ptStart_YAxis.Y = 0
    43. ptEnd_YAxis.X = intYAxisX
    44. ptEnd_YAxis.Y = picDraw.Height
    45. With Me.picDraw.CreateGraphics
    46. .DrawLine(penScale, ptStart_XAxis, ptEnd_XAxis)
    47. .DrawLine(penScale, ptStart_YAxis, ptEnd_YAxis)
    48. End With
    49. End Sub

    Wenn ich unmittelbar nach dessen Ausführung einen Breakpoint setze und die VB-IDE minimiere, so daß mein Programm im Vordergrund ist, liegt das Koordinatenkreuz auch genau da, wo es hin soll.

    Dieser Code berechnet den Graphen für die vorgegebene X-Achse und ruft Invalidate auf, um das Paint-Event auszulösen:

    VB.NET-Quellcode

    1. Friend Function CalcNewGraph() As Boolean
    2. Dim blnSuccess As Boolean = False
    3. Dim dblStart As Double = mtypAxisX.dblStart ' Gleichzeitig auch X-Offset
    4. Dim dblEnd As Double = mtypAxisX.dblEnd
    5. Dim dblYOffset As Double = mtypAxisY.dblEnd
    6. Dim dblX As Double = dblStart
    7. Dim dblY As Double = 0.0
    8. Dim intSteps As Integer = GetSteps()
    9. Dim dblAxisLen As Double = Math.Abs(dblEnd - dblStart)
    10. Dim dblStep As Double = dblAxisLen / intSteps
    11. Dim ptResult As System.Drawing.Point
    12. Dim intError As Integer = ValfDeclares.ERR_SUCCESS
    13. Dim intProgressBarLen As Integer = FPlotMain.gobjFunctions.ProgressbarMax - FPlotMain.gobjFunctions.ProgressbarMin
    14. Dim dblProgressFactor As Double = CDbl(dblAxisLen / intProgressBarLen)
    15. Do
    16. ' Berechnen
    17. intError = FPlotMain.gobjFunctions.ActiveFunction.Calculate(dblY, dblX)
    18. blnSuccess = CBool(intError < ValfDeclares.ERR_CRITICAL) ' Keine kritischen Fehler aufgetreten?
    19. If blnSuccess Then
    20. If CBool(intError = ValfDeclares.ERR_SUCCESS) Then
    21. ' Nur, wenn auch keine mathematischen Fehler aufgetreten sind,
    22. ' wie Funktion nicht definiert oder Disision durch Null, dann zeichnen
    23. ' Punkt in der Grafik berechnen und speichern
    24. ' Offset berechnen, Nullpunkt oben links
    25. ptResult.X = CInt((dblX - dblStart) * mdblFactorX)
    26. ptResult.Y = CInt((dblY + dblYOffset) * mdblFactorY)
    27. mlistGraph.Add(ptResult)
    28. End If
    29. dblX += dblStep ' Nächster X-Wert
    30. FPlotMain.gobjFunctions.SetProgressbar(CInt(Math.Abs(dblX / intProgressBarLen * dblProgressFactor)))
    31. End If
    32. Loop Until CBool(dblX > dblEnd) Or Not blnSuccess
    33. Me.picDraw.Invalidate()
    34. Return blnSuccess
    35. End Function


    Das Paint-Event wird auch gefeuert. Darin wird das Koordinatenkreuz nochmal gezeichnet und die berechneten Punkte sollen als Graph ausgegeben werden:

    VB.NET-Quellcode

    1. Private Sub picDraw_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles picDraw.Paint
    2. ' Hier wird gezeichnet
    3. Dim intIdx As Integer = 0
    4. Dim penGraph As System.Drawing.Pen = Pens.Black
    5. ' Koordinatenkreuz
    6. DrawScale()
    7. ' Graphen zeichnen
    8. For intIdx = 1 To mlistGraph.Count - 1
    9. Me.picDraw.CreateGraphics.DrawLine(penGraph, mlistGraph(intIdx - 1), mlistGraph(intIdx))
    10. Next intIdx
    11. End Sub


    Wenn ich unmittelbar danach einen Breakpoint setze und die IDE minimiere, ist das Koordinatenkreuz immer noch da, aber vom Graphen ist nichts zu sehen. Läuft das Programm normal weiter, ist die ganze Fläche weiß, also auch das Koordinatenkreuz verschwindet wieder.

    Was genau fehlt da noch? Welche Informationen braucht ihr ggf. noch?

    Gruß
    Carbonunit
    wie ich sehe - mein gegebener Link hat dich ja nicht weiter beeinflusst.

    versuch 2 Dinge zu beherzigen:
    1. Vergiss Control.CreateGraphics()! nur mit dem Graphics, was dir im Paint-Event gegeben wird zeichnen!!
    2. Nur im Paint-Event zeichnen und evtl. in Methoden, die du von da aus aufrufst
      Alles woanders gezeichnete Zeug ist nicht persistent und verschwindet bei Gelegenheit wieder - haste nun ja gesehen.
    Ich hab auch ein OwnerDrawing-Tut gemacht, wo die ganze Bandbreite erläutert wird (und bei einem Chart ist die ganze Bandbreite zu kennen leider erforderlich):
    OwnerDrawing



    Wie gesagt: Die Thematik ist sehr schwierig, und an die Qualität des ChartControls (was Beschriftung, Skalierung, Maus-Interaktion etc angeht) ist von uns Hobby-Programmierern eh nicht heranzukommen.

    Es mag unkreativ erscheinen, aber besseres Programmieren, und auch besseres Programmieren Lernen ist, professionelle Infrastruktur richtig benutzen zu lernen, anstatt Selbstgebasteltes in die Welt zu setzen.

    ErfinderDesRades schrieb:

    wie ich sehe - mein gegebener Link hat dich ja nicht weiter beeinflusst.

    Der Link aus #43? Hab ich ehrlich gesagt nicht so richtig verstanden...

    ErfinderDesRades schrieb:

    Ich hab auch ein OwnerDrawing-Tut gemacht

    Das sieht imteressant aus, damit komme ich wohl weiter.

    ErfinderDesRades schrieb:

    Es mag unkreativ erscheinen, aber besseres Programmieren, und auch besseres Programmieren Lernen ist, professionelle Infrastruktur richtig benutzen zu lernen, anstatt Selbstgebasteltes in die Welt zu setzen.

    Das ist natürlich richtig, aber ich will wenigstens einmal sehen wie das geht mit dem Ownerdrawing. Dann kann ich mich besser entscheiden, welchen Weg es weiter geht.

    faxe1008 schrieb:

    Öhm das CBool kannste dir sparen, ein Vergleich erzeugt immer einen boolean als Resultat.

    Auch richtig, aber das wird doch sicher wegoptimiert? Oder nicht?
    Ist so ein bisschen wie das Blinken beim Autofahren. Das habe ich mir bei jedem Richtungswechsel angewöhnt, egal ob auch jemand da ist, der es sieht oder nicht.

    Ergänzung: Mein Koordinatenkreuz bleibt schon mal erhalten, Graph erscheint gar nicht, da stimmt möglicherweise auch was mit den Koordinaten nicht... Es geht voran, muss erstmal etwas die Architektur umbauen.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Carbonunit“ ()

    Carbonunit schrieb:

    Der Link aus #43? Hab ich ehrlich gesagt nicht so richtig verstanden...
    ah - ok - hihi.
    Das ist in diesem Fall garnet so falsch, weil dann hast du verstanden, dasses verdammt kompliziert werden wird.

    Carbonunit schrieb:

    aber ich will wenigstens einmal sehen wie das geht mit dem Ownerdrawing.
    ok - dagegen ist nichts einzuwenden - ging mir ja nicht anders.



    Carbonunit schrieb:

    ...CBool ...
    Auch richtig, aber das wird doch sicher wegoptimiert? Oder nicht?
    Ist so ein bisschen wie das Blinken beim Autofahren. Das habe ich mir bei jedem Richtungswechsel angewöhnt, egal ob auch jemand da ist, der es sieht oder nicht.
    Nein, hier kann Autofahrer-Sicherheits-Denke nicht auf Programmierer-Denke übertragen werden.
    Beim Autofahren darf kein Unfall passieren, aber beim Proggen sollen Exceptions auftreten!
    Wenn Exceptions auftreten dürfen, wo immer Mist gecodet ist, dann ist die Wahrscheinlichkeit exorbitant höher, dass kein Mist gecodet ist - wenn sie nicht auftreten.
    An einer Exception stirbt niemand - sie tut nicht einmal weh. Sondern daran verbesserst du dein Programm.

    Und Typumwandlungen jeder Art sind sowieso mit äusserster Vorsicht zu behandeln - und wo immer geht zu vermeiden. Weil jede Typumwandlung deaktiviert lokal die Compiler-Typ-Kontrolle.

    VB.NET-Quellcode

    1. 'Also an dieser Stelle:
    2. Loop Until CBool(dblX > dblEnd)
    3. 'könnte auch stehen:
    4. Loop Until CBool("dblX > dblEnd")
    5. 'und der Compiler würde es durchlassen
    6. 'aber das hier:
    7. Loop Until "dblX > dblEnd"
    8. 'lässt er nicht durch, sondern sagt dir gleich, dasses Unfug ist.
    9. '(Option Strict vorrausgesetzt)

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Hallöchen,

    hier das erste Beweisfoto:


    Da ist natürlich noch längst nichts in einem vorzeigbaren Zustand, aber die Sache mit dem Ownerdrawing funktioniert schon mal.
    Jeder Speicherplatz meines Funktionsspeichers enthält jetzt noch zwei Listen. Einmal die Rohdaten mit den x- und y-Werten, die sich aus der Berechnung ergeben.
    Dann eine zweite, gleichgroße Liste mit System.Drawing.Point-Objekten. Aus dieser Liste zeichne ich im Paint-Event der Picturebox meine Kurve. Die wird jedes Mal neu berechnet, wenn sich die Größe des Fenster geändert hat. Die Faktoren und Offset-Werte für x und y errechne ich im Resize-Event.
    Das hat den schönen Nebeneffekt, dass sich mein Bild immer schön der Fenstergröße anpaast. Noch geht das alles auch sehr schnell, mal sehen, was passiert, wenn mehrere Kurven auf einer Picturebox sind.
    Eine Baustelle sind natürlich noch die Skalenstriche für das Koordinatenkreuz und so einige Menuepunkte, bei denen noch gar nichts passiert. Aber eine wichtige Klippe ist umschifft.

    Gelernt habe ich heute auch wieder was. So ein Konstrukt ist Mist (Pseudocode, VB-ähnlich):

    VB.NET-Quellcode

    1. With MyListOfObjects(Index)
    2. for Index = 0 to MyListOfObjects.count-1.
    3. If IsNumeric(.SomeTextFromMyObject) Then
    4. DoSomething()
    5. End If
    6. next Index
    7. end with

    Da hat mein Code bestimmte Dinge einfach nicht gemacht, obwohl der Debugger angezeigt hat, dass SomeTextFromMyObject den zum Index passenden Wert hatte. Tatsächlich übergeben wurde aber ein anderer Wert.
    Beginn und Ende des With-Blocks gehören IN die For-Schleife, dann gehts. Das IsNumeric kein .NET ist, weiß ich jetzt auch, wird noch angepasst.

    Zu den CBool-Funktionen beim If: Ja, ihr habt recht, da bemerke ich Fehler sicher schneller, wenn es dann kracht. Schmeiß ich wieder aus, wo ich das sehe.

    Gruß
    Carbonunit

    ErfinderDesRades schrieb:

    deaktiviere den MVB-Namespace-GeneralImport!

    Ja, muss ich noch machen, das gibt im Moment über 80 Fehler, meistens bei so Sachen wie Mid, Len, Instr oder den nicht sichtbaren Zeichen wie vbCrLf. Wenn ich mal wieder Lust zur öden Überarbeitung habe, wird das erledigt. Oder erstmal Dateiweise. Also Generalimport deaktivieren und überall Import Microsoft.VisualBasic drüberschreiben. Sind zum Glück nicht mal alle Dateien des Projekts, wo das drinsteckt.
    So habe ich das mit Option Strict On gemacht, da gibt es noch zwei Stellen, wo es ohne die Einstellung hakt. Ich schäm mich ja auch... ;)
    Das hat aber alles einen TODO-Kommentar und steht damit in der Überarbeitungsliste.

    Gelöschter Benutzer schrieb:

    Und ich dachte immer als "erfahrener" Informatiker macht man gleich alles richtig..
    Ehrlich gesagt hätte ich mehr nach der Vorstellung deiner Person erwartet.


    Niemand macht immer alles richtig.
    Ich bin seit sieben Jahren aus dem Job raus und VB.NET war in den letzten Jahren bis 2010 auch nicht mehr meine Hauptaufgabe. Dafür klappt es aber immer noch ganz ordentlich.
    Im Support, dazu noch in der Höhle des Löwen (Microsoft) ging es vor allem darum, dass die Leute ihr Zeug ans Fliegen bekommen haben, da war es erstmal egal, ob sie das mit .NET oder dem alten Zeug gemacht haben. Schön programmiert wurde da nicht, eher effizient im Sinne von "Schnell das Problem lösen".
    Das war in den letzten Jahren nur noch Geschäft, da musstest du so viele Cases (Anfragen) wie möglich wegschaffen.
    Jetzt kann ich endlich wieder mal pfriemeln und basteln.

    Letzter Entwicklungsstand

    Hallöchen,

    der Funktionsplotter wächst und gedeiht und auch das Ownerdrawing im Paint-Event läuft so flüssig, dasss ich keinen Bedarf für eine Alternative sehe.
    Diese vier Kurven...

    ...wurden in kürzester Zeit gezeichnet. Die Bedienung der Oberfläche hat insgesamt länger gedauert, als das Zeichnen. Das Ganze läuft in zwei Schritten ab:
    Zuerst fülle ich eine Liste (List(Of T) mit den Rohdaten, also den Ergebnissen der Funktion f(x) für die vom Koordinatenkreuz vorgegebenen x-Werte.
    Dann multipliziere ich das mit den Vergrößerungsfaktoren, die sich aus der Größe des Koordinatenkreuzes und der Größe der Zeichenfläche in Pixeln ergeben. Dazu kommen Offsets für den Nullpunkt, der ja nicht mehr oben links in der Ecke liegt, sondern dort, wo sich die Achsen des Kordinatenkreuzes treffen. Diese Wertepaare kommen in eine zweite Tabelle und aus dieser Tabelle wird im Paint-Event in einer Schleife die Zeichnung erstellt. Das geht schnell genug, um das Fenster flüssig verkleinern und vergrößern zu können mit der Maus

    Auch der kleine Ausflug in die Editor-Programmierung hat etwas damit zu tun. Mit diesem Editor, der jetzt wunderbar funktioniert und etwa den Funktionsumfang von Notepad hat, will ich später Makros bearbeiten für meine eigene kleine Makrosprache. Die wird ein bisschen an GW-BASIC erinnern , aber ohne Zeilennummern und man wird Variablen deklarieren müssen. Dafür gibt es GOSUB..Return und solche Geschichten. Alles direkt Zeile für Zeile interpretiert und ausgeführt.

    Anständig programmiert wird natürlich auch, alle Module compilieren jetzt mit gesetztem Option Strict On und der hier sogenannte "Deppen-Namespace" Microsoft.VisualBasic wird so langsam zurückgedrängt. Ich benutze ihn noch, um die wunderbar nützlichen Konstanten wie vbCrLf zu definieren, aber dann nur explizit dafür, er wird nicht mehr importiert, bzw. nur noch bei wenigen Modulen. Einen sinnvollen Ersatz für Asc und Chr, also ASCII-Wert eines Zeichens ermitteln und Zeichen aus ASCII-Wert dastellen, habe ich noch nicht gefunden.

    Gruß
    Carbonunit
    Bilder
    • Fplot-Polynome.jpg

      48,79 kB, 1.129×600, 837 mal angesehen
    erstmal congratulations! - sieht sehr gut aus!

    Carbonunit schrieb:

    "Deppen-Namespace" Microsoft.VisualBasic wird so langsam zurückgedrängt. Ich benutze ihn noch, um die wunderbar nützlichen Konstanten wie vbCrLf zu definieren
    immer feste Visual Studio - Empfohlene Einstellungen studieren, da gibts auch eine Empfehlung, stattdessen Sysem.Microsoft.ControlChars zu importieren.... - vbcrlf ist damit obsolet.

    Carbonunit schrieb:

    Einen sinnvollen Ersatz für Asc und Chr, also ASCII-Wert eines Zeichens ermitteln und Zeichen aus ASCII-Wert dastellen, habe ich noch nicht gefunden.

    immer feste Visual Studio - Empfohlene Einstellungen studieren, ich glaub, da empfehle ich auch iwo den GeneralImport Vb6=Micorsoft.VisualBasic
    Damit wäre der Deppen-Namespace unter dem Kürzel Vb6 nachwivor verfügbar.
    Also statt ihn einzubinden, hiesse es (etwas umständlicher): Vb6.Asc(), Vb6.Chr()

    Aber vlt. hab ich diese Finessen auch erst in Zwei Fragen: Aufteilung eines Programms und VB 2017 auseinandergesetzt - ich bin grad zu faul (und zu betrunken), das alles nochma durchzugehen.
    Einen System.Microsoft.Wasauchimmer - Namespace gibts bei mir nicht.
    Für die nützlichen Sachen aus dem Deppen-Namepsace, die schlecht zu ersetzen sind, hab ich das jetzt so gemacht:

    VB.NET-Quellcode

    1. Friend Const VBCRLF As String = Microsoft.VisualBasic.ControlChars.CrLf ' Zeilenvorschub
    2. ' Ersatz für Asc
    3. Friend Function VB6_Asc(ByVal chrIn As Char) As Integer
    4. Return Microsoft.VisualBasic.Asc(chrIn)
    5. End Function
    6. ' Ersatz für Chr
    7. Friend Function VB6_Chr(ByVal intIn As Integer) As Char
    8. Return Microsoft.VisualBasic.Chr(intIn)
    9. End Function
    Hallöchen,

    heute mal ein Codeschnippsel. Die ist der Teil des Funktionsplotters, in dem die einmalige Arbeit der Umstellung von InFix-Notation (5+3) zu PostFix.Notation (5 3 +) erledigt wird. Darin steckt auch das meiste Hirnschmalz. Ich muss zugeben, dass ich damals nicht alle Erklärungen vom @ErfinderDesRades dazu verstanden habe. Aber als ich einen WikiPedia-Artikel dazu mit dem Bildchen von einem Gleisdreick als Rangierbahnhof gesehen habe, da hat es Klick gemacht.
    Der Code funktioniert, es rechnet bisher richtig und auch zügig. Auch die Kommentare lasse ich mal drin. Wichtig für einen Funktionsplotter ist auch die Laufvariable, so wie eventuelle Konstante, wie im Term A*x^3+B*x^2+C*x+D. Ich nennen die "Symbole" und habe dafür eine extra Liste angelegt. Konstante werden bei der Umwandlung in die PostFix-Notation durch ihren Wert ersetzt. Von der Laufvariablen (oder auch mehreren Laufvariablen) merke ich mir die Position(en) im PostFix-Term und ändere die Werte vor jeder Berechnung.

    VB.NET-Quellcode

    1. ' Überträgt Tokens aus der Liste in InFix-Notation ( 3; +; 4;) in die
    2. ' Liste in PostFix-Notation ( 3; 4; +;)
    3. Private Function InFixTokensToPostFix() As Integer
    4. ' Der Algorithnus heißt auch "Shunting-Yard" also Rangierbahnhof.
    5. ' Dieser Rangierbahnhof ist ein Gleisdreieck, bei dem der rechte Ast der Input ist (Tokens in InFix-Anordnung),
    6. ' der untere Ast der Stack und der linke Ast der Output, also die Tokens (Waggons) in PostFix Anordnung.
    7. ' Regeln:
    8. ' Bevor ein neuer Operator oder Token auf den Stack kommt, werden alle Tokens höheren
    9. ' oder gleichen Ranges in die PostFix-Liste geschrieben. Es kommen nur Operatoren auf den Stack,
    10. ' Zahlen gehen direkt in die Ausgabe.
    11. ' Ausnahmen:
    12. ' Öffnende Klammern (Rang 0) werden auf den Stack gepusht, verdrängen aber dort nichts
    13. ' Schließende Klammern erzwingen ein Auslesen des Stacks und Schreiben in die PostFix-Liste.
    14. ' Danach wird die zugehörige öffnende Klammer vom Stack gepoppt und verworfen
    15. ' Sonderfälle sind die Klammern, Laufvariable, weil die noch immer als Name vorliegt und das #EOT
    16. Dim intError As Integer = ERR_SUCCESS
    17. ' Ende der Schleifen
    18. Dim blnDone As Boolean = False ' Äußere Schleife
    19. Dim blnDoneInnerLoop As Boolean = False ' Innere Schleife zum Flushen des Klammerausdrucks
    20. ' Stack für Tokens
    21. Dim stackTokensInFix As New Stack(Of OneToken)
    22. ' OperatorenRänge
    23. Dim intRankOnInFixStack As Integer = 0
    24. ' Rang des Tokens
    25. Dim intRankNewToken As Integer = 0
    26. ' Numerisches Token
    27. Dim blnNumericNewToken As Boolean = False
    28. ' Text des Tokens
    29. Dim strTokenText As String = ""
    30. ' Klammerebenen zählen, solange größer Null ist mindestens eine Klammereben noch offen
    31. Dim inBracketLevel As Integer = 0
    32. ' True, wenn Laufvariable das Token ist
    33. Dim blnIsControlVar As Boolean = False
    34. ' InFixListe auf Anfang
    35. SetInFixTermToStart()
    36. ' PostFix-Liste löschen
    37. mlistTokensPostFix.Clear()
    38. ' InFix-Stack löschen
    39. stackTokensInFix.Clear()
    40. Do
    41. ' InFixToken laden
    42. Dim objNewInFixToken As New OneToken
    43. objNewInFixToken = GetNextInFixToken()
    44. 'PostFixToken definieren
    45. Dim objNewPostFixToken As New OneSimpleToken
    46. ' Eigenschaften des neuen PostFixTokens setzen
    47. strTokenText = objNewInFixToken.TokenText ' TokenText
    48. objNewPostFixToken.TokenText = strTokenText
    49. objNewPostFixToken.TokenValue = objNewInFixToken.TokenValue
    50. blnIsControlVar = TokenIsControlVar(strTokenText) ' Ist das die Laufvariable?
    51. If blnIsControlVar Then
    52. ' Laufvariable muss beim Sortieren als Zahl behandelt werden
    53. objNewPostFixToken.TokenArguments = 0 ' Hat keine Argumente
    54. intRankNewToken = MAX_OPERATOR_RANK ' Höchster Rang
    55. blnNumericNewToken = True ' Numerisches Token
    56. Else
    57. objNewPostFixToken.TokenArguments = objNewInFixToken.TokenArguments
    58. ' Übrige Eigenschaften des InFix-Tokens merken
    59. intRankNewToken = objNewInFixToken.TokenOpRank
    60. blnNumericNewToken = objNewInFixToken.TokenIsNumeric
    61. End If
    62. ' Ende des Terms erreicht?
    63. blnDone = CBool(objNewInFixToken.TokenText = END_OF_TERM)
    64. If (intRankNewToken = 0) Then ' Sonderfall öffnende Klammer
    65. stackTokensInFix.Push(objNewInFixToken) ' Auf den Stack damit
    66. inBracketLevel += 1
    67. ElseIf (strTokenText = SPECIAL_CLOSE_BRACKET) Then ' Sonderfall schließende Klammer
    68. ' Stack bis zur öffnenden Klammer flushen, öffnende und schließende Klammer verwerfen
    69. blnDoneInnerLoop = False
    70. Do
    71. If (stackTokensInFix.Peek.TokenText <> SPECIAL_OPEN_BRACKET) Then
    72. ' Wenn keine öffnende Klammer oben auf dem Stack
    73. intError = FlushTokenFromStackToPostFixList(stackTokensInFix)
    74. Else
    75. ' öffnende Klammer verwerfen
    76. If CBool(stackTokensInFix.Count) Then
    77. stackTokensInFix.Pop()
    78. Else
    79. intError = ERR_OP_STACK_UNDERFLOW
    80. End If
    81. blnDoneInnerLoop = True
    82. End If
    83. Loop Until blnDoneInnerLoop
    84. inBracketLevel -= 1
    85. ElseIf (strTokenText = END_OF_TERM) Then
    86. Do While CBool(stackTokensInFix.Count) And CBool(intError = ERR_SUCCESS)
    87. ' So lange noch Tokens auf dem Stack sind
    88. intError = FlushTokenFromStackToPostFixList(stackTokensInFix)
    89. Loop
    90. mlistTokensPostFix.Add(objNewPostFixToken) ' EOT ebenfalls in die Liste
    91. blnDone = True ' Fertig
    92. ElseIf blnNumericNewToken Then
    93. ' Token ist eine Zahl oder die Laufvariable als Platzhalter, direkt zur Ausgabe
    94. mlistTokensPostFix.Add(objNewPostFixToken)
    95. ElseIf (intRankNewToken > 0) Then
    96. ' Prüfen ob gleich- oder höherrangige Tokens auf dem Stack
    97. intRankOnInFixStack = GetRankOnInFixStack(stackTokensInFix)
    98. ' Abweisende Schleife, die nicht ausgeführt wird, wenn der neue Rang größer, als der des Tokens auf dem Stack ist.
    99. Do While (intRankNewToken <= intRankOnInFixStack) And CBool(intError = ERR_SUCCESS)
    100. ' So lange der Rang des neuen Tokens kleiner oder gleich dem des Tokens auf dem Stack ist...
    101. intError = FlushTokenFromStackToPostFixList(stackTokensInFix)
    102. ' Rang des Tokens auf dem Stack ermitteln
    103. intRankOnInFixStack = GetRankOnInFixStack(stackTokensInFix)
    104. Loop
    105. ' Stack geflusht, jetzt neues Token auf den Stack
    106. stackTokensInFix.Push(objNewInFixToken)
    107. End If
    108. Loop Until blnDone Or (intError <> ERR_SUCCESS)
    109. If intError = ERR_SUCCESS Then
    110. mintTermStatus = STATUS_PROCESSED
    111. End If
    112. Return intError
    113. End Function


    Die Klasse für ein Token, weches dann auch seinen Rang kennt und weiß, wieviele Argumente es hat:

    VB.NET-Quellcode

    1. Option Strict On
    2. Imports System.Collections.Generic
    3. ' Stellt ein Element oder auch Token der Term-Liste für Valf dar
    4. ' Kann auch für die Liste der Symbole verwendet werden
    5. ' Klasse enthält einen Double Wert und Text in einem Feld
    6. Public Class OneToken
    7. Inherits ValfDeclares
    8. ' ----- Variable -----
    9. ' Der Inhalt des Tokens
    10. Private mdblValue As Double = 0.0 ' Wenn Zahl, dann ist Text die String-Repräsentation der Zahl.
    11. Private mstrText As String = "" ' Wenn Text, dann ist Zahl 0.0
    12. ' Liste der Operatoren, der Index bestimmt den Operatoren-Rang
    13. Private mstrOperators(MAX_OPERATOR_RANK) As String
    14. ' Die Eigenschaften des Tokens
    15. Private mblnIsNumeric As Boolean = False ' True, wenn Zahl
    16. Private mintOpRank As Integer = 0 ' Rang des Operatos
    17. Private mintArguments As Integer = 0 ' Anzahl der Argumente des Operators
    18. ' Fehlercode
    19. Private mintErrorCode As Integer = ERR_SUCCESS
    20. ' Der Wert des Tokens, falls es eine Zahl ist
    21. ' Wenn es keine Zahl ist, kommt hier 0.0 zurück
    22. Friend ReadOnly Property TokenValue As Double
    23. Get
    24. TokenValue = mdblValue
    25. End Get
    26. End Property
    27. ' Das Token wird als String behandelt und intern zu Double gecastet,
    28. ' so dass dieser Wert zur Berechnung sofort abgerufen werden kann.
    29. ' Alle Eigenschaften des Tokens werden bei der Zuweisung ermittelt.
    30. Friend Property TokenText As String
    31. Get
    32. TokenText = mstrText
    33. End Get
    34. ' Hier erfolgt die Ermittlung aller übrigen Eigenschaften
    35. Set(ByVal strNewValue As String)
    36. ' Eigenschaften des Tokens bestimmen
    37. mintErrorCode = SetTokenProperties(strNewValue)
    38. End Set
    39. End Property
    40. ' Ist das Token eine Zahl oder nicht?
    41. Friend ReadOnly Property TokenIsNumeric As Boolean
    42. Get
    43. TokenIsNumeric = mblnIsNumeric
    44. End Get
    45. End Property
    46. ' Operatoren-Rang des Tokens, relevant bei der Sortierung in PostFix-Notation
    47. Friend ReadOnly Property TokenOpRank As Integer
    48. Get
    49. TokenOpRank = mintOpRank
    50. End Get
    51. End Property
    52. ' Anzahl der Argumente des Tokens, relevant wenn es ein Operator ist
    53. Friend ReadOnly Property TokenArguments As Integer
    54. Get
    55. TokenArguments = mintArguments
    56. End Get
    57. End Property
    58. ' Token fehlerfrei erstellt?
    59. Friend ReadOnly Property ErrorCode As Integer
    60. Get
    61. ErrorCode = mintErrorCode
    62. End Get
    63. End Property
    64. Private Function SetTokenProperties(ByVal strToken As String) As Integer
    65. Dim intError As Integer = ERR_SUCCESS
    66. mstrText = strToken
    67. mintArguments = 0 ' Standardmäßig hat das Token keine Argumente
    68. mblnIsNumeric = IsDouble(mstrText) ' Numerisch oder nicht?
    69. If mblnIsNumeric Then
    70. ' Token ist eine Zahl
    71. ' Typumwandlung, um den Double-Wert schnell zur Hand zu haben
    72. mdblValue = CDbl(mstrText)
    73. ' Eine Zahl hat keine Argumente, oben schon festgelegt
    74. mintOpRank = MAX_OPERATOR_RANK ' Eine Zahl hat den höchsten Operatoren-Rang,
    75. ' verdrängt damit das davor liegende Token mit gleichem Rang vom Stack,
    76. ' also nur eine andere Zahl.
    77. Else
    78. ' Token ist ein Operator oder Name
    79. ' Anzahl Argumente bestimmen
    80. If OPERATOR_BINARY.Contains(mstrText) Then
    81. mintArguments = 2 ' binärer Operator
    82. ElseIf OPERATOR_UNARY.Contains(mstrText) Then
    83. mintArguments = 1 ' unärer Operator
    84. End If
    85. ' Operatorenrang bestimmen
    86. mintOpRank = GetOpRank(mstrText)
    87. If mintOpRank > ERR_BASE Then ' "(" - nur öffnende Klammer
    88. ' Das ist ein Name...
    89. ' Sollte nicht auftreten, weil Namen von Symbolen noch in der Postfix-Notations-Phase
    90. ' durch ihre Werte ersetzt werden, also Fehler
    91. intError = mintOpRank
    92. End If
    93. End If
    94. Return intError
    95. End Function
    96. ' Operatorenrang ermitteln
    97. Private Function GetOpRank(ByVal strOperator As String) As Integer
    98. Dim intOpRank As Integer = ERR_VALF_SYNTAX ' Wenn nicht gefunden, stimmt was nicht
    99. Dim intIdx As Integer = 0
    100. For intIdx = 0 To mstrOperators.GetUpperBound(0)
    101. If mstrOperators(intIdx).Contains(strOperator) Then
    102. intOpRank = intIdx
    103. Exit For
    104. End If
    105. Next intIdx
    106. Return intOpRank
    107. End Function
    108. ' Operatoren nach Rangfolge
    109. Private Sub LoadOperatorList()
    110. mstrOperators(0) = OPERATOR_RANK_0
    111. mstrOperators(1) = OPERATOR_RANK_1
    112. mstrOperators(2) = OPERATOR_RANK_2
    113. mstrOperators(3) = OPERATOR_RANK_3
    114. mstrOperators(4) = OPERATOR_RANK_4
    115. mstrOperators(5) = OPERATOR_RANK_5
    116. mstrOperators(6) = OPERATOR_RANK_6
    117. mstrOperators(7) = OPERATOR_RANK_7
    118. mstrOperators(8) = OPERATOR_RANK_8
    119. mstrOperators(9) = OPERATOR_RANK_9
    120. mstrOperators(10) = OPERATOR_RANK_10
    121. End Sub
    122. Public Sub New()
    123. LoadOperatorList()
    124. End Sub
    125. End Class


    In ValfDeclares sind die Regeln hardcodiert, wie die Operatorenränge (Punkt vor Strich) und welcher Operator wieviele Argumente hat. Wenn das Gerüst erstmal steht, ist es auch recht einfach boolesche Operatoren nachzurüsten. Hier habe ich mit Ausrufezeichen und WICHTIG!! WICHTIG!! nicht gespart, weil eine Änderung hier die Rechenregeln kaputt macht.

    VB.NET-Quellcode

    1. #Region " !!! WICHTIG !!! Operatoren-Rang und unäre/binäre Operatoren"
    2. ' Diese Konstanten legen die mathematisch korrekte Ausführung der Berechnungen fest!
    3. '!!! - Unäre und Binäre Operatoren - !!! - WICHTIG- !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!!
    4. '!!! - Operatore-Rangfolge - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!!
    5. ' Rückgabe am Ende des Terms
    6. Protected Const END_OF_TERM As String = "#EOT"
    7. ' Festlegung der Operatorenränge nach Zugehörigkeit zu diesen Gruppen von Zeichen
    8. Protected Const OPERATOR_RANK_0 As String = SPECIAL_OPEN_BRACKET
    9. Protected Const OPERATOR_RANK_1 As String = "<=>" ' Vergleichsoperatoren
    10. Protected Const OPERATOR_RANK_2 As String = "%" ' or
    11. Protected Const OPERATOR_RANK_3 As String = "&" ' and
    12. Protected Const OPERATOR_RANK_4 As String = "NOT"
    13. Protected Const OPERATOR_RANK_5 As String = "+" & SPECIAL_MINUS ' Addition und Subtraktion(+-)
    14. Protected Const OPERATOR_RANK_6 As String = "*/" ' Multiplikation und Division
    15. Protected Const OPERATOR_RANK_7 As String = "^" ' Potenz
    16. Protected Const OPERATOR_RANK_8 As String = "SINCOSTANATNEXPSQR" ' Funktionen (Sin, Cos, etc)
    17. Protected Const OPERATOR_RANK_9 As String = SPECIAL_CLOSE_BRACKET
    18. Protected Const OPERATOR_RANK_10 As String = END_OF_TERM
    19. Protected Const MAX_OPERATOR_RANK As Integer = 10
    20. ' Unäre Operatoren (mit einem Argument)
    21. Protected Const OPERATOR_UNARY As String = OPERATOR_RANK_4 & OPERATOR_RANK_8
    22. ' Binäre Operatoren (mit zwei Argumenten)
    23. Protected Const OPERATOR_BINARY As String = OPERATOR_RANK_1 & OPERATOR_RANK_2 & OPERATOR_RANK_3 & OPERATOR_RANK_5 & OPERATOR_RANK_6 & OPERATOR_RANK_7
    24. '!!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!! - WICHTIG - !!!
    25. #End Region


    Nach der Vorarbeit ist die eigentliche Berechnung stures Abarbeiten der PostFix-Tokenliste. Zahlen kommen auf den Ergebnis-Stack. Operatoren holen entweder ein oder zwei Argumente vom Stack, berechnen sie und das Ergebnis geht auf den Stack: Wenn das Ende des Terms erreicht ist, liegt das Endergebnis hoffentlich als einziger Wert auf dem Stack. Hier der Code dazu:

    VB.NET-Quellcode

    1. ' Ausrechnen des Terms
    2. Friend Function Calculate(ByRef dblResult As Double, Optional ByVal dblVarValue As Double = Double.NaN) As Integer
    3. Dim intError As Integer = ERR_SUCCESS
    4. ' Das Token aus der PostFix-Liste
    5. Dim dblTokenValue As Double = 0.0 ' Wert des Tokens
    6. Dim strTokenText As String = "" ' Text des Tokens
    7. Dim intTokenArguments As Integer = 0 ' Anzahl Argumente des Tokens (wenn 0, dann Zahl)
    8. Dim dblCalcStack As New Stack(Of Double) ' Rechenstack für Zwischenergebnisse
    9. Dim dblRightArg As Double = 0.0 ' Argument rechts vom Operator
    10. Dim dblLeftArg As Double = 0.0 ' Argument links vom Operator
    11. If mintTermStatus >= STATUS_PROCESSED Then
    12. ' Stacks initialisieren
    13. dblCalcStack.Clear()
    14. ' Wert der Laufvariablen eintragen, wenn diese angegeben wurde
    15. If Not Double.IsNaN(dblVarValue) Then
    16. SetControlVarValue(dblVarValue)
    17. End If
    18. ' Elementeliste auf Anfang
    19. SetPostFixTermToStart()
    20. Do ' ------- Loop Until -----
    21. 'Nächstes PostFix-Token holen
    22. GetNextPostFixToken(dblTokenValue, strTokenText, intTokenArguments)
    23. If (strTokenText <> END_OF_TERM) Then
    24. If (intTokenArguments = 0) Then
    25. ' Token ist Zahl, kommt auf den Stack
    26. dblCalcStack.Push(dblTokenValue)
    27. ElseIf dblCalcStack.Count >= intTokenArguments Then
    28. ' Token ist ein Operator mit einem oder zwei Argumenten
    29. If (intTokenArguments = 1) Then
    30. dblRightArg = dblCalcStack.Pop
    31. dblLeftArg = 0.0
    32. ElseIf (intTokenArguments = 2) Then
    33. dblRightArg = dblCalcStack.Pop
    34. dblLeftArg = dblCalcStack.Pop
    35. End If
    36. ' Ausrechnen
    37. intError = Calc(strTokenText, dblRightArg, dblLeftArg, dblResult)
    38. If (intError < ERR_CRITICAL) Then
    39. ' Ergebnis auf den Stack, auch wenn mathematisch nicht definiert
    40. ' oder Überlauf etc.
    41. dblCalcStack.Push(dblResult)
    42. End If
    43. Else
    44. intError = ERR_VAR_STACK_UNDERFLOW
    45. End If
    46. End If
    47. Loop Until (strTokenText = END_OF_TERM) Or (intError > ERR_SUCCESS)
    48. If (intError < ERR_CRITICAL) Then
    49. ' Ergebnis vom Stack holen
    50. If CBool(dblCalcStack.Count) Then
    51. dblResult = dblCalcStack.Pop
    52. Else
    53. intError = ERR_VAR_STACK_UNDERFLOW
    54. End If
    55. End If
    56. Else
    57. intError = ERR_TERM_NOT_LOADED
    58. End If
    59. If intError < ERR_CRITICAL Then
    60. ' Berechnet ohne syntaktiche Fehler
    61. mintTermStatus = STATUS_CALCULATED
    62. Else
    63. mintTermStatus = STATUS_ERROR
    64. End If
    65. mintErrorCode = intError
    66. Return intError
    67. End Function


    Manches erscheint vielleicht umständlich, aber es geht mir auch um Lesbarket des Codes.
    Was man damit so machen kann, dazu im nächsten Post.

    Gruß
    Carbonunit

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Carbonunit“ ()

    Weiter gehts.
    Wichtig für die Mathe-Hausaufgaben waren die Ableitungen der Funktion. Die werden gebraucht, um Verschiedenes über die Funktion herauszufinden. Schon wenn man sie zeichnet, wird Manches klarer:


    Hier habe ich die Original-Funktion (FXC) und deren erste Abletung (FXC'), sowie die zweite Ableitung (FXC'') dargestellt. Die Ableitung zeigt immer die Steigung der Ausgangsfuntion an. Die Lage von Nullstellen in der Ableitung gibt wichtige Informationen über die Ausgangsfunktion.
    Die Berechnung erfolgt mit dem Vorwärtsdifferenzquotienten nach der Formel: f'(x) = (f(x+dx) -f(x)) / dx
    Unschwer zu erkennen, dass man durch nochmalige Anwendung der Formel auf das Ergebnis auch die zweite Ableitung berechnen kann.
    Hier der Code dazu:

    VB.NET-Quellcode

    1. ' Berechnet die erste und zweite Ableitung der Funktion mit dem Vorwärtsdifferenzquotienten
    2. Friend Function Derivation(ByVal intLevel As Integer, ByVal dblX As Double, ByRef dblResult As Double) As Integer
    3. ' intLevel : Level der Ableitung: 0 = f(x), 1 = f'(x), 2 = f''(x) , >=3 = Fehler
    4. ' dblX : Wert für X, an dem die Ableitung gebildet wird
    5. ' dblResult : Ergebnis
    6. ' Rückgabe : Fehlercode
    7. Dim intError As Integer = ERR_SUCCESS
    8. Dim dblY As Double = 0.0 ' Zwischenergebnisse
    9. Dim dblYdx As Double = 0.0
    10. Select Case intLevel
    11. Case 0 ' f(x)
    12. If IsConcat() Then
    13. intError = CalcConcat(dblX, dblResult)
    14. Else
    15. intError = Calculate(dblResult, dblX)
    16. End If
    17. Case 1 ' 1. Ableitung: f'(x) = (f(x+dx1) - f(x)) / dx1
    18. If IsConcat() Then
    19. intError = CalcConcat(dblX + mdblDelta1, dblYdx)
    20. Else
    21. intError = Calculate(dblYdx, dblX + mdblDelta1)
    22. End If
    23. If intError = ERR_SUCCESS Then
    24. If IsConcat() Then
    25. intError = CalcConcat(dblX, dblY)
    26. Else
    27. intError = Calculate(dblY, dblX)
    28. End If
    29. dblResult = (dblYdx - dblY) / mdblDelta1
    30. End If
    31. Case 2 ' 2. Ableitung: f''(x) = (f'(x+dx2) - f'(x)) / dx2
    32. ' Rekursion !
    33. intError = Derivation(1, dblX + mdblDelta2, dblYdx)
    34. If intError = ERR_SUCCESS Then
    35. intError = Derivation(1, dblX, dblY)
    36. dblResult = (dblYdx - dblY) / mdblDelta2
    37. End If
    38. Case Else ' Höhere Ableitung nicht implementiert, die Werte für Delta wären dann zu groß
    39. ' und es wird ungenau.
    40. intError = ERR_DERIVATION_LEVEL_TOO_HIGH
    41. End Select
    42. Return intError
    43. End Function


    mdblDelta1 und mdblDelta2 sind Werte zwischen 10^-7 und 10^-12 für Delta1 (erste Ableitung), sowie 10^-1 und 10^-6 für Delta2 (zweite Ableitung). Vor allem bei der zweiten Ableitung darf das Delta nicht mehr zu klein sein, die Kurve franst dann schnell aus. Aber beide Ableitungen liefern die nötigen Informationen. Die Nullstellen der ersten Ableitung zeigen Minima und Maxima der Funktion an, die Nullstellen der zweiten Ableitung zeigen Wendepunkte der Funktion an. Der Wendepunkt ist dort, wo sich die Richtung der Kurve ändert. Wenn man sich vorstellt, man würde die Kurve entlangradeln, muss man am Wendepunkt aufhören nach links zu lenken, ab da lenkt man nach rechts (oder umgekehrt...).
    Die Nullstellen suche ich nach der Halbierungsmethode. Das funktioniert so: Es gibt einen Startwert (meinetwegen +1,0) und ein Intervall (+/-0,5). Jetzt berechne ich den Wert der Funktion oder der Ableitung für den Startwert und das Intervall um den Startwert herum, also für x=0,5, x=1,0 und x=1,5. Wenn sich irgendwo zwischen diesen Punkten das Vorzeichen ändert, liegt dort eine Nullstelle. Nehmen wir an, das ist zwischen x=1,0 und x=1,5. Jetzt ist mein neuer Startwert x=1,25 und das Intervall ist +/-0,25. Damit beginnt das Spiel von Neuem, bis das Ergebnis für f(x) unter einem vorher eingestellten Grenzwert liegt, in meinem Fall 10^-14.
    Auch dazu der Code:

    VB.NET-Quellcode

    1. ' Nullstellensuche mit dem Halbierungsverfahren
    2. Friend Function Bisection(ByVal dblXStart As Double, _
    3. ByVal intDerivationLevel As Integer, _
    4. ByRef dblXResult As Double, _
    5. ByRef dblYResult As Double, _
    6. ByRef intIterations As Integer) As Boolean
    7. Dim blnFound As Boolean = False
    8. Dim intError As Integer = ValfDeclares.ERR_SUCCESS
    9. Dim blnStop As Boolean = False ' True, wenn im angegebenen Bereich keine Nullstelle existiert
    10. Dim dblInterval As Double = mdblBisectionInterval
    11. Dim dblYStart As Double = 0.0
    12. Dim dblXMinus As Double = dblXStart - dblInterval
    13. Dim dblYMinus As Double = 0.0
    14. Dim dblXPlus As Double = dblXStart + dblInterval
    15. Dim dblYPlus As Double = 0.0
    16. With FPlotMain.gobjFunctions.ActiveFunction
    17. intIterations = 0
    18. Do
    19. intError = .Derivation(intDerivationLevel, dblXStart, dblYStart)
    20. If intError = ValfDeclares.ERR_SUCCESS Then
    21. blnFound = CheckBisectionResults(dblXStart, dblYStart, dblXResult, dblYResult)
    22. intError = .Derivation(intDerivationLevel, dblXMinus, dblYMinus)
    23. If intError = ValfDeclares.ERR_SUCCESS Then
    24. blnFound = CheckBisectionResults(dblXMinus, dblYMinus, dblXResult, dblYResult)
    25. intError = .Derivation(intDerivationLevel, dblXPlus, dblYPlus)
    26. If intError = ValfDeclares.ERR_SUCCESS Then
    27. blnFound = CheckBisectionResults(dblXPlus, dblYPlus, dblXResult, dblYResult)
    28. If Not blnFound Then
    29. ' Noch nicht gefunden, Interval halbieren
    30. dblInterval = dblInterval / 2
    31. If Math.Sign(dblYStart) <> Math.Sign(dblYMinus) Then
    32. ' Im negativen Bereich liegt eine Nullstelle
    33. dblXStart = dblXStart - dblInterval
    34. ElseIf Math.Sign(dblYStart) <> Math.Sign(dblYPlus) Then
    35. ' Im positiven Bereich liegt eine Nullstelle
    36. dblXStart = dblXStart + dblInterval
    37. Else
    38. ' Keine Nullstelle im untersuchten Bereich, Suche abbrechen
    39. Dim strText As String = "Im angegebenen Bereich befindet sich "
    40. blnStop = True
    41. Select Case intDerivationLevel
    42. Case 0
    43. strText = strText & "keine Nullstelle."
    44. Case 1
    45. strText = strText & "kein Minimum oder Maximum."
    46. Case 2
    47. strText = strText & "kein Wendepunkt."
    48. End Select
    49. MessageBox.Show(strText)
    50. End If
    51. dblXMinus = dblXStart - dblInterval
    52. dblXPlus = dblXStart + dblInterval
    53. End If
    54. End If
    55. End If
    56. End If
    57. intIterations += 1
    58. Loop Until blnFound Or blnStop Or (intError <> ValfDeclares.ERR_SUCCESS)
    59. If CBool(intError) Then
    60. blnFound = False
    61. MessageBox.Show(FPlotMain.gobjFunctions.GetErrorMessage(intError))
    62. End If
    63. If blnFound And CBool(intDerivationLevel) Then
    64. ' Wert für f(x) ausgeben, wenn es sich um Minima/Maxima oder Wendepunkte handelt
    65. intError = .Derivation(0, dblXResult, dblYResult)
    66. End If
    67. End With
    68. Return blnFound
    69. End Function
    70. ' Resultate der Halbierungsmethode auf Unterschreitung des Limits prüfen und Ergebnis merken
    71. Private Function CheckBisectionResults(ByVal dblXNew As Double, ByVal dblYNew As Double, ByRef dblXResult As Double, ByRef dblYResult As Double) As Boolean
    72. Dim blnFound As Boolean = False
    73. Dim dblLimit As Double = mdblBisectionLimit
    74. blnFound = Math.Abs(dblYNew) < dblLimit ' Ergebnis klein genug?
    75. If blnFound Then ' Ergebnis merken
    76. dblXResult = dblXNew
    77. dblYResult = dblYNew
    78. End If
    79. Return blnFound
    80. End Function


    Gruß
    Carbonunit
    Bilder
    • F-Plot-Ableitungen.jpg

      96,38 kB, 1.920×1.020, 818 mal angesehen

    Makrosprache F-Skript

    Hallo,

    momentane Baustelle ist eine kleine Makrosprache, die in meinem Funktionsplotter Aufgaben automatisieren soll. Dabei habe ich gar nicht den Ehrgeiz, die Grenzen der objektorientierten Programmierung zu erweitern. Viel eher will ich ein bisschen nostalgisch werden und mit GOSUB und Sprungmarken hantieren. Es gibt aber keine Zeilennnummern und ein unbedingter Sprung mit GOTO ist auch nicht vorgesehen. Damit kann man einfach zu viel Unsinn anrichten.
    Den Formelinterpreter habe ich um ein paar boolesche Operatoren, wie Vergleiche, AND, OR und NOT erweitert. Das war nicht weiter schwierig, die Operatoren müssen den richtigen Rang bekommen, also NOT vor AND vor OR vor Vergleichen und alles im Rang unterhalb von Plus und Minus. Fest verdrahtet werden muss dann noch die eigentliche Berechnung der Operatoren und schon funktionieren auch boolesche Ausdrücke. Das ist wichtig für einfache Kontrollstrukturen, mit denen ich angefangen habe. Dazu gibt es einen Debugger, um die Makros auch testen zu können, ohne im Debugger von VB jedes Makro im Debugger des Debuggers zu debuggen. Der sieht so aus:



    Anders, als noch beim guten alten GW-BASIC, an dem ich mich etwas orientiere, muss man bei meiner Sprache die Variablen einmal deklarieren. Numerische Variable, wie NUMD (Double), NUMI (Integer) oder BOOL (Boolean) nimmt der Formelinterpreter in der Symbol-Liste auf. Das sind im Hintergrund alles Double, darum werde ich die Typprüfungen auch nicht sehr streng machen. Strings (Datentyp TEXT) erfordern eine eigene Verwaltung. Zeichenketten aneinanderhängen funktioniert schon. Für Stringvergleiche wird es eine Funktion geben, deren Ergebnis in einer BOOL-Variablen landet und dann weiterverarbeitet werden kann.
    Wer die Funktion direkt in einem mathematischen Ausdruck aufrufen will, wird wohl den Fehler "Formula too complex" ernten, an den ich mich auch noch beim Spaghetti-Coding erinnern kann.
    Die Kommentare in dem obigen Code sind natürlich sinnfrei, sollen aber hier als Test dienen, ob die bei der Ausführung wirklich ignoriert werden. Der Interpreter ist recht primitiv. Was er zu tun hat, sagt ihm der erste Begriff in der Codezeile. Das muss immer ein Schlüsselwort sein, auch bei Variablenzuweisungen. Ganz alte BASIC-Dialekte hatten da auch noch ein LET, man musste also schreiben LET a%=3, statt a%=3. Bei GW-BASIC konnte man das LET schon weglassen, aber es hat noch zum Sprachumfang gehört.
    Mein Debugger kann Haltepunkte setzen, ganz einfach durch Voranstellen von "<b>" in der Codezeile. Dann schaltet der Debugger in den Einzelschrittmodus. Ich kann auch einen bedingten Haltepunkt formulieren, also z.B. "IndexO=2", um das Programm anzuhalten, wenn der Index der äußeren Schleife 2 beträgt.
    Subs und Functions gibt es nicht. Als Kontrollstrukturen sind bis jetzt IF..THEN..[ELSE]..END IF, LOOP..UNTIL, WHILE..WEND, FOR..TO..[STEP]..NEXT und GOSUB..RETURN vorgesehen. Dazu kommt nach CHAIN als fast vergessenes Relikt aus GW-BASIC. Dieser Befehl lädt neuen Code unter Beibehaltung der bisher verwendeten Variablen. Damit nichts durcheinander kommt, darf CHAIN aber nicht ausgeführt werden, so lange das aktuell laufende Programm noch Adressen auf dem Rücksprungstapel hat. Diese Adressen sind natürlich nur Zeilennummern, meine MacroEngine macht intensiven Gebrauch von den schönen List(Of T)-Arrays.
    Eine Fehlerbehandlung wird durch eine feste Sprungmarke definiert. Dorthin springt die Ausführung im Fehlerfall und man kann versuchen, den Fehler zu behandeln oder sich freundlich vom User verabschieden. Bei nichtbehandeten Fehlern soll auch der Debugger aufgerufen werden.
    Wenn ich die String-Funktionen etabliert habe (Alte Bekannte wie INSTR, MID, VAL, RIGHT, LEFT, usw.) werde ich Funktionalitäten aus F-Plot selbst aufrufbar machen. So soll das Makro Dialoge öffnen können, die Textfelder mit Werten füllen, die Berechnung starten und das Ergebnis speichern können.

    Das alles ist als Spaß-Projekt gedacht und ich habe damit gar nicht den Ehrgeiz, technisch an vorderster Front zu stehen. Dafür müsste ich wohl zuerst die sieben Jahre alte Entwicklungsumgebung (VB-Express 2010) austauschen. Wobei ich natürlich weitere Innovationen auch nicht ausschließe. Je nachdem, wie es mich interessiert.
    Gedacht ist die Sprache für die Automatisierung von Abläufen innerhalb des Funktionsplotters F-Plot, meinetwegen das Laden und Zeichnen von zehn Funktionen in zehn verschiedenen Fenstern. Natürlich lädt das zu Spaghetti-Programmierung ein, was ja auch mal wieder ganz spaßig sein kann. Ich habe damals jedenfalls gerne auch mal mit GW-BASIC herumgespielt und ergreife jetzt die Gelegenheit, mir mein Spielzeug selbst zusammenzubauen.

    Gruß
    Carbonunit
    Bilder
    • F-Plot-Debugger2.jpg

      93,06 kB, 1.920×1.020, 731 mal angesehen