The Nature of Code閱讀心得與Python實作:3.10 The Pendulum

更新於 2024/09/20閱讀時間約 3 分鐘

這一節要模擬的是擺(pendulum)這個裝置中,構造最簡單、具有理想化性質的單擺(simple pendulum)。

單擺是由一條下端連接著一個擺錘(bob),上端懸掛在固定的樞軸(pivot)上的擺臂(arm)組合而成,如圖。當單擺靜止時,擺角,也就是擺臂和鉛垂線之間的夾角,大小會是0;當把把靜止的擺錘往旁邊拉高並放開之後,因為重力及擺臂所提供的張力(tension)的作用,它就會依循著一條弧線來回擺動。

raw-image

在真實世界中,單擺的運動是在3D空間中進行的,但是,因為我們的模擬世界是個2D的世界,所以我們就只考慮在2D空間中的單擺。

因為單擺是懸掛在樞軸上,所以擺錘在受到重力的作用時,並不會直接掉到地上,而會在重力及擺臂所提供的張力共同作用下,來回擺動。這時候,如果想要模擬擺錘的運動,就不能單純的只考慮重力而已,而必須同時考慮擺角的大小,藉此算出擺錘的角加速度以及角速度來進行模擬。

要算出擺錘的角加速度其實不難,簡單的幾何作圖以及三角函數,還有牛頓第二運動定律,就可以辦到。

raw-image

參考上圖。作用於擺錘的重力Fg,可以被分解為Fgx及Fgy兩個互相垂直的分量。Fgy跟擺臂提供的張力T,大小相等但方向相反,所以擺錘在運動時,才沒有往外飛出去或向樞軸的方向飛過去;Fgx垂直於擺臂,是促使擺錘擺動的作用力。因為擺臂跟擺錘的運動方向互相垂直,所以不管是T或Fgy,都不會影響擺錘的運動;會影響擺錘運動的,就是Fgx

為了容易辨識,我們把Fgx改名叫做Fp;又因為它是作用在擺錘上,所以畫圖的時候,可以把起始點移到擺錘的中心點上,這樣更能呈現出它的作用對象是擺錘。

那要怎麼算出Fp呢?假設擺角是θ,由圖可以知道,Fp的大小會是

Fg sinθ

除了大小之外,也必須知道方向。因為Fp的作用方向,是朝著把擺錘拉回靜止時候的位置的緣故,所以當單擺位於鉛垂線右手邊時,Fp<0;而當單擺位於鉛垂線左手邊時,Fp>0。另外,當單擺位於鉛垂線右手邊時,擺角θ的值是正的,所以sinθ>0;而當單擺位於鉛垂線左手邊時,擺角θ的值是負的,所以sinθ<0。因此,根據這些條件以及已知的Fp大小,我們可以得到

Fp = -Fg sinθ

若擺錘的質量是m、重力加速度是g,則

Fg = mg

所以,最後可以得到

Fp = -mg sinθ

有了Fp之後,就可以用牛頓第二定律來找出擺錘的角加速度了。不過,因為擺錘是繞著樞軸旋轉,所以我們用的是牛頓第二定律在這種情況下相對應的版本:

τ = Iα

其中,τ是力矩(torque),是作用在物體上,使物體繞著轉動軸或支點轉動的物理量;I是轉動慣量(moment of inertia),是衡量讓擺繞著樞軸轉動的困難度的物理量;α則是角加速度。假設擺長,也就是擺臂長度,是r,則τ和I的計算方式為

τ = rFp = -rmg sinθ

I = mr2

所以

α = - g sinθ / r

利用這條式子,只要知道擺長、擺角和重力加速度,就可以算出擺錘的角加速度。值得注意的是,在式子中,並未出現擺錘的質量;也就是說,擺錘的角加速度並不會受到擺錘質量大小的影響。

在寫程式時,雖然在真實世界中,地球上的重力加速度是9.8 m/s2,不過,畢竟我們只是要在模擬世界中呈現出擺錘的運動效果,所以就跟在第二章處理萬有引力常數時一樣,在模擬時,我們並不需要去管真正的重力加速度值是多少,只要任意設定一個看起來效果不錯的數字就可以了。

