跳轉到

6.3 龐加萊圓盤模型的 SymPy 實驗(1882 年)#

學習動機#

前兩節,我們做了一件在數學史上罕見的事:把「第五公設可以不要」與「曲率決定幾何」這兩件抽象的事,講成了具體的算式。但它們都還只是紙面上的推理:薩凱里與鮑耶花了一百年也指不出矛盾,黎曼的曲率張量更是抽象到無法「看」。

本節要補上最後一塊拼圖:一個可以拿尺量、可以看圖、可以用程式算的世界。這就是「模型」(model)。

為什麼非要模型不可?因為抽象的公理系統只告訴你「規則是什麼」,卻不告訴你「在這個世界裡,一條線段有多長、三個角加起來是多少」。歷史上的困難正在於此:鮑耶在 1823 年創造出一整套「絕對幾何」,卻無法回答一個很實際的問題——這套幾何真的存在嗎?還是只是紙上的矛盾? 這個問題要到 1868 年貝特拉米(Beltrami)給出第一個模型時才被解決:他發現,雙曲幾何的所有規則,在歐氏平面裡可以用一個「換裝」的方式完整演出。於是:

若雙曲幾何會導出矛盾,則歐氏幾何也會導出矛盾——所以它不矛盾。

這是一個相對一致性(relative consistency)的證明。它的力量在於:把一個「關於另一個世界的斷言」變成「關於我們熟悉的世界的斷言」。只要我們能在紙上把雙曲幾何演出來,它就不可能是空的。

龐加萊(Poincaré)在 1882 年給出了最好用的版本:圓盤模型。整個無限大的雙曲平面,被壓縮進一個有限的圓盤裡;無窮遠被折疊成圓盤的邊界;「直線」變成與邊界正交的圓弧。從此,雙曲幾何變成一門可以用尺、可以用圓規、可以用程式處理的具體學問——這正是 6.1 節末尾薩凱里的「尺規構造」所呼喚的東西。

本節我們要做的事,可以用一句話概括:用 SymPy 把「另一個世界」變成可以親手檢查的數字與圖形。 具體而言:寫出圓盤模型的定義並驗證「測地線=與邊界正交的圓弧」;用莫比烏斯變換把圓盤搬到上半平面;推導「直線」的方程並戳破一個初學者最常犯的符號錯誤;建立龐加萊距離公式並檢查對稱性、三角不等式與「距離被放大」;驗證三角形角虧公式(Hurwitz 公式) \(\text{area} = \pi - (A+B+C)\) ;畫出圓盤、測地線與三角形;最後把雙曲平面 ( \(K=-1\) )、歐氏平面 ( \(K=0\) )、球面 ( \(K=+1\) ) 放在同一張表格上對照——這正是 6.2 節「曲率是唯一密碼」的具體實現。


1. 從公理到模型:貝特拉米的問題#

1.1 一個被重新表述的問題#

6.1 節的最後留下三個問題:

問題 1868 年以前的答案
雙曲幾何的公理系統自洽嗎? 不知道(薩凱里、鮑耶都只做到「推不出矛盾」)
它和歐氏幾何哪裡不一樣? 說不清(只能用「內角和小於 \(\pi\) 」這種描述)
能不能把它畫出來 / 算出來? 不能

第三個問題是關鍵。若非歐幾何只能以公理形式存在,它就永遠只是「可能的世界」;一旦它能被在一個我們熟悉的空間裡實現,它就變成了「就在這裡,只是我們之前沒注意」。

1.2 三位英雄與三個模型#

這段時間軸值得細看,因為它顯示了一種典型的科學節奏:先有直覺,再有模型,再有應用。 貝特拉米 1868 年的模型(今日稱為 Beltrami–Klein 模型)用「弦」當直線——把雙曲平面投影到一個平面上,直線變成圓盤裡的直線段。但弦模型不保角(角度會被歪曲),用起來彆扭。龐加萊 1882 年的模型把弦換成「與邊界正交的圓弧」,於是模型變成保角(conformal)的:圓盤裡量到的角就是雙曲平面裡的角。這個差別在後面的 §6 會變得非常重要——它讓我們可以用歐氏的切線來量雙曲角。

1.3 模型要滿足的三個條件#

一個合格的「非歐模型」必須滿足三件事:自洽(模型裡的每個點、直線、公理都能用歐氏幾何的語言寫出來,而且彼此不矛盾)、完備(不只是公理被滿足,而是「所有」非歐事實都被模型捕捉)、可操作(能拿尺量、能畫圖、能算數)。第三條從來沒有被寫在任何定理裡,卻是本節存在的理由——因為我們要用 SymPy 把它算出來。


2. 龐加萊圓盤模型的定義#

2.1 單位圓盤與模型圓盤#

最標準的版本取單位圓盤

\[ \mathbb{D} = \{ z = x + iy \in \mathbb{C} : x^2 + y^2 < 1 \} \]

也就是平面上以原點為心、半徑 \(1\) 的開圓盤。圓盤的邊界 \(\partial\mathbb{D} = \{ |z| = 1 \}\) 不屬於模型。

如果我們想要曲率 \(K = -1/R^2\) 的雙曲平面,就把圓盤放大成半徑 \(R\) 的圓盤,並把所有距離乘上 \(R\) : \(d_R(z,w) = R \cdot d_1(z/R, w/R)\) 。以下一律討論 \(R=1\) (即 \(K=-1\) )的情形,理由有二:其一,此時距離公式最乾淨(沒有 \(R\) 的多餘因子);其二,6.2 節的球面模型我們也取 \(R=1\) ,三種幾何可以直接對照。

2.2 測地線:與邊界正交的圓弧#

模型中的「直線」(測地線,geodesic)定義為:

定義: \(\mathbb{D}\) 中的一條測地線是下列兩種之一: 1. 一段通過原點的直線段(直徑,直線的退化情形); 2. 一段圓弧,其所在的圓與單位圓正交(在交點處的切線互相垂直)。

「正交」是關鍵:兩圓正交 ⇔ 連接兩圓心的線段長度 \(d\) 滿足

\[ d^2 = r_1^2 + r_2^2 . \]

這是畢氏定理的直接應用,也是 §4 的主角。

2.3 盤內的「直線」與邊界=無窮遠#

模型裡有兩件在其他幾何中不存在的事情:

  1. 「直線」不是直的:它是一段彎的圓弧。兩端點之間的「直線距離」要沿著圓弧算,而不是弦長。
  2. 邊界代表無窮遠:圓盤上的每一條測地線都「從邊界出發」,穿過整個圓盤,再從邊界出去。邊界上的每一點都被當作同一個「無窮遠」。因此在雙曲幾何裡,兩條「互相平行」的測地線,其實是在無窮遠處交會的——它們的夾角是 \(0\) 。這種點叫理想點(ideal point)。

用一句話描述整個模型:把無限大的平面折成一張有限的唱片,唱片的外圈就是無限遠。

2.4 面板度量與共形性#

圓盤模型上的「距離函數」(度量的線元素)是

\[ ds = \frac{2\,|dz|}{1 - |z|^2}, \qquad\text{等價地}\qquad ds^2 = \frac{4\,(dx^2 + dy^2)}{(1 - x^2 - y^2)^2}. \]

這個式子現在看起來是憑空冒出來的,但 §5 會說明它正是距離公式 \(d(z,w) = 2\operatorname{artanh}\left|\frac{z-w}{1-\bar{w}z}\right|\) 的微分形式。先記住它的兩個性質:

  • 在原點附近, \(ds \approx 2|dz|\) :度量是歐氏度量的 兩倍,所以「中心附近的距離是同樣歐氏距離的兩倍」;
  • 越靠近邊界,分母 \(1-|z|^2\) 越小,距離被越拉越大——無窮遠被壓縮在邊界上。

最重要的是,這個度量的度量矩陣是

\[ g = \frac{4}{(1-x^2-y^2)^2}\begin{pmatrix}1 & 0\\0 & 1\end{pmatrix}, \]

