Week 16 — 整合展示

資料夾:week16_constellation/

基礎知識導論

Week 16 基礎知識導論 — 覆蓋、重訪與星座設計

前十五週都在問「這一顆衛星做得到嗎」。
最後一週要問一個不同的問題:「一顆到底夠不夠?不夠的話,要幾顆?」

1. 這一週在解什麼問題

FIREYE 的需求是「單一火山重訪 < 24 hr」。
單顆衛星做得到嗎?做不到的話,補到幾顆才夠?

用 STK 的 Coverage Definition + Figure of Merit (RevisitTime) 量化,
再用 Python 的解析模型做便宜的規模掃描。

2. 核心觀念

2.1 覆蓋的三個常見度量

度量 定義 適用
重訪時間 (Revisit Time) 同一格點連續兩次被覆蓋的間隔 監測型任務(本課)
回應時間 (Response Time) 從「現在下令」到「能觀測」的等待時間 應變型任務
覆蓋百分比 (% Coverage) 任一瞬間被覆蓋的面積比例 通訊星座

選錯度量會得到錯的架構。
火山監測要的是重訪(定期回來看),不是瞬時覆蓋。
但如果需求改成「火山爆發後 30 分鐘內取像」,那就變成回應時間問題,答案會完全不同。

2.2 平均值不是重點,最差格點才是

本教材的實測結果:

構型 平均重訪 最差格點重訪
單顆 FIREYE 19.10 hr 48 hr
Walker 3 x 4(12 顆) 1.75 hr 2.05 hr

單顆的平均值 19.10 hr 看起來滿足 24 hr 需求,
但最差的格點要等 48 小時 —— 需求其實沒達成。

而且注意兩個構型的平均與最差的差距:
單顆是 2.5 倍,星座只有 1.17 倍。

星座真正的價值,不是把平均值變好,而是把服務品質變均勻。

這是本週最重要的一句話。一個服務品質不均勻的系統,
使用者體驗由最差的那一格決定,不由平均值決定。

2.3 報酬遞減

12 顆衛星只換到 10.9 倍的平均重訪改善(19.10 / 1.75)。
衛星數量與覆蓋效能不是線性關係,更不是超線性。

粗略的直覺:對均勻分布的星座,重訪時間大致反比於衛星數。
所以「重訪減半」要「衛星加倍」—— 成本也加倍。
這條曲線決定了架構決策的甜蜜點在哪裡。

2.4 Walker Delta 星座

標準記法:

i : T / P / F

相鄰面的相位差:

Δu = F * 360° / T

各面的 RAAN 均勻分布:ΔΩ = 360° / P(Delta 型;Star 型是 180°/P)。

本週用的是 3 面 x 4 顆 = 12 顆。

設計旋鈕:
- P 多 → 覆蓋更均勻,但發射成本高(每個面要單獨發射或花 delta-V 改 RAAN)
- P 少(多顆同面)→ 一次發射多顆,便宜,但同面衛星覆蓋高度重疊
- F 影響格點的重訪分布均勻度,是最便宜的優化旋鈕(不花任何硬體成本)

2.5 改變 RAAN 很貴,但 J2 是免費的

在軌道上直接改 RAAN 的代價跟改傾角一樣高(Week 15 的 2.4)。
但 J2 攝動會讓不同高度的軌道以不同速率進動(Week 4 的 2.3):

dΩ/dt = -(3/2) * n * J2 * (Re/p)² * cos(i)

所以可以用「暫時改變高度 → 等 RAAN 漂到位 → 改回來」的方式建立軌道面,
代價只有兩次小的高度變更。這叫 J2-assisted plane change,是星座部署的標準技巧,
代價是時間(數週到數月)。

2.6 STK Coverage 的運作方式

CoverageDefinition            定義感興趣的區域與格點解析度
   + Asset(哪些衛星/感測器)
   + AdvancedGridPoint 設定
        ↓
Figure of Merit (FOM)         在格點上計算某個度量
   例:RevisitTime / Access Duration / Response Time
        ↓
報表 / 等高線圖

格點解析度是精度與速度的權衡:格點太粗會漏掉最差區域,
太細則計算時間爆炸。本教材用美西 90 個格點、2 天分析窗。

2.7 解析模型的用途:便宜的掃描

Python 解析模型與 STK 的對照:

案例 解析模型 STK 比值
單顆 — 19.10 hr 系統性低估約 6.5 倍
Walker 3x4 — 1.75 hr 同樣約 6.5 倍

解析模型系統性低估約 6.5 倍,但兩個案例的比值一致。

這代表:解析模型抓對了趨勢,只是常數不對。
所以可以用 STK 算兩三個點來校準它,再用校準後的解析模型便宜地掃描整個設計空間。

校準後的結論:要達到 3 hr 重訪目標,約需 7 顆衛星。

這是本課方法論的收尾:
高精度工具(STK)負責校準與驗證,
便宜的解析模型負責掃描設計空間。
兩者分工,而不是二選一。

2.8 星座的其他代價

決定「加衛星」之前,要把這些一起算進去:

3. 名詞與符號

名詞 意義 本課值
Revisit Time 重訪時間 單顆 19.10 hr / Walker 1.75 hr
Response Time 回應時間 —
FOM Figure of Merit 本課用 RevisitTime
Walker Delta i:T/P/F 星座記法 3 面 x 4 顆
P 軌道面數 3
F 相位因子 0 至 P−1
格點 (grid point) 覆蓋計算的取樣點 美西 90 點
分析窗 — 2 天

4. 觀念 → 本週實驗對照

觀念 出現在本週的哪裡
Coverage + RevisitTime FOM week16_walker_coverage_Connect.txt(38 條指令)
Walker 3 x 4 建構 同上
解析模型與規模掃描 week16_constellation_tradeoff.py
校準後推 7 顆 同上
圖表 week16_constellation.png(重訪時間 vs 星座規模)
驗證段 week16_verify.py
實驗單 Week16_Constellation_Lab_Handout.md

5. 常見誤解

  1. 「平均重訪 19 hr 滿足 24 hr 需求。」 — 最差格點是 48 hr。
    需求是對每一個目標成立,不是對平均值成立。
  2. 「12 顆就快 12 倍。」 — 實測只有 10.9 倍,而且真正的價值在均勻度,不在平均值。
  3. 「解析模型差 6.5 倍,所以沒用。」 — 反過來:比值一致代表趨勢正確。
    校準後它是最便宜的掃描工具。
  4. 「加軌道面比加同面衛星好。」 — 覆蓋均勻度是好,但發射成本高很多。
    要看你的成本模型。
  5. 「星座設計就是選 T/P/F。」 — 還有部署策略、地面段、去軌、頻率協調。
    後面這些常常才是真正的成本。

6. 延伸閱讀

星座設計

巨型星座與其代價

工具

7. 課前自我檢核

  1. 平均重訪 19.10 hr、最差格點 48 hr。你的任務需求「重訪 < 24 hr」達成了嗎?為什麼?
  2. 用一句話說明星座的核心價值。(提示:不是「平均值變好」)
  3. Walker i:T/P/F 的 F 改變會影響什麼?它需要花錢嗎?
  4. 為什麼「暫時改高度」可以用來改變 RAAN?代價是什麼?
  5. 解析模型系統性低估 6.5 倍。這代表它沒用,還是可以用?你會怎麼處理?

Week16_Constellation_Lab_Handout.md

Week 16 實驗單 — STK SmallSat 整合展示:從一顆到一個星座

主題:用 Coverage / Figure of Merit 量化「一顆 FIREYE 夠不夠」,並回答「不夠要幾顆」
工具:STK Coverage(主)+ Python 解析模型(設計掃描)
檔案:week16_walker_coverage_Connect.txt、week16_constellation_tradeoff.py


為什麼這週擺在最後

前面十五週都在把一顆 FIREYE 做到能用:選軌道、配電力、定姿態、開鏈路、排通聯。
這週問一個系統層級的問題 —— 火山噴發預警需要多快的重訪?一顆做得到嗎?

答案會很直接:做不到。而「要幾顆」正是把全學期的分析轉成任務架構決策的地方,
也是期末報告「未來趨勢/巨型星座」章節該有的量化依據。


學習目標

  1. 建立 CoverageDefinition:定義區域、格點解析度、指派 asset。
  2. 理解 asset 要指到感測器而非衛星 —— 前者是 FOV 限制的覆蓋,後者只是「有沒有飛過」。
  3. 用 Walker Delta 從單顆種子長出星座,理解 NumPlanes / NumSatsPerPlane / InterPlanePhaseIncrement 的意義。
  4. 用 Figure of Merit(Revisit Time) 量化改善,並解讀「格點之間的差異」而不只看平均。
  5. 判讀次線性回報:12 倍的衛星換到幾倍的重訪改善?為什麼不是 12 倍?

