Doelen¶
In hoofdstukken 5 en 6 van het boek wordt er verder ingegaan op de reversibiliteit van processen. De klassieke mechanica eist alleen behoud van energie en impuls, maar er is geen richtingsvoorkeur in processen. Dat is in strijd met je dagelijkse ervaring: van een video-opname kun je vrijwel altijd zeggen of deze vooruit of achteruit wordt gespeeld.
Deze richtingsvoorkeur wordt het best gemodelleerd door de entropie. Dit is een kwantitatieve grootheid die vaak een beetje mysterieus wordt gevonden. In dit werkblad zullen we kijken of we wat meer over deze entropie te weten kunnen komen.
In dit werkblad komen de volgende onderdelen langs:
We herhalen de belangrijke stukken code met gekozen constanten en het gedrag van deeltjes.
We introduceren een nieuwe klasse voor het beheersvolume die de reeds ontwikkelde functies bevat.
We controleren of deze code gelijkwaardige resultaten geeft als de code in voorgaande werkbladen.
We bekijken een ingewikkelder modelsysteem van gekoppelde zuigers en kijken hier naar verschillende processen.
We nemen een reversibele en een niet-reversibele route en kijken naar het verschil in entropie.
Laden van eerdere code¶
Net als bij de eerdere simulaties, gaan we uit van dezelfde set van constanten.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# Constanten voor de simulatie
BOX_SIZE_0 = 0.1 # Hoogte en lengte startvolume (bijvoorbeeld 0.1 mm of in willekeurige eenheden)
N = 40 # Aantal deeltjes
V_0 = 1.0 # Start snelheid van deeltjes (arbitraire eenheid)
RADIUS = 0.005 # Straal van moleculen (diameter = 0.01)
DT = 0.01 # Tijds stap (klein genoeg om botsingen goed te modelleren)
V_PISTON_0 = -0.02 * V_0 # Startsnelheid van zuiger; negatief betekent naar binnen gericht
k_B = 1.38e-23 # Boltzmann‑constante
En maken we daarnaast ook gebruik van een klasse die het gedrag van deeltjes omschrijft:
class ParticleClass:
def __init__(self, m, v, r, R):
""" maakt een deeltje (constructor) """
self.m = m
self.v = np.array(v, dtype=float)
self.r = np.array(r, dtype=float)
self.R = R
def update_position(self):
""" verandert positie voor één tijdstap """
self.r += self.v * DT
@property
def momentum(self):
return self.m * self.v
@property
def kin_energy(self):
return 1/2 * self.m * np.dot(self.v, self.v)
def collide_detection(p1: ParticleClass, p2: ParticleClass) -> bool:
""" Geeft TRUE als de deeltjes overlappen """
dx = p1.r[0] - p2.r[0]
dy = p1.r[1] - p2.r[1]
rr = p1.R + p2.R
return dx**2+dy**2 < rr**2
def particle_collision(p1: ParticleClass, p2: ParticleClass):
""" past snelheden aan uitgaande van overlap """
m1, m2 = p1.m, p2.m
delta_r = p1.r - p2.r
delta_v = p1.v - p2.v
dot_product = np.dot(delta_r, delta_v)
# Als deeltjes van elkaar weg bewegen dan geen botsing
if dot_product >= 0: # '='-teken voorkomt ook problemen als delta_r == \vec{0}
return
distance_squared = np.dot(delta_r, delta_r)
# Botsing oplossen volgens elastische botsing in 2D
p1.v -= 2 * m2 / (m1 + m2) * dot_product / distance_squared * delta_r
p2.v += 2 * m1 / (m1 + m2) * dot_product / distance_squared * delta_rHet beheersvolume¶
In het boek wordt er vaak gesproken over een ‘control volume’. Dit is een reëel of denkbeeldig volume waaraan een aantal macroscopische eigenschappen worden toegekend. Het Nederlandse woord hiervoor is ‘beheersvolume’. Veel van de functies die we tot nu toe hebben ontwikkeld gaan over eigenschappen en gedrag van een dergelijk beheersvolume - deze hebben we slechts impliciet gebruikt. Om de code beter te structureren, maken we hier een klasse voor. Dit maakt het vooral makkelijk om bijvoorbeeld met twee beheersvolumes te rekenen (later in dit werkblad is dat ons doel).
Als je de code hieronder vergelijkt met die van de vorige werkbladen, zal je een aantal zaken opvallen:
Elke functie heeft een parameter
selfgekregen. Daarmee begrijpt Python dat het om een functie van de klasse gaat.De variabelen die als
globalstonden vermeld zijn nu een variabele van de klasse en hoeven dus niet meer gedeclareerd te worden in elke functie. Ze moeten in de code wel telkens worden voorafgegaan door het woordself, om dit duidelijk te maken.Om de eigenschappen
heat,workenpressurealleen te laten lezen en niet te laten schrijven, maken we gebruik van een Python conventie waarbij we een ‘geheime’ variabele gebruiken die wordt voorafgegaan door een laag liggend streepje ‘_’ (Engels: underscore).
class ControlVolume:
def __init__(self, width: float, height: float, x_init: float, *args):
"""Een vereenvoudigd beheersvolume voor een 2D-ideaal gas.
width en height geven de beginafmetingen van het volume. x_init is de
initiële zuigerpositie (niet gebruikt in deze vereenvoudigde versie).
Er kunnen twee of drie extra argumenten worden meegegeven:
(num_deeltjes, v_piston, temp) of (v_piston, temp). In het laatste
geval wordt het globaal gedefinieerde aantal deeltjes N gebruikt.
v_piston is de snelheid van de zuiger (negatief betekent compressie),
temp is de begintemperatuur in K.
"""
if len(args) == 3:
self.N = args[0]
self.v_piston = args[1]
self.temp = args[2]
elif len(args) == 2:
# Gebruik het globale N uit de constanten hierboven
self.N = N
self.v_piston = args[0]
self.temp = args[1]
else:
raise ValueError("ControlVolume expects 5 or 6 arguments.")
# Geometrie
self.width = width
self.height = height
self._work = 0.0
self._heat = 0.0
@property
def area(self) -> float:
"""Huidig oppervlak (2D volume) van het gas."""
return self.width * self.height
@property
def pressure(self) -> float:
"""Bereken de druk via de 2D-ideale gaswet: P * A = N k_B T."""
return (self.N * k_B * self.temp) / self.area
@property
def temperature(self) -> float:
return self.temp
@property
def heat(self) -> float:
"""Totale warmte die door het volume is opgenomen (>0) of afgestaan (<0)."""
return self._heat
@property
def work(self) -> float:
"""Totaal verrichte arbeid door het gas (>0 bij expansie, <0 bij compressie)."""
return self._work
def set_temp(self, new_temp: float) -> None:
"""Stel de temperatuur direct in en bereken de benodigde warmte. De interne
energie van een 2D-ideaal gas is U = N k_B T, zodat dU = N k_B dT.
Het verschil in interne energie wordt toegekend aan warmteuitwisseling.
"""
dU = self.N * k_B * (new_temp - self.temp)
self._heat += dU
self.temp = new_temp
def take_time_step(self) -> None:
"""Voer één tijdstap uit voor het beheersvolume. De zuiger verplaatst zich
met snelheid v_piston, waardoor de breedte verandert met d_width = v_piston * DT.
Dit resulteert in een oppervlakteverandering dA = d_width * height. De
arbeid die het gas verricht is P * dA. Voor een adiabatische stap
(geen warmte-uitwisseling) verandert de interne energie met -P dA,
waardoor de temperatuur aangepast wordt.
"""
# Verander breedte op basis van de zuigersnelheid
d_width = self.v_piston * DT
# Pas de breedte aan; voorkom negatieve breedte
self.width += d_width
# Oppervlakteverandering
dA = d_width * self.height
# Druk op het nieuwe volume (kleine dt-benadering)
P = self.pressure
# Arbeid verricht door het gas
work_done_by_gas = P * dA
self._work += work_done_by_gas
# Interne energieverandering (adiabatisch): dU = -P dV
dU = -work_done_by_gas
# Pas de temperatuur aan volgens U = N k_B T
dT = dU / (self.N * k_B)
self.temp += dT
Herhaling oude test¶
Om zeker te zijn dat de code functioneert herhalen we eerst een berekening, waarvan we het antwoord al kennen. Dit is de simulatie van de druk en de temperatuur als functie van het volume voor een isotherm proces (waarbij de temperatuur dus constant wordt gehouden met behulp van een thermostaat).
num_steps = round(2/3 * BOX_SIZE_0 / (2 * -V_PISTON_0 * DT))
volumes = np.zeros(num_steps, dtype=float)
pressures = np.zeros(num_steps, dtype=float)
temperatures = np.zeros(num_steps, dtype=float)
cv = ControlVolume(BOX_SIZE_0, BOX_SIZE_0, 40, V_PISTON_0, 300)
for i in range(num_steps):
cv.take_time_step()
volumes[i] = cv.area
pressures[i] = cv.pressure
temperatures[i] = cv.temperature
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3))
ax1.set_xlabel('Volume')
ax1.set_ylabel('Pressure')
ax2.set_xlabel('Volume')
ax2.set_ylabel('Temperature')
fig.tight_layout
ax1.plot(volumes, pressures, '-r')
ax2.plot(volumes, temperatures, '-b')
plt.show()
Systeem van twee gekoppelde zuigers¶
Om nu een studie te doen naar de entropie van een systeem maken we gebruik van twee gekoppelde zuigers, zie Figure 1. Dit zijn twee zuigers genummerd ‘1’ en ‘2’ die samen een constant volume hebben, maar waarbij de beweging van de zuiger(s) het ene volume verkleint en het andere volume evenveel vergroot. Dit systeem houden we voor de rest van dit weerkblad thermisch geïsoleerd ten opzichte van de omgeving, zodat we een goede boekhouding kunnen doen van de totale hoeveelheid energie.

