Week 7 — 功率系統

資料夾:week07_power/

基礎知識導論

Week 7 基礎知識導論 — 電力系統:發電、儲能與收支

電力是衛星唯一不能中斷的資源。任何子系統失效通常還救得回來,
電池放光了,衛星就永遠關機。功率預算是整個任務的硬底線。

1. 這一週在解什麼問題

FIREYE 的太陽能板一圈能發多少電?扣掉地影、扣掉 cosine loss、扣掉衰減之後,
還撐不撐得住 CONOPS 裡的成像與下傳?
電池要多大,才能兩年後還活著?

本週用 STK 的 Solar Panel Tool 算真實幾何下的發電曲線,
再用 Python 對這條曲線積分,做成收支。

2. 核心觀念

2.1 EPS 的四個組成

太陽能板  →  功率調節 (DET / MPPT)  →  電池  →  配電 (PDU)  →  各負載
元件 決定什麼
太陽能板 發電上限
功率調節 能不能把板子的功率真的取出來
電池 地影能不能撐過、壽命多長
配電 保護、開關、遙測

2.2 發電量:一連串的折扣

P = S * A * η * cos(θ) * F_pack * F_deg * F_temp
因子 意義 典型值
S 太陽常數 1361 W/m²(地球附近,隨日地距 ±3.4%)
A 板面積 6U 側板約 0.02 m²/面
η 電池效率 三接面 GaAs 約 0.28–0.32
cos(θ) 入射角餘弦 這是本週的重點
F_pack 封裝係數(有效面積佔比) 0.85–0.90
F_deg 壽命衰減(輻射) 每年約 2–3%
F_temp 溫度修正 高溫時 Voc 下降,效率降低

cos(θ) 是本週的核心。 它由姿態與軌道幾何共同決定,不是常數,
是一條隨時間變動的曲線 —— 這就是為什麼要用 STK 算,不能手估。

2.3 為什麼構型比效率重要

同樣的電池效率,不同的板子配置差距巨大:

構型 特性
六面貼板 (body-mounted) 任何姿態都有部分發電,但平均 cos(θ) 很低
單面朝日 (+X) 姿態對時發電最大,姿態錯了幾乎歸零
展開式 (deployable) 面積倍增,但增加機構風險與慣性矩(影響 Week 8)
對日追蹤 發電最佳,但需要驅動機構與更複雜的 ADCS

本週要比較 FIREYE(標準)vs FIREYEPosX(+X 面加強) 兩種構型,
用 STK 算出來的發電曲線做定量比較,而不是憑感覺。

2.4 能量收支必須對時間積分

一圈的能量平衡:
   ∫ P_gen(t) dt   ≥   ∫ P_load(t) dt / η_batt_round_trip

收支要看兩個層次:一圈平衡(能不能持續),與瞬時餘裕(電池夠不夠深)。

2.5 電池容量:壽命才是驅動條件

一個常見的錯誤推導:

錯:  容量 = 地影時間 x 平均負載         → 得到一個很小的電池

正確的推導要考慮放電深度 (DOD) 與循環壽命:

容量 = (地影能量需求) / (DOD_allow x η_disc)

LEO 兩年約 1 萬次循環。鋰離子要撐 1 萬次,單圈 DOD 通常要壓在 10–20%。
於是電池容量會變成「地影需求」的 5–10 倍。

還要留 EOL(壽命末期)餘裕:電池容量會隨循環衰減,設計要用 EOL 容量,不是 BOL。

2.6 DET vs MPPT

架構 原理 適用
DET (Direct Energy Transfer) 板子直接接匯流排,多餘功率被分流掉 簡單、可靠、功率小
MPPT (Max Power Point Tracking) 追蹤最大功率點 效率高 5–20%,尤其溫度變化大時

CubeSat 現在多用 MPPT,但 MPPT 本身也耗電、也會失效。
這是一個典型的「效率 vs 複雜度」權衡。

