The Nature of Code閱讀心得與Python實作:2.6 The n-Body Problem

更新於 發佈於 閱讀時間約 25 分鐘

到目前為止,我們所模擬的萬有引力,是一個物體吸引另一個物體,或者是一個物體吸引多個物體。然而,在真實世界中,每個物體都會互相吸引,所以在這一節中,就來把模擬的情境,擴展成多個物體互相吸引。

先前我們所寫的MoverAttractor兩個類別,在模擬時,就只有Attractor類別的物件會吸引Mover類別的物件。但既然在真實世界中,每個物體都會互相吸引,所以我們不再使用MoverAttractor這兩個類別,而是設計一個新的Body類別,讓Body類別的不同物件互相吸引。

描述多個物體因萬有引力彼此互相吸引時的運動情形,叫做「多體問題」(n-body problem)。多體問題中,只考慮兩個物體的「雙體問題」(two-body problem),存有解析解,也就是能夠用數學式子精確地描述這兩個物體的運動情形。然而,只比兩個物體再多一個物體的「三體問題」(three-body problem),看起來雖然很單純,但它們的運動情形,卻複雜到不存在解析解。劉慈欣著名的科幻小說《三體》,就是以三體問題為引子所展開的故事。

雖然多體問題很複雜,不過利用這一章所提到的方法,倒是可以很粗略地模擬出多體問題的效果。接下來就來設計Body類別。

Body類別的設計很簡單,就是把把Attractor類別中的attract()方法,給複製到Mover類別中,然後稍微修改一下就可以了。修改後的attract()方法,程式如下:

def attract(self, body):
# 引力方向
d = self.position - body.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)

distance = d.magnitude()
# 限制距離數值的範圍,避免極端狀況發生
if distance < 5:
distance = 5
elif distance > 25:
distance = 25

# 引力大小
strength = self.G*self.mass*body.mass/distance**2

# 引力
force = strength*r_hat

# 將引力作用到物體上
body.apply_force(force)

這裡要注意一下,這裡的attract()寫法,跟上一節在萬有引力部分最後所採用的attract()寫法不太一樣。上一節的寫法,attract()會傳回計算出來的引力,然後再使用mover物件的apply_force()方法,來將引力作用在mover上;但是這裡的寫法,是直接在attract()內,就將引力作用在要作用的物件上。兩種寫法都能達到目的,不過如果以程式閱讀的角度來看,現在這種寫法,讀起來會比較順暢,接近一般的文字閱讀;通常當我們說「物體A吸引物體B」時,就認定引力已經作用在物體B上了。

下面這個例子,就是利用Body類別建立兩個物件,然後讓它們互相吸引。對於「多體問題」而言,物體最後所呈現出的運動情形,會完全取決於它們的初始條件。所以在這個例子中,分別設定了兩個物體方向相反的初始速度,最後兩個物體會呈現出繞圈圈的運動模式。

Example 2.8: Two-Body Attraction

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

self.G = G

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

# 物體的質量越大,尺寸就會越大
self.size = (16*self.mass)**0.5
self.radius = self.size/2

# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)

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

# 設定body所在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):
# 使用具透明度的白色把body所在的surface清空
self.surface.fill((255, 255, 255, 0))

# 畫出具有透明度的body
center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, (0, 0, 0, 50), center, self.radius)

# 把body所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)

def attract(self, body):
# 引力方向
d = self.position - body.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)

distance = d.magnitude()
# 限制距離數值的範圍,避免極端狀況發生
if distance < 5:
distance = 5
elif distance > 25:
distance = 25

# 引力大小
strength = self.G*self.mass*body.mass/distance**2

# 引力
force = strength*r_hat

# 將引力作用到物體上
body.apply_force(force)


# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 2.8: Two-Body Attraction")

WHITE = (255, 255, 255)

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

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

bodyA = Body(320, 100, 30, 0.5)
bodyB = Body(320, 260, 30, 0.5)

# 設定兩個物體方向相反的初始速度
bodyA.velocity = pygame.Vector2(1, 0)
bodyB.velocity = pygame.Vector2(-1, 0)

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

screen.fill(WHITE)

# A吸引B;B也吸引A
bodyA.attract(bodyB)
bodyB.attract(bodyA)

bodyA.update()
bodyA.show()

bodyB.update()
bodyB.show()

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

Exercise 2.14

加入一個物體,變成三個物體。

# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 2.14")

WHITE = (255, 255, 255)

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

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

