6.3 DRILL — Differenslikninger

Full drill på H-sjangeren, med roterende innpakning og fast fokus på oppdateringsrekkefølge og plott.

75 min
10 oppgaver
DRILLDifferenslikninger
Din fremgang i kapitlet
0 / 10 oppgaver

Sist du var her

Drillen forutsetter kap. 6.2 (differenslikninger), kap. 4.1 (NumPy-tabeller) og kap. 4.3 (plotting). Her er de tre grepene du kommer til å bruke i hver eneste oppgave, oppfrisket med kjørt utskrift.

1. Allokeringsmønsteret. Lag tabellen, sett startverdien før løkka, oppdater n+1.

import numpy as np

def simuler(x0, N):
    x = np.zeros(N+1)
    x[0] = x0
    for n in range(N):
        x[n+1] = 0.8*x[n] + 4
    return x

print(simuler(50.0, 4))

Utskrift:

[50.    44.    39.2   35.36  32.288]

2. Tellingen. N skritt gir N+1 verdier, fordi startverdien også er en verdi.

import numpy as np

x = np.zeros(5)
x[0] = 2.0
for n in range(3):
    x[n+1] = 3*x[n] - 1
print(x)
print(x.size, x[-1])

Utskrift:

[ 2.  5. 14. 41.  0.]
5 0.0

Legg merke til den siste nullen: tabellen har fem plasser, men løkka gjorde bare tre oppdateringer, så x[4] er aldri skrevet til. Det er nettopp av-én-feilen i sin mest lumske form — programmet krasjer ikke, det gir bare en tabell med en null på slutten.

3. Plottet. Én label= per kurve, ett plt.legend(), og navngitte akser.

import matplotlib.pyplot as plt

plt.plot(t, B, label='byttedyr')
plt.plot(t, R, label='rovdyr')
plt.xlabel('tid (maaneder)')
plt.ylabel('antall dyr')
plt.legend()
plt.show()

Blokken over er et fragment: t, B og R kommer fra simuleringen og finnes ikke alene, så den er ikke kjørbar som den står.

Er noe av dette uklart, les kap. 6.2 først — drillen forklarer ikke konstruksjonene på nytt.

Forkunnskaper

- Differenslikninger — allokering, startverdi og oppdateringsrekkefølge: kap. 6.2.
- NumPy-tabeller, np.zeros og np.linspace: kap. 4.1.
- Plotting med label= og plt.legend(): kap. 4.3.
- Løkker og range: kap. 1.3.
- Funksjoner som returnerer flere verdier: kap. 1.6.
- f-strenger til pen utskrift: kap. 1.4.

📜Løsningsoppskriften for sjanger H — sju steg

Les likningene i oppgaven, og gjør så dette, i rekkefølge.

1. Tell størrelsene. Én likning gir én tabell; to koblede gir to tabeller. Navnene tas fra oppgaven: B og R, S og I, x og y.
2. Allokér med np.zeros(N+1) — én linje per størrelse. N er antall skritt, og N+1 er antall verdier.
3. Sett startverdiene før løkka: x[0] = x0, y[0] = y0. Glemmer du én, starter den størrelsen på 0 og hele forløpet blir feil.
4. Én løkke, for n in range(N): — ikke to løkker etter hverandre. Begge størrelsene oppdateres i samme runde.
5. Bestem oppdateringsrekkefølgen. Spør: bruker den ene likningen den nye verdien av den andre? Står det yn+1y_{n+1} i uttrykket for xn+1x_{n+1}, må y[n+1] regnes først. Bruker begge bare xnx_n og yny_n, spiller rekkefølgen ingen rolle.
6. return tabellene i den rekkefølgen oppgaven navngir dem, utenfor løkka.
7. Del (b) — plottet. Lag tidsaksen med np.linspace(0, T, N+1) eller bruk range(len(x)), ett plot-kall per kurve med label=, aksenavn, legend() og show().

Skjelettet er alltid dette, og det er verdt å skrive ned først, før du fyller inn uttrykkene:

import numpy as np

def simuler(x0, y0, N):
    x = np.zeros(N+1)
    y = np.zeros(N+1)
    x[0] = x0
    y[0] = y0
    for n in range(N):
        y[n+1] = y[n]
        x[n+1] = x[n]
    return x, y

