uwezi icon

en animerad kurvanpassing (python, manim)

uwezi | PRO | 03/05/22 10:54:26 PM UTC (Edited) | 0 ⭐ | 837 👁️ | Never ⏰ | []
Python |

8.51 KB

|

None

|

0 👍

/

0 👎

from manim import *
 
from matplotlib import pyplot as plt
plt.rcParams['figure.figsize'] = [12, 9]
import scipy.optimize as opt
import numpy as np
import math
 
# jag skapar en lista "guesses" där parametrarna till alla anrop
# av vår modellfunktion kommer sparas - egna anrop såsom anrop via
# "curve_fit"
 
guesses = [] 
 
# här defineras modellfunktionen, en 3e-grads polynom
# "guesses" deklareras som global, dvs ändringar i listan görs globalt
 
def func(x, a, b, c, d):
    global guesses
    guesses.append([a, b, c, d])
    return a*x*x*x + b*x*x + c*x + d
 
# lite förberedelser, en lista med x-koordinater, 100 punkter mellan 0 och 10
xdata = np.linspace(0, 10, 100)
 
# de ursprungliga parametrarna till min 3e-grads funktion som utgångspunkt, dvs
# de SANNA värden som vi ska försöka återskapa ur den brusiga mätdatan senare.
# Jag använde DESMOS för att hitta en funktion som såg rimligt ut
#  f(x) = 0.04 x^3 - 0.4 x^2 + 0.2 x + 4.8
 
parameters = [.04, -0.4, 0.2, 4.8]
 
# sedan gäller det att beräkna de SANNA y-värden i listan "y"
 
y = func(xdata, *parameters)
 
# här skapas normalfördelade slumptal med en standardavvikelse på 0.5
# ( np.random_normal() har en standardavvikelse på 1.0 )
 
y_noise = .5 * np.random.normal(size=xdata.size)
 
# och här fuskar vi fram våra simulerade mätpunkter med de slumpmässiga mätfelen
 
ydata = y + y_noise
 
# vi ger curve_fit() en startpunkt på f(x) = x^3 + x^2 + x + 1 
# därifrån ska sedan de parametrarna bestämmas, som passar bäst till våra mätpunkter
 
startparams = [1, 1, 1, 1]
 
# här görs kurvanpassningen: curve_fit() kommer anropa vår modellfunktion ett
# antal gånger på jakt efter paramterar som minimerar summan av de kvadratiska avstånden
# mellan vår mätpunkter och modellfunktionen
 
optimizedParameters, pcov = opt.curve_fit(f=func, xdata=xdata, ydata=ydata, p0=[1,2,1,1], method="trf", verbose=2)
 