bodyA = Body(320, 40, 32, 0.5)
bodyB = Body(320, 200, 32, 0.5)
bodyC = Body(320, 320, 32, 0.5)

# 設定A、B方向相反的初始速度,C則一開始靜止不動
bodyA.velocity = pygame.Vector2(2, 0)
bodyB.velocity = pygame.Vector2(-2, 0)
bodyC.velocity = pygame.Vector2(0, 0)

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

screen.fill(WHITE)

# 物體間互相吸引
bodyA.attract(bodyB)
bodyA.attract(bodyC)
bodyB.attract(bodyA)
bodyB.attract(bodyC)
bodyC.attract(bodyA)
bodyC.attract(bodyB)

bodyA.update()
bodyA.show()

bodyB.update()
bodyB.show()

bodyC.update()
bodyC.show()

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

這個練習應該用list搭配迴圈來寫會比較省時省力,而且程式也比較簡潔易讀。接下來就來看看,要怎麼用list搭配迴圈來寫包含多個物體的模擬程式。

假設bodys這個list裡頭有很多個Body物件。當bodys中一個叫bodyA的物件被其他bodys中的物件吸引時,程式這樣寫:

for bodyB in bodys:
# 自己不會吸引自己
if bodyB is not bodyA:
bodyB.attract(bodyA)

bodyA.update()
bodyA.show()

每一個在bodys裡頭的物件都這麼做一次,程式這樣寫:

for bodyA in bodys:
for bodyB in bodys:
# 自己不會吸引自己
if bodyB is not bodyA:
bodyB.attract(bodyA)

for body in bodys:
body.update()
body.show()

所以這樣子的程式,就可以模擬多個物體彼此吸引的情況。

這段程式,原書的寫法是把bodyA.update()bodyA.show()放在bodyA的迴圈裡頭,寫成

for bodyA in bodys:
    for bodyB in bodys:
        # 自己不會吸引自己
        if bodyB is not bodyA:
            bodyB.attract(bodyA)

    bodyA.update()
    bodyA.show()

這種寫法,其實並不是那麼的準確。怎麼說呢?我們來分析一下A、B兩個物體互相吸引時的情況,就可以知道了。要模擬A、B兩個物體互相吸引,我們需要先後處理A吸引B,以及B吸引A兩種情況;當然,我們先前就已經知道,這兩個吸引力,大小相同方向相反。假設A和B間的距離是1,依照原書的寫法,我們會先處理B吸引A,也就是

B.attract(A)

當完成後,就馬上更新A的狀態,也就是

A.update()

當A的狀態更新之後,其位置會因為B的引力作用而有所改變,因而A、B間的距離會縮短,不再是1。所以,當接下來在處理A吸引B時,也就是執行

A.attract(B)

時,因為A、B間的距離已經改變了,並不是原來的1,所以計算出來的A作用在B上的引力大小,與先前計算出來的B作用在A上的引力大小,兩者不會一樣大。這結果顯然是有問題的,不過所幸如果A、B間的距離變化不太大,那在模擬時,應該是很難看出有什麼不對勁的。

Example 2.9: n Bodies

# python version 3.10.9
import random
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 2.9: n Bodies")

WHITE = (255, 255, 255)

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

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

bodys = [Body(random.randint(0, WIDTH),
random.randint(0, HEIGHT),
random.uniform(10, 50),
0.1)
for i in range(10)]

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

screen.fill(WHITE)

for bodyA in bodys:
for bodyB in bodys:
# 自己不會吸引自己
if bodyB is not bodyA:
bodyB.attract(bodyA)

for body in bodys:
body.update()
body.show()

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

Exercise 2.15

要把引力變成斥力,只需要在引力前面加個負號就可以了。

Body類別中新增repel()方法:

def repel(self, body):
# 引力方向
d = self.position - body.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)

distance = d.magnitude()
# 限制距離數值的範圍,避免極端狀況發生
if distance < 5:
distance = 5
elif distance > 25:
distance = 25

# 引力大小
strength = self.G*self.mass*body.mass/distance**2

# 引力加個負號變成斥力
force = -strength*r_hat

# 將斥力作用到物體上
body.apply_force(force)

在主程式中,造一個Body物件當作attractor,並把它的位置設成滑鼠游標的位置。

# python version 3.10.9
import random
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 2.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()

bodys = [Body(random.randint(0, WIDTH),
random.randint(0, HEIGHT),
random.uniform(10, 30),
0.05)
for i in range(10)]