Figure 1:De gekoppelde zuigers, waarbij het totaal volume constant is.
Tijdens dit experiment wisselen de twee beheersvolumes warmte uit, maar dit gebeurt zo langzaam dat op elk moment beide volumes vrijwel dezelfde temperatuur hebben. Er is dus geen eindige temperatuurgradiënt waarover warmte stroomt. Hierdoor wordt er geen entropie geproduceerd en kan het proces in het ideaalgeval als reversibel worden beschouwd.
# Simuleer de gekoppelde beheersvolumes. We nemen een totale verplaatsing gelijk
# aan tweederde van de startbreedte, verdeeld over twee zuigers. De totale
# oppervlakte (volume) blijft constant. De zuigersnelheid V_PISTON_0 is
# negatief zodat cv1 comprimeert en cv2 expandeert.
num_steps = round((2/3) * BOX_SIZE_0 / (2 * -V_PISTON_0 * DT))
volumes1 = np.zeros(num_steps, dtype=float)
volumes2 = np.zeros(num_steps, dtype=float)
pressures1 = np.zeros(num_steps, dtype=float)
pressures2 = np.zeros(num_steps, dtype=float)
temperatures1 = np.zeros(num_steps, dtype=float)
temperatures2 = np.zeros(num_steps, dtype=float)
# Maak twee beheersvolumes aan. Het derde argument (0) wordt in deze simpele
# implementatie genegeerd. De laatste twee argumenten zijn v_piston en temp.
cv1 = ControlVolume(BOX_SIZE_0, BOX_SIZE_0, 0, V_PISTON_0, 300)
cv2 = ControlVolume(BOX_SIZE_0, BOX_SIZE_0, 0, -V_PISTON_0, 300)
for i in range(num_steps):
# Voer een adiabatische stap uit (geen warmtewisseling tijdens de stap)
cv1.take_time_step()
cv2.take_time_step()
# Temperatuur gelijkmaken door warmte-uitwisseling tussen de volumes
T_avg = 0.5 * (cv1.temperature + cv2.temperature)
cv1.set_temp(T_avg)
cv2.set_temp(T_avg)
# Sla de actuele waarden op
volumes1[i] = cv1.area
volumes2[i] = cv2.area
pressures1[i] = cv1.pressure
pressures2[i] = cv2.pressure
temperatures1[i] = cv1.temperature
temperatures2[i] = cv2.temperature
# plotten van de druk en temperatuur van beide volumes
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3))
ax1.set_xlabel('Volume')
ax1.set_ylabel('Pressure')
ax2.set_xlabel('Volume')
ax2.set_ylabel('Temperature')
fig.tight_layout
#druk als functie van volume
ax1.plot(volumes1, pressures1, '-r', label='cv1')
ax1.plot(volumes2, pressures2, '-m',label='cv2')
ax1.legend()
#temperatuur als functie van volume
ax2.plot(volumes1, temperatures1, '-b',label='cv1')
ax2.plot(volumes2, temperatures2, '-g',label='cv2')
ax2.legend()
plt.show()
Wanneer de zuiger uit het midden beweegt, wordt het gas aan de ene kant samengedrukt en aan de andere kant uitgedijd. De compressie betekent dat er arbeid op het gas wordt verricht; de deeltjes botsen vaker en sneller waardoor hun gemiddelde kinetische energie en dus de temperatuur toeneemt. Door het thermisch contact tussen de twee volumes wordt deze extra energie onmiddellijk verdeeld, zodat beide volumes dezelfde temperatuur houden. Hoe verder de zuiger wordt uitgweken, hoe groter het drukverschil tussen beide kanten en hoe meer arbeid per stap op het gas wordt uitgeoefend. Hierdoor groeit de temperatuur toename bij grotere uitwijkingen sneller.
Dit systeem is zo gekozen dat het mogelijk is om de temperatuur analytisch te bepalen met behulp van een wiskundige afleiding.
We gaan ervan uit dat het systeem bij de start helemaal symmetrisch is (zoals in de simulatie) en definiëren hierbij als de positie van de zuiger.
Bij staat de zuiger helemaal aan de kant van cv1, zodat en .
Bij staat de zuiger helemaal aan de andere kant, zodat en .
Dan moet dus gelden dat:
Omdat ons gas twee vrijheidsgraden heeft, wordt de interne energie van elk van de volumes () gegeven door:
Met het aantal deeltjes aan beide kanten gelijk betekent dit voor de interne energie van het totale systeem
Voor het verplaatsen van de zuiger geldt voor beide kanten:
Als we deze twee formules samennemen dan levert dit:
Vullen we de startformules voor en in, dan wordt dit:
import numpy as np
import matplotlib.pyplot as plt
# Basisgrootheden
V0 = BOX_SIZE_0 * BOX_SIZE_0
# x-waarde uit de simulatie; x = V1/V0 - 1
a_vol1 = np.array(volumes1)
x_vals = a_vol1 / V0 - 1
# gesimuleerde temperatuur (beide volumes hebben dezelfde T na elke stap)
T_sim = np.array(temperatures1)
T0 = T_sim[0]
# Theorie: dT/dx = x/(1-x**2) * T ⇒ T(x) = T0 / sqrt(1 - x**2)
T_theory = T0 / np.sqrt(1 - x_vals**2)
plt.figure()
plt.plot(x_vals, T_sim, label="simulatie")
plt.plot(x_vals, T_theory, label="theorie", linestyle="--")
plt.xlabel("$x$")
plt.ylabel("Temperatuur (K)")
plt.legend()
plt.grid(True)
plt.show()