假設變數gravity裡頭放的,是我們設定的重力加速度值、擺角是anglearm_length是擺長。利用前面導出的擺錘角加速度計算式,以及先前處理角速度和角加速度的方法,模擬的程式可以寫成:

angular_acceleration = -gravity*math.sin(angle)/arm_length
angular_velocity += angular_acceleration
angle += angular_velocity

有了擺角之後,因為擺長是已知的,而且樞軸的位置是我們自己選的,所以就可以算出擺錘的位置。假設樞軸的位置在(x0, y0),而擺角和擺長分別是θ和r,從下圖可以很容易就看出來,擺錘的位置會在

(x0 + r sinθ, y0 + r cosθ)

raw-image

所以,如果pivot=pygame.Vector2(x0, y0)是樞軸位置,計算擺錘位置的程式可以這樣寫

x = arm_length*math.sin(angle)
y = arm_length*math.cos(angle)
bob_position = pivot + pygame.Vector2(x, y)

或者更精簡地寫成

x, y = math.sin(angle), math.cos(angle)
bob_position = pivot + arm_length*pygame.Vector2(x, y)

有了這些之後,就可以設計模擬所需的Pendulum類別了。設計完成的Pendulum類別如下:

class Pendulum:
def __init__(self, x, y, arm_length, angle_deg):
# 取得顯示畫面
self.screen = pygame.display.get_surface()

# 樞軸位置,由傳入的x、y數值決定
self.pivot = pygame.Vector2(x, y)

# 擺長、擺角
self.arm_length = arm_length
self.angle_rad = math.radians(angle_deg)

# 擺錘的初始角加速度及初始角速度
self.angular_acceleration = 0
self.angular_velocity = 0

# 擺錘位置
x, y = math.sin(self.angle_rad), math.cos(self.angle_rad)
self.bob_position = self.pivot + self.arm_length*pygame.Vector2(x, y)

# 阻尼
self.damping = 0.995

def update(self):
# 重力加速度
gravity = 0.4

self.angular_acceleration = -gravity*math.sin(self.angle_rad)/self.arm_length
self.angular_velocity += self.angular_acceleration
self.angle_rad += self.angular_velocity

x, y = math.sin(self.angle_rad), math.cos(self.angle_rad)
self.bob_position = self.pivot + self.arm_length*pygame.Vector2(x, y)

self.angular_velocity *= self.damping

def show(self):
pygame.draw.line(self.screen, (0, 0, 0), self.pivot, self.bob_position)
pygame.draw.circle(self.screen, (0, 0, 0), self.bob_position, 16)

在設計Pendulum類別時,我們加入了阻尼(damping),來讓單擺擺動幅度越來越小,這樣子模擬出來的效果,會比較像真實世界中單擺的擺動方式。

先前我們分析所得到的結果,都是針對具理想化性質的單擺所做的分析和推導。這也就是說,我們不僅假設擺錘的質量集中在中心點的位置,同時也沒有考慮樞軸的摩擦力、擺臂的重量、空氣阻力、擺臂是不是會彎曲等等問題;這些在真實世界中,都會對單擺的運動造成影響。不過,畢竟在模擬世界中的單擺,不需要鉅細靡遺地去斤斤計較這麼多,只要它擺動的方式,看起來跟在真實世界中的擺動方式一樣就可以了。在真實世界中的單擺,不管設計再精良,擺錘的擺動幅度,一定會越來越小,最後停止擺動。那要怎麼讓我們模擬出來的單擺,也有這樣子的特性呢?

要讓模擬世界中的單擺擺動幅度越來越小,做法很簡單,只要加個阻尼,讓每一幀的角速度都比原來小一點就可以了。程式可以這樣寫:

angular_velocity *= damping

這裡的damping裡頭放的,是一個小於1的數字。這樣子,當每次計算角速度時,都會比原先的數值小一些,進而使得擺錘的擺動幅度越來越小,到最後停止擺動。