Part A — STK:單顆 vs 星座(60 min)

  1. 報告目錄要先存在(路徑見 tools/stk_paths.py,預設 C:\STK_Reports\)—— Connect 寫檔失敗是靜默失敗。
    建起來:python -c "import sys;sys.path.insert(0,'tools');import stk_paths;print(stk_paths.ensure_reports_dir())"
  2. 開啟 STK 12 桌面版,執行 week16_walker_coverage_Connect.txt。
    不要用無介面的 STK Engine —— Graphics ... SetColor、OrbitWizard ... Color
    等指令在 Engine 上會被拒絕。
  3. 腳本會產生兩份報告:
    - C:\STK_Reports\week16_revisit_1sat.txt(單顆)
    - C:\STK_Reports\week16_revisit_12sat.txt(Walker 3 面 × 4 顆)
  4. 在 3D 視窗看 Walker 星座的軌道面分布,以及覆蓋格點的顏色變化。

基準結果(本機 STK 12.10.0 實測,分析窗 2 天,美西 30–55°N / 130–110°W,90 個格點):

星座 平均重訪 中位數 最差格點
1 顆 19.10 hr 15.98 hr 48.00 hr
12 顆(Walker 3×4) 1.75 hr 1.81 hr 2.05 hr

紀錄:兩組的平均/中位/最差,以及改善倍率。

判讀重點:
- 單顆的「最差格點 48 小時」意味著什麼?對火山預警可接受嗎?
- 12 顆的平均與最差只差 0.3 hr,單顆卻差了 32 hr —— 星座除了縮短重訪,
更重要的是把服務品質變均勻。這一點在平均值裡看不出來,要看分布。


Part B — Python:回答「要幾顆」(45 min)

在 STK 裡把 N 從 1 掃到 48 太貴。改用解析模型掃描,再用 Part A 的實測校準。

執行 python week16_constellation_tradeoff.py,它會:
1. 讀 Part A 的兩份報告(找不到會明講,不會靜默假裝)。
2. 用 streets-of-coverage 一階模型算重訪 vs 星座規模。
3. 用實測校準模型後,回答目標重訪所需的衛星數。

基準結果:

星座 解析模型 STK 實測 比值
1 顆 3.07 hr 19.10 hr 6.2
12 顆 0.26 hr 1.75 hr 6.8

解析模型系統性低估約 6.5 倍,但關鍵是 —— 兩個案例的比值幾乎一樣。
這代表模型抓對了「隨 N 縮放」的關係,只是絕對值有偏差。所以只要用一次 STK 實測
定出比例因子 k,就能拿模型去做便宜的規模掃描。

校準後(k = 6.52),目標重訪 3 小時 → 約需 7 顆。

紀錄:k 值、達成 3 hr 所需衛星數,以及你認為 k 為什麼是 6.5 而不是 1。


討論題(報告必答)

  1. 解析模型為什麼低估? 至少列出三個原因。提示:升軌與降軌的通過會擠在一起
    算成兩次機會嗎?太陽同步軌道的地面軌跡有回歸週期,和模型假設的「均勻分布」一樣嗎?
    「感測器看得到」等於「拍得到有用的影像」嗎(太陽高度角、雲遮)?

  2. 次線性回報:12 倍衛星只換到 10.9 倍改善。如果繼續加到 24 顆,你預期改善多少?
    什麼因素會讓回報進一步遞減?

  3. 平面數 vs 每面顆數:總數固定 12 顆,3 面×4 顆 和 4 面×3 顆、2 面×6 顆
    會一樣嗎?改 Connect 腳本的 NumPlanes / NumSatsPerPlane 實測,並解釋差異。

  4. 架構決策:7 顆 CubeSat 的發射與運營成本 vs 一顆大衛星。用 Week 3 的預算表
    和 Week 15 的發射分析支撐你的建議。

  5. 接回主線:把星座的重訪改善接回 Week 12 的資料預算 —— 7 顆衛星產生的資料量
    是一顆的 7 倍,現有的地面站與下傳速率吃得下嗎?


評分