Om te verifiëren dat dit proces reversibel is moeten we het proces ook terug kunnen uitvoeren.
# beginwaardes voor de teruggaande beweging
volumes1_rev = []
volumes2_rev = []
temperatures_rev = []
# eindtoestand van het heenproces
a1_end = volumes1[-1]
a2_end = volumes2[-1]
w1_end = a1_end / BOX_SIZE_0
w2_end = a2_end / BOX_SIZE_0
# maak nieuwe control volumes met omgekeerde zuigersnelheid
t1_end = temperatures1[-1]
t2_end = temperatures2[-1]
cv1_rev = ControlVolume(w1_end, BOX_SIZE_0, 0, -V_PISTON_0, t1_end)
cv2_rev = ControlVolume(w2_end, BOX_SIZE_0, 0, V_PISTON_0, t2_end)
for _ in range(num_steps):
cv1_rev.take_time_step()
cv2_rev.take_time_step()
T_avg = (cv1_rev.temperature + cv2_rev.temperature)/2
cv1_rev.set_temp(T_avg)
cv2_rev.set_temp(T_avg)
volumes1_rev.append(cv1_rev.area)
volumes2_rev.append(cv2_rev.area)
temperatures_rev.append(cv1_rev.temperature)
# vergelijk heen en terug
a_vol1 = np.array(volumes1)
x_forward = a_vol1 / V0 - 1
x_reverse = np.array(volumes1_rev) / V0 - 1
T_forward = np.array(temperatures1)
T_reverse = np.array(temperatures_rev)
plt.figure()
plt.plot(x_forward, T_forward, label="heen")
plt.plot(x_reverse[::-1], T_reverse, label="terug", linestyle="--")
plt.xlabel("$x$")
plt.ylabel("Temperatuur (K)")
plt.legend()
plt.grid(True)
plt.show()

