Week 3 — 系統工程
week03_budgets/基礎知識導論
Week 3 基礎知識導論 — 任務預算表與餘裕管理
預算表 (budget) 是系統工程的控制器,不是報表。
它的用途是:任何一個設計變更發生時,在幾秒內知道有沒有東西被撞破。
1. 這一週在解什麼問題
四張表:質量、功率、鏈路、資料。每一張的結構都一樣:
需求(上限或下限) <--> 目前設計值 <--> 餘裕 (margin)
本週的技能不是「會加總」,而是:
- 知道每一項的數字從哪裡來(哪一週的 STK 結果?哪一份規格書?還是猜的?)
- 知道改動一個數字會沿著耦合鏈傳到哪裡
- 知道餘裕該留多少、什麼時候可以收掉
2. 核心觀念
2.1 餘裕的定義與慣例
margin = (能力 - 需求) / 需求 x 100%
設計成熟度愈低,餘裕要愈大。業界常見的階段性慣例(量級參考,實際依專案規範):
| 階段 | 質量餘裕 | 功率餘裕 |
|---|---|---|
| 概念 / Pre-Phase A | 25–30% | 30% 以上 |
| PDR | 20% | 20–25% |
| CDR | 10–15% | 15% |
| 交付 | 0–5%(改用實測值) | 5–10% |
餘裕不是保守,是把「你還不知道的事」寫成數字。 餘裕被吃光時,
代表未知已經變成已知 —— 這時候要嘛設計凍結,要嘛承認風險。
2.2 質量預算
- 逐元件列表 → 子系統小計 → 加上 contingency(每項依成熟度給不同百分比)→ 系統總質量。
- 上限來自兩處:CDS 的每 U 質量上限與發射服務 dispenser 規格。兩者取嚴。
- 別忘了:線束、螺絲、膠、屏蔽、配重。這些在早期最常被漏掉,後期最常爆掉。
2.3 功率預算(最容易做錯的一張)
功率預算必須沿著 CONOPS 的時間軸做,不能只做一個「平均功耗」:
一圈發電量 一圈耗電量
∫(一圈) P_gen dt >= Σ_i ( P_i x D_i ) x T_orbit + 充放電損失
其中 D_i 是第 i 個負載的 duty cycle。三個常見陷阱:
- 忘了地影:LEO 一圈約 90–100 分鐘,地影可佔 30–40%,發電只在日照段發生。
- 忘了 cosine loss:太陽能板不會永遠正對太陽,
P = η x A x S x cos(θ)。 - 忘了電池 DOD 限制:鋰電池若要撐 2 年(約 1 萬圈),每圈 DOD 通常壓在 10–20%。
這會讓所需電池容量遠大於「一圈用電量」。
2.4 鏈路預算(Week 11 的預告)
以 dB 表示的一串加減:
Eb/N0 [dB] = EIRP - L_FS - L_atm - L_point + G/T - k - 10*log10(Rb)
EIRP= 發射功率 + 天線增益 − 饋線損耗(dBW)- 自由空間損耗
L_FS = 20*log10(4*pi*d/λ)dB,d 為斜距、λ 為波長 k = -228.6dBW/(K·Hz),Boltzmann 常數- 餘裕 = 實際
Eb/N0− 該調變/編碼所需門檻(未編碼 BPSK 在 BER 1e-5 約 9.6 dB) - 本課要求餘裕 >= 3 dB
2.5 資料預算
每日產生量 <= 每日可下傳量
- 產生量 = 成像次數 x 每張影像大小 x (1 − 壓縮比)
- 可下傳量 = 下傳速率 x 每日 access 總時長 x 鏈路效率(協定開銷、重傳)
- 還要檢查瞬時:記錄器容量必須撐得過「兩次下傳之間累積的最大量」,
這是一條積分曲線的峰值,不是日平均(Week 12 會實際畫出來)。
2.6 預算表之間是連動的
成像次數↑ → 資料量↑ → 需要更高下傳速率 → 發射功率↑ → 功率預算↑
→ 電池↑ → 質量↑ → 可能超出 6U 上限
→ 或改成更高增益天線 → 指向精度需求↑ → ADCS 規格↑ → 功率↑
這就是為什麼四張表要放在同一支程式裡。 分開做的預算表一定會不一致。
3. 名詞與符號
| 符號 | 意義 | 單位 | 本課典型值 |
|---|---|---|---|
S |
太陽常數 | W/m² | 1361 |
η |
太陽能電池效率 | — | 0.28–0.32(三接面) |
D |
duty cycle | — | 0–1 |
DOD |
放電深度 | % | 每圈 10–20% |
EIRP |
等效全向輻射功率 | dBW | — |
G/T |
接收品質因數 | dB/K | 欄位可能吃線性比值,見誤解 4 |
Eb/N0 |
每位元能量對雜訊功率密度比 | dB | 門檻約 9.6(未編碼 BPSK) |
Rb |
位元率 | bit/s | — |
GSD |
地面解析度 | m | 30 |
margin |
餘裕 | % | 見 2.1 |
4. 觀念 → 本週實驗對照
| 觀念 | 出現在本週的哪裡 |
|---|---|
| 四表結構 | week3_mission_budgets.py,四個資料表 |
| 餘裕與 pass/fail | 每張表末尾的判定;改數字看判定翻轉 |
| 表與表連動 | 改成像速率 → 資料表 → 鏈路表 → 功率表 |
| 數字來源可追溯 | Week 7 / 11 / 12 的 STK 結果會回填進這裡 |
| 視覺化 | week3_budgets.png 四宮格 |
5. 常見誤解
- 「先算平均功耗就好。」 — 平均過得去,但地影段電池撐不住,一樣是失敗。
功率預算必須是時間函數。 - 「餘裕留著好看。」 — 餘裕是資源,遲早要花掉。重點是有意識地花,
並在每次設計評審記錄「這次花掉多少、換到什麼」。 - 「壓縮可以解決資料量問題。」 — 有損壓縮直接影響科學產出。
壓縮比必須由酬載需求決定,不能由下傳能力反推。 - 「dB 可以直接相乘。」 — dB 是對數,加減對應線性的乘除。
本教材的 Week 11 就踩過Model.GOverT -10的坑 —— 那個欄位吃的是線性比值,
−10 dB 要寫成0.1。
6. 延伸閱讀
系統工程與餘裕管理
- NASA Technical Reports Server (NTRS)
— 搜尋 "NASA Systems Engineering Handbook SP-2016-6105",第 6 章是技術資源餘裕管理的標準流程。 - ECSS 標準總表
— ECSS-E-ST-10(系統工程)、ECSS-M-ST-10(專案階段與餘裕)。 - NASA CubeSat 101(PDF)
— 第 4–5 章給了 CubeSat 尺度的實際質量與功率數字。
把「猜的數字」換成「查到的數字」
- NASA State of the Art of Small Spacecraft Technology
— 各次系統市售元件的性能範圍,本課最推薦的元件數字來源。 - eoPortal 衛星任務資料庫 — 用真實任務反推合理的預算量級。
Python 工具
- pandas 說明文件 — 預算表用 DataFrame 表達最自然。
- matplotlib 說明文件 — 四宮格圖的作法。
- NumPy 說明文件
7. 課前自我檢核
- 你的功率預算裡,地影時間是從哪裡來的?是估的還是 STK 算的?
- 電池容量若只依「一圈用電量」決定,兩年後會發生什麼事?
- 成像速率提高 2 倍,四張表分別會怎麼動?哪一張會先破?
- 「餘裕 15%」在 PDR 階段代表健康還是危險?判斷依據是什麼?
- 為什麼
G/T欄位填 −10 會是錯的?正確值是多少?
Week3_6_14_OpenSource_Lab_Handouts.md
開源補位週次實驗單:Week 3 / 6 / 14
STK 做不到的三週,用 Python/開源補上。每份都是 numpy/scipy/matplotlib 即可跑,
Week 6 與 Week 14 另有吃 STK 輸出 / 與 STK 交叉驗證的接口。
Week 3 — 系統工程:任務預算表 (Python/pandas)
檔案:week3_mission_budgets.py → week3_budgets.png + budgets_mass.csv
概念:系統工程的核心是「預算 + margin 管理」,不是物理模擬。物理量由各週工具算
(STK 給 link/power/data、FEA 給結構),這裡彙整成系統級四大預算並管 margin。
四大預算:
- 質量:各子系統質量 × (1+margin),對照 1U 限制。
- 功率:各模式負載 × 占空比 → 平均負載,對照 STK 發電。
- 鏈路:各項 dB 累加 → Rx power,對照靈敏度,餘裕 ≥ 3 dB。
- 資料:每日產生 vs 下傳(來自 STK access)。
操作:改各表數字 → 看 pass/fail 如何翻轉;把 Week 7/11/12 的 STK 結果填進對應格。
交付:四大預算表 + margin 分析 + 哪個預算最緊。
評分:四表正確 40% / margin 管理 30% / 與 STK 結果串接 30%。
Week 6 — 節點熱網路 (Python + STK 日照時間線)
檔案:week6_thermal_network.py → week6_thermal.png
分工:STK 給「何時日照/地影」(Lighting 時間線),Python 解「節點溫度」。
模型:6 面 + 1 內部節點集總參數熱網路,每節點能量平衡(太陽/反照/地球紅外/內部
發熱/輻射散熱/節點傳導),日照期太陽輸入開、地影期關。
操作:
1. 直接跑(內建 beta-angle 幾何產生日照率)——基準案例電池會低於 0°C(FAIL)。
2. Part B:改塗層 α/ε、內部發熱、beta angle,讓內部節點守住工作區間。
3. Part C:在 STK 對衛星出 Sunlight 時間線 CSV,load_stk_lighting() 載入,用真實
軌道日照重算,對比內建幾何。
交付:溫度曲線 + 電池工作區間 pass/fail + 熱控建議(加熱器/隔熱/塗層)。
評分:基準判讀 25% / 熱控迭代 30% / STK 日照接入 30% / 討論 15%。
Week 14 — 地面站通聯排程 (sgp4,與 STK access 交叉驗證)
檔案:week14_pass_scheduler.py → week14_passes.png + passes.csv
概念:運作階段從 TLE 算對地面站的通聯窗(rise/set/最大仰角),純 sgp4 不需下載星曆。
可與 STK access 互相驗證——同衛星同地面站,兩者算的 pass 應一致。
操作:
1. 改 TLE 為你的 CubeSat(Space-Track/CelesTrak 抓)、改地面站經緯度。
2. 跑出未來 N 天的 pass 時刻表(AOS/LOS/時長/最大仰角/最近距離)。
3. Part B(交叉驗證):在 STK 對同一顆衛星、同一地面站算 access,比對 Python 結果。
4. 排一天的下傳排程,對照 Week 12 的資料預算(通聯總時間夠不夠清空儲存)。
交付:pass 時刻表 + 仰角曲線圖 + 與 STK access 的比對誤差。
評分:pass 計算 30% / 時刻表 25% / STK 交叉驗證 30% / 與資料預算串接 15%。
圖