也就是「純比例矩陣」:圓盤模型是保角的(conformal)。所以在圓盤裡用歐氏方法量的角,就是雙曲平面裡的角。這是 §6 能用切線量角的依據,也是龐加萊模型相對克萊因模型的最大優勢。


3. 莫比烏斯變換:把圓盤搬上半平面#

3.1 一張地圖就足夠#

我們常用另一個模型:上半平面模型 \(\mathbb{H} = \{ w \in \mathbb{C} : \operatorname{Im} w > 0 \}\) ,其中的測地線是與實軸正交的半圓(以及垂直線)。它與圓盤模型之間有一張非常漂亮的「地圖」:

\[ w = i\,\frac{1+z}{1-z}, \qquad\text{逆地圖}\qquad z = \frac{w-i}{w+i}. \]

這是一個莫比烏斯變換(Möbius transformation),也就是複平面上的分式線性變換 \(w = \dfrac{az+b}{cz+d}\) 。它把 \(|z|<1\) 送到上半平面,把單位圓送到實軸。

我們用 SymPy 逐一檢查這些說法。

from sympy import *
from sympy.simplify.fu import fu

z, w = symbols('z w', complex=True)
xr, yr = symbols('xr yr', real=True)
theta = symbols('theta', real=True)

print("=== 3.1 莫比烏斯變換 w = i(1+z)/(1-z) ===")
W = I*(1 + z)/(1 - z)
print("w(z)  =", W)
print("dw/dz =", simplify(diff(W, z)))
u = 1/(z - 1)**2
print("|dw/dz|^2 - 4/|1-z|^4 =",
      simplify(((re(u))**2 + (im(u))**2 - 1/((1 - xr)**2 + yr**2)**2).subs(z, xr + I*yr)))

print()
print("=== 3.2 圓盤內 ↔ 上半平面 ===")
Wxy = W.subs(z, xr + I*yr)
print("Im(w) =", simplify(im(Wxy)))
print("圓盤邊界 |z|=1 時 Im(w) =", simplify(im(Wxy).subs(yr**2, 1 - xr**2)))
print("往返檢驗 w((w-i)/(w+i)) - w =", simplify(W.subs(z, (w - I)/(w + I)) - w))

print()
print("=== 3.3 兩個模型的度量被這張地圖保持 ===")
lhs = ((re(diff(W, z)))**2 + (im(diff(W, z)))**2)/im(W)**2
rhs = 4/(1 - xr**2 - yr**2)**2
print("|dw|^2/Im(w)^2 - 4/(1-|z|^2)^2 =", simplify(lhs.subs(z, xr + I*yr) - rhs))

print()
print("=== 3.4 測地線圓弧 → 半圓 / 垂直線 ===")
w1 = simplify(W.subs(z, cos(theta) + I*sin(theta)))
print("w(e^{iθ}) 的虛部 =", simplify(im(w1)), "  → 單位圓被送到實軸")
print("w(e^{iθ}) 的實部 + cot(θ/2) =", fu(re(w1) + cot(theta/2)), "  → w = -cot(θ/2)")

an = 2
xi = Rational(1, an)
yi = sqrt(1 - xi**2)
for zz in [xi + I*yi, xi - I*yi]:
    val = W.subs(z, zz).evalf()
    print("圓心 (2,0)、半徑 sqrt(3) 的測地線,與單位圓交點 z =", zz,
          " → w =", N(re(val)), "+", N(im(val)), "i")
print("→ 上半平面中以 0 為圓心、半徑 sqrt(3) =", sqrt(3).evalf(), " 的半圓")
print("直徑(實軸上的線段)→ 虛軸:z=0 -> w =", W.subs(z, 0),
      " ; z=1/2 -> w =", simplify(W.subs(z, Rational(1, 2))),
      " ; Re =", simplify(re(W.subs(z, Rational(1, 2)))))

實際執行結果:

=== 3.1 莫比烏斯變換 w = i(1+z)/(1-z) ===
w(z)  = I*(z + 1)/(1 - z)
dw/dz = 2*I/(z - 1)**2
|dw/dz|^2 - 4/|1-z|^4 = 0

=== 3.2 圓盤內 ↔ 上半平面 ===
Im(w) = (-yr**2 - (xr - 1)*(xr + 1))/(yr**2 + (xr - 1)**2)
圓盤邊界 |z|=1 時 Im(w) = 0
往返檢驗 w((w-i)/(w+i)) - w = 0

=== 3.3 兩個模型的度量被這張地圖保持 ===
|dw|^2/Im(w)^2 - 4/(1-|z|^2)^2 = 0

=== 3.4 測地線圓弧 → 半圓 / 垂直線 ===
w(e^{iθ}) 的虛部 = 0   → 單位圓被送到實軸
w(e^{iθ}) 的實部 + cot(θ/2) = 0   → w = -cot(θ/2)
圓心 (2,0)、半徑 sqrt(3) 的測地線,與單位圓交點 z = 1/2 + sqrt(3)*I/2  → w = -1.73205080756888 + 2.71050543121376e-20 i
圓心 (2,0)、半徑 sqrt(3) 的測地線,與單位圓交點 z = 1/2 - sqrt(3)*I/2  → w = 1.73205080756888 + 2.71050543121376e-20 i
→ 上半平面中以 0 為圓心、半徑 sqrt(3) = 1.73205080756888  的半圓
直徑(實軸上的線段)→ 虛軸:z=0 -> w = I  ; z=1/2 -> w = 3*I  ; Re = 0

逐條解讀:

  • **Im(w) 的分子是 \(1-x^2-y^2\) **:在圓盤內為正 → 映到上半平面;在單位圓上為零 → 圓盤邊界被送到實軸。整張地圖的「上半」與「邊界」對應關係,就這樣被一個符號決定了。
  • 往返檢驗 = 0:地圖與逆地圖互為反函數,沒有資訊遺失。
  • 度量被保持: \(\frac{|dw|^2}{\operatorname{Im}(w)^2} = \frac{4}{(1-|z|^2)^2}\) 恰好等於圓盤度量的係數。這說明莫比烏斯變換不只是「換張畫布」,而是等距變換——上半平面模型與圓盤模型度量的是同一個世界。
  • 圓弧變半圓:圓心 \((2,0)\) 、半徑 \(\sqrt{3}\) 的測地線,兩端點在單位圓上的位置映射到實軸上的 \(\pm\sqrt{3}\) ,而圓弧本身映射成以 \(0\) 為圓心、半徑 \(\sqrt{3}\) 的半圓——正是上半平面模型裡的「直線」。
  • 直徑變垂直線:實軸上的線段( \(z=0\) 與 \(z=1/2\) )映射成 \(i\) 與 \(3i\) ,落在虛軸上。

順帶一提:莫比烏斯變換是保角變換(複分析的基本定理),所以圓盤模型與上半平面模型都是保角的——這與 §2.4 的度量計算互相印證。


4. 「直線」的方程推導:圓心、半徑與一個常見陷阱#

4.1 正交條件#

要寫出測地線的方程,先寫出「兩圓正交」的代數條件。設單位圓的圓心為 \(O=(0,0)\) 、半徑 \(1\) ;另一圓的圓心 \(C=(a,0)\) (先取圓心在 \(x\) 軸上的情形)、半徑 \(\rho\) 。兩圓正交 ⟺ 連心線長度平方等於兩半徑平方和:

\[ |C-O|^2 = \rho^2 + 1^2 \quad\Longleftrightarrow\quad a^2 = \rho^2 + 1 . \]

所以

\[ \boxed{\ \rho = \sqrt{a^2 - 1}\ },\qquad |a| > 1 . \]

測地線的方程因此是

\[ (x-a)^2 + y^2 = a^2 - 1 \quad\Longleftrightarrow\quad x^2 + y^2 - 2ax + 1 = 0 . \]