Adiabatische verplaatsing zuiger¶
We kunnen in een vergelijkbare eindsituatie komen door de zuiger eerst adiabatisch te verplaatsen (dat wil zeggen: zonder onderling thermisch contact tussen de volumes) en pas daarna het thermische contact toe te staan. Om dat in onze simulatie te modelleren concentreren we ons eerst op de adiabatische verplaatsing van de zuigerwand en nemen we het thermische contact nog niet mee.
# adiabatische verplaatsing zonder warmtetransfer
import numpy as np
import matplotlib.pyplot as plt
# doelverplaatsing x ≈ -0.8
target_x = -0.8
final_width1 = (1 + target_x) * BOX_SIZE_0
steps_adiabatic = int(abs(final_width1 - BOX_SIZE_0) / (abs(V_PISTON_0) * DT))
cv1_ad = ControlVolume(BOX_SIZE_0, BOX_SIZE_0, 0, V_PISTON_0, 300)
cv2_ad = ControlVolume(BOX_SIZE_0, BOX_SIZE_0, 0, -V_PISTON_0, 300)
volumes1_ad = []
volumes2_ad = []
temperatures1_ad = []
temperatures2_ad = []
for _ in range(steps_adiabatic):
cv1_ad.take_time_step()
cv2_ad.take_time_step()
volumes1_ad.append(cv1_ad.area)
volumes2_ad.append(cv2_ad.area)
temperatures1_ad.append(cv1_ad.temperature)
temperatures2_ad.append(cv2_ad.temperature)
# visualiseer het temperatuurverloop
x_ad = np.array(volumes1_ad) / V0 - 1
plt.figure()
plt.plot(x_ad, temperatures1_ad, label="temp volume 1")
plt.plot(x_ad, temperatures2_ad, label="temp volume 2")
plt.xlabel("$x$")
plt.ylabel("Temperatuur (K)")
plt.legend()
plt.grid(True)
plt.show()