week03_budgets/week3_budgets.png
程式碼
# -*- coding: utf-8 -*-
# ================================================================
# Week 3 驗證段 — 四大預算的基準結果與 PASS/FAIL 判定
#
# 實驗單寫死了四張預算表的基準數字與 pass/fail。教材裡最容易悄悄過期的就是這種數字:
# 改了假設卻忘了改實驗單,學生跑出來對不上就會以為是自己弄錯。
# 這裡直接 import 實驗腳本、斷言它算出來的值,而不是解析主控台輸出。
#
# 執行:python week03_verify.py
# 離開碼 0 = 全部通過,1 = 有項目對不上
# ================================================================
import os
import sys
# notebook 的 kernel 沒有 __file__。而且啟動 STK Engine 會改變 process 的
# 工作目錄,所以不能事後才靠 os.getcwd() —— notebook 開頭的 bootstrap cell
# 會先把週次目錄釘在 WEEK_DIR。
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
REPO = os.path.abspath(os.path.join(HERE, os.pardir))
sys.path.insert(0, os.path.join(REPO, "tools"))
from stk_verify import Verifier, load_lab # noqa: E402
def main():
m = load_lab(os.path.join(HERE, "week3_mission_budgets.py"))
v = Verifier(None) # 純數值檢查,不需要 STK
# (1) 質量預算
total_mass = float(m.mass["Mass_kg"].sum())
with_margin = float((m.mass["Mass_kg"] * (1 + m.mass["Margin_pct"] / 100)).sum())
v.expect_true(f"含 margin 質量 <= 1U 限制 {m.MASS_LIMIT} kg", with_margin <= m.MASS_LIMIT,
f"小計 {total_mass:.3f} kg,含 margin {with_margin:.3f} kg")
# (2) 功率預算:軌道平均負載必須低於 STK Solar Panel 給的發電量
v.expect("平均負載", m.AVG_LOAD, 1.93, tol=0.05, unit=" W")
v.expect("軌道平均發電", m.GEN_ORBIT_AVG, 3.20, tol=0.01, unit=" W")
v.expect_true("功率收支 PASS", m.AVG_LOAD < m.GEN_ORBIT_AVG,
f"餘裕 {m.GEN_ORBIT_AVG - m.AVG_LOAD:+.2f} W")
# (3) 鏈路預算:餘裕須 >= 需求
rx = float(m.link["dB"].sum())
margin = rx - m.RX_SENS
v.expect("接收功率", rx, -101.5, tol=0.2, unit=" dBm")
v.expect("鏈路餘裕", margin, 8.5, tol=0.2, unit=" dB")
v.expect_true(f"餘裕 >= 需求 {m.REQ_MARGIN} dB", margin >= m.REQ_MARGIN)
ok = v.report("Week 3 驗證段")
return 0 if ok else 1
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
# ================================================================
# Week 3 Lab - 系統工程:任務預算表(mass / power / link / data)
#
# 系統工程的核心不是物理模擬,是「預算 + margin 管理」。
# 各子系統的物理量由別週的工具算出(STK 算 link/power/data、
# FEA 算結構),這裡把它們彙整成帶 margin 的系統級預算表。
#
# 輸出:week3_budgets.png(四張預算圖)+ 主控台表格 + budgets.csv
# ================================================================
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import os
# 產出要落在**週次目錄**,不是 process 的工作目錄。每一支 weekNN_verify.py
# 開頭都有這個慣用法並註明「啟動 STK Engine 會改變 process 的工作目錄」,
# 但 lab 這一側先前沒有套用 —— 結果庫根目錄散落著九個產出,
# 其中一份 week15_launch.png 還是過期的。notebook 的 kernel 沒有 __file__,
# 所以留 WEEK_DIR(notebook 開頭的 bootstrap cell 會設)當後備。
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
# ---------------- 1. 質量預算(Mass budget)----------------
# 每列:子系統, 質量(kg), margin(%)
mass = pd.DataFrame([
("Structure", 0.30, 5),
("EPS (battery+panels)", 0.25, 10),
("ADCS", 0.20, 10),
("OBC", 0.08, 10),
("COMM", 0.12, 10),
("Payload (imager)", 0.30, 15),
("Harness/misc", 0.10, 20),
], columns=["Subsystem","Mass_kg","Margin_pct"])
mass["Mass_with_margin"] = mass["Mass_kg"]*(1+mass["Margin_pct"]/100)
MASS_LIMIT = 2.00 # 1U 限制(含裕度)
# ---------------- 2. 功率預算(Power budget)----------------
# 每列:模式負載(W);再比較平均消耗 vs 發電(來自 STK Solar Panel Tool)
power_loads = pd.DataFrame([
("Idle/holding", 0.75, 0.40), # (W, 每軌占空比)
("Imaging", 4.00, 0.15),
("Downlink (Tx)", 6.75, 0.10),
("ADCS active", 1.00, 0.35),
], columns=["Mode","Load_W","DutyCycle"])
power_loads["AvgContribution_W"] = power_loads["Load_W"]*power_loads["DutyCycle"]
AVG_LOAD = power_loads["AvgContribution_W"].sum()
GEN_ORBIT_AVG = 3.2 # W,來自 STK Solar Panel Tool 的軌道平均發電(FIREYE_SolarPanel)
# ---------------- 3. 鏈路預算(Link budget)- 下行 UHF ----------------
# 各項 dB(來自 STK Comm / 手算),最後看 margin >= 3 dB
link = pd.DataFrame([
("Tx power (2 W)", 33.0), # dBm
("Tx line loss", -1.0),
("Tx antenna gain", 2.0),
("Free-space path loss", -145.0),
("Atmospheric loss", -1.5),
("Polarization loss", -3.0),
("Rx antenna gain", 15.0),
("Rx line loss", -1.0),
], columns=["Item","dB"])
RX_SENS = -110.0 # dBm 接收靈敏度(含所需 Eb/No)
REQ_MARGIN = 3.0
# ---------------- 4. 資料預算(Data budget)- 每日 ----------------
# 產生 vs 下傳(來自 STK access + storage_vs_time.py)
IMG_RATE_MBIT_S = 5.0
IMG_TIME_S_DAY = 6*60 # 每日成像秒數(來自 STK 成像 access)
DL_RATE_MBIT_S = 10.0*0.90 # 扣 10% overhead
DL_TIME_S_DAY = 4*8*60 # 每日下傳秒數(4 passes x 8 min,來自 STK)
# ================================================================
def main():
# ---- 質量 ----
m_total = mass["Mass_with_margin"].sum()
print("=== MASS BUDGET ===")
print(mass.to_string(index=False))
print(f"Total (with margin): {m_total:.3f} kg / limit {MASS_LIMIT} kg -> "
f"{'PASS' if m_total<=MASS_LIMIT else 'FAIL'}\n")
# ---- 功率 ----
print("=== POWER BUDGET ===")
print(power_loads.to_string(index=False))
margin_pwr = GEN_ORBIT_AVG - AVG_LOAD
print(f"Avg load {AVG_LOAD:.2f} W vs generation {GEN_ORBIT_AVG:.2f} W -> "
f"margin {margin_pwr:+.2f} W {'PASS' if margin_pwr>0 else 'FAIL'}\n")
# ---- 鏈路 ----
rx_power = link["dB"].sum()
link_margin = rx_power - RX_SENS
print("=== LINK BUDGET (downlink) ===")
print(link.to_string(index=False))
print(f"Rx power {rx_power:.1f} dBm, sensitivity {RX_SENS} dBm -> "
f"margin {link_margin:.1f} dB (req {REQ_MARGIN}) "
f"{'PASS' if link_margin>=REQ_MARGIN else 'FAIL'}\n")
# ---- 資料 ----
gen_day = IMG_RATE_MBIT_S*IMG_TIME_S_DAY/8/1000 # GB/day
dl_day = DL_RATE_MBIT_S*DL_TIME_S_DAY/8/1000
print("=== DATA BUDGET (per day) ===")
print(f"Generated {gen_day:.2f} GB/day, downlinked {dl_day:.2f} GB/day -> "
f"balance {dl_day-gen_day:+.2f} GB "
f"{'PASS' if dl_day>=gen_day else 'FAIL (backlog)'}\n")
# ---- 存 CSV ----
mass.to_csv(os.path.join(HERE, "budgets_mass.csv"), index=False)
# ---- 四張圖 ----
fig, ax = plt.subplots(2, 2, figsize=(13, 9))
ax[0,0].barh(mass["Subsystem"], mass["Mass_with_margin"], color="tab:blue")
ax[0,0].axvline(MASS_LIMIT, color="red", ls="--", label=f"limit {MASS_LIMIT} kg")
ax[0,0].set_xlabel("mass with margin (kg)")
ax[0,0].set_title(f"Mass budget (total {m_total:.2f} kg)"); ax[0,0].legend(fontsize=8)
ax[0,1].bar(power_loads["Mode"], power_loads["AvgContribution_W"], color="tab:orange")
ax[0,1].axhline(GEN_ORBIT_AVG, color="green", ls="--", label=f"gen {GEN_ORBIT_AVG} W")
ax[0,1].set_ylabel("avg power (W)"); ax[0,1].set_title(f"Power (avg load {AVG_LOAD:.2f} W)")
ax[0,1].tick_params(axis="x", rotation=20); ax[0,1].legend(fontsize=8)
cum = np.cumsum(link["dB"].values)
ax[1,0].step(range(len(link)+1), [0]+list(cum), where="post", color="tab:purple")
ax[1,0].axhline(RX_SENS, color="red", ls="--", label=f"sensitivity {RX_SENS} dBm")
ax[1,0].set_xticks(range(len(link)))
ax[1,0].set_xticklabels(link["Item"], rotation=40, ha="right", fontsize=7)
ax[1,0].set_ylabel("cumulative (dBm)")
ax[1,0].set_title(f"Link budget (margin {link_margin:.1f} dB)"); ax[1,0].legend(fontsize=8)
ax[1,1].bar(["Generated","Downlinked"], [gen_day, dl_day],
color=["tab:red","tab:green"])
ax[1,1].set_ylabel("GB / day")
ax[1,1].set_title(f"Data budget (balance {dl_day-gen_day:+.2f} GB/day)")
fig.suptitle("Week 3 - Mission Budgets (mass / power / link / data)", fontsize=13)
fig.tight_layout(rect=[0,0,1,0.97])
fig.savefig(os.path.join(HERE, "week3_budgets.png"), dpi=130)
print("Wrote week3_budgets.png, budgets_mass.csv")
if __name__ == "__main__":
main()