2.7 STK Solar Panel Tool 在做什麼

它做的是幾何與遮蔽的計算:

  1. 依姿態與太陽方向算每一面板的入射角
  2. 判斷是否在地影(以及半影)
  3. 計算自我遮蔽(其他結構擋住板子)
  4. 輸出功率 vs 時間

它不做:電池模型、MPPT 效率、溫度修正。
那些是你在 Python 端要自己補上的 —— 這正是本課「STK 主導、Python 補位」的分工。

本教材踩過的坑:Week 4 腳本裡的 SolarPanel DeleteData 會把資料清掉,
導致 Week 7 的發電報告變成 57 bytes 的空檔案 —— 而且沒有任何錯誤訊息。
這是「ACK 不等於生效」的典型案例,現已在兩支腳本裡註解掉。

3. 名詞與符號

符號 意義 單位 典型值
S 太陽常數 W/m² 1361
η 電池轉換效率 — 0.28–0.32
θ 太陽入射角 ° 隨時間變
DOD 放電深度 % 每圈 10–20%
BOL / EOL 壽命初期 / 末期 — 設計用 EOL
DET / MPPT 功率調節架構 — 見 2.6
Voc / Isc 開路電壓 / 短路電流 V / A 隨溫度變
W·hr 能量單位 — 電池容量常用

4. 觀念 → 本週實驗對照

觀念 出現在本週的哪裡
幾何發電曲線 FIREYE_SolarPanel_Connect.txt,STK Solar Panel Tool
構型比較 FIREYE vs FIREYEPosX
能量積分與收支 tools/storage_vs_time.py 的積分工具
選型比較 三種太陽能板 / 電池組合
空檔案的靜默失敗 week07_verify.py 檢查報告是否真的有資料列

注意:week07_verify.py 目前有一項刻意保留的 FAIL ——
發電報告的 Power 欄從某一列起變成「距分析起點的經過秒數」。
這個輸出異常尚未查明,教材選擇把它做成具名的檢查項顯示出來,而不是隱藏它。
這本身就是一個教學點:驗證段的價值在於讓未知變成可見的未知。

5. 常見誤解

  1. 「板子面積乘效率就是發電量。」 — 少了 cos(θ)、封裝、衰減、溫度,實際值可能只有一半。
  2. 「電池容量 = 地影耗電量。」 — 見 2.5,壽命(DOD 限制)才是驅動條件,差 5–10 倍。
  3. 「太陽能板效率愈高愈好。」 — 效率高的電池溫度係數與成本也不同,
    而且構型的影響常常大於效率的影響。
  4. 「發電曲線是平滑的正弦。」 — 有自我遮蔽、有半影、有姿態機動,真實曲線很不規則。
    這是必須用 STK 而不能手算的原因。
  5. 「報告檔產生了就是算好了。」 — 空檔案也會產生。要檢查裡面真的有數值列。

6. 延伸閱讀

電力系統

工具

7. 課前自我檢核

  1. 六面貼板的 6U CubeSat,任一時刻大約有幾面在發電?平均 cos(θ) 大概多少?
  2. 電池只按「地影耗電量」設計,兩年後會發生什麼事?請用循環壽命解釋。
  3. FIREYE 與 FIREYEPosX 哪一個發電多?在什麼姿態假設下這個答案會反過來?
  4. 若發電報告只有 57 bytes,你怎麼在程式裡自動偵測這件事?
  5. 你的功率餘裕用的是 BOL 還是 EOL 的電池容量?差多少?

圖


week07_power/storage_compare_demo.png


week07_power/storage_vs_time.png

程式碼

