Simulation chaotischer Masse-Feder-Systeme mit Python 3 (Software)

Martin Vogel ⌂ @, Dortmund / Bochum, Dienstag, 02.02.2016, 13:23 (vor 902 Tagen)

Während man bei einfachen Faden- oder Federpendeln gut berechnen kann, welchen Zustand sie zu jedem beliebigen Zeitpunkt haben werden (sonst gäbe es keine mechanischen Uhren), neigt die Kombination von mehreren Pendeln zu einem hochgradig chaotischen Verhalten.

Ein kleines Python-3-Programm, dass unten heruntergeladen werden kann, erlaubt die Konstruktion komplexer Masse-Feder-Systeme, bei denen sich zahlreiche Parameter über Schieberegler beeinflussen lassen.

[image]

Mathematisch steckt kaum etwas dahinter. Auf einen Massepunkt wirken Kräfte, diese bewirken pro Berechnungsschritt eine Beschleunigung, jene verändert die Geschwindigkeit des Massepunkts, wodurch dieser letztlich nach jedem Rechenschritt eine neue Position erhält. Da an den Massepunkten Federn hängen, sorgt die Positionsänderung für eine Dehnung oder Stauchung ihrer Endpunkte, wodurch wieder Kräfte entstehen, die im nächsten Durchlauf wieder auf die Massepunkte wirken und so fort.

Durch diese einfachen Rechnungen entstehen hochkomplexe Bewegungen, welche vom Programm aufgezeichnet werden können.

Wer sich schon immer einmal mit Python 3 beschäftigen wollte, ist herzlich eingeladen, mit dem Programm herumzuspielen und es für seine Zwecke anzupassen.

Kurzanleitung:

Das physikalische Modell besteht aus blauen Lagerpunkten, roten Massepunkten und grauen Stabfedern.
Die Lagerpunkte und die Massepunkte können mit der Maus verschoben werden.
Die Federn folgen dieser Bewegung.

Neue Massepunkte und Lagerpunkte lassen sich mit den Tasten „M“ und „L“ sowie mit der rechten Maustaste erzeugen.
Massepunkte werden mittels einer Feder mit dem nächstliegenden verschieblichen Punkt verbunden, wenn die Gravitation ungleich null ist. Zum schnellen Einschalten der Schwerelosigkeit dient der Button „Antigravity“.

Neue Federn werden durch zweimaliges Drücken der rechten Maustaste oder der Taste „F“ jeweils am Start- und am Zielobjekt angebunden. Unerwünschte Druck- oder Zugspannungen in den Federn, die während des Konstruierens entstehen können, lassen sich mit dem Button „Entspannen“ auf null setzen.

Bei hohen Federsteifigkeiten und geringen Kugelmassen neigt das Modell dazu, in dramatischer Weise aufzuschwingen. Der Dämpfungsregler sollte daher niemals auf einem zu kleinen Wert stehen.

Das Löschen von Objekten ist noch nicht implementiert. Auch das Laden und Speichern von Modellen und Reglerstellungen wird der geneigten Leserin und dem geneigten Leser des Python-3-Quelltextes dieses Programms überlassen.

Viel Spaß damit![image]

Download: [image]ZIP-Archiv_XQGMILX91.zip

--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums

Heute schon programmiert? Einführung in Python 3 (PDF)

Genauere Zeitnahme in Windows

Martin Vogel ⌂ @, Dortmund / Bochum, Donnerstag, 22.12.2016, 12:27 (vor 578 Tagen) @ Martin Vogel

Leider gibt es wohl viele Windows-Rechner, bei denen die Python-Funktion time.time() nur recht ungenaue Werte liefert, da sie nicht im Milli- oder Mikrosekundentakt wie unter Linux aktualisiert wird, sondern nur ungefähr 60 Mal pro Sekunde. Da sich damit schwerlich die zwischen zwei Rechenschritten verstrichene Zeit messen lässt, verwendet das Programm nun für Zeitmessungen die mit Python 3.3 neu eingeführte, ungleich präzisere Funktion time.perf_counter(), die selbst unter Windows brauchbar ist.


[image]

