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)





15會員
129內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。