這裡有兩件事值得停下來:

  1. 直線是 \(a \to \infty\) 的極限:當 \(|a|\) 趨於無窮大, \(\sqrt{a^2-1}\to |a|\) ,圓弧越來越平,方程 \(x^2+y^2-2ax+1=0\) 在 \(|x|<1\) 的極限就是 \(y=0\) 。所以「通過原點的直線」其實是退化情形:它是半徑趨於無窮的圓弧。
  2. 圓心距與半徑的關係可以反過來寫:半徑為 \(\rho\) 的測地線,其圓心到原點的距離是 \(\sqrt{1+\rho^2}\) 。這個式子常被寫成「圓心 \((a,0)\) 、半徑 \(\sqrt{1+a^2}\) 」——這正是初學者最常犯的符號錯誤,我們用 SymPy 把它戳破。

用 SymPy 驗證以上全部:

from sympy import *

x, y, a, p = symbols('x y a p', positive=True)

print("=== 4.1 正交條件:|c1-c2|^2 = r1^2 + r2^2 ===")
rho = sqrt(a**2 - 1)                    # 由 a^2 = 1 + rho^2 得來
print("候選半徑 rho =", rho)
print("驗算 a^2 - (1 + rho^2) =", simplify(a**2 - (1 + rho**2)))

xi = Rational(1, 1)/a
yi = sqrt(1 - xi**2)
r1 = Matrix([xi, yi])                   # O -> 交點
r2 = Matrix([xi - a, yi])               # C2 -> 交點
print("兩半徑向量內積 =", simplify(r1.dot(r2)))
print("切線夾角 =", simplify(acos(r1.dot(r2)/(sqrt(r1.dot(r1))*sqrt(r2.dot(r2))))))

print()
print("=== 4.2 陷阱:圓心 (a,0)、半徑 sqrt(1+a^2) 的圓過哪些點? ===")
rho_plus = sqrt(1 + a**2)
print("(x-a)^2+y^2 = 1+a^2  令 x=0 → y^2 =", simplify(rho_plus**2 - a**2))
print("→ 它通過單位圓上的兩個極點 (0, ±1)")
v1 = Matrix([0, 1]); v2 = Matrix([-a, 1])
print("在 (0,1) 處兩半徑向量內積 =", simplify(v1.dot(v2)), "(正交應為 0)")
ang = acos(v1.dot(v2)/(sqrt(v1.dot(v1))*sqrt(v2.dot(v2))))
print("它與單位圓的切線夾角 =", simplify(ang), " ; a=2 時 =",
      N(ang.subs(a, 2)), "rad =", N(deg(ang.subs(a, 2))), "度")

print()
print("=== 4.3 通過圓盤內定點 P=(p,0)、且圓心在 x 軸上的測地線 ===")
cx, rr = symbols('cx rr', positive=True)
sol = solve([Eq(cx**2, rr**2 + 1),                   # 與單位圓正交
             Eq((p - cx)**2, rr**2)],               # 通過 P
            [cx, rr], dict=True)
for s in sol:
    print("解:", {k: simplify(v) for k, v in s.items()})
print("圓心的另一個表達:半徑 rr 的測地線,其圓心距原點 = sqrt(1+rr^2) =",
      simplify(sqrt(1 + sol[0][rr]**2)))
print("與 x 軸的另一交點 =", simplify(sol[0][cx] + sol[0][rr]), " (= 1/p )")

實際執行結果:

=== 4.1 正交條件:|c1-c2|^2 = r1^2 + r2^2 ===
候選半徑 rho = sqrt(a**2 - 1)
驗算 a^2 - (1 + rho^2) = 0
兩半徑向量內積 = 0
切線夾角 = pi/2

=== 4.2 陷阱:圓心 (a,0)、半徑 sqrt(1+a^2) 的圓過哪些點? ===
(x-a)^2+y^2 = 1+a^2  令 x=0 → y^2 = 1
→ 它通過單位圓上的兩個極點 (0, ±1)
在 (0,1) 處兩半徑向量內積 = 1 (正交應為 0)
它與單位圓的切線夾角 = acos(1/sqrt(a**2 + 1))  ; a=2 時 = 1.10714871779409 rad = 63.4349488229220 度

=== 4.3 通過圓盤內定點 P=(p,0)、且圓心在 x 軸上的測地線 ===
解: {cx: (p**2 + 1)/(2*p), rr: (1 - p**2)/(2*p)}
解: {cx: (p**2 + 1)/(2*p), rr: (p**2 - 1)/(2*p)}
圓心的另一個表達:半徑 rr 的測地線,其圓心距原點 = sqrt(1+rr^2) = sqrt(p**4 + 2*p**2 + 1)/(2*p)
與 x 軸的另一交點 = 1/p  (= 1/p )

三個結論:

  • §4.1:正交條件 \(a^2 = \rho^2+1\) 被 SymPy 化簡為恆等式 \(0\) ;在交點處兩半徑向量的內積為 \(0\) ,切線夾角恰為 \(\pi/2\) 。
  • §4.2(陷阱):圓心 \((a,0)\) 、半徑 \(\sqrt{1+a^2}\) 的圓確實通過單位圓上的兩個極點 \((0,\pm1)\) ——這是它唯一正確的地方。但它與單位圓的切線夾角在 \(a=2\) 時是 \(63.43°\) 而不是 \(90°\) ,所以它不是測地線。正確的式子是 \(\rho = \sqrt{a^2-1}\) ;或者說,**半徑為 \(\rho\) 的測地線,其圓心距原點為 \(\sqrt{1+\rho^2}\) **。兩個式子的角色一旦互換,就會得到「看起來很像但幾何意義完全錯誤」的圓。這正是為什麼我們需要電腦來檢查——人眼在這裡幾乎必然出錯。
  • §4.3:過內點 \(P=(p,0)\) 、且圓心在 \(x\) 軸上的測地線,圓心為 \(\left(\frac{1+p^2}{2p}, 0\right)\) ,半徑 \(\frac{1-p^2}{2p}\) ;它與 \(x\) 軸的另一個交點是 \(1/p\) (在圓盤之外,正是「無窮遠的方向」)。

4.2 更一般的公式:過任意內點的測地線族#

設測地線圓心 \(C=(c_x,c_y)\) 、半徑 \(\rho\) ,且它通過內點 \(P=(p_x,p_y)\) 。正交條件給 \(|C|^2 = \rho^2+1\) ,通過條件給 \(|C-P|^2 = \rho^2\) ,兩式相減:

\[ |C|^2 - |C-P|^2 = 1 \quad\Longrightarrow\quad C\cdot P = \frac{1+|P|^2}{2}. \]

也就是說:所有通過 \(P\) 的測地線,其圓心都在直線 \(\{C : C\cdot P = (1+|P|^2)/2\}\) 上——這條直線正是 \(P\) 與其反演點 \(P^* = P/|P|^2\) 的中垂線。於是「過一點的測地線」是一個一參數族,幾何意義完全清楚:它們在邊界上的端點,就是從 \(P\) 看去與 \(P^*\) 的連線在單位圓上的交點。


5. 龐加萊距離公式#

5.1 三種等價形式#

圓盤模型上兩點 \(z,w \in \mathbb{D}\) 的距離定義為

\[ d(z,w) = 2\,\operatorname{artanh}\left|\frac{z-w}{1-\bar{w}z}\right|, \]

等價的寫法有兩種:

\[ d(z,w) = 2\,\operatorname{arcsinh}\frac{|z-w|}{\sqrt{(1-|z|^2)(1-|w|^2)}} = \operatorname{arcosh}\left(1 + \frac{2|z-w|^2}{(1-|z|^2)(1-|w|^2)}\right). \]

三者的等價性來自一個關鍵恆等式:

\[ |1-\bar{w}z|^2 = (1-|z|^2)(1-|w|^2) + |z-w|^2 . \]

用這條恆等式, \(\left|\frac{z-w}{1-\bar wz}\right|\) 立刻可以寫成 \(\frac{u}{\sqrt{1+u^2}}\) ,其中 \(u=\frac{|z-w|}{\sqrt{(1-|z|^2)(1-|w|^2)}}\) ,而 \(\operatorname{artanh}\frac{u}{\sqrt{1+u^2}} = \operatorname{arcsinh}u\) , \(2\operatorname{arcsinh}u = \operatorname{arcosh}(1+2u^2)\) 。