Download:
[image]ZIP-Archiv_XQGMILX91.zip

  1. # Interaktive Physiksimulation mit Massen und Federn
  2. # 2016-12-22 Martin Vogel, Hochschule Bochum
  3. # cc-by-sa-3.0 https://creativecommons.org/licenses/by-sa/3.0/de/
  4.  
  5. # Fragen und Diskussion:
  6. # http://bauforum.wirklichewelt.de/index.php?nr=11343
  7.  
  8. from tkinter import *
  9. from math import hypot,atan2,sin,cos
  10. from random import randint
  11. from time import perf_counter, sleep
  12. from threading import Thread, Lock
  13.  
  14. # Das Hauptfenster des Programms wird erzeugt und der TCL-Interpreter
  15. # geladen. Alle weiteren davon abhängigen Fenster werden mittels Toplevel()
  16. # erzeugt.
  17. T = Tk()
  18. T.title("Massen und Federn (Hilfe mit F1)")
  19.  
  20. # Die Zeichenfläche Canvas soll sich Größenänderungen des Hauptfensters
  21. # anpassen können.
  22. C = Canvas(T,width=800,height=600,bg="white")
  23. C.pack(expand="yes",fill="both")
  24.  
  25. # Kurze Erläuterung der Programmfunktionen:
  26. def Hilfe(event):
  27. messagebox.showinfo("Hilfe",
  28. "Massen und Federn",
  29. detail="
  30. Dieses physikalische Modell besteht aus blauen Lagerpunkten, roten
  31. Massepunkten und grauen Stabfedern. Die Lagerpunkte und die Massepunkte
  32. können mit der Maus verschoben werden. Die Federn folgen dieser Bewegung.
  33. nn
  34. Neue Massepunkte und Lagerpunkte lassen sich mit den Tasten „M“ und „L“ sowie
  35. mit der rechten Maustaste erzeugen. Massepunkte werden mittels einer Feder
  36. mit dem nächstliegenden verschieblichen Punkt verbunden, wenn die Gravitation
  37. ungleich null ist. Zum schnellen Einschalten der Schwerelosigkeit dient der
  38. Button „Antigravity“.
  39. nn
  40. Neue Federn werden durch zweimaliges Drücken der rechten Maustaste oder der
  41. Taste „F“ jeweils am Start- und am Zielobjekt angebunden. Unerwünschte Druck-
  42. oder Zugspannungen in den Federn, die während des Konstruierens entstehen
  43. können, lassen sich mit dem Button „Entspannen“ auf null setzen.
  44. nn
  45. Bei hohen Federsteifigkeiten und geringen Kugelmassen neigt das Modell dazu,
  46. in dramatischer Weise aufzuschwingen. Der Dämpfungsregler sollte daher niemals
  47. auf einem zu kleinen Wert stehen.
  48. nn
  49. Das Löschen von Objekten ist noch nicht implementiert. Auch das Laden und
  50. Speichern von Modellen und Reglerstellungen wird der geneigten Leserin und
  51. dem geneigten Leser des Python-3-Quelltextes dieses Programms überlassen.
  52. nn
  53. Viel Spaß damit!
  54. nn
  55. Martin Vogel, Hochschule Bochum, im Dezember 2016")
  56.  
  57. class C_Einstellungen:
  58. """
  59. Einige allgemeine Einstellungen, die zu experimentellen
  60. Zwecken variiert werden können."""
  61. Gravitation = 9.81
  62. # Die Einheit der Gravitationsbeschleunigung ist hier Hektopixel
  63. # pro Quadratsekunde.
  64.  
  65. Federsteifigkeit = 100
  66. # 100 Krafteinheiten werden benötigt, um die Länge einer Feder
  67. # um das Maß ihrer Länge im entspannten Zustand zu verändern.
  68.  
  69. Masse = 5
  70. # Die Masse der roten „Kugeln“.
  71.  
  72. Dämpfung = 5
  73. # Bei einer zu geringen Dämpfung wird die Simulation instabil.
  74. # Bei hohen Federsteifigkeiten und geringen Massen muss unbedingt
  75. # eine stärkere Dämpfung eingestellt werden.
  76.  
  77. # todo: Die ganze Krampferei mit der Dämpfung gegen Aufschaukeln
  78. # könnte man sich sparen, wenn regelmäßig die aktuelle Gesamtenergie
  79. # des Systems ermittelt würde und bei Abdriften alle Geschwindigkeiten
  80. # um einen Korrekturfaktor reduziert würden.
  81.  
  82. Zeige_Federkräfte = False
  83. # Numerische Anzeige der Stabkräfte
  84.  
  85. Zeige_Auflagerkräfte = False
  86. # Anzeige der Auflagerkräfte
  87.  
  88. Zeige_Spur = False
  89. # Aufzeichnung der Massepunktbewegung
  90.  
  91. PPS = 240
  92. # Physikschritte pro Sekunde. Dieser Wert passt sich
  93. # dynamisch an die Möglichkeiten der CPU an. Leider unterstützt
  94. # tkinter nur einen Thread, sodass es nicht möglich ist, die
  95. # Grafikausgabe zu parallelisieren. Für die Berechnungen selbst
  96. # wird jedoch Multithreading eingesetzt.
  97.  
  98. # Eine Instanz dieser Klasse speichert unsere ganzen Programmeinstellungen
  99. Einstellungen = C_Einstellungen()
  100.  
  101. class Feder:
  102. # Feder von Objekt1 nach Objekt2
  103. def __init__(self,Objekt1,Objekt2,k=Einstellungen.Federsteifigkeit):
  104. # Start- und Zielpunkt
  105. self.x1, self.y1 = Objekt1.x, Objekt1.y
  106. self.x2, self.y2 = Objekt2.x, Objekt2.y
  107. # Federkonstante k (könnte auch Federsteifigkeit D heißen)
  108. self.k = k
  109. # aktuelle Länge
  110. self.l = hypot(self.x2-self.x1,self.y2-self.y1)
  111. # Anzeige der Federkraft (None oder Canvas-ID des Textes)
  112. self.Kraftanzeige = None
  113. # Weil an diese Feder zwei Kugeln angeschlossen sein können,
  114. # die wegen des Multithreadings praktisch gleichzeitig auf die
  115. # Methoden der Feder zugreifen können, brauchen wir ein Stoppschild,
  116. # welches die Anfrage der zweiten Kugel bei Bedarf etwas zurückhält:
  117. self.Lock = Lock()
  118. # Die Federn sollen nicht alle denselben Grauton haben.
  119. Grauton = randint(100,200)
  120. Farbcode = "#"+3*("%0X"%Grauton) # z.B. "#C8C8C8"
  121. # Canvas-Grafikobjekt der Feder; Form und Lage später durch update()
  122. self.ID = C.create_line(0,0,0,0, width=3, fill=Farbcode)
  123. # Die Federn werden hinter allen verschieblichen Objekten (Auflager und
  124. # Kugeln) angeordnet.
  125. C.tag_lower(self.ID)
  126. # Breite und Windungszahl festlegen:
  127. self.reset()
  128. # Federkraft berechnen (bzw. feststellen, dass diese null ist)
  129. # und Feder zum ersten Mal zeichnen:
  130. self.update()
  131. # Federn in die Federnliste der Start- und Zielobjekte aufnehmen
  132. Objekt1.verbinde(self,1)
  133. Objekt2.verbinde(self,2)
  134.  
  135. def reset(self):
  136. """Entspannte Länge und Aussehen der Feder festlegen"""
  137. # Entspannte Länge auf aktuelle Länge setzen
  138. self.l0 = self.l
  139. # Die Feder wird als graue Zickzacklinie dargestellt:
  140. self.Federbreite = 6 # Verändert sich bei Stauchung/Streckung
  141. # Die Windungszahl ist von der Länge abhängig. Die Zacken
  142. # haben dadurch einen Winkel von etwa 60° zur Federachse.
  143. self.Wz=max(2,int(self.l0/self.Federbreite))
  144.  
  145. def update(self):
  146. """Anpassung der Feder an die neue Lage ihrer Endpunkte"""
  147. # Lage von P2 zu P1
  148. dxy = self.x2-self.x1, self.y2-self.y1
  149. # aktuelle geometrische Länge
  150. self.l = hypot(*dxy)
  151. # aktuelle Zugkraft
  152. F = self.k*(self.l-self.l0)
  153. # Winkel zwischen P1 und P2
  154. W = atan2(*dxy)
  155. # x- und y-Komponente der Kraft
  156. self.Fx, self.Fy = F*sin(W), F*cos(W)
  157. # Grafik nachführen:
  158. # Die Federn bestehen aus einem kurzen geraden Stück
  159. # am Anfangs- und Endpunkt sowie einer Zickzacklinie dazwischen.
  160. # Die Anzahl der Zacken ist so gewählt, dass der Zackenabstand
  161. # im unbelasteten Zustand der Federbreite entspricht.
  162. # Startpunkt der Feder:
  163. P=[self.x1, self.y1,
  164. self.x1+(self.x2-self.x1)/self.Wz,
  165. self.y1+(self.y2-self.y1)/self.Wz]
  166. # Abstand der Zacken von der Mittellinie der Feder:
  167. # Die Modifikation der Breite um den Kehrwert der Streckung
  168. # wirkt nur innerhalb gewisser Grenzen natürlich.
  169. # Federn, die wie in dieser "Simulation" um ein Vielfaches
  170. # ihrer Länge gestreckt werden oder auf eine Länge nahe null
  171. # zusammengeschoben werden, gibt es ja in der Realität
  172. # üblicherweise eher nicht so. Wir beschränken die Streckung
  173. # daher auf das Doppelte bzw. die Hälfte des Grundwertes.
  174. Streckung = max(0.5,min(2,self.l/self.l0))
  175. Zx = (self.y2-self.y1)/self.l*self.Federbreite/Streckung
  176. Zy = (self.x2-self.x1)/self.l*self.Federbreite/Streckung
  177. # Schleife für Zickzacklinie; immer abwechselnd ein Punkt links
  178. # und rechts von der Mittellinie …
  179. for i in range(2,self.Wz-2,2):
  180. P.append(self.x1+i*(self.x2-self.x1)/self.Wz+Zx)
  181. P.append(self.y1+i*(self.y2-self.y1)/self.Wz-Zy)
  182. P.append(self.x1+(i+1)*(self.x2-self.x1)/self.Wz-Zx)
  183. P.append(self.y1+(i+1)*(self.y2-self.y1)/self.Wz+Zy)
  184. # Zum Schluss die beiden Endpunkte:
  185. P.append(self.x2-(self.x2-self.x1)/self.Wz)
  186. P.append(self.y2-(self.y2-self.y1)/self.Wz)
  187. P.append(self.x2)
  188. P.append(self.y2)
  189. C.coords(self.ID,*P)
  190. # Soll die Federkraft angezeigt werden?
  191. if Einstellungen.Zeige_Federkräfte:
  192. # Wo ist die Mitte der Feder?
  193. fmx, fmy = (self.x1+self.x2)/2, (self.y1+self.y2)/2
  194. # Wird die Federkraft schon angezeigt?
  195. if self.Kraftanzeige:
  196. C.itemconfig(self.Kraftanzeige,text="%.0f"%F)
  197. C.coords(self.Kraftanzeige, fmx, fmy)
  198. else:
  199. # Feder blasser zeichnen, damit der Text besser lesbar ist.
  200. Grauton = randint(200,220)
  201. C.itemconfig(self.ID, fill="#"+3*("%0X"%Grauton))
  202. # Text der Federkraftanzeige erzeugen
  203. self.Kraftanzeige = C.create_text(fmx,fmy,text="%.0f"%F,
  204. fill="black",
  205. font=("Helvetica",12,"bold"))
  206. else:
  207. # Wird die Federkraft immer noch angezeigt?
  208. if self.Kraftanzeige:
  209. # Text löschen
  210. C.delete(self.Kraftanzeige)
  211. self.Kraftanzeige = None
  212. # Feder wieder kräftiger zeichnen
  213. Grauton = randint(100,200)
  214. C.itemconfig(self.ID, fill="#"+3*("%0X"%Grauton))
  215.  
  216. def set_P(self,i,x,y,beachteLock=True):
  217. """Ändert Koordinaten von Punkt P1 oder P2"""
  218. if beachteLock:
  219. # Stoppschild fürs Multithreading: falls zwei Kugeln
  220. # gleichzeitig diese Methode aufrufen, muss die zweite
  221. # hier warten.
  222. self.Lock.acquire()
  223. if i == 1: # Punkt 1 der Feder?
  224. self.x1, self.y1 = x, y
  225. else:
  226. self.x2, self.y2 = x, y
  227. # Kräfte durch neue Länge berechnen
  228. self.update()
  229. if beachteLock:
  230. # Für den Fall, dass eine zweite Kugel warten musste,
  231. # darf diese jetzt auch die Feder beeinflussen:
  232. self.Lock.release()
  233.  
  234. def get_F(self,i):
  235. # In P1 hat die Federkraft ein anderes Vorzeichen als in P2
  236. if i == 1:
  237. return self.Fx, self.Fy
  238. else:
  239. return -self.Fx, -self.Fy
  240.  
  241. class Masse:
  242. """Ein verschieblicher Massepunkt"""
  243. def __init__(self,x,y,r=10,m=Einstellungen.Masse):
  244. self.x,self.y = x,y # Mittelpunkt
  245. self.r = r # Radius
  246. self.m = m # Masse
  247. # ID-Nummer des Kreises auf der Canvas
  248. self.ID = C.create_oval(x-r,y-r,x+r,y+r,fill = "red")
  249. self.Federn = [] # Liste verbundener Federn
  250. self.Spur = [] # Spuraufzeichnung
  251. self.vx = self.vy = 0 # Geschwindigkeitskomponenten
  252. # Um die tatsächliche Schrittzeit zu messen, holen wir uns die
  253. # aktuelle Zeit in Sekunden.
  254. self.time = perf_counter()
  255. # Multithreading: Die Berechnungen aller Massepunkte
  256. # finden parallel und unabhängig voneinander statt.
  257. self.Thread = Thread(target=self.step)
  258. self.stopped = False # Wird beim Programmende auf True gesetzt.
  259. self.Thread.start() # Werde lebendig!
  260.  
  261. # Verbinde den Massepunkt mit dem ersten oder zweiten Anschlusspunkt
  262. # einer Feder
  263. def verbinde(self,Feder,Anschlusspunkt):
  264. self.Federn.append((Feder,Anschlusspunkt))
  265.  
  266. # Neuer Ort
  267. def schiebe(self,x,y):
  268. self.vx = self.vy = 0
  269. self.x, self.y = x, y
  270.  
  271. # Ende der Endlosschleife in step()
  272. def stop(self):
  273. self.stopped=True
  274.  
  275. # Animation der Kugeln
  276. def step(self):
  277. # Diese Endlosschleife läuft in einem eigenen Thread
  278. while not self.stopped:
  279. # Aktuelle Vergleichszeit in Sekunden holen (als Gleitkommazahl
  280. # mit ungefähr Mikrosekundengenauigkeit, je nach Betriebssystem)
  281. t = perf_counter()
  282. # Der Zeitschritt dt für die Berechnung entspricht der
  283. # tatsächlich verstrichenen Zeit seit dem letzten Durchgang.
  284. # Zur Vermeidung von Nulldivisionen bei extrem schnellen Rechnern
  285. # wird ein Mindestwert von einer Mikrosekunde gesetzt.
  286. dt = max(0.000001,t-self.time)
  287. # Gelesene Zeit bis zum nächsten Aufruf merken
  288. self.time = t
  289.  
  290. # Gleitende Ermittlung der Physikschritte pro Sekunde
  291. # über die letzten 200 Durchläufe:
  292. Einstellungen.PPS = Einstellungen.PPS*0.995 + 0.005/dt
  293.  
  294. # Resultierende Kraft der angeschlossenen Federn addieren:
  295. Fx = Fy = 0
  296. for Feder, Punkt in self.Federn:
  297. dFx, dFy = Feder.get_F(Punkt)
  298. Fx += dFx
  299. Fy += dFy
  300.  
  301. # Dämpfung: 1 == keine Dämpfung, 0.01 == maximale Dämpfung
  302. D = (1-min(0.99,dt*Einstellungen.Dämpfung**2/100))
  303.  
  304. # Neue Geschwindigkeit
  305. self.vx = (self.vx+Fx/self.m*dt)*D
  306. self.vy = (self.vy+(Fy/self.m+Einstellungen.Gravitation*100)*dt)*D
  307.  
  308. # Neuer Ort
  309. self.x += self.vx * dt
  310. self.y += self.vy * dt
  311.  
  312. # Grafik aktualisieren
  313. C.coords(self.ID,self.x-self.r,self.y-self.r,
  314. self.x+self.r,self.y+self.r)
  315.  
  316. # Federn hinterherziehen
  317. for Feder,Punkt in self.Federn:
  318. Feder.set_P(Punkt,self.x,self.y)
  319.  
  320. # Soll Spurlinie gezeichnet werden?
  321. if Einstellungen.Zeige_Spur:
  322. # Gibt es schon eine Spurlinie?
  323. if self.Spur:
  324. # Wenn sich die Kugel ein Stück bewegt hat, (z. B. um
  325. # einen Kugelradius), wird die Spurlinie um
  326. # einen Punkt verlängert.
  327. if hypot(self.x-self.Spur[-2],
  328. self.y-self.Spur[-1]) > self.r:
  329. self.Spur.append(self.x)
  330. self.Spur.append(self.y)
  331. C.coords(self.Spur[0],*self.Spur[1:])
  332. # Sonst neue Spurlinie anlegen
  333. else:
  334. # Linienfarbe soll irgendein Grünton sein
  335. r = randint(0,150) # bisschen rot …
  336. b = randint(0,150) # bisschen blau …
  337. g = randint(max(r,b),255) # … mehr grün
  338. grünlich = "#%02x%02x%02x"%(r,g,b)
  339. # Das erste Listenelement ist die Canvas-ID, danach
  340. # folgen die einzelnen Punkte der Spurlinie. Der erste
  341. # Punkt ist doppelt vorhanden, weil eine Linie aus
  342. # mindestens zwei Punkten bestehen muss.
  343. self.Spur=[C.create_line(self.x,self.y,
  344. self.x,self.y,
  345. fill=grünlich,
  346. ),
  347. self.x,self.y,
  348. self.x,self.y
  349. ]
  350. # Die Spur soll auf der Canvas ganz unten liegen, um nichts
  351. # zu verdecken.
  352. C.tag_lower(self.Spur[0])
  353. # Spurlinie soll nicht gezeichnet werden:
  354. else:
  355. if self.Spur:
  356. # Weg damit!
  357. C.delete(self.Spur[0])
  358. self.Spur=[]
  359.  
  360. # Nicht zu schnell werden …
  361. # der maximale CPU-Anteil für diesen Thread wird mit wachsender
  362. # Kugelzahl geringer.
  363. sleep(dt*(1-1/len(Masse_Elemente)))
  364.  
  365. # Markierung beim Zeichnen neuer Federn
  366. def markiere(self,einschalten):
  367. if einschalten:
  368. R = self.r*1.5
  369. self.markID = C.create_oval(self.x-R,self.y-R,
  370. self.x+R,self.y+R,
  371. width=5,outline="green",dash=(10,))
  372. else:
  373. C.delete(self.markID)
  374.  
  375.  
  376. class Lager:
  377. """Festes Auflager"""
  378. def __init__(self,x,y,r=5):
  379. self.x,self.y = x,y # Mittelpunkt
  380. self.r = r # Radius
  381. self.ID = C.create_oval(x-r,y-r,x+r,y+r,fill="blue")
  382. self.Federn = [] # Liste verbundener Federn
  383. self.F = 0 # resultierende Auflagerkraft
  384. self.Auflagerkraft_angezeigt = False
  385. self.step() # Für die Animation der Lagerkräfte
  386. # Verbinde Lager mit dem ersten oder zweiten Punkt einer Feder
  387. def verbinde(self,Feder,Punkt):
  388. self.Federn.append((Feder,Punkt))
  389. # Neuer Ort
  390. def schiebe(self,x,y):
  391. self.x,self.y = x,y
  392. # Punkt auf der Canvas verschieben:
  393. C.coords(self.ID,self.x-self.r,self.y-self.r,
  394. self.x+self.r,self.y+self.r)
  395. # Angeschlossene Federn nachziehen:
  396. # Das Multithreading-Lock in Feder.set_P, das verhindern soll,
  397. # dass zwei Kugeln gleichzeitig auf eine Feder zugreifen, darf
  398. # hier nicht greifen, sonst blockiert das Programm.
  399. for Feder,Punkt in self.Federn:
  400. Feder.set_P(Punkt,self.x,self.y,beachteLock=False)
  401. # Markierung beim Zeichnen neuer Federn
  402. def markiere(self,einschalten):
  403. if einschalten:
  404. R = self.r*1.5
  405. self.markID = C.create_oval(self.x-R,self.y-R,
  406. self.x+R,self.y+R,
  407. width=5,outline="green",dash=(10,))
  408. else:
  409. C.delete(self.markID)
  410. # Anzeige der Lagerkräfte:
  411. def step(self):
  412. # Soll die Auflagerkraft überhaupt angezeigt werden?
  413. if Einstellungen.Zeige_Auflagerkräfte:
  414. # Summe der auf das Lager wirkenden Kräfte bilden
  415. Fx = Fy = 0
  416. for Feder,Punkt in self.Federn:
  417. dFx, dFy = Feder.get_F(Punkt)
  418. Fx += dFx
  419. Fy += dFy
  420. # Resultierende Kraft ausrechnen
  421. self.F = hypot(Fx,Fy)
  422.  
  423. # Skalierung für eine freundliche Darstellungsgröße;
  424. # Die größte Auflagerkraft erhält die Länge 100.
  425. max_abs_F = 1
  426. for E in Lager_Elemente:
  427. max_abs_F = max(max_abs_F, E.F)
  428. F_Skalierung = 100 / max_abs_F
  429.  
  430. # Position des Textes
  431. x_F = self.x + Fx * F_Skalierung
  432. y_F = self.y + Fy * F_Skalierung
  433.  
  434. # Endpunkte des Kraftpfeils
  435. x1_P = self.x + self.r * Fx/self.F if self.F !=0 else self.x
  436. y1_P = self.y + self.r * Fy/self.F if self.F !=0 else self.y
  437. x2_P = self.x + 0.9 * (x_F-self.x)
  438. y2_P = self.y + 0.9 * (y_F-self.y)
  439.  
  440. # Wird die Auflagerkraft schon angezeigt?
  441. if self.Auflagerkraft_angezeigt:
  442. C.itemconfig(self.AT_ID,text="%.0f"%self.F)
  443. C.coords(self.AT_ID,x_F,y_F)
  444. C.coords(self.AP_ID,x1_P,y1_P,x2_P,y2_P)
  445. else:
  446. self.AP_ID = C.create_line(x1_P,y1_P,x2_P,y2_P,
  447. arrow="first",arrowshape=(10,12,6),
  448. width=3,fill="blue")
  449. self.AT_ID = C.create_text(x_F,y_F,
  450. text="%.0f"%self.F,fill="black",
  451. font=("Helvetica",12,"bold"))
  452. self.Auflagerkraft_angezeigt = True
  453. else:
  454. # Wird sie immer noch angezeigt?
  455. if self.Auflagerkraft_angezeigt:
  456. C.delete(self.AT_ID)
  457. C.delete(self.AP_ID)
  458. self.Auflagerkraft_angezeigt = False
  459.  
  460. # Bis gleich …
  461. T.after(40,self.step) # 40 ms; 1/25 s
  462. # Nicht zu oft; 25 mal pro Sekunde ist mehr als genug.
  463.  
  464. # Wer mag, kann die beiden Klassen „Lager“ und „Masse“ auch von einer
  465. # neu anzulegenden Klasse „Verschieblicher_Punkt“ ableiten, damit oben
  466. # keine Redundanzen auftreten.
  467.  
  468. # Geometrie und Zusammenhänge einer Beispielzusammenstellung:
  469.  
  470. # Variante 1: „Hampelmann“
  471. ##L1 = Lager(300,100)
  472. ##L2 = Lager(500,100)
  473. ##L3 = Lager(200,600)
  474. ##L4 = Lager(600,600)
  475. ##
  476. ##M1 = Masse(400,200)
  477. ##M2 = Masse(400,300)
  478. ##
  479. ##F1 = Feder(L1,M1)
  480. ##F2 = Feder(L2,M1)
  481. ##F3 = Feder(M2,M1)
  482. ##F4 = Feder(M2,L3)
  483. ##F5 = Feder(M2,L4)
  484. ##
  485. ##Masse_Elemente = [M1,M2]
  486. ##Lager_Elemente = [L1,L2,L3,L4]
  487. ##Verschiebliche_Elemente = [M1,M2,L1,L2,L3,L4]
  488. ##Abhängige_Elemente = [F1,F2,F3,F4,F5]
  489.  
  490. # Variante 2: „Minimalistisch“
  491. L1 = Lager(400,100)
  492.  
  493. M1 = Masse(400,200)
  494.  
  495. F1 = Feder(L1,M1)
  496.  
  497. Masse_Elemente = [M1]
  498. Lager_Elemente = [L1]
  499. Verschiebliche_Elemente = [M1,L1]
  500. Abhängige_Elemente = [F1]
  501.  
  502. #
  503. # Interaktionsteil
  504. #
  505.  
  506. # Die vorhandenen Massen und Lager können durch Anklicken und Ziehen
  507. # verschoben werden.
  508.  
  509. # Welches ist das zur Position x,y nächstliegende Objekt?
  510. def NächstesElement(x,y):
  511. a_min = 1E10
  512. for i,E in enumerate(Verschiebliche_Elemente):
  513. a = hypot(x-E.x,y-E.y)
  514. if a<a_min:
  515. a_min=a
  516. i_min=i
  517. return Verschiebliche_Elemente[i_min]
  518.  
  519. # Verschiebe das dem Mauszeiger am nächsten liegende Objekt
  520. class dragdrop:
  521. """Merkt sich das gerade zu verschiebende Objekt"""
  522. E = None
  523.  
  524. def drag(event):
  525. """Klicken und Ziehen"""
  526. if not dragdrop.E:
  527. dragdrop.E = NächstesElement(event.x,event.y)
  528. C.config(cursor="fleur") # Wird als zugreifende Hand dargestellt
  529. dragdrop.E.schiebe(event.x,event.y)
  530.  
  531. def drop(event):
  532. """Loslassen"""
  533. if dragdrop.E:
  534. dragdrop.E = None
  535. C.config(cursor="")
  536.  
  537. # Erzeuge einen neuen Massepunkt und verbinde ihn (nur bei vorhandener
  538. # Gravitation) mit dem nächsten Element, damit er nicht herunterfällt.
  539. def neueMasse(event):
  540. M = Masse(event.x,event.y,m=Einstellungen.Masse)
  541. if Einstellungen.Gravitation:
  542. E = NächstesElement(event.x,event.y)
  543. F = Feder(E,M,Einstellungen.Federsteifigkeit)
  544. Abhängige_Elemente.append(F)
  545. Masse_Elemente.append(M)
  546. Verschiebliche_Elemente.append(M)
  547.  
  548. # Erzeuge neues Lager
  549. def neuesLager(event):
  550. L = Lager(event.x,event.y)
  551. Lager_Elemente.append(L)
  552. Verschiebliche_Elemente.append(L)
  553.  
  554. # Erzeuge eine neue Feder in zwei Schritten durch die Auswahl eines
  555. # Start- und eines Zielelements. Jeweils das dem Mauszeiger am nächsten
  556. # liegende Objekt wird ausgewählt.
  557.  
  558. class Federstatus:
  559. """Zur Speicherung des Startelements einer neuen Feder"""
  560. Startelement = None
  561.  
  562. def neueFeder(event):
  563. """Erzeugt neue Feder vom Start- zum Zielobjekt"""
  564. E = NächstesElement(event.x,event.y)
  565. # Gibt es noch kein Startelement? -> neue Feder
  566. if not Federstatus.Startelement:
  567. # Wir markieren das Element, von dem die Feder ausgehen soll.
  568. E.markiere(True)
  569. # Außerdem merken wir uns dieses Element für später …
  570. Federstatus.Startelement = E
  571. else:
  572. # … also für jetzt ;-)
  573. # Das ist doch jetzt wirklich ein anderes Element als vorhin, oder?
  574. if E != Federstatus.Startelement:
  575. # Dann her mit der Feder!
  576. F = Feder(Federstatus.Startelement,E,Einstellungen.Federsteifigkeit)
  577. Abhängige_Elemente.append(F)
  578. # Die Markierung kann wieder weg
  579. Federstatus.Startelement.markiere(False)
  580. Federstatus.Startelement = None
  581.  
  582. def Rechtsklick(event):
  583. """Entspricht dem Drücken der Tasten „M“, „L“ oder „F“"""
  584. if RR_Variable.get()=="M": neueMasse(event)
  585. elif RR_Variable.get()=="L": neuesLager(event)
  586. elif RR_Variable.get()=="F": neueFeder(event)
  587.  
  588. # Geordnetes Beenden der ganzen parallel laufenden Threads.
  589. # Wenn wir uns nicht darum kümmern, gibt es unästhetische Fehlermeldungen
  590. # wie z.B. _tkinter.TclError: invalid command name “.4302957584”
  591. def Canvasschließen(event=None):
  592. # Im Moment laufen alle Kugeln in ihren eigenen
  593. # Endlosschleifen. Wir teilen ihnen mit, dass sie diese
  594. # doch jetzt bitte bald mal beenden sollen.
  595. for E in Masse_Elemente:
  596. E.stop()
  597. # Wir geben ihnen noch reichlich Zeit …
  598. sleep(0.1)
  599. TE.destroy() # Schon mal das Einstellungsfenster schließen …
  600. # … noch ein ganz kleines Päuschen …
  601. sleep(0.1)
  602. # … letzte Aufräumarbeiten auf der Canvas …
  603. T.update()
  604. # … und weg!
  605. T.destroy() # Jetzt auch das Canvasfenster schließen.
  606.  
  607.  
  608. ########## Event-Bindungen im Canvas-Fenster #############
  609.  
  610. C.bind("<B1-Motion>",drag)
  611. C.bind("<ButtonRelease-1>",drop)
  612. C.bind("<Button-3>",Rechtsklick)
  613. T.bind("m",neueMasse)
  614. T.bind("l",neuesLager)
  615. T.bind("f",neueFeder)
  616. T.bind("<F1>",Hilfe)
  617. T.protocol("WM_DELETE_WINDOW", Canvasschließen)
  618.  
  619. ########### Einstellungen-Fenster ###########
  620. TE = Toplevel()
  621. TE.title("Einstellungen")
  622. TE.bind("<F1>",Hilfe)
  623. TE.protocol("WM_DELETE_WINDOW", Canvasschließen)
  624.  
  625. # Die Gridzeile 0 mit dem Reglerpult soll sich in
  626. # der Größe anpassen, wenn das Fenster vergrößert wird.
  627. TE.rowconfigure(0,weight=1)
  628. # Die 2 Spalten des Hauptfensters sollen sich gleichmäßig verteilen können.
  629. TE.columnconfigure(0,weight=1)
  630. TE.columnconfigure(1,weight=1)
  631.  
  632. # Das Reglerpult
  633. SLF = LabelFrame(TE,text="Reglerpult:",padx=5,pady=5)
  634. SLF.grid(row=0,column=0,columnspan=2,sticky="wens",padx=5,pady=5)
  635.  
  636. # Zeile 1 im Labelframe (die Regler) soll dessen Größenanpassung übernehmen,
  637. # außerdem soll sie mindestens 200 Pixel hoch sein, damit die
  638. # Skalenzahlen gut zu erkennen sind.
  639. SLF.rowconfigure(1,weight=1,minsize=200)
  640. # Die 4 Spalten sollen sich gleichmäßig verteilen können.
  641. SLF.columnconfigure(0,weight=1)
  642. SLF.columnconfigure(1,weight=1)
  643. SLF.columnconfigure(2,weight=1)
  644. SLF.columnconfigure(3,weight=1)
  645.  
  646. # Schieberegler Gravitation
  647. Label(SLF,text="Gravitation").grid(row=0,column=0)
  648.  
  649. def SG_command(event):
  650. Einstellungen.Gravitation = SG.get()
  651.  
  652. SG = Scale(SLF, from_=-2, to=20, width=20, orient="vertical",
  653. tickinterval=2, resolution=0.01, command=SG_command)
  654. SG.set(Einstellungen.Gravitation) # Anfangswert des Reglers: 9.81
  655. SG.grid(row=1,column=0,sticky="nsew")
  656.  
  657. # Schieberegler Federsteifigkeit
  658. Label(SLF,text="Feder-nsteifigkeit").grid(row=0,column=1)
  659.  
  660. def SF_command(event):
  661. Einstellungen.Federsteifigkeit = SF.get()
  662. # Allen Federn die eingestellte Steifigkeit zuweisen:
  663. for Feder in Abhängige_Elemente:
  664. Feder.k = Einstellungen.Federsteifigkeit
  665.  
  666. SF = Scale(SLF, from_=0, to=10000, width=20, orient="vertical",
  667. tickinterval=1000, command=SF_command)
  668. SF.set(Einstellungen.Federsteifigkeit) # Anfangswert des Reglers: 100
  669. SF.grid(row=1,column=1,sticky="nsew")
  670.  
  671. # Schieberegler Kugelmasse
  672. Label(SLF,text="Kugelmasse").grid(row=0,column=2)
  673.  
  674. def SM_command(event):
  675. Einstellungen.Masse = SM.get()
  676. # Der Regler beginnt zwar aus gestalterischen Gründen bei null,
  677. # dennoch ist eine Masse von null unerwünscht (Nulldivision bei
  678. # Beschleunigung auf Warp 10).
  679. # Die Mindestmasse wird daher immer auf 1 gesetzt.
  680. if Einstellungen.Masse == 0:
  681. Einstellungen.Masse=1
  682. SM.set(Einstellungen.Masse)
  683. # Alle Massepunkte des Systems abarbeiten:
  684. for E in Masse_Elemente:
  685. E.m = Einstellungen.Masse
  686.  
  687. SM = Scale(SLF, from_=0, to=100, width=20, orient="vertical",
  688. tickinterval=10, command=SM_command)
  689. SM.set(Einstellungen.Masse) # Anfangswert 5
  690. SM.grid(row=1,column=2,sticky="nsew")
  691.  
  692. # Schieberegler Dämpfung
  693. Label(SLF,text="Dämpfung").grid(row=0,column=3)
  694.  
  695. def SD_command(event=None):
  696. Einstellungen.Dämpfung = SD.get()
  697.  
  698. SD = Scale(SLF, from_=0, to=100, width=20, orient="vertical",
  699. tickinterval=10, command=SD_command)
  700. SD.set(Einstellungen.Dämpfung)
  701. SD.grid(row=1,column=3,sticky="nsew")
  702.  
  703. # Checkbuttons zur Anzeigesteuerung
  704.  
  705. CLF = LabelFrame(TE,text="Anzeigen:",padx=5,pady=5)
  706. CLF.grid(row=1,column=0,sticky="wen",padx=5,pady=5)
  707.  
  708. # Checkbutton Federkräfte
  709. def CF_command():
  710. Einstellungen.Zeige_Federkräfte = CF_Variable.get()=="ja"
  711.  
  712. CF_Variable = StringVar()
  713. CF_Variable.set("nein")
  714.  
  715. CF = Checkbutton(CLF, command=CF_command, variable=CF_Variable,
  716. onvalue="ja", offvalue="nein",
  717. text = "Federkräfte")
  718. CF.grid(row=0, column=0, sticky="w")
  719.  
  720. # Checkbutton Auflagerkräfte
  721. def CA_command():
  722. Einstellungen.Zeige_Auflagerkräfte = CA_Variable.get()=="ja"
  723.  
  724. CA_Variable = StringVar()
  725. CA_Variable.set("nein")
  726.  
  727. CA = Checkbutton(CLF, command=CA_command, variable=CA_Variable,
  728. onvalue="ja", offvalue="nein",
  729. text = "Auflagerkräfte")
  730. CA.grid(row=1, column=0, sticky="w")
  731.  
  732. # Checkbutton Spuraufzeichnung
  733. def CS_command():
  734. Einstellungen.Zeige_Spur = CS_Variable.get()=="ja"
  735.  
  736. CS_Variable = StringVar()
  737. CS_Variable.set("nein")
  738.  
  739. CS = Checkbutton(CLF,command=CS_command, variable=CS_Variable,
  740. onvalue="ja", offvalue="nein",
  741. text = "Spurlinien")
  742. CS.grid(row=2, column=0, sticky="w")
  743.  
  744. # Radiobuttons für Auswahl der Rechtsklick-Aktion
  745. RLF = LabelFrame(TE,text="Rechtsklick erzeugt:",padx=5,pady=5)
  746. RLF.grid(row=1,column=1,sticky="wen",padx=5,pady=5)
  747.  
  748. RR_Variable = StringVar()
  749. RR_Variable.set("M")
  750.  
  751. Radiobutton(RLF,variable=RR_Variable,text="Massepunkt",value="M",underline=0
  752. ).grid(row=0,column=0,sticky="w")
  753. Radiobutton(RLF,variable=RR_Variable,text="Auflager",value="L",underline=3
  754. ).grid(row=1,column=0,sticky="w")
  755. Radiobutton(RLF,variable=RR_Variable,text="Feder",value="F",underline=0
  756. ).grid(row=2,column=0,sticky="w")
  757.  
  758. # Buttons für Schnelleinstellungen
  759.  
  760. # Button "Antigravity" (https://xkcd.com/353/)
  761. def BG_command():
  762. """Schaltet Schwerelosigkeit ein"""
  763. SG.set(0)
  764.  
  765. Button(TE,text="Antigravity",command=BG_command
  766. ).grid(row=2,column=0,sticky="we")
  767.  
  768. # Button "Entspannen"
  769. def BE_command():
  770. """Setzt Federkräfte auf null.
  771. Die aktuelle Federlänge l wird zur entspannten Länge l0."""
  772. for Feder in Abhängige_Elemente:
  773. Feder.reset()
  774.  
  775. Button(TE,text="Entspannen",command=BE_command
  776. ).grid(row=2,column=1,sticky="we")
  777.  
  778. # Infozeile wird sekündlich aktualisiert
  779. def Infozeilenupdate(event=None):
  780. A = len(Lager_Elemente)
  781. M = len(Masse_Elemente)
  782. F = len(Abhängige_Elemente)
  783. Infozeile.config(text=
  784. "%i Auflager, %i Massepunkt%s, %i Feder%s, %i Physikschritte/s"%(
  785. A,M,"" if M==1 else "e",F,"" if F==1 else "n",Einstellungen.PPS))
  786. TE.after(1000,Infozeilenupdate)
  787.  
  788. Infozeile = Label(TE)
  789. Infozeile.grid(row=3,column=0,columnspan=2,sticky="w")
  790. Infozeilenupdate()
  791.  
  792. # Go!
  793. T.mainloop()

--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums

Heute schon programmiert? Einführung in Python 3 (PDF)

Tags:
Python, Simulation, Chaos, Pendel, Masse, Feder, Schwingung

RSS-Feed dieser Diskussion
powered by my little forum