Na de adiabatische verplaatsing moeten de twee volumes alsnog in thermisch evenwicht gebracht worden. Dat doen we met de code hieronder.
times = []
temp1 = []
temp2 = []
average_temp = (cv1.temperature + cv2.temperature) / 2
cv1.set_temp = average_temp
cv2.set_temp = average_temp
cv1.v_piston = 0.0
cv2.v_piston = 0.0
time = 0.0
while cv2.temperature < 0.99 * cv2.set_temp:
time += DT
cv1.take_time_step()
cv2.take_time_step()
temp1.append(cv1.temperature)
temp2.append(cv2.temperature)
times.append(time)
plt.figure()
plt.xlabel('$t$')
plt.ylabel('$T$')
plt.plot(times, temp1, '-r', label='cv1')
plt.plot(times, temp2, 'b-', label='cv2')
plt.legend()
plt.show()

Vergelijk van de twee experimenten¶
We hebben de zuiger in twee experimenten verschoven:
We begonnen in toestand 0, waarbij de zuiger in het midden stond en naar toestand 1 bewoog, met thermisch contact tussen de twee volumes.
We begonnen in toestand 0 en verplaatsten de zuiger zonder thermisch contact (adiabatisch) naar toestand 2. Daarna brachten we de twee volumes in thermisch contact en zonder volumeverandering naar evenwicht kwamen in toestand 3.
Deze processen zijn schematisch weergegeven in Figure 2.

Figure 2:Schematische weergave van de twee processen in een -diagram met de verschillende toestanden genummerd weergegeven.
Uit de thermodynamische formules en uit de simulatie kun je concluderen dat de eindtemperatuur na deze twee experimenten verschillend is. Omdat we het systeem nooit in thermisch contact met de omgeving hebben gebracht, moet de energie hiervoor uit de arbeid komen die de zuigerwand heeft verricht. Dit laat zien dat de arbeid die nodig is van de ene in de andere toestand te komen, afhankelijk is van het gekozen proces.
Een reversibele route is dan het ‘goedkoopste’ proces om in een toestand te komen. Voor het experiment zou het perfect uitgevoerde isotherme proces reversibel zijn en daarom de laagste temperatuur opleveren bij .
Willen we maximale arbeid halen in de stap van toestand 2 naar toestand 3, dan kunnen we dat doen met twee reversibele warmtepompen die de twee beheersvolumes koppelen aan een denkbeeldig bad. Entropie is een toestandsgrootheid, zodat de entropieproductie in dit proces net zo groot moet zijn als in onze zuiger van daarnet. Voor een reversibel proces zoals we met deze warmtepompen uitvoeren is er geen entropieproductie. Dan moet er dus gelden dat:
Als we de omgevingstemperatuur op een waarde stellen van dan moet er dus gelden:
Dit betekent dat het systeem tijdens dit reversibele proces deze hoeveelheid moet accepteren van de omgeving om de eindtoestand te bereiken. Omdat we daarnaast van de eerste hoofdwet weten dat:
en er moet gelden dat de interne energie van het systeem niet verandert tijdens dit temperatuurverloop kunnen we concluderen dat:
In het proces dat bij onze simulatie heeft plaatsgevonden is er geen enkele arbeid door het systeem uitgevoerd. We zien dus dat de entropie die we in dit geval hebben berekend een maat is voor de arbeid die we hadden kunnen winnen door op dezelfde eindtoestand te komen via een reversibel proces. We kunnen dit ook andersom zeggen: de hoeveelheid entropie die tijdens het proces gecreëerd wordt is een maat voor de hoeveelheid beschikbare arbeid die we tijdens het proces hebben verloren door het niet perfect reversibel uit te voeren.