5.2 數值實驗:對稱性、距離放大與三角不等式#

from sympy import *
import random

xr, yr, xw, yw = symbols('xr yr xw yw', real=True)
r, t = symbols('r t', positive=True)

print("=== 5.1 關鍵恆等式:|1-w̄z|^2 = (1-|z|^2)(1-|w|^2) + |z-w|^2 ===")
lhs = (1 - (xw*xr + yw*yr))**2 + (yw*xr - xw*yr)**2
rhs = (1 - xr**2 - yr**2)*(1 - xw**2 - yw**2) + (xr-xw)**2 + (yr-yw)**2
print("lhs - rhs =", simplify(lhs - rhs))

print()
print("=== 5.2 三種距離公式等價:先用一個正數 u 把它們化開 ===")
u = symbols('u', positive=True)
x = u/sqrt(1 + u**2)
print("atanh(x) = asinh(u) 的關鍵:x/sqrt(1-x^2) - u =", simplify(x/sqrt(1 - x**2) - u))
print("cosh(2 asinh u) - (1 + 2 u^2) =",
      expand_trig(cosh(2*asinh(u)) - 1 - 2*sinh(asinh(u))**2))
print("(於是 atanh 形式 = asinh 形式 = arcosh 形式)")

print()
print("=== 5.3 精確有理數檢查:z=0, w=r = 1/2 ===")
rr = Rational(1, 2)
d1 = 2*atanh(rr)
d2 = 2*asinh(rr/sqrt(1 - rr**2))
d3 = acosh(1 + 2*rr**2/(1 - rr**2))
print("d1 =", d1, "= 2*atanh(1/2)")
print("d2 =", d2, "= 2*asinh(1/sqrt(3))")
print("d3 =", d3, "= acosh(5/3)")
print("d1.rewrite(log) - d2.rewrite(log) =", simplify(d1.rewrite(log) - d2.rewrite(log)))
print("d1.rewrite(log) - d3.rewrite(log) =", simplify(d1.rewrite(log) - d3.rewrite(log)))
print("三者共同的對數形式 =", simplify(d1.rewrite(log)))

print()
print("=== 5.4 沿半徑的距離:雙曲距離把圓盤撐開 ===")
print("沿半徑的度量:ds = 2|dz|/(1-|z|^2) ,沿 z = t 的原函数")
print("  d/dt log((1+t)/(1-t)) - 2/(1-t^2) =", simplify(diff(log((1+t)/(1-t)), t) - 2/(1-t**2)))
print("  log((1+r)/(1-r)) - 2*atanh(r) =",
      simplify(expand_log(log((1+r)/(1-r)), force=True) - (2*atanh(r)).rewrite(log)))
print()
print("   r        歐氏距離      龐加萊距離 2*atanh(r)    比值 d/r")
for rr in [0.1, 0.25, 0.5, 0.75, 0.9, 0.99]:
    d = 2*atanh(rr).evalf()
    print(f"  {rr:<8}{rr:<13}{d:<24.10f}{d/rr:.6f}")
print("  r -> 0 時 d/r ->", limit(2*atanh(r)/r, r, 0), " ; r -> 1 時 d/r ->",
      limit(2*atanh(r)/r, r, 1))

print()
print("=== 5.5 對稱性與三角不等式(數值實驗) ===")
def pd(z1, z2):
    return 2*atanh(abs(z1 - z2)/abs(1 - conjugate(z2)*z1))

pts = [0, 0.3+0.2*I, 0.7*I, -0.5+0.4*I, 0.9]
worst = max(abs(pd(A, B) - pd(B, A)) for A in pts for B in pts)
print("對稱性 d(z,w) = d(w,z):最大誤差 =", worst)
random.seed(42)
bad = 0
for _ in range(3000):
    A = complex(random.uniform(-0.9, 0.9), random.uniform(-0.9, 0.9))
    B = complex(random.uniform(-0.9, 0.9), random.uniform(-0.9, 0.9))
    C = complex(random.uniform(-0.9, 0.9), random.uniform(-0.9, 0.9))
    if max(abs(A), abs(B), abs(C)) >= 1:
        continue
    if pd(A, C) > pd(A, B) + pd(B, C) + 1e-12:
        bad += 1
print("3000 組隨機點違反三角不等式的次數 =", bad)

實際執行結果:

=== 5.1 關鍵恆等式:|1-w̄z|^2 = (1-|z|^2)(1-|w|^2) + |z-w|^2 ===
lhs - rhs = 0

=== 5.2 三種距離公式等價:先用一個正數 u 把它們化開 ===
atanh(x) = asinh(u) 的關鍵:x/sqrt(1-x^2) - u = 0
cosh(2 asinh u) - (1 + 2 u^2) = 0
(於是 atanh 形式 = asinh 形式 = arcosh 形式)

=== 5.3 精確有理數檢查:z=0, w=r = 1/2 ===
d1 = 2*atanh(1/2) = 2*atanh(1/2)
d2 = 2*asinh(sqrt(3)/3) = 2*asinh(1/sqrt(3))
d3 = acosh(5/3) = acosh(5/3)
d1.rewrite(log) - d2.rewrite(log) = 0
d1.rewrite(log) - d3.rewrite(log) = 0
三者共同的對數形式 = log(3)

=== 5.4 沿半徑的距離:雙曲距離把圓盤撐開 ===
沿半徑的度量:ds = 2|dz|/(1-|z|^2) ,沿 z = t 的原函数
  d/dt log((1+t)/(1-t)) - 2/(1-t^2) = 0
  log((1+r)/(1-r)) - 2*atanh(r) = 0

   r        歐氏距離      龐加萊距離 2*atanh(r)    比值 d/r
  0.1     0.1          0.2006706955            2.006707
  0.25    0.25         0.5108256238            2.043302
  0.5     0.5          1.0986122887            2.197225
  0.75    0.75         1.9459101491            2.594547
  0.9     0.9          2.9444389792            3.271599
  0.99    0.99         5.2933048247            5.346773
  r -> 0 時 d/r -> 2  ; r -> 1 時 d/r -> oo

=== 5.5 對稱性與三角不等式(數值實驗) ===
對稱性 d(z,w) = d(w,z):最大誤差 = 0
3000 組隨機點違反三角不等式的次數 = 0

幾個要點:

  • §5.3 的精確檢查:三種公式在 \(r=1/2\) 時給出 \(2\operatorname{artanh}(1/2)\) 、 \(2\operatorname{arcsinh}(1/\sqrt3)\) 、 \(\operatorname{arcosh}(5/3)\) ,三者化為對數後都是 \(\log 3\) ——完全相同,不是「接近」相同。
  • §5.4 的距離放大:從圓心出發、歐氏半徑為 \(r\) 的點,雙曲距離是 \(2\operatorname{artanh}r\) 。比值 \(d/r\) 從 \(r\to 0\) 時的 \(2\) 一路上升到 \(r\to 1\) 時的 \(\infty\) 。所以「圓盤中的距離被放大」不是一句話,而是有精確函數關係的:離邊界越近,放大倍數越誇張。這正是「無窮遠被壓縮在邊界上」的定量表現。
  • §5.5:對稱性誤差為 \(0\) ;3000 組隨機點沒有一組違反三角不等式。距離函數確實是個度量。

6. 三角形角虧:Hurwitz 公式#

6.1 共形 ⇒ 角可以用歐氏切線量#

由 §2.4,圓盤模型的面板度量是純比例的,所以雙曲角 = 兩條測地線在頂點處的切線夾角。這讓「算角」變成一件非常平凡的事:用 NumPy 求切向量,再用 arccos 取夾角。

6.2 面積可以直接算出來#

由度量的行列式,面積元素是

\[ dA = \frac{4\,dx\,dy}{(1-x^2-y^2)^2}, \]

所以「這塊雙曲面積是多少」可以用 Riemann 網格積分直接算出來,不必靠公式。而 Hurwitz 公式說的是:曲率 \(K=-1\) 時,

\[ \text{area} = \pi - (A+B+C), \]

也就是角虧等於面積。兩個獨立算出來的數字若相符,這條公式就被驗證了。