項目 比重
Coverage 建構與 FOM 判讀 30%
Walker 星座參數研究(討論題 3) 25%
解析模型與 STK 對照、k 值解釋 25%
架構決策與跨週串接(討論題 4、5) 20%

延伸

圖


week16_constellation/week16_constellation.png

程式碼

week16_constellation/week16_constellation_tradeoff.py
# -*- coding: utf-8 -*-
# ================================================================
# Week 16 Lab — 星座規模 vs 重訪時間:STK Coverage 對照解析模型
#
# 全學期的收束問題:「一顆 FIREYE 看火山夠嗎?不夠的話要幾顆?」
#
# 雙軌(同 Week 10 的哲學):
#   (A) STK Coverage/FOM = 正牌答案。讀 week16_walker_coverage_Connect.txt
#       產生的兩份重訪報告(單顆 vs Walker 3x4)。
#   (B) Python 解析模型 = 先估量級,並回答「要幾顆」這種設計問題 ——
#       掃描星座規模比在 STK 裡重建 N 次便宜得多。
#
# 解析模型(streets-of-coverage 一階近似):
#   感測器半錐角 eta、高度 h -> 地心半角 lambda = asin(sin(eta)*(RE+h)/RE) - eta
#   地面刈幅半寬 w = RE * lambda
#   緯度 phi 上相鄰地面軌跡的經度間距 dL = 2*pi*RE*cos(phi) * (T_orbit/T_earth)
#   每日通過次數 ~ N * (T_earth/T_orbit) * min(1, 2w/dL)
#   平均重訪 ~ T_earth / 每日通過次數
#
# 這個模型會系統性**低估**重訪時間(見實驗單的討論題),原因包括:
#   忽略升/降軌通過會擠在一起、忽略太陽同步軌道的回歸特性、
#   把「感測器看得到」等同「有效觀測」(未計太陽高度角、雲遮)。
#   Lab 的重點就是解釋這個落差,而不是讓兩邊數字對上。
#
# 輸出:week16_constellation.png + 主控台摘要
# ================================================================

import os
import re
import sys

import numpy as np
import matplotlib.pyplot as plt

HERE = (os.path.dirname(os.path.abspath(__file__))
        if "__file__" in globals() else os.path.abspath(os.getcwd()))

# ---------------- 任務參數(與 Connect 腳本一致)----------------
RE        = 6378.137        # km
MU        = 398600.4418     # km^3/s^2
ALT       = 600.0           # km,SSO
CONE_DEG  = 40.0            # 感測器半錐角
LAT_MID   = 42.5            # 覆蓋區中緯(30~55 的中點)
T_EARTH   = 86164.0         # s,恆星日
TARGET_REVISIT_HR = 3.0     # 火山早期預警的目標重訪

sys.path.insert(0, os.path.join(HERE, os.pardir, "tools"))
import stk_paths                       # noqa: E402  報告目錄的單一來源

REPORTS = {1:  stk_paths.report("week16_revisit_1sat.txt"),
           12: stk_paths.report("week16_revisit_12sat.txt")}


# ---------------- 1. 讀 STK 重訪報告 ----------------
_ROW = re.compile(r"^\s*(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(\d+\.\d+)\s*$")

def parse_revisit(path):
    """STK 'Value By Grid Point' 報告 -> (lat, lon, revisit_sec) 三個陣列。"""
    lat, lon, val = [], [], []
    with open(path, encoding="utf-8", errors="ignore") as f:
        for line in f:
            m = _ROW.match(line)
            if m:
                lat.append(float(m.group(1)))
                lon.append(float(m.group(2)))
                val.append(float(m.group(3)))
    return np.array(lat), np.array(lon), np.array(val)


def load_stk():
    """回傳 {n_sat: revisit_array};缺檔就跳過並明講(不靜默)。"""
    out = {}
    for n, p in REPORTS.items():
        if os.path.exists(p):
            _, _, v = parse_revisit(p)
            if len(v):
                out[n] = v
                print(f"[STK] {os.path.basename(p)}: {len(v)} 個格點")
                continue
        print(f"[STK] 找不到或無法解析 {p}")
    if not out:
        print("      先在 STK 執行 week16_walker_coverage_Connect.txt 產生報告;"
              "以下只顯示解析模型。")
    return out


# ---------------- 2. 解析模型 ----------------
def orbit_period(alt_km):
    a = RE + alt_km
    return 2.0 * np.pi * np.sqrt(a ** 3 / MU)