x, y = simuler(3.0, 1.0, 4)
print(x, y)

Utskrift:

[3. 3. 3. 3. 3.] [1. 1. 1. 1. 1.]

Skjelettet gir uttelling i seg selv — her holder hver størrelse seg konstant, fordi oppdateringslinjene ennå ikke gjør noe. Fyller du inn uttrykkene feil, mister du ett eller to poeng; glemmer du skjelettet, mister du oppgaven.

Det gjennomarbeidede caset (~20 min)

Under står en komplett, todelt sjanger-H-oppgave av samme type og vekt som på et virkelig sett, med sensorkommentar ved hvert steg.

Oppgaven (9 poeng). I et økosystem beskrives antall byttedyr BnB_n og rovdyr RnR_n ved tidspunkt nn av differenslikningene

Bn+1=Bn+Δt(aBnbBnRn)B_{n+1} = B_n + \Delta t\,(aB_n - bB_nR_n)
Rn+1=Rn+Δt(cBnRndRn)R_{n+1} = R_n + \Delta t\,(cB_nR_n - dR_n)

der aa er byttedyrenes fødselsrate, bb hvor effektivt rovdyrene jakter, cc hvor mye et fanget byttedyr bidrar til rovdyrbestanden, og dd rovdyrenes dødsrate.

a) (6 p) Skriv lotka_volterra(B0, R0, a, b, c, d, dt, N) som returnerer tabellene B og R.

b) (3 p) Skriv koden som simulerer 200 måneder med B0=40B_0 = 40, R0=9R_0 = 9, a=0,4a = 0{,}4, b=0,02b = 0{,}02, c=0,01c = 0{,}01, d=0,3d = 0{,}3, Δt=0,05\Delta t = 0{,}05 og N=4000N = 4000, og plotter begge bestandene mot tid. Beskriv figuren.

✏️Modellbesvarelse del (a): to tabeller, én løkke

Del (a), 6 poeng: lotka_volterra(B0, R0, a, b, c, d, dt, N) skal returnere tabellene B og R.

Gå gjennom oppskriften. To størrelser gir to tabeller. Begge likningene bruker bare BnB_n og RnR_n — de gamle verdiene — så rekkefølgen inne i løkka spiller ingen rolle her. Det er verdt å si i én setning i besvarelsen: det viser at du har sjekket.

import numpy as np

def lotka_volterra(B0, R0, a, b, c, d, dt, N):
    B = np.zeros(N+1)
    R = np.zeros(N+1)
    B[0] = B0
    R[0] = R0
    for n in range(N):
        B[n+1] = B[n] + dt*(a*B[n] - b*B[n]*R[n])
        R[n+1] = R[n] + dt*(c*B[n]*R[n] - d*R[n])
    return B, R


B, R = lotka_volterra(40.0, 9.0, 0.4, 0.02, 0.01, 0.3, 0.05, 4000)
print(f'B[0] = {B[0]:.2f}   R[0] = {R[0]:.2f}')
print(f'B[1] = {B[1]:.4f}   R[1] = {R[1]:.4f}')
print(f'maks B = {B.max():.2f}   maks R = {R.max():.2f}')
print(f'B[4000] = {B[-1]:.2f}   R[4000] = {R[-1]:.2f}')

Utskrift:

B[0] = 40.00   R[0] = 9.00
B[1] = 40.4400   R[1] = 9.0450
maks B = 97.85   maks R = 58.04
B[4000] = 29.87   R[4000] = 3.65

Regn gjerne første skritt for hånd som kontroll: B1=40+0,05(0,4400,02409)=40+0,058,8=40,44B_1 = 40 + 0{,}05\,(0{,}4\cdot 40 - 0{,}02\cdot 40\cdot 9) = 40 + 0{,}05\cdot 8{,}8 = 40{,}44. Utskriften bekrefter det.

Sensorkommentar, steg for steg:

- import numpy as np0,5 p. Nødvendige importer skal med når oppgaven ber om kjørbar kode.
- B = np.zeros(N+1) og R = np.zeros(N+1)1,5 p. Her ligger av-én-poenget: np.zeros(N) gir IndexError på siste runde.
- B[0] = B0 og R[0] = R0 før løkka — 1 p. Glemmer du én, starter den bestanden på 0 og hele forløpet kollapser.
- for n in range(N):1 p. N skritt, ikke N+1.
- De to oppdateringslinjene — 1,5 p, altså 0,75 hver. Bommer du på ett fortegn, får du halve.
- return B, R utenfor løkka — 0,5 p. Står return inne i løkka, returneres tabellene etter første skritt, med nuller i resten.

Hva som er godt nok. Regner du de to endringene i egne variabler først, er det like riktig og lettere å lese:

import numpy as np

def lotka_volterra(B0, R0, a, b, c, d, dt, N):
    B = np.zeros(N+1)
    R = np.zeros(N+1)
    B[0] = B0
    R[0] = R0
    for n in range(N):
        dB = a*B[n] - b*B[n]*R[n]
        dR = c*B[n]*R[n] - d*R[n]
        B[n+1] = B[n] + dt*dB
        R[n+1] = R[n] + dt*dR
    return B, R

B, R = lotka_volterra(40.0, 9.0, 0.4, 0.02, 0.01, 0.3, 0.05, 4000)
print(f'{B[1]:.4f} {R[1]:.4f}')

Utskrift:

40.4400 9.0450

Merk at dB og dR her regnes begge ut fra de gamle verdiene før noen tabell oppdateres. Det er en robust vane: skriver du det slik, kan rekkefølgen aldri bli feil.

✏️Modellbesvarelse del (b): plottet, og figuren i ord

Del (b), 3 poeng: simuler 200 måneder og plott begge bestandene mot tid.

Tidsaksen lages med np.linspace. Med Δt=0,05\Delta t = 0{,}05 og 4000 skritt dekker simuleringen 40000,05=2004000\cdot 0{,}05 = 200 måneder, og tabellene har 4001 verdier — derfor np.linspace(0, 200, 4001).

import numpy as np
import matplotlib.pyplot as plt

def lotka_volterra(B0, R0, a, b, c, d, dt, N):
    B = np.zeros(N+1)
    R = np.zeros(N+1)
    B[0] = B0
    R[0] = R0
    for n in range(N):
        B[n+1] = B[n] + dt*(a*B[n] - b*B[n]*R[n])
        R[n+1] = R[n] + dt*(c*B[n]*R[n] - d*R[n])
    return B, R


B, R = lotka_volterra(40.0, 9.0, 0.4, 0.02, 0.01, 0.3, 0.05, 4000)
t = np.linspace(0, 200, 4001)
plt.plot(t, B, label='byttedyr')
plt.plot(t, R, label='rovdyr')
plt.xlabel('tid (maaneder)')
plt.ylabel('antall dyr')
plt.legend()
plt.show()
print('likevekt B =', 0.3/0.01, ' R =', 0.4/0.02)

Utskrift:

likevekt B = 30.0  R = 20.0

Figuren i ord. Førsteaksen viser tid fra 0 til 200 måneder, andreaksen antall dyr fra 0 til rundt 100. Byttedyrkurven starter i 40 og stiger til en topp på nesten 98; rovdyrkurven starter i 9, ligger lavt en stund, og stiger så til sin topp på rundt 58 etter byttedyrtoppen. Deretter faller byttedyrene bratt, rovdyrene følger etter med samme forsinkelse, og mønsteret gjentar seg. Kurvene svinger altså i takt, men rovdyrene ligger et kvart svingeslag bak — det er den karakteristiske forsinkelsen i rovdyr–byttedyr-dynamikk. Forklaringen navngir de to kurvene.

Svingningene er ikke helt jevne over de 200 månedene: amplituden vokser sakte. Det er ikke en feil i modellen, men i metoden — den enkle skrittformelen legger til litt energi hver runde. Nevner du det, viser du at du forstår forskjellen på modell og numerisk metode. Det kreves ikke for full score.

Likevektspunktet kan regnes ut for hånd: bestandene står stille når aB=bBRaB = bBR og cBR=dRcBR = dR, altså R=a/b=20R = a/b = 20 og B=d/c=30B = d/c = 30. Utskriften bekrefter tallene, og de er sentrum kurvene svinger rundt.