最後,需要提到的是,在設計Pendulum類別時,原書是把計算擺錘位置的程式碼,放在show()方法中,而在這裡,則是放在update()方法中。之所以會如此做,是因為在模擬時,我們不見得會想要畫出擺錘。這時,如果依照原書的設計方式,我們不會執行show(),那當然也就不會知道擺錘的位置在哪裡。所以,把計算擺錘位置的程式寫在update()中,讓處理系統狀態的部分和處理呈現畫面部分的功能各自獨立,這樣子才不會糾纏不清,應該會是比較好的做法;事實上,先前在設計Mover()類別時,就是這麼做的。

Example 3.11: Swinging Pendulum

# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 3.11: Swinging Pendulum")

WHITE = (255, 255, 255)

WIDTH, HEIGHT = screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

# 樞軸位置
x, y = WIDTH//2, 10

# 擺長
arm_length = 225

# 擺角,單位是「度」
angle_deg = 45

pendulum = Pendulum(x, y, arm_length, angle_deg)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

pendulum.update()
pendulum.show()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 3.15

# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 3.15")

WHITE = (255, 255, 255)

WIDTH, HEIGHT = screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

# 單擺數量
n = 5 # number of pendulums

# 樞軸位置
x, y = WIDTH//2, 5

# 擺長
arm_length = 50

# 擺角,單位是「度」
angle = 25 # in degree

pendulums = [Pendulum(x, y, arm_length+7*i, angle+5*i) for i in range(n)]

# 把所有單擺的阻尼都設為1,讓它們永遠都動個不停,這樣看起來會更有趣
for pendulum in pendulums:
pendulum.damping = 1

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

pendulums[0].update()
for i in range(1,n):
# 把樞軸放在上一個單擺擺錘的位置上
pendulums[i].pivot = pendulums[i-1].bob_position
pendulums[i].update()

for pendulum in pendulums:
pendulum.show()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 3.16

正向力的大小為Fgravity cosθ

raw-image

Exercise 3.17

假設方塊的質量是m,而重力加速度是g,則

Fgravity = mg

若斜面的摩擦係數是μ,因方塊下滑的力道為Fgravity sinθ,而摩擦力為-μFgravity cosθ,所以合力為Fgravity(sinθ - μcosθ)。換句話說,當sinθ ≤ μcosθ時,方塊是靜止不動的。

class Box:
def __init__(self, x, y, mass):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

# 讓傳遞進來的數值來決定物體的質量
self.mass = mass

# 物體的質量越大,尺寸就會越大
self.size = 16*self.mass

# box的傾斜角度
self.angle_deg = 0

# 垂直於斜面,長度為box邊長一半的向量
u = pygame.Vector2(self.size/2, 0).rotate(self.angle_deg+45)

# box在斜面上的起始位置
self.position = pygame.Vector2(x, y) - u

# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)

# 設定box所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)

def apply_force(self, force):
self.acceleration += force/self.mass

def update(self):
self.velocity += self.acceleration
self.position += self.velocity

self.acceleration *= 0

def show(self):
rect = pygame.Rect(0, 0, self.size, self.size)
pygame.draw.rect(self.surface, (0, 0, 0, 50), rect)

rotated_surface = pygame.transform.rotate(self.surface, self.angle_deg)
rect_new = rotated_surface.get_rect(center=self.position)
self.screen.blit(rotated_surface, rect_new)

def check_edges(self):
if self.position.x - self.size/2 < 0:
self.position.x = self.size/2
self.velocity *= 0

if self.position.y + self.size/2 >= self.height:
self.position.y = self.height - self.size/2
self.velocity.y = 0


class Incline:
def __init__(self, x, y, angle_deg):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

# 斜面的傾斜角度,單位是「度」
self.angle_deg = angle_deg

# 斜面頂部位置
self.top = pygame.Vector2(x, y)

# 斜面底部位置
dy = self.height - y
dx = dy/math.tan(math.radians(angle_deg))
self.bottom = pygame.Vector2(x-dx, self.height)