6.3 完整實驗#

import numpy as np
from sympy import *

print("=== 6.1 圓盤模型的面板度量是共形的 → 雙曲角 = 歐氏角 ===")
x, y = symbols('x y', real=True)
g = Matrix([[4/(1 - x**2 - y**2)**2, 0],
            [0, 4/(1 - x**2 - y**2)**2]])
print("g =", g)
print("g12 =", g[0, 1], "  g11 - g22 =", simplify(g[0, 0] - g[1, 1]))

# ------------------------------------------------------------------ 工具
def geodesic_circle(P, Q):
    """過 P、Q 且與單位圓正交的圓(圓心、半徑)"""
    M = np.array([P, Q], dtype=float)
    rhs = np.array([(1 + P@P)/2, (1 + Q@Q)/2])
    c = np.linalg.solve(M, rhs)
    return c, np.sqrt(c @ c - 1)

def tangent_at(P, c, rho, toward):
    r = (P - c)/rho
    t = np.array([-r[1], r[0]])
    return -t if t @ (toward - P) < 0 else t

def angle(u, v):
    u = u/np.linalg.norm(u); v = v/np.linalg.norm(v)
    return np.arccos(np.clip(u @ v, -1, 1))

def triangle_data(V):
    centers = [geodesic_circle(V[i], V[(i+1) % 3]) for i in range(3)]
    angs = []
    for i in range(3):
        P = V[i]
        cp, rp = centers[(i-1) % 3]
        cn, rn = centers[i]
        angs.append(angle(tangent_at(P, cp, rp, V[(i-1) % 3]),
                          tangent_at(P, cn, rn, V[(i+1) % 3])))
    return angs, centers

def area_grid(centers, N=2000):
    """在圓盤內對三角形做 Riemann 網格積分(面積元素 4 dx dy/(1-r^2)^2)"""
    xs = np.linspace(-1, 1, N+1); ys = np.linspace(-1, 1, N+1)
    X, Y = np.meshgrid(xs, ys, indexing='ij')
    dx = xs[1]-xs[0]; dy = ys[1]-ys[0]
    r2 = X**2 + Y**2
    mask = r2 < 1 - 1e-12
    for (c, r) in centers:
        mask &= ((X-c[0])**2 + (Y-c[1])**2) >= r**2
    dens = np.where(r2 < 1-1e-12, 4.0/np.maximum(1-r2, 1e-12)**2, 0.0)
    return dens[mask].sum()*dx*dy

def area_grid_right(A, B, C, c, rho, N=2000):
    """直角型三角形:兩條直邊 + 一條圓弧;區域 = 盒子 ∩ 圓弧外側"""
    xs = np.linspace(A[0], B[0], N+1); ys = np.linspace(C[1], A[1], N+1)
    X, Y = np.meshgrid(xs, ys, indexing='ij')
    dx = xs[1]-xs[0]; dy = ys[1]-ys[0]
    mask = ((X - c[0])**2 + (Y - c[1])**2) >= rho**2
    dens = 4.0/(1 - X**2 - Y**2)**2
    return dens[mask].sum()*dx*dy

# ------------------------------------------------------------------ 6.2
print()
print("=== 6.2 一個具體的雙曲三角形 A=(0,0), B=(0.5,0), C=(0,-0.5) ===")
A = np.array([0.0, 0.0]); B = np.array([0.5, 0.0]); C = np.array([0.0, -0.5])
c0, rho0 = geodesic_circle(B, C)
print("弧 BC 的圓心 =", c0, " 半徑 =", rho0)
angA = angle(B - A, C - A)                     # A 處兩邊是直徑,夾角 90 度
angB = angle(A - B, tangent_at(B, c0, rho0, C))
angC = angle(A - C, tangent_at(C, c0, rho0, B))
for nm, a_ in [("A", angA), ("B", angB), ("C", angC)]:
    print(f"  角 {nm} = {np.degrees(a_):.6f} 度  (= {a_:.10f} rad)")
defect = np.pi - (angA + angB + angC)
print("角虧 π-(A+B+C) =", defect, " =", np.degrees(defect), "度")

print()
print("=== 6.3 用網格積分直接算面積,與 Hurwitz 公式對照 ===")
for N in [500, 1000, 2000, 4000]:
    num = area_grid_right(A, B, C, c0, rho0, N)
    print(f"  N={N:<6} 網格面積 = {num:.10f}   Hurwitz = {defect:.10f}   差 = {abs(num-defect):.2e}")

# ------------------------------------------------------------------ 6.4
print()
print("=== 6.4 等邊三角形族:頂點在 Euclidean 半徑 r 的圓上 ===")
print("   r      角 A(度)     角虧=Hurwitz 面積    網格積分面積        差")
for r in [0.3, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99]:
    th = np.array([0.0, 2*np.pi/3, 4*np.pi/3])
    V = np.column_stack([r*np.cos(th), r*np.sin(th)])
    angs, centers = triangle_data(V)
    defect = np.pi - sum(angs)
    num = area_grid(centers, 1200)
    print(f"  {r:<6}{np.degrees(angs[0]):<12.6f}{defect:<20.10f}{num:<20.10f}{abs(num-defect):.2e}")

# ------------------------------------------------------------------ 6.5
print()
print("=== 6.5 理想三角形(頂點在邊界上):面積 = π ===")
th = np.array([0.0, 2*np.pi/3, 4*np.pi/3])
V = np.column_stack([np.cos(th), np.sin(th)])
angs, centers = triangle_data(V)
print("三條邊的圓心與半徑 =", [(tuple(np.round(c, 6)), round(r, 6)) for (c, r) in centers])
print("三個內角 =", angs, "(理想頂點:兩邊相切 → 角為 0)")
print("Hurwitz 面積 = π - 0 =", np.pi)

# 用極座標做數值積分(θ = s² 消去尖點的可積奇性)
def r_max(t):
    c = 2*np.cos(t - np.pi/3)
    return c - np.sqrt(np.maximum(c**2 - 1, 0.0))

def integrand(t):
    r = min(r_max(t), 1 - 1e-13)
    return 2.0/(1 - r**2) - 2.0

for N in [1000, 4000, 16000]:
    s = np.linspace(0, np.sqrt(np.pi/3), N+1)
    val = np.trapezoid([2*ss*integrand(ss**2) for ss in s], s)
    print(f"  N={N:<7} 極座標數值積分面積 = {6*val:.10f}   π = {np.pi:.10f}   差 = {6*val-np.pi:.2e}")

實際執行結果:

=== 6.1 圓盤模型的面板度量是共形的 → 雙曲角 = 歐氏角 ===
g = Matrix([[4/(-x**2 - y**2 + 1)**2, 0], [0, 4/(-x**2 - y**2 + 1)**2]])
g12 = 0   g11 - g22 = 0

=== 6.2 一個具體的雙曲三角形 A=(0,0), B=(0.5,0), C=(0,-0.5) ===
弧 BC 的圓心 = [ 1.25 -1.25]  半徑 = 1.4577379737113252
  角 A = 90.000000 度  (= 1.5707963268 rad)
  角 B = 30.963757 度  (= 0.5404195003 rad)
  角 C = 30.963757 度  (= 0.5404195003 rad)
角虧 π-(A+B+C) = 0.48995732625372845  = 28.072486935852968 度

=== 6.3 用網格積分直接算面積,與 Hurwitz 公式對照 ===
  N=500    網格面積 = 0.4924496802   Hurwitz = 0.4899573263   差 = 2.49e-03
  N=1000   網格面積 = 0.4911888904   Hurwitz = 0.4899573263   差 = 1.23e-03
  N=2000   網格面積 = 0.4905653005   Hurwitz = 0.4899573263   差 = 6.08e-04
  N=4000   網格面積 = 0.4902601377   Hurwitz = 0.4899573263   差 = 3.03e-04