Sensorkommentar del (b): tidsaksen med riktig antall punkter (1 p) · to plot-kall med label= (1 p) · aksenavn, legend() og show() (1 p). Bruker du range(len(B)) som førsteakse i stedet for np.linspace, er det like riktig — men da er enheten skritt, ikke måneder, og aksenavnet må si det.

Oppgaver: samme mønster, ny innpakning (~30 min for oppgave 1–5)

— naturlig pausepunkt —

Alle oppgavene under er på eksamensnivå. Innpakningen roterer bevisst, og noen av likningene er koblet gjennom nye verdier mens andre ikke er det. Steg 5 i oppskriften er derfor det første du skal svare på hver gang.

📝Oppgave 1

(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Hva skriver programmet ut? NumPy skriver tabeller uten komma.

import numpy as np

x = np.zeros(5)
x[0] = 2.0
for n in range(3):
    x[n+1] = 3*x[n] - 1
print(x)
print(x.size, x[-1])

📝Oppgave 2
Eksamensnivå, sjanger H
En fiskebestand i et oppdrettsanlegg vokser etter den diskrete logistiske modellen

xn+1=xn+rxn(1xnK)x_{n+1} = x_n + rx_n\left(1 - \frac{x_n}{K}\right)

der rr er veksttakten per uke og KK er bæreevnen.

a) Skriv logistisk(x0, r, K, N) som returnerer tabellen x.

b) Kjør den med x0=20x_0 = 20, r=0,35r = 0{,}35, K=900K = 900 og N=40N = 40, og skriv ut startverdien, første skritt, verdien etter 10 uker og etter 40 uker.

c) Hvor mange verdier har tabellen, og hvorfor?

📝Oppgave 3
Eksamensnivå, sjanger H
En forenklet modell for en oscillerende kjemisk reaksjon er

yn+1=yn+ayn(1xn)y_{n+1} = y_n + a\,y_n(1 - x_n)
xn+1=xn+yn+1x_{n+1} = x_n + y_{n+1}

Legg merke til at xn+1x_{n+1} bruker den nye verdien yn+1y_{n+1}.

a) Skriv simuler(x0, y0, a, N) som returnerer tabellene x og y.

b) Kjør med x0=0,2x_0 = 0{,}2, y0=0,1y_0 = 0{,}1, a=0,5a = 0{,}5 og N=5N = 5, og skriv ut alle verdiene.

c) Hva skjer hvis du bytter om de to oppdateringslinjene? Vis tallene.

📝Oppgave 4
Eksamensnivå, sjanger H
Et smitteutbrudd modelleres dag for dag med mottakelige SnS_n, smittsomme InI_n og immune RnR_n:

Sn+1=SnβSnIn,In+1=In+βSnInγIn,Rn+1=Rn+γInS_{n+1} = S_n - \beta S_nI_n, \qquad I_{n+1} = I_n + \beta S_nI_n - \gamma I_n, \qquad R_{n+1} = R_n + \gamma I_n

a) Skriv sir(S0, I0, R0, beta, gamma, N) som returnerer de tre tabellene.

b) Kjør med S0=9990S_0 = 9990, I0=10I_0 = 10, R0=0R_0 = 0, β=3105\beta = 3\cdot 10^{-5}, γ=0,12\gamma = 0{,}12 og N=120N = 120. Skriv ut første skritt, toppen i II med dagen den kommer, og sluttverdiene.

c) Skriv plottekoden for de tre kurvene mot dag, og beskriv figuren.

d) Summen S+I+RS + I + R er en fornuftssjekk. Forklar hvorfor.

📝Oppgave 5
Eksamensnivå, sjanger O
Programmet skal simulere to sammenkoblede vanntanker der væske strømmer fra den fulleste til den tommeste:

overført=kΔt(AnBn),An+1=Anoverført,Bn+1=Bn+overført\text{overført} = k\,\Delta t\,(A_n - B_n), \qquad A_{n+1} = A_n - \text{overført}, \qquad B_{n+1} = B_n + \text{overført}

Programmet har tre feil.

import numpy as np

