7.4 Subklasse av ODESolver: skriv `advance`
Implementere en ny ODE-metode ved arv — skrive kun `advance` og arve resten — inkludert baklengs Euler med `scipy.optimize.root`.
Dette er kapitlet som skiller toppen fra midten. Oppgavetypen heter i denne boka sjanger K — subklasse av løser-hierarkiet, altså «utvid det utleverte hierarkiet med en ny metode ved å skrive én eneste metode». Den er topp-differensierende og teller 3–5 poeng: den dukker gjerne opp som siste deloppgave på det avsluttende settet, etter at systemoppgaven er unnagjort.
Belegget i arkivet er konkret. En andreordens Runge–Kutta-metode som subklasse er gitt på settene fra høsten 2018 og høsten 2020. Høsten 2025 kom den tyngste varianten: baklengs Euler som subklasse, der hvert tidssteg løser en likning med root fra scipy.optimize. Den kombinerer tre ting eksamen liker å teste samtidig — differensiallikninger, likningsløsing og arv — og er derfor en sannsynlig gjenganger.
Prioriteten her er nivå 3, «kjenne». Boka bruker tre nivåer: «må sitte» (nivå 1), «må kunne» (nivå 2) og «kjenne» (nivå 3). At dette er nivå 3, betyr ikke at du kan hoppe over det — Del 7 som helhet må sitte — men at du kan få en god karakter uten å ha denne siste deloppgaven helt på plass. C er en god og vanlig karakter, og den får du på systemoppgaven i kap. 7.3, ikke her. Er du ute etter A, er det derimot nettopp her poengene ligger.
Den gode nyheten er at oppgaven er liten. Du skal ikke skrive en løser. Du skal skrive fire til seks linjer inne i en klasse som arver alt annet.
Løkke 1 — arv én metode (~12 min) · Løkke 2 — attributtene inne i metoden (~14 min) · Løkke 3 — egne parametre i subklassen (~13 min) · Løkke 4 — implisitt metode med likningsløsing (~16 min)
Forkunnskaper — sist du var her
1. Hierarkiet i bruk (kap. 7.2 og kap. 7.3). Tre kall, og løsningen er ferdig. Her er utgangspunktet vi bygger videre på:
from ODESolver import ForwardEuler
def f(t, u):
return -0.3*u + 1.2
solver = ForwardEuler(f)
solver.set_initial_condition(10)
t, u = solver.solve((0, 20), 40)
print(f"{len(t)} punkter, u(20) = {u[-1]:.6f}")Utskrift:
41 punkter, u(20) = 4.009014Det viktige for dette kapitlet er hva som skjer inne i solve: den lager tidsarrayet, setter startverdien og går i løkke over range(N). For hvert steg kaller den én metode — advance() — som regner ut den neste verdien. Det er den metoden, og bare den, du skal skrive.
2. Arv med super().__init__ (kap. 5.3). En subklasse arver alt fra basisklassen. Skriver den sin egen __init__ — altså sin egen oppstartsmetode — må basisklassens oppstart kalles først, ellers mister du attributtene den setter:
class Maaler:
def __init__(self, enhet):
self.enhet = enhet
def vis(self, x):
return f"{x} {self.enhet}"
class Presis(Maaler):
def __init__(self, enhet, desimaler):
super().__init__(enhet)
self.desimaler = desimaler
def vis(self, x):
return f"{x:.{self.desimaler}f} {self.enhet}"
print(Maaler("m").vis(3.14159))
print(Presis("m", 3).vis(3.14159))Utskrift:
3.14159 m
3.142 m3. root fra scipy.optimize (kap. 6.1). Løser numerisk. Funksjonen tar ett array inn og gir ett array ut, startgjetningen er også et array, og svaret ligger i sol.x:
import numpy as np
from scipy.optimize import root
def g(x):
return np.array([x[0]**3 - 2*x[0] - 5])
sol = root(g, np.array([2.0]))
print(f"{sol.x[0]:.6f}", sol.success)Utskrift:
2.094551 TrueTrenger du bare det første og det andre punktet, kan du lese Løkke 1 til 3 og la Løkke 4 ligge til repetisjonen — den siste løkka er nivå 3-stoffet.
Løkke 1 — arbeidsdelingen: løkka nedarves, formelen skrives
Tenk på en bilfabrikk som lager samme understell til alle modellene sine. Karosseriet varierer, understellet gjør det ikke. ODESolver er understellet: den vet hvordan et tidsarray lages, hvor startverdien settes inn, og hvor mange ganger det skal itereres. Karosseriet er formelen for ett steg — og den er forskjellig for Euler, Heun og Runge–Kutta.
Derfor er svaret på «implementér en ny ODE-metode» alltid det samme: arv fra ODESolver og skriv bare advance. Å skrive om hele klassen er ikke bare unødvendig arbeid; det er en feilkilde, og sensor gir ikke ekstra poeng for det.
En liten, men avgjørende detalj: advance skal returnere den neste verdien. Den skal ikke skrive den inn i self.u selv. Basisklassen tar imot returverdien og legger den på riktig plass.
Arv fra basisklassen i løser-hierarkiet. Klassen på innsiden av parentesen kalles basisklassen, og den nye klassen arver alle metodene dens: set_initial_condition, solve og oppstartsmetoden __init__.
Subklassen trenger derfor ingen __init__ med mindre den skal lagre noe eget, og den skal aldri skrive sin egen solve. Bruken er identisk med de utleverte metodene:
from ODESolver import ODESolver
class Metode(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
return u[n] + dt*f(t[n], u[n])
def f(t, u):
return -u
solver = Metode(f)
solver.set_initial_condition(1)
t, u = solver.solve((0, 1), 4)
print(f"{u[-1]:.6f}")Utskrift:
0.316406Merk hvor lite som står i klassen: én metode, to linjer. Alt det andre kom med arven.
Dette er malen du skal kunne skrive fra bunnen, uten å slå opp. Den er den samme hver gang, og bare formellinja skifter.
1. Klasselinja. class NavnPaaMetoden(ODESolver): — navnet er ditt, basisklassen er den utleverte.
2. Metodelinja. def advance(self): — ingen argumenter utenom self. Alt metoden trenger, ligger allerede på instansen.
3. Uthentingslinja. u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt — korte lokale navn, slik at formelen blir lesbar. Den er valgfri; du kan skrive self.u[self.n] hele veien, men da blir linjene lange.
4. Formelen, med return. Ett eller flere prøvesteg, og til slutt return av den nye verdien.
from ODESolver import ODESolver
class Heun(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
k1 = f(t[n], u[n])
k2 = f(t[n] + dt, u[n] + dt*k1)
return u[n] + dt*(k1 + k2)/2Utskrift: ingen (bare klassedefinisjonen; den brukes i Eksempel 1).
Malen virker uendret for systemer. Er u[n] en vektor med fire komponenter, er k1 også en vektor med fire komponenter, og aritmetikken er elementvis. Du skriver altså samme advance enten likningen har én eller ti ukjente — det er en av grunnene til at hierarkiet er bygget slik.
Den eksakte løsningen er . Utvid hierarkiet med Heuns metode som en subklasse, løs på med 40 steg, og mål hvor stor feilen blir.
Heuns metode tar først et Euler-steg som prøvesteg, regner høyresiden på nytt der, og bruker gjennomsnittet av de to stigningstallene.
import numpy as np
from ODESolver import ODESolver
class Heun(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
k1 = f(t[n], u[n])
k2 = f(t[n] + dt, u[n] + dt*k1)
return u[n] + dt*(k1 + k2)/2
def f(t, u):
return -0.3*u + 1.2
solver = Heun(f)
solver.set_initial_condition(10)
t, u = solver.solve((0, 20), 40)
eksakt = 4 + 6*np.exp(-0.3*t)
print(f"{len(t)} punkter, dt = {t[1] - t[0]}")
print(f"u(20) = {u[-1]:.6f} eksakt = {eksakt[-1]:.6f}")
print(f"maksfeil = {np.max(np.abs(u - eksakt)):.2e}")Utskrift:
41 punkter, dt = 0.5
u(20) = 4.015252 eksakt = 4.014873
maksfeil = 9.27e-03Sammenlign med forkunnskapsblokken øverst: ForwardEuler ga på nøyaktig samme oppgave, med en største avvik på . Heun kommer til mot den eksakte , og største avvik er — nesten tjue ganger mindre, for prisen av to ekstra linjer og ett ekstra kall til høyresiden per steg. Det er hele poenget med å legge til en metode.
Legg merke til hva som ikke står i klassen: ingen __init__, ingen solve, ingen set_initial_condition, ingen løkke og ingen np.linspace. Fire linjer, og resten er arvet.
(Innsteg — hva arves, og hva skriver du?) Du skal utvide det utleverte hierarkiet med en ny metode.
a) Hvilke tre metoder arver subklassen fra ODESolver uten at du skriver en linje?
b) Hva skal advance returnere?
c) Du løser en skalar likning på med 24 steg. Hvor mange ganger blir advance kalt, og hvor mange rader har u?
(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Modulen ODESolver er den fra kap. 7.2. Skriv utskriften nøyaktig.
from ODESolver import ODESolver
class HalvtSteg(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
return u[n] + dt*f(t[n], u[n])/2
def f(t, u):
return u
solver = HalvtSteg(f)
solver.set_initial_condition(2)
t, u = solver.solve((0, 2), 4)
print(len(u))
print(u)Løkke 2 — de fem attributtene du har til rådighet (~14 min)
advance tar ingen argumenter. Alt den trenger, ligger allerede lagret på instansen, og basisklassen har oppdatert det før hvert kall. Kjenner du de fem navnene, kan du skrive hvilken som helst eksplisitt metode.
Den vanligste feilen her er ikke matematisk, men navnemessig: du skriver self.dt i en subklasse der den utleverte modulen ikke har noe self.dt. Derfor kommer den robuste formen rett etter tabellen — den virker uansett årgang.
De fem attributtene basisklassen stiller til rådighet. Et attributt er en verdi som er lagret på objektet selv.
| Attributt | Hva det er | Typisk bruk |
|---|---|---|
self.u | hele løsningsarrayet, med radene før n ferdig utfylt | self.u[self.n] er verdien nå |
self.t | hele tidsarrayet | self.t[self.n] er tiden nå |
self.n | indeksen for steget som skal gjøres | grensetilfeller, som det aller første steget |
self.f | høyresiden, kallbar som self.f(t, u) | self.f(self.t[self.n], self.u[self.n]) |
self.dt | steglengden | faktoren foran stigningstallet |
Alle fem er satt av basisklassen før kallet. Du skal aldri sette dem selv inne i
advance, og du skal ikke skrive til self.u — bare lese fra det.Én ting til: fordi
self.u inneholder alle tidligere verdier, kan du også se bakover. self.u[self.n-1] er verdien fra forrige steg, og det er nettopp det flerstegsmetoder trenger.Den robuste måten å finne steglengden på. Den regner differansen mellom to nabotidspunkter i tidsarrayet, i stedet for å stole på at modulen har lagret et attributt som heter dt.
Grunnen til å kunne den: attributtnavnene i modulen du får utlevert kan variere fra år til år. Tidsarrayet heter derimot alltid self.t, for det er halvparten av det solve returnerer.
from ODESolver import ODESolver
class EulerRobust(ODESolver):
def advance(self):
u, f, t, n = self.u, self.f, self.t, self.n
dt = t[n+1] - t[n]
return u[n] + dt*f(t[n], u[n])
def f(t, u):
return -u
solver = EulerRobust(f)
solver.set_initial_condition(1)
t, u = solver.solve((0, 2), 8)
print(f"{u[-1]:.6f}")Utskrift:
0.100113Indeksen n+1 er alltid trygg her: advance kalles bare for n opp til N-1, og tidsarrayet har N+1 elementer.
med , og startverdiene , , . Skriv midtpunktsmetoden som subklasse — den tar et halvt Euler-steg fram til midten av intervallet, regner stigningstallet der, og bruker det for hele steget — og løs på med 400 steg. Når topper mellomproduktet B seg?
Subklassen bruker den robuste formen for dt, så den virker også om årets vedlegg ikke har self.dt.
import numpy as np
from ODESolver import ODESolver
class Midtpunkt(ODESolver):
def advance(self):
u, f, t, n = self.u, self.f, self.t, self.n
dt = t[n+1] - t[n]
k1 = f(t[n], u[n])
k2 = f(t[n] + dt/2, u[n] + dt*k1/2)
return u[n] + dt*k2
ka, kb = 0.7, 0.25
def f(t, u):
A, B, C = u
return [-ka*A, ka*A - kb*B, kb*B]
solver = Midtpunkt(f)
solver.set_initial_condition([1.0, 0.0, 0.0])
t, u = solver.solve((0, 20), 400)
A, B, C = u[:, 0], u[:, 1], u[:, 2]
print("u.shape =", u.shape)
print(f"B topper seg ved t = {t[np.argmax(B)]:.2f} med B = {B.max():.4f}")
print(f"ved t = 20: A = {A[-1]:.5f} B = {B[-1]:.5f} C = {C[-1]:.5f}")
print(f"summen ved t = 20: {A[-1] + B[-1] + C[-1]:.6f}")Utskrift:
u.shape = (401, 3)
B topper seg ved t = 2.30 med B = 0.5643
ved t = 20: A = 0.00000 B = 0.01048 C = 0.98952
summen ved t = 20: 1.000000To ting er verdt å merke seg. For det første er advance ordrett den samme som i skalartilfellet — u[n] er nå en vektor med tre tall, og k1 og k2 likeså, men aritmetikken er elementvis og formelen uendret. For det andre er summen nøyaktig 1 hele veien: stoffet forsvinner ikke, det flytter seg. Den slags bevart størrelse er en gratis fornuftssjekk, og på eksamen er det verdt å nevne den i en setning.
Legg også merke til at parametrene heter ka og kb, ikke k1 og k2. Prøvestegene inne i advance heter allerede k1 og k2, og selv om de er lokale variabler og derfor ikke krasjer, blir koden umulig å lese med to betydninger av samme bokstav.
__init__, set_initial_condition og en egen solve på nytt. Det tar ti minutter, gir ingen ekstra poeng, og introduserer nye feil. Sensor ser etter én ting: at du har forstått at bare advance skal skrives.Å skrive i stedet for å returnere. Skriver du u[n+1] = ... inne i advance og glemmer return, returnerer metoden None. Basisklassen legger da None inn i et flyttallsarray, og du får nan i alle radene — uten feilmelding. Dette er den styggeste feilen i kapitlet, fordi programmet ser ut til å kjøre.
Feil attributtnavn. Er du usikker på om vedlegget har self.dt, bruk self.t[self.n+1] - self.t[self.n]. Er du usikker på om stegtelleren heter self.n eller noe annet, må du lese vedlegget — det er den ene tingen du ikke kan gjette deg til.
#3 — glemme self. Å skrive u[n] uten å ha hentet det ut fra self.u først gir NameError. Uthentingslinja øverst i advance finnes nettopp for å gjøre dette vanskeligere å glemme.
#14 — API-drift. Høyresiden kalles self.f(t, u), tid først. I eldre sett var rekkefølgen motsatt, f(u, t). Bruker du feil rekkefølge i et system der høyresiden ikke bruker t, får du ingen feilmelding — bare feil svar. Les vedlegget før du skriver.
Rekursjon. Det kan friste å la advance kalle seg selv for å ta et halvt steg til. Ikke gjør det: rekursjon er utenfor pensum i dette emnet og markeres som uønsket i de offisielle løsningsforslagene. Trenger du to prøvesteg, skriver du dem som to linjer.
a) Skriv subklassen Ralston av ODESolver med kun metoden advance.
b) En bakteriekultur vokser logistisk, , med celler. Løs med Ralston på (i timer) med 300 steg, og skriv ut steglengden, bestanden etter 10 timer og bestanden ved slutten.
En medstudents subklasse kjører uten feilmelding, men gir dette:
[ 1. nan nan nan nan]Koden er:
class Glemsom(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
u[n+1] = u[n] + dt*f(t[n], u[n])a) Forklar hvorfor utskriften blir nan fra og med den andre verdien, når selve Euler-formelen er riktig.
b) Rett feilen med minst mulig endring.
Løkke 3 — når subklassen trenger noe eget (~13 min)
Så langt har subklassene bare hatt advance. Noen ganger trenger metoden en parameter — en vekt, en toleranse, en teller. Da må subklassen ha sin egen oppstartsmetode, og det er her arven kan gå galt.
Basisklassens __init__ gjør noe viktig: den tar imot høyresiden f og pakker den slik at den alltid gir et NumPy-array tilbake. Overskriver du __init__ uten å kalle basisklassens, mister du den pakkingen, og self.f finnes ikke i det hele tatt. Symptomet er AttributeError ved første steg.
Løsningen er én linje, og den skal stå først.
Kallet som kjører basisklassens oppstartsmetode fra subklassens egen. Det skal stå som første linje i subklassens __init__, før du setter dine egne attributter.
from ODESolver import ODESolver
class EulerMedTelling(ODESolver):
def __init__(self, f):
super().__init__(f)
self.antall_kall = 0
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
self.antall_kall = self.antall_kall + 1
return u[n] + dt*f(t[n], u[n])
def f(t, u):
return -0.5*u
solver = EulerMedTelling(f)
solver.set_initial_condition(4)
t, u = solver.solve((0, 6), 60)
print(f"u(6) = {u[-1]:.5f}")
print("kall til hoyresiden:", solver.antall_kall)Utskrift:
u(6) = 0.18428
kall til hoyresiden: 60Regelen er den samme som i kap. 5.3: super().__init__(...) først, egne attributter etterpå. Trenger subklassen ingen egne attributter, skriver du ingen __init__ i det hele tatt — den arves.
Tallet 60 er verdt en tanke: forlengs Euler kaller høyresiden én gang per steg. Runge–Kutta 4 ville brukt 240 kall på samme antall steg. Det er den prisen du betaler for høyere orden.
Med får du midtpunktsmetoden, med Ralstons metode og med Heuns metode. Skriv én subklasse som dekker alle tre, og sammenlign dem på , , over med 50 steg. Den eksakte løsningen er .
Parameteren lagres i subklassens egen oppstartsmetode, med basisklassens kalt først.
import numpy as np
from ODESolver import ODESolver
class RK2Alpha(ODESolver):
def __init__(self, f, alpha):
super().__init__(f)
self.alpha = alpha
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
a = self.alpha
k1 = f(t[n], u[n])
k2 = f(t[n] + a*dt, u[n] + a*dt*k1)
return u[n] + dt*((1 - 1/(2*a))*k1 + k2/(2*a))
def f(t, u):
return -u + np.sin(t)
for alpha in [0.5, 2/3, 1.0]:
solver = RK2Alpha(f, alpha)
solver.set_initial_condition(1)
t, u = solver.solve((0, 10), 50)
eksakt = 1.5*np.exp(-t) + (np.sin(t) - np.cos(t))/2
print(f"alpha = {alpha:.4f} u(10) = {u[-1]:.6f} maksfeil = {np.max(np.abs(u - eksakt)):.2e}")Utskrift:
alpha = 0.5000 u(10) = 0.144672 maksfeil = 5.71e-03
alpha = 0.6667 u(10) = 0.144362 maksfeil = 5.26e-03
alpha = 1.0000 u(10) = 0.143827 maksfeil = 5.94e-03Alle tre er andreordens, og feilene er derfor av samme størrelsesorden. Ralstons valg er marginalt best her, og det er ikke tilfeldig: den er konstruert for å gjøre den ledende feilkoeffisienten så liten som mulig. Forskjellen er likevel liten nok til at valget mellom dem sjelden betyr noe i praksis.
Det instruktive er konstruksjonen: én klasse, tre metoder, og instansieringen RK2Alpha(f, 2/3) tar nå to argumenter i stedet for ett. Så snart subklassen har en egen __init__, endres måten du lager løseren på — og det er lett å glemme når du skal bruke den senere i besvarelsen.
EulerSkalert(ODESolver) som tar imot høyresiden og et positivt tall vekt, og som gjør et forlengs Euler-steg der bidraget fra høyresiden ganges med vekt:a) Skriv klassen. Husk at den skal kunne lages med EulerSkalert(f, 1.0).
b) Løs , , på med 8 steg for vekt lik 1,0 og 0,5, og skriv ut sluttverdien for hver. Forklar i én setning hvorfor den ene er riktig og den andre ikke.
a) Skriv subklassen RK3.
b) Bruk den på , , over , som har den eksakte løsningen . Regn ut den største avviket for .
c) Hva sier tallrekken om ordenen til metoden?
Løkke 4 — implisitte metoder: når hvert steg er en likning (~16 min)
— naturlig pausepunkt —
Denne siste løkka er nivå 3-stoffet i kapitlet: bør kjenne til. Den er den tyngste enkeltdelen i Del 7, og den er samtidig den mest sannsynlige toppoppgaven. Har du kort tid, ta den etter at resten av Del 7 sitter.
Alle metodene så langt har vært eksplisitte: den nye verdien regnes ut av gamle verdier, ferdig. Baklengs Euler er implisitt — den bruker høyresiden evaluert i det nye punktet:
Den ukjente står på begge sider. Det er ikke en formel du kan regne ut; det er en likning du må løse, én gang per tidssteg. Og det er nettopp derfor oppgaven er attraktiv på eksamen: den krever at du kombinerer ODE-løsing med root fra kap. 6.1.
Hvorfor gidde? Fordi implisitte metoder er stabile. For såkalt stive problemer — der noe skjer svært raskt i forhold til tidsrommet du er interessert i — sprenger forlengs Euler med mindre steget er absurd lite, mens baklengs Euler holder seg i ro. Eksempel 4 viser forskjellen.
Nullpunktet er den nye verdien. Alt annet — , og — er kjent når steget starter, og fungerer som konstanter i likningen.
Formen er den samme for systemer: da er en vektor, returnerer en vektor like lang, og du løser like mange likninger som du har ukjente funksjoner. Du skriver ikke noe ekstra for det tilfellet.
Startgjetningen er nesten alltid u[n], altså den forrige verdien. Den er per definisjon nær svaret når steget er lite, og det er akkurat det en nullpunktsmetode trenger for å konvergere raskt.
Nullpunktsløseren fra kap. 6.1, brukt én gang per tidssteg. Mønsteret er fast:
from scipy.optimize import root
from ODESolver import ODESolver
class BaklengsEuler(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
def likning(v):
return v - u[n] - dt*f(t[n+1], v)
sol = root(likning, u[n])
return sol.xUtskrift: ingen (bare klassedefinisjonen; den brukes i Eksempel 4).
Tre detaljer avgjør om det virker:
- likning defineres inne i advance. Den må se u[n], t[n+1] og dt, og de er forskjellige for hvert steg. En funksjon definert inne i en annen ser den ytre funksjonens variabler.
- Tiden er t[n+1], ikke t[n]. Det er hele forskjellen på baklengs og forlengs Euler. Skriver du t[n], har du laget en tungvint variant av forlengs Euler.
- Svaret er sol.x. root returnerer et resultatobjekt, ikke et tall. sol.x er alltid et array — også når likningen har én ukjent — og det er nettopp den formen basisklassen vil ha.
Dokumentasjonen for root leveres som vedlegg på eksamen, så signaturen trenger du ikke pugge. Innpakningen må du kunne.
Den eksakte løsningen er , som er nede på romtemperatur nesten med én gang. Løs på med bare 8 steg, med ForwardEuler fra vedlegget og med en egenskrevet baklengs Euler. Hva skjer?
import numpy as np
from scipy.optimize import root
from ODESolver import ODESolver, ForwardEuler
class BaklengsEuler(ODESolver):
def advance(self):
u, f, t, n, dt = self.u, self.f, self.t, self.n, self.dt
def likning(v):
return v - u[n] - dt*f(t[n+1], v)
sol = root(likning, u[n])
return sol.x
def f(t, u):
return -15*(u - 20)
for Metode in [ForwardEuler, BaklengsEuler]:
solver = Metode(f)
solver.set_initial_condition(100)
t, u = solver.solve((0, 2), 8)
eksakt = 20 + 80*np.exp(-15*t)
print(f"{Metode.__name__:15s} u(2) = {u[-1]:14.4f} eksakt = {eksakt[-1]:.4f}")Utskrift:
ForwardEuler u(2) = 261688.5559 eksakt = 20.0000
BaklengsEuler u(2) = 20.0003 eksakt = 20.0000Steglengden er , og forlengs Euler ganger med hvert steg. Tallet skifter fortegn og blir 2,75 ganger større hver runde, og etter åtte steg er temperaturen «over to hundre tusen grader». Det er ikke en programmeringsfeil — det er metodens stabilitetsgrense, og den er nådd fordi problemet er stivt.
Baklengs Euler ganger med i stedet, og faller derfor pent mot 20 uansett hvor stort steget er. Prisen er at hvert steg krever et kall til root.
Merk at for Metode in [ForwardEuler, BaklengsEuler] løkker over selve klassene, ikke over instanser. Metode(f) lager instansen inne i løkka, og Metode.__name__ gir klassenavnet som tekst. Det er en kompakt måte å sammenligne to metoder på, og den er verdt å kunne til en «sammenlign metodene»-deloppgave.
med og .
a) Skriv subklassen BaklengsEuler(ODESolver) med kun advance, der hvert steg løser likningen for den nye verdien med root.
b) Løs på med 50 steg. Skriv ut formen på u, sluttverdiene og summen ved slutten.
c) Hvorfor er summen en god fornuftssjekk her, og hvorfor er det en fordel at metoden er implisitt?
f(t, u) og kallet solver.solve(t_span, N), som er formen fra 2023 og senere. På settene fra 2020 til 2022 var høyresiden f(u, t) og løseren ble kalt solve(time_points) med et ferdig array av tidspunkter. Det er feilkode #14 — API-forveksling — og den rammer subklasser hardt, fordi advance da må kalle self.f(u[k], t[k]) og finne steget fra tidsarrayet. Begge variantene står i sin helhet i kapitlets siste boks og i kap. 7.2.Steg-indeksen kan hete noe annet. I den eldre varianten heter den self.k. Skriver du self.n da, får du AttributeError ved første steg. Dette er den ene tingen du må lese ut av vedlegget — den kan ikke gjettes.
self.dt finnes ikke alltid. Bruk dt = self.t[self.n+1] - self.t[self.n] når du er usikker.
Rekursjon. En implisitt metode frister til å «kalle seg selv til svaret er godt nok». Ikke gjør det: rekursjon er utenfor pensum i dette emnet og markeres som uønsket i de offisielle løsningsforslagene. Bruk root, eller en while-løkke om oppgaven ber om en egen iterasjon.
Til slutt: riktig overordnet struktur belønnes. Har du riktig klasselinje, riktig advance-signatur og riktig oppbygging av formelen, gir sensor uttelling selv om en koeffisient eller en indeks er feil. Å få alt 100 % riktig uten å kunne kjøre koden er nesten umulig, og oppgavene er poengsatt deretter.
Det finnes ikke noe punkt før det første, så det aller første steget må gjøres med forlengs Euler.
a) Skriv subklassen AdamsBashforth2(ODESolver) med kun advance. Behandle det første steget som et spesialtilfelle.
b) To vanntanker står over hverandre. Den øvre tømmes med raten per time, alt renner ned i den nedre, og den nedre tømmes med raten per time:
med og kubikkmeter. Løs på timer med 500 steg, og skriv ut formen på u, når den nedre tanken er på sitt fylleste, og volumene ved slutten.
c) Plott begge volumene mot tiden med navngitte akser og legende, og beskriv figuren i ord.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
Bokas navn på oppgavetypen «utvid det utleverte løser-hierarkiet med en ny metode». Den er topp-differensierende og teller 3–5 poeng, ofte som siste deloppgave på det avsluttende settet.
Kjennetegnet i oppgaveteksten: du får en formel for ett steg og blir bedt om å implementere den «ved arv» eller «som en subklasse». Svaret er alltid en klasselinje, en advance og ingenting mer.
class Ny(ODESolver):. Subklassen er den nye. Subklassen arver alle basisklassens metoder og kan legge til nye eller overskrive enkelte.I løser-hierarkiet er arbeidsdelingen fast: basisklassen eier tidsløkka, subklassen eier formelen for ett steg.
En metode der den nye verdien regnes ut av verdier som allerede er kjent. Forlengs Euler, Heun, midtpunkt, Ralston og Runge–Kutta 4 er alle eksplisitte.
Fordelen er at hvert steg er én utregning. Ulempen er en stabilitetsgrense: for stive problemer må steget være svært lite, ellers sprenger løsningen.
En metode der den nye verdien står på begge sider av likhetstegnet, slik som baklengs Euler: .
Hvert steg krever at du løser en likning — i denne boka med root. Prisen er regnearbeid; gevinsten er stabilitet uansett steglengde.
Et problem der noe skjer svært raskt sammenlignet med tidsrommet du er interessert i — for eksempel studert over to tidsenheter.
En eksplisitt metode må da ta absurd små steg for å holde seg i ro. En implisitt metode slipper unna med store steg. Kjennetegnet i en oppgave er en stor negativ rate.
Mellomregninger inne i advance: høyresiden evaluert i punkter du foreløpig bare gjetter deg fram til. Antallet prøvesteg er omtrent metodens orden, og hvert prøvesteg koster ett kall til høyresiden.
Navnene k1, k2, k3 er standard. Bruk dem ikke om modellparametre samtidig — kall parametrene noe annet, som ka og kb.
Halvér steget og se hva feilen ganges med. Faktor 2 betyr orden 1, faktor 4 orden 2, faktor 8 orden 3 og faktor 16 orden 4.
Tallene treffer sjelden eksakt for grove steg; du ser på tendensen over flere halveringer, ikke på ett enkelt forholdstall.
En metode som bruker mer enn den siste verdien — for eksempel Adams–Bashforth med to steg, som også trenger u[n-1] og høyresiden der.
Fordi self.u inneholder hele historien, er det lett å slå opp bakover. Det aller første steget må gjøres med en énstegsmetode, siden u[-1] ville pekt på siste rad i arrayet.
Det arrayet root starter fra. I en implisitt ODE-metode er u[n] det naturlige valget: den forrige verdien ligger nær den nye når steget er lite.
En dårlig startgjetning kan gi sol.success lik False, og da er svaret i sol.x ikke til å stole på.
En funksjon definert inne i en annen funksjon eller metode. Den ser den ytre funksjonens lokale variabler, og det er nettopp derfor likning(v) defineres inne i advance: den trenger u[n], t[n+1] og dt, som skifter for hvert steg.
Definerer du likning utenfor, må du sende alle disse verdiene inn som argumenter — det virker, men blir tungvint.
Resultatet når advance glemmer return: metoden gir None, basisklassen lagrer det i et flyttallsarray, og alle radene fra og med den andre blir nan.
Symptomet er farlig fordi ingen exception kastes. Ser du nan i et løsningsarray, er den første mistanken en manglende return.
Sensorprinsippet som gjelder hele Del 7: riktig klasselinje, riktig advance-signatur og riktig oppbygging av formelen gir uttelling selv når en koeffisient eller en indeks er feil.
Å skrive plettfri kode uten å kunne kjøre den er nesten umulig, og oppgavene er poengsatt med det i tankene. Skriv derfor strukturen først, og fyll inn detaljene etterpå.
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.
Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.