#=========================================================================
# OCH NU BÖRJAR ANIMERINGEN MED MANIM (efter 3blue1brown, Grant Sanderson)
#
# listan "guesses[]" innehåller nu en serie med alla parametrar som har använts
# i funktionsanrop till vår modellfunktion.
# 
#   lista[0] är vårt anrop med de SANNA parametrarna
#
#   till varje steg i anpassningen så utförde curve_fit() faktiskt 5 anrop av
#   vår modellfunktion med bara små ändringar i parametrarna för att internt
#   kunna beräkna de partiella derivatorna av summan av avståndskvadraten
#
class curvefit_x3(Scene):
    def construct(self):
 
        # Axes() skapar representationen av ett koordinatsystem
        ax = Axes(
            x_range=[-0.5,np.ceil(max(xdata)),1],
            y_range=[-0.5,np.ceil(max(ydata)),1],
            x_length=8,
            tips= False,
            )
        
        ax.add_coordinates()
        
        y_label = ax.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=0.4)
        x_label = ax.get_x_axis_label("x")
        
        # self.play() är den typiska animeringen där objekt ritas 3blue1brown-style
        # här ska nu alltså ritas upp koordinatsystemet och texten längs axlarna
        
        self.play(Create(ax),Create(x_label),Create(y_label))
        
        # Dot() kan användas för att rita en punkt i grafen
        # VGroup() skapar en sammanhängande lista av objekt, i det här 
        # fallet en lista med alla våra mätpunkter dvs punkterna som
        # skapades med hjälp av de slumpmässiga felen
        
        # ax.c2p(x,y) behövs så att punkterna hamnar på rätt position i koordinatsystemet
        
        datapts = VGroup()
        for i in range(len(xdata)):
            datapts.add(Dot([ax.c2p(xdata[i],ydata[i])], radius=.05, color=RED))
            
        # nu ritar vi in punkterna i grafen    
        self.play(Create(datapts))
        
        # self.wait() fryser animeringen för den angivna tiden (i sekunder)
        self.wait(1)
 
        # DecimalNumber skapar ett objekt på scenen som visar ett numeriskt värde
        # "sum" är namnet på objektet genom vilket vi kan ändra objektets egenskaper
        # såsom värdet som ska visas
        #
        # MathTex() skapar ett text-objekt med LaTeX-formatering av texten
        
        sum = DecimalNumber(0, num_decimal_places=2,group_with_commas=False)        
        eq  = MathTex(r"\sum_{i=0}^n \left(y_i - f(x_i)\right)^2=")
        
        # sum.next_to() placerar talet "sum" till höger om texten "eq"
        sum.next_to(eq, RIGHT)
        
        # jag grupperar ihop "eq" och "sum" till en ny grupp "sqsum" så
        # att jag kan förflytta dessa två objekt tillsammans på scenen
        
        sqsum = VGroup(eq,sum)
        sqsum.to_edge(UP)
        self.play(Create(sqsum))
 
        # jag gör en kopia av vår lista över parametrar till vår 
        # modellfunktionen - detta på grund av att vi nu kommer 
        # själva anropa modellfunktionen upprepade gånger som varje gång skulle lägga
        # till nya element i listan som vi inte vill ha med i animeringen
        local_guesses = guesses.copy()
        params = startparams
 
        # lambda-funktioner är fortfarande lite suspekta för mig i Python, men 
        # här används en sådan för att rita upp den första kurvan som
        # är vår modellfunktion med startvärden som parametrar som gavs 
        # som startvärden till curve_fit()
        graph = ax.plot(
            lambda x: func(x, *params),
            color = BLUE,
        )   
        self.play(Create(graph))
        
        # och längst nere på skärmen skrivs det ut den faktiska ekvationen med ytterligare ett MathTex-objekt
        eq = MathTex(r"f_(x)= {:.3f}\,x^3 {:+.3f}\,x^2 {:+.3f}\,x {:+.3f}".format(*params), color=BLUE)
        eq.to_edge(DOWN, buff=0.1)                 
        self.play(Create(eq))
        
        # nu skapas det en VGroup() som kommer innehålla de gula linjerna 
        # som ritades mellan varje röd märpunkt och den motsvarande platsen längs
        # den blåa funktionsgrafen
        # För varje x-koordinat xdata[i] räknas det fram det motsvarande funktionsvärdet fi
        # "start" och "end" är de motsvarande start- och slutpunkterna av de gula linjerna
        # och värdet i "sum" ökas med kvadraten av linjens längd
        #
        # detta är alltså den första sådan ritning som då gäller för startvärdet hos parametrarna
        
        fit = VGroup()
        for i in range(len(xdata)):
            fi    = func(xdata[i],*params)
            start = ax.c2p(xdata[i],ydata[i])
            end   = ax.c2p(xdata[i],fi)
            fit.add(Line(start=start, end=end, color=YELLOW))
            self.add(fit)
            self.wait(.2)
            sum.set_value(sum.get_value() + (ydata[i]-fi)**2)
        
        # nu stegar vi oss fram genom hela listan av paramterar som modellfunktionen
        # anropades med. Eftersom det var 5 anrop för varje steg i kurvanpassningen 
        # gör vi en slice där vi bara tar vart femte anrop och vi utelämnar det första anropet
        for params in local_guesses[1::5]:
        
            # samma procedur som ovan 
            graph1 = ax.plot(
                lambda x: func(x, *params),
                color = BLUE,
            )   
 
            newsum = 0
            fit1 = VGroup()
            for i in range(len(xdata)):
                fi    = func(xdata[i],*params)
                start = ax.c2p(xdata[i],ydata[i])
                end   = ax.c2p(xdata[i],fi)
                newsum += (ydata[i]-fi)**2
                fit1.add(Line(start=start, end=end, color=YELLOW))
            eq1 = MathTex(r"f_(x)= {:.3f}\,x^3 {:+.3f}\,x^2 {:+.3f}\,x {:+.3f}".format(*params), color=BLUE)
            eq1.to_edge(DOWN, buff=.1)                 
            self.play(sum.animate.set_value(newsum),Transform(graph,graph1),Transform(fit,fit1),Transform(eq,eq1))    
            self.wait(3)    
            
        # färdigt, vi har gått genom hela sekvensen av kurvanpassningen och ritar en blå ram runt
        # den slutgiltiga funktionen    
        srect = SurroundingRectangle(eq, color=BLUE)
        self.play(Create(srect))    
        
        self.wait(2)
        
        # sista steget är att rita den SANNA funktionen, dvs den som vi ursprungligen började med...     
        graph2 = ax.plot(
            lambda x: func(x, *parameters),
            color = RED,
        ) 
        
        self.play(Create(graph2))  
 
        self.wait(5)

Comments