def swath_half_width(alt_km, cone_deg):
    """地心半角 -> 地面刈幅半寬(km)。"""
    eta = np.radians(cone_deg)
    s = np.sin(eta) * (RE + alt_km) / RE
    if s >= 1.0:                       # 錐角大到看見地平線
        lam = np.pi / 2 - np.arcsin(RE / (RE + alt_km))
    else:
        lam = np.arcsin(s) - eta
    return RE * lam


def mean_revisit_analytic(n_sat, alt_km=ALT, cone_deg=CONE_DEG, lat_deg=LAT_MID):
    T = orbit_period(alt_km)
    w = swath_half_width(alt_km, cone_deg)
    dL = 2.0 * np.pi * RE * np.cos(np.radians(lat_deg)) * (T / T_EARTH)
    frac = min(1.0, 2.0 * w / dL)              # 落在刈幅內的軌跡比例
    passes_per_day = n_sat * (T_EARTH / T) * frac
    return T_EARTH / passes_per_day if passes_per_day else np.inf


# ---------------- 執行 ----------------
def main():
    stk = load_stk()

    T = orbit_period(ALT)
    w = swath_half_width(ALT, CONE_DEG)
    print(f"\n軌道週期 {T:.0f} s ({T/60:.1f} min) | 每日 {T_EARTH/T:.2f} 圈")
    print(f"感測器 {CONE_DEG:.0f} 度半錐角 @ {ALT:.0f} km -> 刈幅半寬 {w:.1f} km"
          f"(全寬 {2*w:.0f} km)")

    print("\n=== STK Coverage 實測(緯度 30-55,經度 -130~-110)===")
    for n in sorted(stk):
        v = stk[n]
        print(f"  {n:>2} 顆 | 平均 {v.mean()/3600:6.2f} hr | 中位 {np.median(v)/3600:6.2f} hr"
              f" | 最差 {v.max()/3600:6.2f} hr")
    if len(stk) >= 2:
        a, b = sorted(stk)
        gain = stk[a].mean() / stk[b].mean()
        print(f"  {a} -> {b} 顆:衛星數 {b/a:.0f} 倍,重訪改善 {gain:.1f} 倍"
              f" —— 次線性回報,這是星座設計的核心權衡")

    print("\n=== 解析模型 ===")
    for n in sorted(stk) or (1, 12):
        print(f"  {n:>2} 顆 | 解析 {mean_revisit_analytic(n)/3600:6.2f} hr", end="")
        if n in stk:
            r = stk[n].mean() / mean_revisit_analytic(n)
            print(f"  vs STK {stk[n].mean()/3600:6.2f} hr  (STK 是解析的 {r:.1f} 倍)")
        else:
            print()

    ns = np.arange(1, 49)
    analytic = np.array([mean_revisit_analytic(int(n)) for n in ns]) / 3600.0

    # 用 STK 的實測點校準解析模型(單一比例因子),再回答「要幾顆」
    if stk:
        k = np.mean([stk[n].mean() / mean_revisit_analytic(n) for n in stk])
        calibrated = analytic * k
        need = ns[calibrated <= TARGET_REVISIT_HR]
        print(f"\n=== 設計問題:重訪要壓到 {TARGET_REVISIT_HR:.0f} hr 以內,需要幾顆? ===")
        print(f"  用 STK 實測校準解析模型(比例因子 k = {k:.2f})")
        print(f"  -> 約需 {need.min() if len(need) else '>48'} 顆")
    else:
        calibrated, k = None, None

    # ---------------- 出圖 ----------------
    fig, ax = plt.subplots(1, 2, figsize=(13, 5))

    ax[0].plot(ns, analytic, lw=1.6, ls="--", color="tab:gray",
               label="analytic (uncalibrated)")
    if calibrated is not None:
        ax[0].plot(ns, calibrated, lw=2.0, color="tab:blue",
                   label=f"analytic x k={k:.2f} (calibrated to STK)")
    for n in sorted(stk):
        ax[0].plot(n, stk[n].mean()/3600, "o", ms=9, color="tab:red",
                   label=f"STK: {n} sat" if n == min(stk) else f"STK: {n} sat")
    ax[0].axhline(TARGET_REVISIT_HR, color="k", ls=":", lw=1.2,
                  label=f"target {TARGET_REVISIT_HR:.0f} hr")
    ax[0].set_xlabel("number of satellites")
    ax[0].set_ylabel("mean revisit time (hr)")
    ax[0].set_title(f"Revisit vs constellation size  |  {CONE_DEG:.0f}$^\\circ$ cone @ {ALT:.0f} km")
    ax[0].set_yscale("log"); ax[0].grid(alpha=0.3, which="both"); ax[0].legend()

    if stk:
        data = [stk[n]/3600 for n in sorted(stk)]
        ax[1].boxplot(data, tick_labels=[f"{n} sat" for n in sorted(stk)])
        ax[1].set_ylabel("revisit time per grid point (hr)")
        ax[1].set_title("STK coverage: spread across the volcano region")
        ax[1].grid(alpha=0.3, axis="y")
    else:
        ax[1].text(0.5, 0.5, "no STK reports\nrun the Connect script first",
                   ha="center", va="center"); ax[1].axis("off")

    fig.tight_layout()
    out = os.path.join(HERE, "week16_constellation.png")
    fig.savefig(out, dpi=130)
    print(f"\nWrote {os.path.basename(out)}")