def on_incline(self, position_x):
# 假設物體已在斜面上,所以只檢查水平位置
x1, x2 = self.top.x, self.bottom.x
x_max, x_min = max(x1, x2), min(x1, x2)
if x_min <= position_x <= x_max:
return True
else:
return False

def show(self):
pygame.draw.line(self.screen, (0, 0, 0), self.top, self.bottom)


def friction_force(mu, velocity, N=1):
if velocity.length() > 0:
v_hat = velocity.normalize()
else:
v_hat = pygame.Vector2(0, 0)

return -mu*N*v_hat


# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 3.17")

WHITE = (255, 255, 255)

width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 20
frame_rate = pygame.time.Clock()

# 斜面頂部位置
x, y = 600, 150

# 斜面傾斜角度
angle_deg = 45
angle_rad = math.radians(angle_deg)

incline = Incline(x, y, angle_deg)
slide_direction = (incline.bottom - incline.top).normalize()

# box質量
mass = 3

box = Box(x, y, mass)

# 重力加速度
g = 1

# 斜面及水平面的摩擦係數
mu = 0.8

# 方塊是否正在滑下
sliding = True if math.sin(angle_rad)-mu*math.cos(angle_rad)>0 else False

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

if incline.on_incline(box.position.x):
box.angle_deg = incline.angle_deg

if sliding:
slide_force = mass*g*math.sin(angle_rad)*slide_direction

normal_force = mass*g*math.cos(angle_rad)
friction = friction_force(mu, slide_direction, normal_force)

net_force = slide_force - friction
else:
# 方塊為靜止狀態,合力為0
net_force = pygame.Vector2(0, 0)
else:
# 讓方塊滑到底部之後往前翻滾
if box.position.x + box.size/2 >= incline.bottom.x:
box.angle_deg += 5
else:
box.angle_deg = 90

# 在底部滑動時,如果算出來的摩擦力方向反轉,代表方塊已呈靜止狀態,
# 而摩擦力已由動摩擦力轉變成靜摩擦力,所以合力應為0
friction = friction_force(mu, box.velocity, mass*g)
if friction.x < 0:
box.velocity = pygame.Vector2(0, 0)
net_force = pygame.Vector2(0, 0)
else:
net_force = friction

box.apply_force(net_force)
box.update()
box.check_edges()
box.show()

incline.show()

pygame.display.update()
frame_rate.tick(FPS)