week07_power/FIREYE_SolarPanel_Connect.txt
# ============================================================
# FIREYE Solar Panel power generation - STK native (VO SolarPanel)
# Reproduces the SmallSat/eSatellite FIREYE vs FIREYEPosX
# configuration comparison WITHOUT SOLIS.
#
# Reference: help.agi.com/stk .../Content/cmd_VOSolarPanel.htm  (v13)
#
# CRITICAL PREREQUISITE - panel geometry lives in the 3D MODEL:
#   VO SolarPanel can only Compute over panel GROUPS that are
#   already defined inside the satellite's model file
#   (.mdl / .glb), each as "SolarPanelGroup <Name> <Efficiency%>".
#   Connect CANNOT create a panel face or change its normal.
#   => The "3-face vs 5-face" comparison = two different models
#      (or two group selections), exactly like FIREYE vs FIREYEPosX.
#
# HOW TO PREP THE MODEL (one-time, in the model file):
#   Add SolarPanelGroup lines naming each face + efficiency, e.g.
#     SolarPanelGroup PosX 30.0
#     SolarPanelGroup NegX 30.0
#     SolarPanelGroup PosY 30.0
#     SolarPanelGroup NegY 30.0
#     SolarPanelGroup PosZ 30.0     <- only in the "PosX" (5-face) variant
#   (0.30 efficiency from the tutorial Solar Cell Type.)
#
# Output units: Power = scenario power unit (set below), plus
#   solar intensity 0..1 per step.
# ============================================================

# ---- Scenario power unit -> Watts (so the report reads in W) ----
Units_Set * Report Power W
SetUnits / km

# ============================================================
# CONFIG A - FIREYE (imager on +Z / nadir; NO +Z panel = "5-face
# minus nadir" per tutorial: panels on all sides except imager face)
# Assumes model FIREYE has groups: PosX NegX PosY NegY NegZ
# ============================================================
# (Assumes */Satellite/FIREYE already exists from eSatellite_STK_Connect.txt)

# Optionally show the solar-panel 3D view while computing:
# VO */Satellite/FIREYE SolarPanel Visualization Radius On 60 AddGroup PosX AddGroup NegX AddGroup PosY AddGroup NegY AddGroup NegZ View On

# Add Earth as an obstruction so eclipse is modeled:
VO */Satellite/FIREYE SolarPanel Object On CentralBody/Earth

# Compute POWER over ~2 orbits (700 km SSO ~ 98.8 min) at 60 s steps.
# {TimeInterval} <StepSize> {ReportType} "<FilePath>"
VO */Satellite/FIREYE SolarPanel Compute "UseAnalysisStartTime" "UseAnalysisStopTime" 60 Power "C:\temp\FIREYE_power.txt"

# (Optional) illuminated AREA vs time:
# VO */Satellite/FIREYE SolarPanel Compute "UseAnalysisStartTime" "UseAnalysisStopTime" 60 Area "C:\temp\FIREYE_area.txt"

# Free the illumination data when done:
# DeleteData 會清掉太陽能板的計算狀態,之後即使重新 Object On,
# 下一次 Compute 也只會產出空報告(而且照樣花兩分鐘、照樣 ACK)。
# Week 7 接在本檔之後,正是被這一行害的 —— 報告已經寫出來了,
# 這行沒有實際用途,註解掉。
#VO */Satellite/FIREYE SolarPanel DeleteData

# ============================================================
# CONFIG B - FIREYEPosX (imager moved to +X; +Z now carries a panel;
# tutorial removes the +X panel and ADDS a +Z panel)
# Assumes model FIREYEPosX has groups: NegX PosY NegY NegZ PosZ
# ============================================================
# (Assumes */Satellite/FIREYEPosX exists - a 2nd satellite using the
#  PosX-configuration model. Create it the same way as FIREYE but
#  load the FIREYEPosX model and set the imager/SBand on +X.)

# NOTE: no script in this course creates FIREYEPosX, so these three lines
# fail unless you build that second satellite first (see the block above).
# Uncomment once */Satellite/FIREYEPosX exists with the PosX-configuration model.
#VO */Satellite/FIREYEPosX SolarPanel Object On CentralBody/Earth
#VO */Satellite/FIREYEPosX SolarPanel Compute "UseAnalysisStartTime" "UseAnalysisStopTime" 60 Power "C:\temp\FIREYEPosX_power.txt"
#VO */Satellite/FIREYEPosX SolarPanel DeleteData