if __name__ == "__main__":
    main()
week16_constellation/week16_verify.py
# -*- coding: utf-8 -*-
# ================================================================
# Week 16 驗證段 — 星座覆蓋改善與解析模型的一致性
#
# 本週的教學重點是「解析模型低估但比例一致」。那個一致性一旦不成立,
# 用一次 STK 實測校準模型的做法就不能用了 —— 所以它必須被檢查。
#
# 執行:python week16_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():
    import numpy as np
    m = load_lab(os.path.join(HERE, "week16_constellation_tradeoff.py"))
    v = Verifier(None)

    stk = m.load_stk()
    if len(stk) < 2:
        v.expect_true("需要兩份 STK 重訪報告", False,
                      "先執行 week16_walker_coverage_Connect.txt")
        return 0 if v.report("Week 16 驗證段") else 1

    v.expect("單顆平均重訪", stk[1].mean() / 3600, 19.10, tol=1.0, unit=" hr")
    v.expect("12 顆平均重訪", stk[12].mean() / 3600, 1.75, tol=0.2, unit=" hr")
    gain = stk[1].mean() / stk[12].mean()
    v.expect("重訪改善倍率", gain, 10.9, tol=1.0, unit=" 倍")
    v.expect_true("次線性回報:改善倍率 < 衛星數倍率", gain < 12.0,
                  f"12 倍衛星換到 {gain:.1f} 倍改善")
    v.expect_true("星座讓服務品質變均勻",
                  (stk[12].max() - stk[12].mean()) < (stk[1].max() - stk[1].mean()) / 10,
                  f"最差-平均:單顆 {(stk[1].max()-stk[1].mean())/3600:.1f} hr,"
                  f"12 顆 {(stk[12].max()-stk[12].mean())/3600:.2f} hr")

    ks = [stk[n].mean() / m.mean_revisit_analytic(n) for n in sorted(stk)]
    v.expect_between("解析模型的低估比例 k", float(np.mean(ks)), 5.5, 7.5)
    v.expect_true("兩個案例的 k 一致(差異 < 15%)",
                  abs(ks[0] - ks[1]) / np.mean(ks) < 0.15,
                  f"k = {ks[0]:.2f} 與 {ks[1]:.2f}")

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


if __name__ == "__main__":
    _rc = main()
    if "ipykernel" not in sys.modules:
        raise SystemExit(_rc)
week16_constellation/week16_walker_coverage_Connect.txt
# ============================================================
# Week 16 — STK SmallSat 整合展示:從單顆 FIREYE 到 Walker 星座
#
# 這是全學期的收束:把前面各週建立的 FIREYE 火山觀測任務,
# 用 STK 的 Coverage / Figure of Merit 量化「一顆不夠,那要幾顆?」
#
# 兩段對照(同一片區域、同一個感測器、同一段分析窗):
#   Part A  單顆 FIREYE            -> 重訪時間 FOM
#   Part B  Walker Delta 3 面 x 4  -> 重訪時間 FOM
# 差異就是星座的價值,也是期末報告「未來趨勢」章節的量化依據。
#
# 前置:報告輸出資料夾 C:\STK_Reports\ 需先手動建立(寫檔失敗為靜默失敗)。
# 需求:STK Enterprise(Coverage 模組)。無介面的 STK Engine 跑不動
#       Graphics/VO 相關指令,請用桌面版。
#
# 驗證過的語法重點(這門課反覆踩過的坑):
#   - Walker 的新語法是 Type/NumPlanes/NumSatsPerPlane/InterPlanePhaseIncrement
#   - Coverage 網格用 Grid AreaOfInterest LatLonRegion <minLat> <minLon> <maxLat> <maxLon>
#   - asset 要指到**感測器**才是 FOV 限制的覆蓋;指到衛星是「有沒有飛過」
#   - Cov ... Asset ... Group 不被接受,要用 Assign
# ============================================================

