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
ysf的沙龍
27會員
168內容數
寫點東西自娛娛人
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
邏輯是我們思考的基礎,影響著我們如何看待世界和進行推論。透過假設前提和推論,我們可以從邏輯的角度來思考生活中的各種情況和決策。深入瞭解邏輯可以幫助我們更清晰地思考,理解事物之間的關聯。
Thumbnail
邏輯是我們思考的基礎,影響著我們如何看待世界和進行推論。透過假設前提和推論,我們可以從邏輯的角度來思考生活中的各種情況和決策。深入瞭解邏輯可以幫助我們更清晰地思考,理解事物之間的關聯。
Thumbnail
在物理的領域裏 不變的物質有著恆常的定律 於是乎 月球繞著地球轉 地球繞著太陽轉 太陽繞著銀河系   在化學的領域裏 物質隨著原子們的排列組合 形成了 形色多變銀河系 各有千秋太陽系 繽紛美麗的地球   在數學的領域裏 數字的跳躍翻轉變化萬千中 綜言是 正負平方開根
Thumbnail
在物理的領域裏 不變的物質有著恆常的定律 於是乎 月球繞著地球轉 地球繞著太陽轉 太陽繞著銀河系   在化學的領域裏 物質隨著原子們的排列組合 形成了 形色多變銀河系 各有千秋太陽系 繽紛美麗的地球   在數學的領域裏 數字的跳躍翻轉變化萬千中 綜言是 正負平方開根
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
Thumbnail
大語言模型(如GPT-3和GPT-4)的出現改變了我們與機器互動的方式。這些模型能夠理解和生成自然語言,實現許多以前無法想像的應用。然而,你可能會好奇,這些模型究竟是如何理解語言的?這裡,我們來探討一個關鍵的概念:「一切語義都是關係」。
Thumbnail
大語言模型(如GPT-3和GPT-4)的出現改變了我們與機器互動的方式。這些模型能夠理解和生成自然語言,實現許多以前無法想像的應用。然而,你可能會好奇,這些模型究竟是如何理解語言的?這裡,我們來探討一個關鍵的概念:「一切語義都是關係」。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News