avatar-img
15會員
130內容數
寫點東西自娛娛人
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
我們曾經利用sin函數來模擬彈簧吊錘(bob)的運動,雖然這樣子的做法程式很容易寫,但是卻沒辦法模擬彈簧吊錘受到如風力、重力等環境中其他作用力的影響下,在空間中的運動狀況。要克服這樣子的問題,就不能再倚靠sin函數,而必須改用能夠用來計算彈簧彈力的虎克定律(Hooke's law)。
在x軸上依序取一些點,然後把這些點以及其對應的sin函數的值所構成的二維座標點畫出來時,就可以看到由這個sin函數所產生的像波一樣的圖案,也就是波型(wave pattern)。不同樣式的波型,可以用來設計生物的軀幹或肢體,也可以用來模擬像水這類柔軟的表面。
藉由設定振幅、頻率、週期等性質,我們可以模擬出真實世界中的振盪現象。其實,用稍微簡單一點的方式來處理,依舊可以得到相同的效果。
這一節談的是振盪(oscillation)。日常生活中,隨處都可見到振盪的現象。例如,彈奏弦樂器時,弦的振動、盪鞦韆時的來回擺動、音叉的振動、單擺的來回擺動、彈簧的振動等。除了這些眼睛看得到的之外,麥克風、交流電、收音機、手機等許許多多的電子產品,也都是利用振盪的原理來運作的。
除了直角座標系統外,極座標(polar coordinate)系統是另一種相當有用的座標系統。
在模擬運動中的物體時,如果物體是圓形,那就不需要考慮旋轉的問題,畢竟不管怎麼轉,圓還是圓,看起來都一樣。但是,如果物體不是圓形而是其他形狀呢?
我們曾經利用sin函數來模擬彈簧吊錘(bob)的運動,雖然這樣子的做法程式很容易寫,但是卻沒辦法模擬彈簧吊錘受到如風力、重力等環境中其他作用力的影響下,在空間中的運動狀況。要克服這樣子的問題,就不能再倚靠sin函數,而必須改用能夠用來計算彈簧彈力的虎克定律(Hooke's law)。
在x軸上依序取一些點,然後把這些點以及其對應的sin函數的值所構成的二維座標點畫出來時,就可以看到由這個sin函數所產生的像波一樣的圖案,也就是波型(wave pattern)。不同樣式的波型,可以用來設計生物的軀幹或肢體,也可以用來模擬像水這類柔軟的表面。
藉由設定振幅、頻率、週期等性質,我們可以模擬出真實世界中的振盪現象。其實,用稍微簡單一點的方式來處理,依舊可以得到相同的效果。
這一節談的是振盪(oscillation)。日常生活中,隨處都可見到振盪的現象。例如,彈奏弦樂器時,弦的振動、盪鞦韆時的來回擺動、音叉的振動、單擺的來回擺動、彈簧的振動等。除了這些眼睛看得到的之外,麥克風、交流電、收音機、手機等許許多多的電子產品,也都是利用振盪的原理來運作的。
除了直角座標系統外,極座標(polar coordinate)系統是另一種相當有用的座標系統。
在模擬運動中的物體時,如果物體是圓形,那就不需要考慮旋轉的問題,畢竟不管怎麼轉,圓還是圓,看起來都一樣。但是,如果物體不是圓形而是其他形狀呢?
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
到目前為止,我們所模擬的萬有引力,是一個物體吸引另一個物體,或者是一個物體吸引多個物體。然而,在真實世界中,每個物體都會互相吸引,所以在這一節中,就來把模擬的情境,擴展成多個物體互相吸引。
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 1.2.2 一個速度問題 1.2.3 幾何的方法 1.2.4 微積分的記法 1.2.5弦的振動 八 在關於振動弦通解的這場論爭之中,函數概念默默地向兩個方面推前了一大步。 一方面,特朗貝爾和歐拉等擴大了
使用向量來處理問題有很多好處,其中一個好處,就是可以減少變數的數量。在這節中,會用一個簡單的例子來介紹,使用向量跟不使用向量,對變數的數量會有什麼樣的影響。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 1.2.2 一個速度問題 1.2.3 幾何的方法 1.2.4 微積分的記法 1.2.5弦的振動  七 雖然論爭沒有得出任何定論,但對函數概念的演化卻影嚮頗深。 在這次歷時多年的論爭中,函數概念得以擴大而包括
Thumbnail
這篇介紹如何用加速度取得傾斜角度。 用的是和前篇一樣的<basicMpu6050.h>
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
到目前為止,我們所模擬的萬有引力,是一個物體吸引另一個物體,或者是一個物體吸引多個物體。然而,在真實世界中,每個物體都會互相吸引,所以在這一節中,就來把模擬的情境,擴展成多個物體互相吸引。
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 1.2.2 一個速度問題 1.2.3 幾何的方法 1.2.4 微積分的記法 1.2.5弦的振動 八 在關於振動弦通解的這場論爭之中,函數概念默默地向兩個方面推前了一大步。 一方面,特朗貝爾和歐拉等擴大了
使用向量來處理問題有很多好處,其中一個好處,就是可以減少變數的數量。在這節中,會用一個簡單的例子來介紹,使用向量跟不使用向量,對變數的數量會有什麼樣的影響。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 1.2.2 一個速度問題 1.2.3 幾何的方法 1.2.4 微積分的記法 1.2.5弦的振動  七 雖然論爭沒有得出任何定論,但對函數概念的演化卻影嚮頗深。 在這次歷時多年的論爭中,函數概念得以擴大而包括
Thumbnail
這篇介紹如何用加速度取得傾斜角度。 用的是和前篇一樣的<basicMpu6050.h>