New / Scenario Week16_Constellation
SetAnalysisTimePeriod * "1 Jan 2024 00:00:00.000" "3 Jan 2024 00:00:00.000"
SetTimePeriod * "1 Jan 2024 00:00:00.000" "3 Jan 2024 00:00:00.000"
SetUnits / km

# ---- 種子衛星:沿用 FIREYE 的 SSO 設定 ----
New / */Satellite FIREYE
OrbitWizard */Satellite/FIREYE SunSynchronous Altitude 600 Color yellow
SetAttitude */Satellite/FIREYE Profile NadirECFVel Offset 0.0

# 成像酬載:40 度半錐角(與 Week 4 的 FullScenario 一致)
New / */Satellite/FIREYE/Sensor Imager
Define */Satellite/FIREYE/Sensor/Imager SimpleCone 40
Graphics */Satellite/FIREYE/Sensor/Imager SetColor red

# ============================================================
# Part A — 單顆衛星的區域覆蓋
# 區域:美西火山帶(北緯 30-55,西經 130-110),涵蓋喀斯開山脈到阿留申
# ============================================================
New / */CoverageDefinition USWest_Cov
Cov */CoverageDefinition/USWest_Cov Grid AreaOfInterest LatLonRegion 30 -130 55 -110
Cov */CoverageDefinition/USWest_Cov Grid PointGranularity LatLon 2
Cov */CoverageDefinition/USWest_Cov Asset */Satellite/FIREYE/Sensor/Imager Assign
Cov */CoverageDefinition/USWest_Cov Access Compute

New / */CoverageDefinition/USWest_Cov/FigureOfMerit Revisit
Cov */CoverageDefinition/USWest_Cov/FigureOfMerit/Revisit FOMDefine Definition RevisitTime Compute Average
ReportCreate */CoverageDefinition/USWest_Cov/FigureOfMerit/Revisit Type Save Style "Value By Grid Point" File "C:\STK_Reports\week16_revisit_1sat.txt"

# ============================================================
# Part B — Walker Delta 星座(3 面 x 4 顆,相位增量 1)
# Walker 會以 FIREYE 為種子複製 12 顆,連 Imager 一起複製,
# 命名為 FIREYE11..FIREYE34(平面編號 + 平面內編號)。
# ============================================================
Walker */Satellite/FIREYE Type Delta NumPlanes 3 NumSatsPerPlane 4 InterPlanePhaseIncrement 1 ColorByPlane Yes

New / */CoverageDefinition Walker_Cov
Cov */CoverageDefinition/Walker_Cov Grid AreaOfInterest LatLonRegion 30 -130 55 -110
Cov */CoverageDefinition/Walker_Cov Grid PointGranularity LatLon 2

# 12 顆全部指派為 asset(指到感測器,不是衛星)
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE11/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE12/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE13/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE14/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE21/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE22/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE23/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE24/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE31/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE32/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE33/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Asset */Satellite/FIREYE34/Sensor/Imager Assign
Cov */CoverageDefinition/Walker_Cov Access Compute

New / */CoverageDefinition/Walker_Cov/FigureOfMerit Revisit
Cov */CoverageDefinition/Walker_Cov/FigureOfMerit/Revisit FOMDefine Definition RevisitTime Compute Average
ReportCreate */CoverageDefinition/Walker_Cov/FigureOfMerit/Revisit Type Save Style "Value By Grid Point" File "C:\STK_Reports\week16_revisit_12sat.txt"

# ============================================================
# 讀數:兩份報告的平均重訪時間差多少?
# 用 week16_constellation_tradeoff.py 讀這兩份檔並與解析模型對照。
# ============================================================