def tanker(A0, B0, k, dt, N):
    A = np.zeros(N)
    B = np.zeros(N)
    A[0] = A0
    for n in range(N):
        overfort = k*dt*(A[n] - B[n])
        A[n+1] = A[n] - overfort
        B[n+1] = B[n] - overfort
    return A, B

a) Nevn de tre feilene og si hva hver av dem fører til.

b) Skriv den rettede koden, og kjør den med A0=80A_0 = 80, B0=20B_0 = 20, k=0,4k = 0{,}4, Δt=0,1\Delta t = 0{,}1 og N=200N = 200.

Oppgave 6–10: variantene som skiller (~25 min)

— naturlig pausepunkt —

De neste fem oppgavene ligner mest på det du møter når sjangeren er verdt ti poeng eller mer: en fasekurve i stedet for et tidsplott, en modell der koblingen går gjennom nye verdier, en påstandstabell, og en full tredelt oppgave.

📝Oppgave 6
Eksamensnivå, del-b på…

Du har tabellene B (byttedyr) og R (rovdyr) fra det gjennomarbeidede caset.

a) Skriv koden som plotter fasekurven, altså rovdyr mot byttedyr, med byttedyr langs førsteaksen.

b) Kurven blir en lukket løkke som beveger seg mot klokka rundt punktet (30,20)(30, 20). Forklar i ord hva det forteller om de to bestandene, og hva punktet er.

c) Hva er forskjellen på et tidsplott og en fasekurve, og hvilken informasjon mister du i den siste?

📝Oppgave 7
Eksamensnivå, sjanger H
Sedimentlaget i en innsjø vokser med en hastighet som avtar når laget blir tykt:

vn+1=vn+a(1hnb),hn+1=hn+vn+1v_{n+1} = v_n + a\left(1 - \frac{h_n}{b}\right), \qquad h_{n+1} = h_n + v_{n+1}

der hnh_n er tykkelsen i millimeter etter nn år og vnv_n er årets tilvekst.

a) Skriv sediment(h0, v0, a, b, N) som returnerer tabellene h og v.

b) Kjør med h0=0h_0 = 0, v0=1v_0 = 1, a=0,2a = 0{,}2, b=50b = 50 og N=8N = 8, og skriv ut begge tabellene år for år.

c) Hvilken av de to likningene må regnes først, og hvordan ser du det i oppgaveteksten?

📝Oppgave 8

(Midtveisnivå, sjanger D — «hvilke av påstandene er riktige?». Sjanger D er matche-typen der du avgjør om hver påstand holder.) Se på denne funksjonen:

def simuler(x0, N):
    x = np.zeros(N+1)
    x[0] = x0
    for n in range(N):
        x[n+1] = 0.9*x[n] + 2
    return x

Avgjør for hver påstand om den er riktig eller gal.

RadPåstand
1Tabellen x har N+1 plasser
2Løkka gjør N+1 oppdateringer
3x[N] blir skrevet til i siste runde
4Bytter du til range(N+1), får du IndexError
5Startverdien kan like gjerne settes inne i løkka
6Med N = 0 returneres en tabell med bare startverdien

📝Oppgave 9
Eksamensnivå, sjanger H
I havet beskrives biomassen av planteplankton PnP_n og dyreplankton ZnZ_n av

Pn+1=Pn+Δt(aPnbPnZn),Zn+1=Zn+Δt(cPnZndZn)P_{n+1} = P_n + \Delta t\,(aP_n - bP_nZ_n), \qquad Z_{n+1} = Z_n + \Delta t\,(cP_nZ_n - dZ_n)

med målt i milligram per kubikkmeter og Δt\Delta t i døgn.

a) Skriv biomasse(P0, Z0, a, b, c, d, dt, N).

b) Kjør med P0=120P_0 = 120, Z0=15Z_0 = 15, a=0,5a = 0{,}5, b=0,01b = 0{,}01, c=0,005c = 0{,}005, d=0,4d = 0{,}4, Δt=0,02\Delta t = 0{,}02 og N=10000N = 10000. Skriv ut første skritt, samt høyeste og laveste verdi av PP.

c) Regn ut likevektsverdiene for hånd, og sjekk at simuleringen svinger rundt dem.