=== 6.4 等邊三角形族:頂點在 Euclidean 半徑 r 的圓上 ===
   r      角 A(度)     角虧=Hurwitz 面積    網格積分面積        差
  0.3   51.468879   0.4466884268        0.4467728554        8.44e-05
  0.5   38.213211   1.1407536201        1.1409671109        2.13e-04
  0.7   22.357125   1.9709763351        1.9712668237        2.90e-04
  0.8   14.445796   2.3852125290        2.3853185920        1.06e-04
  0.9   6.936432    2.7784019173        2.7793066645        9.05e-04
  0.95  3.389575    2.9641149156        2.9732292002        9.11e-03
  0.99  0.664895    3.1067788318        3.2167591677        1.10e-01

=== 6.5 理想三角形(頂點在邊界上):面積 = π ===
三條邊的圓心與半徑 = [((np.float64(1.0), np.float64(1.732051)), np.float64(1.732051)), ((np.float64(-2.0), np.float64(0.0)), np.float64(1.732051)), ((np.float64(1.0), np.float64(-1.732051)), np.float64(1.732051))]
三個內角 = [np.float64(0.0), np.float64(0.0), np.float64(0.0)] (理想頂點:兩邊相切 → 角為 0)
Hurwitz 面積 = π - 0 = 3.141592653589793
  N=1000    極座標數值積分面積 = 3.1382949545   π = 3.1415926536   差 = -3.30e-03
  N=4000    極座標數值積分面積 = 3.1407680021   π = 3.1415926536   差 = -8.25e-04
  N=16000   極座標數值積分面積 = 3.1413864765   π = 3.1415926536   差 = -2.06e-04

6.4 從這張表裡能讀出什麼#

  1. Hurwitz 公式被兩個獨立方法交叉驗證:角虧(用切線算)與面積(用積分算)在誤差 \(10^{-4}\) 量級內一致,而且誤差隨網格加密而線性減半——這是標準的 \(O(h)\) Riemann 和誤差,說明兩者真的在逼近同一個數,不是碰巧接近。
  2. 雙曲等邊三角形的角一律小於 \(60°\) ( \(r=0.3\) 時 \(51.47°\) , \(r=0.99\) 時只有 \(0.66°\) )。這是本節最漂亮的一個「意外」:在歐氏平面裡,等邊三角形的三個角永遠是 \(60°\) ,而且與大小無關;雙曲平面裡則是「角隨大小而變」——邊越長,角越小。這正是 6.1 節所說的「雙曲幾何中沒有相似但不全等的三角形」的具體樣貌。
  3. **理想三角形的面積是 \(\pi\) **:三個角都是 \(0\) (理想點處兩邊相切),所以 \(\text{area} = \pi - 0 = \pi\) 。我們用極座標做數值積分驗證,結果 \(3.1414\ldots \to \pi\) 。
  4. 一個計算上的教訓:當 \(r\) 接近 \(1\) 時,網格積分的誤差明顯變大( \(r=0.99\) 時差 \(0.11\) )。原因是三角形的尖角(cusp)逼近單位圓,面積元素 \(\frac{4}{(1-r^2)^2}\) 在那裡劇烈膨脹;用笛卡兒均勻網格去逼近一個奇異積分,必須先用變數替換把奇性消掉(我們用 \(\theta = s^2\) ),否則收斂極慢。這是「數值方法本身就是幾何」的又一個例子。

7. 視覺化實驗#

抽象的定義必須落到看得見的圖上。以下程式用 SymPy 的 plot_parametric 畫出圓盤邊界,再用 matplotlib 的 Arc(弧)與 Circle(圓)補上測地線圓弧,並把莫比烏斯變換的效果畫出來。

import matplotlib
matplotlib.use('Agg')
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc as MplArc, Circle as MplCircle
from sympy import *
from sympy.plotting import plot_parametric

plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'Heiti TC', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

t = symbols('t', real=True)

def arc_angles(cx, cy, rho):
    """圓心 (cx,cy)、半徑 rho 的圓落在單位圓內的那一段弧的角度範圍"""
    phi0 = np.arctan2(cy, cx)
    psi = np.arccos(-rho/np.hypot(cx, cy))         # 交點相對圓心的角度偏角
    return np.degrees(phi0 + psi), np.degrees(phi0 + 2*np.pi - psi)

def draw_arc(ax, cx, cy, rho, **kw):
    t1, t2 = arc_angles(cx, cy, rho)
    ax.add_patch(MplArc((cx, cy), 2*rho, 2*rho, theta1=t1, theta2=t2, **kw))

def geodesic_circle(P, Q):
    M = np.array([P, Q], dtype=float)
    rhs = np.array([(1 + P@P)/2, (1 + Q@Q)/2])
    c = np.linalg.solve(M, rhs)
    return c, np.sqrt(c @ c - 1)

# ------------------------------------------------- 圖 1:圓盤與測地線
p = plot_parametric((cos(t), sin(t)), (t, 0, 2*pi))     # sympy 畫邊界
fig = p.fig                                          # 取用 sympy 產生的 figure/axes
ax = p.ax
ax.add_patch(MplCircle((0, 0), 1, fill=False, ls='--', lw=1.2, color='0.45'))
for (cx, cy) in [(2, 0), (0, 2), (-2, 0), (1.5, 0.5), (0.5, -1.5)]:
    draw_arc(ax, cx, cy, np.sqrt(cx**2 + cy**2 - 1), color='C0', lw=1.7)
ax.plot([-1, 1], [0, 0], color='C3', lw=1.8)
ax.plot([0, 0], [-1, 1], color='C3', lw=1.8)
ax.set_xlim(-1.15, 1.15); ax.set_ylim(-1.15, 1.15)
ax.set_aspect('equal'); ax.grid(alpha=.25)
ax.set_title("龐加萊圓盤:藍=與邊界正交的圓弧(測地線),\n紅=直徑(退化情形),灰虛線=邊界=無窮遠")
fig.savefig("fig1_disk.png", dpi=140, bbox_inches='tight')
print("fig1_disk.png 完成")

# ------------------------------------------------- 圖 2:雙曲三角形
A = np.array([0.0, 0.0]); B = np.array([0.5, 0.0]); C = np.array([0.0, -0.5])
c0, rho0 = geodesic_circle(B, C)
fig, ax = plt.subplots(figsize=(6.2, 6.2))
ax.add_patch(MplCircle((0, 0), 1, fill=False, ls='--', lw=1.2, color='0.45'))
ax.plot([A[0], B[0]], [A[1], B[1]], 'C3', lw=2.2)
ax.plot([A[0], C[0]], [A[1], C[1]], 'C3', lw=2.2)
draw_arc(ax, c0[0], c0[1], rho0, color='C0', lw=2.2)
ax.plot(*zip(A, B, C, A), 'ko', ms=5)
for P, lab in [(A, "A"), (B, "B"), (C, "C")]:
    ax.annotate(lab, P, textcoords="offset points", xytext=(6, 6), fontsize=12)
ax.set_xlim(-1.1, 1.1); ax.set_ylim(-1.1, 1.1); ax.set_aspect('equal'); ax.grid(alpha=.25)
ax.set_title("雙曲三角形:紅=直徑,藍=正交圓弧;角要用『切線』量")
fig.savefig("fig2_triangle.png", dpi=140, bbox_inches='tight')
print("fig2_triangle.png 完成")

# ------------------------------------------------- 圖 3:莫比烏斯變換
fig, ax = plt.subplots(figsize=(6.4, 4.4))
for (cx, cy) in [(2, 0), (0, 2), (-2, 0)]:
    rho = np.sqrt(cx**2 + cy**2 - 1)
    t1, t2 = arc_angles(cx, cy, rho)
    tt = np.radians(np.linspace(t1, t2, 500))
    zs = (cx + rho*np.cos(tt)) + 1j*(cy + rho*np.sin(tt))     # 圓弧上的點 z
    ws = 1j*(1 + zs)/(1 - zs)                                # 莫比烏斯變換 w = i(1+z)/(1-z)
    ax.plot(np.real(ws), np.imag(ws), 'C0', lw=2)