# 在滑鼠游標處造一個attractor
x, y = pygame.mouse.get_pos()
attractor = Body(x, y, 25, 0.5)

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

screen.fill(WHITE)

# 把attractor的位置設定成滑鼠游標的位置
attractor.position = pygame.Vector2(pygame.mouse.get_pos())

for bodyA in bodys:
attractor.attract(bodyA)
for bodyB in bodys:
if bodyB is not bodyA:
bodyB.repel(bodyA)

for body in bodys:
body.update()
body.show()

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

Exercise 2.16

在一個引力很大的物體外圍的圓上,放置一些較小的物體。較小物體的初始速度方向,是所在圓位置的切線方向。調整初始速度的大小,就可以讓這些較小的物體,繞著引力很大的物體進行圓周運動。

半徑為r,圓心位於(0, 0)的圓,在圓上的點(r, 0),其切線方向的單位向量是(0, 1)或(0, -1)。把位置向量(r, 0)旋轉一個角度,即可得到位於圓上一點的座標;至於該點切線方向的單位向量,則可以將(0, 1)或(0, -1)旋轉相同的角度算出。

# python version 3.10.9
import math
import random
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 2.16")

WHITE = (255, 255, 255)

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

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

attractor = Body(WIDTH/2, HEIGHT/2, 500)

# body數量
n = 30

bodys = []
for i in range(n):
r = random.uniform(50, 200) # body到attractor的距離
theta = random.uniform(0, 2*math.pi)

# 建立body之後,再設定其初始位置和速度
body = Body(0, 0, 25, 0.2)
body.position = pygame.Vector2(WIDTH/2, HEIGHT/2) + pygame.Vector2(r, 0).rotate_rad(theta)
body.velocity = 15*pygame.Vector2(0, -1).rotate_rad(theta)

bodys.append(body)

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

screen.fill(WHITE)

for bodyA in bodys:
attractor.attract(bodyA)
for bodyB in bodys:
if bodyB is not bodyA:
bodyB.attract(bodyA)

for body in bodys:
body.update()
body.show()

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





留言
avatar-img
留言分享你的想法!
avatar-img
ysf的沙龍
19會員
157內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2024/08/09
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
2024/08/09
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
2024/08/02
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
2024/08/02
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
2024/07/29
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
2024/07/29
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
看更多
你可能也想看
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
在物理的領域裏 不變的物質有著恆常的定律 於是乎 月球繞著地球轉 地球繞著太陽轉 太陽繞著銀河系   在化學的領域裏 物質隨著原子們的排列組合 形成了 形色多變銀河系 各有千秋太陽系 繽紛美麗的地球   在數學的領域裏 數字的跳躍翻轉變化萬千中 綜言是 正負平方開根
Thumbnail
在物理的領域裏 不變的物質有著恆常的定律 於是乎 月球繞著地球轉 地球繞著太陽轉 太陽繞著銀河系   在化學的領域裏 物質隨著原子們的排列組合 形成了 形色多變銀河系 各有千秋太陽系 繽紛美麗的地球   在數學的領域裏 數字的跳躍翻轉變化萬千中 綜言是 正負平方開根
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
大語言模型(如GPT-3和GPT-4)的出現改變了我們與機器互動的方式。這些模型能夠理解和生成自然語言,實現許多以前無法想像的應用。然而,你可能會好奇,這些模型究竟是如何理解語言的?這裡,我們來探討一個關鍵的概念:「一切語義都是關係」。
Thumbnail
大語言模型(如GPT-3和GPT-4)的出現改變了我們與機器互動的方式。這些模型能夠理解和生成自然語言,實現許多以前無法想像的應用。然而,你可能會好奇,這些模型究竟是如何理解語言的?這裡,我們來探討一個關鍵的概念:「一切語義都是關係」。
Thumbnail
邏輯是我們思考的基礎,影響著我們如何看待世界和進行推論。透過假設前提和推論,我們可以從邏輯的角度來思考生活中的各種情況和決策。深入瞭解邏輯可以幫助我們更清晰地思考,理解事物之間的關聯。
Thumbnail
邏輯是我們思考的基礎,影響著我們如何看待世界和進行推論。透過假設前提和推論,我們可以從邏輯的角度來思考生活中的各種情況和決策。深入瞭解邏輯可以幫助我們更清晰地思考,理解事物之間的關聯。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News