d) Skriv plottekoden for begge kurvene mot tid.

📝Oppgave 10
Eksamensnivå, sjanger H

Bruk logistisk(x0, r, K, N) fra oppgave 2.

a) Kjør modellen med K=900K = 900, x0=20x_0 = 20, N=60N = 60 og tre verdier av rr: 0,350{,}35, 2,12{,}1 og 2,62{,}6. Skriv ut de fem siste verdiene for hver.

b) Beskriv hva som skjer i hvert av de tre tilfellene.

c) Er dette en programmeringsfeil eller en egenskap ved modellen? Begrunn.

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Sjanger H

Differenslikninger: oppgaven der du regner ut hvordan en eller flere størrelser utvikler seg skritt for skritt. Sjangeren står i 6 av 8 gjennomgåtte avsluttende sett og i 4 av 4 siste, og teller 5–12 poeng. Den er nesten alltid todelt: (a) skriv simuleringen, (b) plott resultatet.

Steg 1 og 2 — tell størrelsene og allokér

Én likning gir én tabell, to koblede gir to. Hver tabell lages med np.zeros(N+1), der N er antall skritt. Å allokere betyr å be om lagerplassen på forhånd, før du vet hva som skal stå der.

Steg 3 — startverdiene før løkka
x[0] = x0 og y[0] = y0 settes før løkka, én linje per tabell. Er antall startverdier mindre enn antall tabeller, starter en av størrelsene på 0 og hele forløpet blir feil.
Steg 4 — én løkke for alle størrelsene
for n in range(N):, og begge oppdateringene inne i samme runde. To løkker etter hverandre er feil: da bruker den andre løkka ferdige verdier fra den første i stedet for verdier fra samme skritt.
Steg 5 — oppdateringsrekkefølgen

Spørsmålet er: bruker den ene likningen den nye verdien av den andre? Signalet er en indeks n+1n+1 på høyre side av likhetstegnet. Da må den likningen stå sist. Har alle høyresider bare nn-indekser, er rekkefølgen likegyldig — og det er verdt en setning å si at du har sjekket.

Skritt mot verdier
N skritt gir N+1 verdier, fordi startverdien også er en verdi. range(N) gir n fra 0 til N-1, og siste tilordning treffer x[N] — den siste plassen i en tabell med N+1 plasser. Alt passer nøyaktig.
Av-én-feilens tre former
np.zeros(N) gir IndexError på siste runde. range(N+1) gir samme feil. np.zeros(N+1) med for kort løkke gir ingen feilmelding i det hele tatt, bare nuller på slutten av tabellen — den siste er verst, fordi den ser riktig ut.
Bevaringssjekken

I modeller der ingenting forsvinner — smittemodellen, to sammenkoblede beholdere — skal summen av størrelsene være konstant. Det er den eneste kontrollen du kan gjøre uten fasit og uten datamaskin, og den er verdt et poeng å nevne.

Likevektspunkt

Verdiene der ingen av størrelsene endrer seg, altså der alle endringsuttrykkene er null. For rovdyr–byttedyr-modellen er de B=d/cB = d/c og R=a/bR = a/b. Simuleringen svinger rundt dem, og de regnes ut med penn og papir, ikke med kode.

Tidsplott mot fasekurve

I et tidsplott er førsteaksen tid og hver kurve viser én størrelse. I en fasekurve er begge aksene størrelser, og tiden er bare rekkefølgen punktene tegnes i. Fasekurven viser om systemet nærmer seg en likevekt, fjerner seg fra den, eller gjentar seg — men ikke når noe skjer.

Tidsaksen i del (b)
np.linspace(0, T, N+1) der T=NΔtT = N\cdot\Delta t, eller range(len(x)) når enheten er skritt. Aksen må ha like mange punkter som tabellene, ellers stopper plottekallet med en feil om ulik lengde.
Modellegenskap mot programmeringsfeil

Endrer den rare oppførselen seg når du endrer en parameter, er den modellens; er svaret feil for alle parametere, er den kodens. Den diskrete logistiske likningen gir rolig vekst, periodiske svingninger eller uregelmessig forløp avhengig av én parameter alene — helt uten at koden er gal.

Repetisjon: oppskriften på ett kort

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.