ax.plot([0, 0], [0, 4], 'C3', lw=2)                  # 直徑 → 虛軸
ax.axhline(0, color='0.45', ls='--')
ax.set_xlim(-6, 6); ax.set_ylim(-0.4, 4.2)
ax.set_title("莫比烏斯變換 $w=i\\frac{1+z}{1-z}$:圓弧 → 半圓,直徑 → 垂直線")
fig.savefig("fig3_mobius.png", dpi=140, bbox_inches='tight')
print("fig3_mobius.png 完成")

# ------------------------------------------------- 圖 4:三種幾何的距離
fig, ax = plt.subplots(figsize=(6.2, 4.2))
rs = np.linspace(0.001, 0.999, 400)
ax.plot(rs, 2*np.arctanh(rs), 'C0', lw=2, label="雙曲 K=-1:$2\\operatorname{artanh}(r)$")
ax.plot(rs, rs, 'k--', lw=1.5, label="歐氏 K=0:$r$")
ax.set_xlabel("從圓心出發的 Euclidean 半徑 $r$")
ax.set_ylabel("距離")
ax.set_title("同一條半徑上,雙曲距離把圓盤「撐開」")
ax.legend(); ax.grid(alpha=.3)
fig.savefig("fig4_distance.png", dpi=140, bbox_inches='tight')
print("fig4_distance.png 完成")

實際執行結果(四張圖都產生成功,無例外):

fig1_disk.png 完成
fig2_triangle.png 完成
fig3_mobius.png 完成
fig4_distance.png 完成

四張圖各自要說的事:

  • 圖 1:藍色圓弧全部在圓盤內,且與灰色邊界圓垂直相交;紅色直徑是「 \(|a|\to\infty\) 的退化情形」。請特別注意那些圓弧「彎向圓盤外」——它們在圓盤內的部分是彎的,這就是雙曲「直線」的真面目。
  • 圖 2:三角形 \(A=(0,0)\) 、 \(B=(0.5,0)\) 、 \(C=(0,-0.5)\) 。注意藍弧從 \(B\) 伸向 \(C\) ,然後繼續延伸到邊界——因為在模型裡,測地線是無界的,它從「無窮遠」來、往「無窮遠」去。
  • 圖 3:三條圓弧被莫比烏斯變換送到上半平面,變成三個以實軸上某點為圓心、半徑不等的半圓;紅色直徑變成虛軸上的垂直線。這就是「圓盤模型」與「上半平面模型」是同一個世界的視覺證據。
  • 圖 4:雙曲距離曲線 \(2\operatorname{artanh}r\) 一開始斜率是 \(2\) (度量在圓心附近是歐氏的兩倍),然後急遽上升、在 \(r\to 1\) 時趨於無窮。無窮遠被壓縮在邊界上這句話,在這張圖上比任何文字都清楚。

8. 對照:雙曲平面、歐氏平面與球面#

6.2 節的表格列出了三種幾何的差異,但表格只是「宣稱」。現在我們可以用 SymPy 把同一個問題在三種幾何裡各算一遍,讓數字自己說話。

8.1 同樣的邊長,三種不同的角#

考慮一個等邊三角形,邊長 \(a=1\) 。在歐氏平面裡它的角是 \(60°\) ;在雙曲平面( \(K=-1\) )與球面( \(K=+1\) )裡,三個角仍然相等,但每個角的值不同:

  • 雙曲:用雙曲餘弦定理的角的形式 \(\cos A = -\cos B\cos C + \sin B\sin C\cosh a\) ,等邊情形化簡得 \(\cosh a = \frac{\cos A}{1-\cos A}\) ,故 \(\cos A = \frac{\cosh a}{\cosh a + 1}\) ;
  • 球面:用球面餘弦定理 \(\cos A = -\cos B\cos C + \sin B\sin C\cos a\) ,等邊情形得 \(\cos A = \frac{\cos a}{1+\cos a}\) 。
from sympy import *

a = symbols('a', positive=True)          # 等邊三角形的邊長(K = ±1 時單位半徑)

print("=== 8.1 同樣的邊長 a = 1,在三種幾何裡給出不同的角與面積 ===")
A_eu = pi/3
A_hyp = acos(cosh(a)/(cosh(a) + 1))       # 雙曲:cos A = cosh a/(cosh a + 1)
A_sph = acos(cos(a)/(1 + cos(a)))         # 球面:cos A = cos a/(1 + cos a)

for name, A in [("歐氏 K=0", A_eu), ("雙曲 K=-1", A_hyp), ("球面 K=+1", A_sph)]:
    ang = A.subs(a, 1)
    if name.startswith("雙曲"):
        area = pi - 3*ang
    elif name.startswith("球面"):
        area = 3*ang - pi
    else:
        area = sqrt(3)/4
    print(f"  {name:<10} 內角 A = {N(deg(ang)):.6f} 度    面積 = {N(area):.10f}")

print()
print("=== 8.2 反過來:角固定為 60-60-60 時,邊長是多少? ===")
ch = symbols('ch', positive=True)
eq_h = Eq(cos(pi/3), -cos(pi/3)*cos(pi/3) + sin(pi/3)*sin(pi/3)*ch)
print("雙曲:cos A = -cos B cos C + sin B sin C cosh a  =>", solve(eq_h, ch))
print("      ⇒ cosh a =", solve(eq_h, ch)[0], " ⇒ a = acosh(1) =", acosh(1), "(退化成一點!)")
cs = symbols('cs', real=True)
eq_s = Eq(cos(pi/3), -cos(pi/3)*cos(pi/3) + sin(pi/3)*sin(pi/3)*cs)
print("球面:cos A = -cos B cos C + sin B sin C cos a   =>", solve(eq_s, cs))
print("      ⇒ cos a =", solve(eq_s, cs)[0], " ⇒ a = 0(也退化!)")
print("歐氏:a 任意正數皆可 ⇒ 存在相似但不全等的等邊三角形 ✓")

實際執行結果:

=== 8.1 同樣的邊長 a = 1,在三種幾何裡給出不同的角與面積 ===
  歐氏 K=0     內角 A = 60.000000 度    面積 = 0.4330127019
  雙曲 K=-1    內角 A = 52.643240 度    面積 = 0.3851990371
  球面 K=+1    內角 A = 69.465165 度    面積 = 0.4955948957

=== 8.2 反過來:角固定為 60-60-60 時,邊長是多少? ===
雙曲:cos A = -cos B cos C + sin B sin C cosh a  => [1]
      ⇒ cosh a = 1  ⇒ a = acosh(1) = 0 (退化成一點!)
球面:cos A = -cos B cos C + sin B sin C cos a   => [1]
      ⇒ cos a = 1  ⇒ a = 0(也退化!)
歐氏:a 任意正數皆可 ⇒ 存在相似但不全等的等邊三角形 ✓

8.2 這張表教我們的事#

  • 同一個邊長 \(a=1\) :歐氏角 \(60°\) 、雙曲角 \(52.64°\) 、球面角 \(69.47°\) 。三者的面積也各不相同。「邊」與「角」的對應關係,就是曲率的簽名。
  • 反過來問:把角固定成 \(60°\) - \(60°\) - \(60°\) 會怎樣?歐氏平面裡有一整族(相似三角形存在);但在雙曲平面與球面上,這樣的三角形退化成一點(邊長 \(0\) )。這是《幾何原本》第一卷命題 I.1(作等邊三角形)的深處:在歐氏平面它「容易到不值一提」,在別的幾何裡它根本做不出來。
  • 三種幾何的「內角和 \(\pi\) / 角虧 / 角盈」正是同一件事的三種面貌: \(\text{area} = \pi - (A+B+C)\) 對雙曲成立( \(K=-1\) ), \(\text{area} = (A+B+C) - \pi\) 對球面成立( \(K=+1\) , \(R=1\) ),而歐氏平面裡面積與角和無關。

9. 前因後果:模型如何改變物理#

9.1 龐加萊的下一站:三體問題與拓樸學#

龐加萊不是為幾何學而做模型。1882 年之後,他做的事情是把非歐幾何帶進天體力學:三體問題(三個天體互相吸引的運動)在歐氏空間裡無法用初等積分解出,而在雙曲模型裡,某些區域的運動可以用「雙曲圓盤中的直線運動」來描述。1890 年的《動力學方程與三體問題》(Sur les équations de la dynamique et le problème des trois corps)裡,圓盤模型是主力工具。