# ============================================================
# Compare: plot FIREYE_power.txt vs FIREYEPosX_power.txt
# (avg power, eclipse depth, whether generation >= load).
# The Python script storage_vs_time.py can also ingest these.
# ============================================================
week07_power/week07_verify.py
# -*- coding: utf-8 -*-
# ================================================================
# Week 7 驗證段 — 太陽能板發電計算真的產出資料了嗎
#
# VO SolarPanel Compute 是本週的核心,而它有兩個靜默失敗的入口:
# 一是模型裡沒有 SolarPanelGroup 定義(算不出東西但照樣 ACK),
# 二是輸出檔寫不出來(資料夾不存在時 Connect 靜默失敗)。
#
# 執行:python week07_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
from agi.stk12.stkdesktop import STKDesktop
from stk_verify import run_script      # noqa: E402


def main():
    app = STKDesktop.AttachToApplication()
    root = app.Root
    if root.CurrentScenario is not None:
        root.CloseScenario()
    # 本週沒有自己的 New / Scenario,接在 Week 4 的完整場景上
    run_script(root, os.path.join(REPO, "week04_orbit",
                                  "FIREYE_Volcano_FullScenario_Connect.txt"), echo_fail=False)
    ack, nack = run_script(root, os.path.join(HERE, "FIREYE_SolarPanel_Connect.txt"))

    v = Verifier(root)
    v.object_exists("*/Satellite/FIREYE")
    v.expect_true("腳本沒有 NACK", nack == 0, f"{ack} ACK / {nack} NACK")

    # 發電報告必須有資料列,而且功率值要落在物理上合理的範圍
    out = os.path.join("C:" + os.sep, "temp", "FIREYE_power.txt")
    # 報告每列是:  1 Jan 2024 00:01:00.000   <Power W>   <Solar Intensity>
    # 日期佔 4 個 token,所以 Power 是倒數第二欄(不是最後一欄)
    vals = []
    if os.path.exists(out):
        for line in open(out, encoding="utf-8", errors="ignore"):
            p = line.split()
            if len(p) == 6 and p[0].isdigit():
                try:
                    vals.append(float(p[-2]))
                except ValueError:
                    pass
    v.expect_true("發電報告有資料列", len(vals) > 100, f"{len(vals)} 列")
    if vals:
        import numpy as np
        a = np.array(vals)
        v.expect_true("有地影期(功率 = 0)", a.min() <= 1e-9,
                      f"最小 {a.min():.3f} —— 沒有 0 就表示地影沒被算進去")

        # 真實發電功率會隨軌道週期性振盪(日照 -> 地影 -> 日照)。
        # 若整段單調遞增,表示這一欄根本不是功率。
        # 已知問題:本機 STK 12.10.0 的輸出從某一列起,Power 欄的值
        # 等於「距分析起點的經過秒數」(例如 7 Jan 22:38 那列是 599880),
        # 顯然不是瓦特。這一項會 FAIL —— 它是刻意留著的,用來標記這個
        # 尚未查明的輸出異常,而不是假裝沒看到。
        rising = float(np.mean(np.diff(a) > 0))
        v.expect_true("功率隨軌道振盪(不是單調遞增)", rising < 0.9,
                      f"{rising*100:.1f}% 的相鄰點在上升;"
                      f"最大值 {a.max():.0f} —— 若接近分析窗秒數就是欄位錯亂")
        v.expect_between("功率峰值的量級(6U CubeSat 合理範圍 1~60 W)",
                         float(np.percentile(a[a > 0], 95)) if (a > 0).any() else 0, 1, 60)

    ok = v.report("Week 7 驗證段")
    return 0 if ok else 1


if __name__ == "__main__":
    _rc = main()
    if "ipykernel" not in sys.modules:
        raise SystemExit(_rc)