同樣是「把幾何放進圓盤」的策略,在另一個方向上產生了 20 世紀數學最基礎的一門學科:1908 年的《論位置分析》(Sur l'analyse situs)。這篇文章處理的是這樣的問題:

不考慮距離與角度,只考慮「哪些曲線互相纏繞、哪些曲線可以分開拉開」——這種資訊夠不夠把曲面上的圖案分類?

答案夠,而且分類出的結構極其豐富。龐加萊把它叫做 analyse situs(位置分析),後來改名為拓樸學(topology)。他在這裡提出的「龐加萊同胚定理」與「龐加萊猜想」,分別在 1904 年與 1900 年提出,並由佩雷爾曼(Perelman)在 2002–2003 年以 Ricci 流的方法證實——那是 20 世紀數學最轟動的證明事件之一。

9.2 愛因斯坦的宇宙與「圓盤裡的空間」#

1919 年 4 月,愛因斯坦在寫給蘇黎世學生史托克(Stachel)的信中提到,他曾研讀克萊因與龐加萊的非歐幾何模型,用以思考廣義相對論中「空間形狀」的問題。這不是巧合:1915 年的廣義相對論需要一個「彎曲的四維時空」,而「非歐幾何」正是人類為此準備的思想工具(6.2 節已說明黎曼 → 愛因斯坦的橋樑)。

今天我們還可以在物理中找到圓盤模型的直接後代:

  • AdS/CFT 對偶:反德西特空間(Anti-de Sitter space)在 \(2+1\) 維時,正好就是龐加萊圓盤(邊界是圓,時間是徑向座標)。這是弦論最著名的對偶之一,也是「全息原理」最乾淨的模型。
  • 量子資訊:布洛赫球(Bloch sphere)內的量子態構成「龐加萊球」,其幾何正是龐加萊圓盤模型;量子資訊裡的「負體積」(negative volume)與此直接相關。
  • 雙曲流體:在負曲率空間中的流體力學,邊界幾何會影響整體運動,圓盤模型是標準的玩具模型。

9.3 「把幾何放進圓盤」是一種思考策略#

回頭看,圓盤模型的價值不只在雙曲幾何本身,而在於它示範了一種可重複使用的策略:

  1. 壓縮無窮:把無限大的空間摺進一個有限的區域(圓盤 → 邊界即無窮遠;上半平面 → 實軸即無窮遠)。無窮遠從「無法想像」變成「一條可畫的線」。
  2. 局部歐氏,全域彎曲:圓盤模型在每個點附近都像一張平面紙,但整體彎曲。把「彎曲」變成可以攤在桌上研究的東西。
  3. 保留角,放棄直:保角模型(龐加萊圓盤、保角映射)保留角度,放棄直線;克萊因模型保留直線,放棄角度。要什麼就給什麼——這是模型設計的核心取捨。
  4. 把不可解問題變成可算問題:一看到「不可解」,先問「有沒有模型能把它變成有限、可畫的東西?」龐加萊在三體問題上正是這樣做的。

這四條策略,在今天的數學與物理裡到處都是:複變數的黎曼面、數學物理中的「有效場論」、機器學習中的 Poincaré 球式 embedding、隨機幾何中的「隨機圓盤模型」……都用了同一招:找一個能把無窮壓縮、把抽象變具體的座標系。


10. 本節小結#

概念 歐氏平面 龐加萊圓盤( \(K=-1\) )
空間 無界的平面 單位圓盤(含邊界=無窮遠)
「直線」 直線段 與邊界正交的圓弧(直徑為退化情形)
圓弧方程 — \((x-a)^2+y^2 = a^2-1\) ,圓心距 \(\sqrt{1+\rho^2}\)
距離 \(\|z-w\|\) $2\operatorname{artanh}\left
度量 \(ds^2=dx^2+dy^2\) \(ds^2=\frac{4(dx^2+dy^2)}{(1-x^2-y^2)^2}\) (共形)
三角形內角和 \(=\pi\) \(<\pi\) ,角虧 \(=\pi-(A+B+C)=\) 面積
面積 與角和無關 \(\text{area}=\pi-(A+B+C)\) (Hurwitz)
等邊三角形 角恆為 \(60°\) 角 \(<60°\) ,隨邊長而變
對照模型 — 莫比烏斯變換 \(w=i\frac{1+z}{1-z}\) → 上半平面

本節的核心成果:

  • 模型 = 相對一致性:只要能在歐氏空間裡演出雙曲幾何的全部規則,它就不可能是空的。
  • 圓盤模型是保角的: \(g = \frac{4}{(1-r^2)^2}I\) ,所以雙曲角可以用歐氏切線量——這是所有 SymPy 實驗得以進行的技術基礎。
  • 莫比烏斯變換是等距的: \(\frac{|dw|^2}{\operatorname{Im}(w)^2} = \frac{4}{(1-|z|^2)^2}\) ,圓盤模型與上半平面模型是同一個世界。
  • Hurwitz 公式獲得雙重驗證:角虧(切線算法)與面積(數值積分)獨立算出同一個數,誤差隨網格加密而收斂。
  • 符號陷阱需要電腦守衛:半徑 \(\sqrt{a^2-1}\) 與圓心距 \(\sqrt{1+\rho^2}\) 這兩個式子一旦互換,就會造出一個看起來合理卻幾何意義全錯的圓。SymPy 只需要三行就能戳破它。

習題#

習題 6.3.1#

用 SymPy 證明:若 \(C\) 是與單位圓正交的圓、圓心 \(C=(c_x,c_y)\) 、半徑 \(\rho\) ,且 \(C\) 通過圓盤內一點 \(P=(p_x,p_y)\) ,則 \(C\cdot P = \frac{1+|P|^2}{2}\) 。並說明為什麼這意味著「通過 \(P\) 的測地線構成一個一參數族」;用數值方法畫出通過 \(P=(0.3,0.2)\) 的五條測地線。

習題 6.3.2#

把 §5 的距離公式推廣到半徑為 \(R\) 的模型圓盤(曲率 \(K=-1/R^2\) ):寫出 \(d_R(z,w)\) ,並用 SymPy 驗證 \(R\to\infty\) 時 \(d_R\) 趨近歐氏距離(在固定 \(z,w\) 下)。同時說明:為什麼「曲率半徑 \(R\) 」是雙曲幾何中唯一的尺度參數(提示:對比 6.1 節的鮑耶參數 \(k\) )。

習題 6.3.3#

(Hurwitz 公式的極限情形)計算理想三角形(頂點在單位圓上、彼此相隔 \(120°\) )的雙曲面積:(a) 用角虧公式說明其面積為 \(\pi\) ;(b) 用 SymPy 寫一個數值積分程式驗證,並解釋為什麼在接近理想頂點處會出現可積奇性、以及如何用變數替換 \(\theta=s^2\) 消除它。

習題 6.3.4#

在圓盤模型中證明(或用 SymPy 驗證)下列對應於 6.2 節的圓周長公式:半徑為 \(r\) (雙曲半徑)的「圓」在圓盤中對應的歐氏圓半徑是 \(\tanh(r/2)\) ,而其歐氏周長是 \(2\pi\tanh(r/2)\) ;比較雙曲周長 \(2\pi\sinh r\) 與歐氏周長 \(2\pi r\) ,說明為何雙曲圓的「每單位雙曲長度所對應的歐氏弧長」在靠近邊界時趨近於零。

習題 6.3.5#

(歷史與物理)用本章與 6.1、6.2 節的材料,回答:為什麼「非歐幾何在 19 世紀被發明、卻在 20 世紀才被物理採納」?請至少引用三個理由:(a) 技術上(模型與代數工具何時成熟);(b) 物理上(相對論何時需要它);(c) 觀念上(康德主義的阻礙何時被移除)。最後用一句話說明:龐加萊圓盤模型本身,除了「證明非歐幾何不矛盾」之外,還貢獻了什麼方法論上的示範。