Week 4 — 任務規劃/軌道選擇
week04_orbit/基礎知識導論
Week 4 基礎知識導論 — 軌道力學與軌道選擇
軌道是任務設計裡最早凍結、最難更改的決定。它一旦定了,
覆蓋、功率、熱、通訊、壽命全部跟著定下來。這一週是全課的分水嶺。
1. 這一週在解什麼問題
給定「監測環太平洋火山、GSD 30 m、重訪 < 24 hr」,要回答:
高度多少?傾角多少?升交點時角(LTAN)多少?
方法不是查表,是掃描:讓 STK 在 (a, i) 平面上跑一遍,用 access 統計當目標函數,
看出最佳區域,再理解「為什麼是那裡」。
2. 核心觀念
2.1 六個軌道根數
| 根數 | 符號 | 決定什麼 |
|---|---|---|
| 半長軸 | a | 軌道大小 → 週期、高度、速度 |
| 離心率 | e | 形狀(0 = 圓) |
| 傾角 | i | 軌道面對赤道的傾斜 → 能飛到多高緯度 |
| 升交點赤經 | RAAN (Ω) | 軌道面在慣性空間的方位 → 決定地方時 |
| 近地點幅角 | ω | 近地點在軌道面內的位置 |
| 真近點角 | ν | 衛星此刻在軌道上的位置 |
週期只由 a 決定:
T = 2π * sqrt(a³ / μ) μ_地球 = 398600.4418 km³/s²
本課 FIREYE 選定的任務軌道:a = 6928 km(高度約 550 km)、i = 56°。
560 km 圓軌道的週期約 95.6 分鐘,一天約 15 圈。
2.2 傾角決定緯度覆蓋 —— 沒有例外
一顆傾角 i 的衛星,星下點緯度永遠不會超過 ±i。所以:
- 想看赤道帶火山 → 低傾角就夠,而且過境更頻繁。
- 想看高緯度 → 必須加大傾角,代價是同一目標的過境變稀疏(在高緯度反而更密)。
- 想全球覆蓋 → 近極軌 (i ≈ 97–98°)。
FIREYE 選 56° 是一個折衷:涵蓋環太平洋主要火山帶,又不必付極軌的發射代價。
2.3 J2 攝動:地球不是球
地球的赤道隆起(J2 = 1.0826e-3)造成兩個一階效應:
昇交點漂移 dΩ/dt = -(3/2) * n * J2 * (Re/p)² * cos(i)
近地點漂移 dω/dt = (3/4) * n * J2 * (Re/p)² * (5cos²(i) - 1)
其中 n 是平均角速度、p = a(1-e²)、Re = 6378.137 km。
兩個重要推論:
- 太陽同步軌道 (SSO):當
dΩ/dt = +0.9856 °/day(地球繞日的速率)時,
軌道面相對太陽的方位固定 → 每次過同一緯度都是同一個地方時。
因為cos(i)必須為負,SSO 一定是逆行軌道(i > 90°,約 97–98°)。
對地球觀測任務,這代表光照條件一致、太陽能板受照穩定。 5cos²(i) = 1時dω/dt = 0→i = 63.4°(臨界傾角),
近地點不漂移。Molniya 軌道就是靠這個。
2.4 地面軌跡與重複軌道
一圈之內地球轉了 T x 15.04 °/hr,所以下一圈的地面軌跡會西移。
若 軌道圈數 / 天數 是有理數,地面軌跡會重複 —— 這叫回歸軌道 (repeat ground track)。
回歸軌道的意義:過境時間可預測、影像可比對(同一視角)。
代價是可選的高度變成離散的幾個值,不再連續可調。
2.5 Access 的幾何
地面站或目標能看到衛星,需要仰角 el 大於遮罩角(本課設 5°)。
圓軌道下,最大地心半角:
sin(ρ) = Re / (Re + h) 地球視半角
cos(λ_max) = (Re / (Re+h)) * cos(el_min) ... 幾何解出地心角 λ_max
實務上直接讓 STK 算。要記住的是趨勢:
- 高度↑ → 可見圈子變大 → 單次過境變長、access 變多 → 但 GSD 變差、鏈路損耗變大。
- 仰角遮罩↑ → access 大幅減少(5° 改 10° 常常砍掉三成以上)。
這就是 Week 2 那個「coverage 換 volume」的量化版本。
2.6 傳播器的精度階梯
| 傳播器 | 忽略了什麼 | 7 天後的量級誤差 |
|---|---|---|
| TwoBody | J2、阻力、三體、SRP | 數百 km |
| J2Perturbation | 阻力、三體、SRP | 數十 km |
| HPOP | 依設定,可含 21x21 重力場 + 大氣 + 三體 + SRP | km 以下 |
本教材實測:同一組初始條件,HPOP 的設定若沒有真正生效,7 天後位置差約 107 km。
HPOP 的一個致命陷阱(本課實測):HPOP ... Force / Drag / Integrator 這類指令是
延後提交的 —— 要等下一次傳播才寫進去。STK 12 的 ExportDataFile 不會觸發傳播,
所以必須自己補一行 Propagate;STK 13 會自動觸發。忘了補,你拿到的是舊力學模型的軌道,
而且沒有任何錯誤訊息。
2.7 掃描 (parameter sweep) 的方法論
- 先固定其他變數,一次只掃 1–2 個維度。
- 掃描範圍要含已知不合理的區域,才看得出趨勢與邊界。
- 記錄的不只是最佳值,還有平原有多平 —— 平原愈平,設計愈耐受製造誤差。
- 掃描起點與任務軌道是兩回事。本教材的 Week 4 起點是 250 km / i = 0,
那是掃描的一端,不是選定的軌道。曾經有一週(Week 11)誤用了這個起點,
結果鏈路完全沒有 access。
3. 名詞與符號
| 符號 | 意義 | 值 / 單位 |
|---|---|---|
μ |
地球重力參數 | 398600.4418 km³/s² |
Re |
地球赤道半徑 | 6378.137 km |
J2 |
地球扁率二階項 | 1.0826e-3 |
a, e, i, Ω, ω, ν |
六個經典根數 | — |
n |
平均角速度 | rad/s |
h |
軌道高度 | km |
el_min |
仰角遮罩 | 本課 5° |
| SSO | 太陽同步軌道 | i ≈ 97–98° |
| LTAN | 昇交點地方時 | hh:mm |
| GSD | 地面解析度 | m |
4. 觀念 → 本週實驗對照
| 觀念 | 出現在本週的哪裡 |
|---|---|
| (a, i) 掃描 | Lab1OrbitDesign_Connect.m、OrbitDesignLab_Connect.txt |
| 完整任務場景 | FIREYE_Volcano_FullScenario_Connect.txt |
| HPOP 與延後提交 | STKSat_HPOP_Connect.txt,結尾必須有 Propagate |
| 物件模型自動化 | fireye_volcano_python.py(Week 13 會再深入) |
| ACK ≠ 生效 | week04_verify.py,回讀傳播器型別與力學模型設定 |
5. 常見誤解
- 「高度愈高覆蓋愈好,所以選高一點。」 — 覆蓋變好,但 GSD 變差、
鏈路損耗增加、輻射劑量上升、去軌時間變長(可能違反去軌規範)。 - 「i = 56° 就是只能看到 56° 以下的目標。」 — 正確說法是星下點不超過 ±56°,
但斜視(感測器有錐角)可以看到更高緯度,只是視角變差。 - 「SSO 只是為了光照。」 — SSO 同時穩定了熱環境與發電幾何,對 CubeSat 尤其重要。
- 「TLE 可以當初始根數。」 — TLE 是 SGP4 專用的平均根數,直接餵給 HPOP 會有系統性偏差。
- 「指令下了 HPOP 就是用 HPOP。」 — 見 2.6。這是本教材抓到的真實 bug。
6. 延伸閱讀
軌道力學
- Braeunig, Rocket & Space Technology — 免費、公式齊全的軌道力學入門。
- Wikipedia: Sun-synchronous orbit
— SSO 條件的推導與傾角/高度對照表。 - Wikipedia: Hohmann transfer orbit
— Week 10 會用到,先看無妨。 - 教科書:Curtis, Orbital Mechanics for Engineering Students(J2 攝動與 SSO 推導);
Vallado, Fundamentals of Astrodynamics and Applications(本領域最完整的參考書)。
傳播器與精度
- CelesTrak — TLE 資料源與大量技術專欄。
- CelesTrak 技術專欄 — Kelso 的文章,講 TLE 的正確用法。
- Vallado, "Revisiting Spacetrack Report #3" (AIAA 2006-6753)
— SGP4 的權威說明,包含它不能做什麼。 - Ansys STK 線上說明 — HPOP 力學模型設定。
本機:C:\Program Files\AGI\STK 12\Help\Content\hpop\hpop.htm。
其他工具(交叉驗證用)
- NASA GMAT — 免費的任務分析軟體,可與 STK 對答案。
- Orekit — 開源高精度軌道力學函式庫。
- Skyfield、sgp4 (PyPI)
— Week 14 會用到的 Python 軌道工具。
7. 課前自我檢核
- 週期 95 分鐘的圓軌道,高度大約多少?自己用
T = 2π√(a³/μ)算一次。 - 為什麼太陽同步軌道一定是逆行軌道?
- 把仰角遮罩從 5° 改成 10°,你預期 access 總時長變化多少?先猜,再用 STK 驗證。
- 掃描結果的最佳點若落在掃描範圍邊界上,代表什麼?該怎麼辦?
- 你怎麼證明HPOP 的力學模型設定真的生效了,而不只是收到 ACK?
Week4_7_11_12_STK_Lab_Handouts.md
STK 主場週次實驗單:Week 4 / 7 / 11 / 12
四週共用同一條 FIREYE 火山觀測任務主線;每週往同一場景疊一層。
所有 Connect 腳本先確認 STK Connect Server 已啟用(Edit→Preferences→Connect,勾 Allow Connect,port 5001),
報告輸出資料夾要先存在(路徑見 tools/stk_paths.py,預設 C:\STK_Reports)。
建起來:python -c "import sys;sys.path.insert(0,'tools');import stk_paths;print(stk_paths.ensure_reports_dir())"
Week 4 — 任務規劃與軌道選擇 ◎ STK 核心週
目標:為火山觀測任務選出使觀測 access 最大化的軌道。
現成檔案:Lab1OrbitDesign_Connect.m(MATLAB 掃描)、FIREYE_Volcano_FullScenario_Connect.txt(完整場景 + 軌道選項)、fireye_volcano_python.py(Python 物件模型版)。
步驟:
1. 載入 FIREYE_Volcano_FullScenario_Connect.txt,建好衛星 + 20 火山 + 成像 chain。
2. 用 Lab1OrbitDesign_Connect.m 掃描半長軸 a(6600–6928 km)與傾角 i(0–90°),記錄每組的總 access 時數。
3. 找出最大 access 的 (a, i);對照 Lab1 解(圓軌道 a=6928, i≈56;橢圓 a=6778, e=0.0221)。
4. 切換到三種發射載具軌道(腳本內註解),比較各自 access。
交付:access vs (a, i) 的掃描表/曲面圖 + 選定軌道的理由。
評分:掃描完整度 30% / 最佳解正確 30% / 發射載具比較 20% / CONOPS 討論 20%。
Week 7 — 功率系統 ◎
目標:用 STK 原生 Solar Panel Tool 算發電,建立功率收支。
現成檔案:FIREYE_SolarPanel_Connect.txt(發電)、storage_vs_time.py(儲存/電量積分)。
前提:FIREYE 需用「已註冊太陽能板的 3D 模型」(模型檔含 SolarPanelGroup;見腳本檔頭)。
步驟:
1. 執行 VO ... SolarPanel Compute ... Power,得軌道發電曲線。
2. 比較三種太陽能板方案(SmallSat Part 2:效率 0.28/0.21/0.168)的軌道平均發電。
3. 對照負載預算(待機 0.75 W / 成像 4 W / 下傳 6.75 W),算發電 − 負載收支。
4. 重現 FIREYE vs FIREYEPosX 構型比較(+Z 是否裝板)。
交付:發電曲線 + 功率收支表 + 板/電池選型建議。
評分:發電計算 30% / 收支分析 30% / 選型權衡 25% / 構型比較 15%。
Week 11 — 通訊系統 ◎
目標:建上/下行 RF 鏈路,驗證鏈路餘裕 ≥ 3 dB。
現成檔案:STKSat_Comm_Connect.txt(UHF 435 下行 / VHF 145 上行雙向鏈路)。
步驟:
1. 載入腳本,建地面站 + 衛星收發機 + 天線。
2. 出 ReportCreate ... "Link Budget - Detailed",讀 Eb/No、C/N、鏈路餘裕。
3. 檢查兩條鏈路餘裕是否 ≥ 3 dB;不足則調發射功率/天線增益。
4. 開 RF 環境(雨衰 ITU-R P618、大氣吸收 P676),看損耗變化。
5. 觀察 UHF vs VHF 都卜勒頻移差異(對照 SmallSat Part 3 結論)。
交付:上/下行鏈路預算表 + 餘裕 pass/fail + 環境損耗分析。
評分:鏈路預算 30% / 餘裕驗證 25% / 環境模型 25% / 都卜勒討論 20%。
Week 12 — 酬載與資料處理 ◎
目標:建立資料預算——成像產生 vs 下傳能力,判斷儲存是否會滿。
現成檔案:FIREYE_Volcano_FullScenario_Connect.txt(成像/下傳 chain)、storage_vs_time.py(儲存填充曲線)。
步驟:
1. 出成像 access(Imager→火山)與下傳 access(SBand→地面站)報告。
2. 用 storage_vs_time.py 讀這兩份報告,畫「儲存填充 vs 時間」。
3. 調感測器解析度/資料率,看資料產生量變化;調下傳窗,看能否清空。
4. 找出「儲存不溢出」的成像速率上限(對照 eSatellite 教學的錄影機填滿問題)。
交付:資料預算 + 儲存填充曲線 + 成像速率上限建議。
評分:access 計算 25% / 資料預算 30% / 儲存曲線 25% / 速率優化 20%。
Week 11 附錄 — 驗證段(ACK 不等於生效)
檔案:week11_comm/week11_verify.py(共用工具在 tools/stk_verify.py)
為什麼通訊週要有這一段
鏈路預算是一串乘法。任何一個參數靜默沒生效——功率、頻率、天線設計頻率、G/T——
Link Budget 報告照樣會印出一個「看起來很合理」的餘裕值,而且是錯的。
Connect 的 ACK 完全不會告訴你這件事。
本週腳本藏著三個這種問題,三個都只有回讀看得出來:
| 問題 | 症狀 | 修法 |
|---|---|---|
Model.DesignFrequency 屬性不存在 |
ACK,值沒寫進去 | 改寫到 Model.AntennaControl.Antenna.DesignFrequency |
| 設定天線型別會換掉天線物件 | ACK,先前寫的 DesignFrequency 被沖回預設 14.5 GHz | 先設 Antenna Isotropic,再設它的 DesignFrequency |
| STKSat 沿用 Lab1 的掃描起點軌道(250 km, i=0) | 鏈路報告印 "Data Unavailable",全部指令仍 ACK | 本週開頭改用選定的任務軌道(550 km, i=56) |
第三項最值得注意:赤道 250 km 軌道對北緯 40 度的測站完全沒有 access,
所以「餘裕 ≥ 3 dB」這個要求在修正前根本無從驗證,而腳本從頭到尾 0 個 NACK。
執行
python week11_comm/week11_verify.py # 離開碼 0 = 全部通過
會做三類檢查,共 37 項:
- 自動回讀:腳本設過的每一個
SetValue,用Transmitter_RM / Receiver_RM / RFEnvironment_RM ... GetValue讀回來比對(容許單位換算,如 20.6 degC vs 293.75 K)。 - 結構檢查:測站位置、仰角約束、感測器與收發機物件是否存在。
- 鏈路判準:產生 Link Budget 報告,取最差 Eb/No 減去 BPSK@BER 1e-5 的
門檻 9.6 dB,確認餘裕 ≥ 3 dB。這是本週真正的 pass/fail,原本從未被檢查。
基準結果:37 項全 PASS,最差 Eb/No 47.12 dB → 餘裕 37.52 dB。
作業
- 跑一次驗證段,貼上 37 項的輸出。
- 故意弄壞它:把
Model.Power改成 0、或把Model.GOverT改回-10、
或把本週開頭的SetState那行註解掉。各跑一次,回答:
- 腳本回報幾個 NACK?驗證段抓到幾項 FAIL?
- 哪一種錯誤是「ACK 但無效」,哪一種會被 Connect 直接拒絕?
- 如果沒有驗證段,你要多久才會發現? - 為
Model.Modulator與Model.DataRate各加一項自訂檢查,
說明為什麼這兩個值錯了會讓餘裕算出來偏高。
程式碼
# ================================================================
# FIREYE Volcano-Observation - COMPLETE SCENARIO (STK Connect)
# One batch build: satellite + 20 volcano points + access +
# solar-panel power report + storage-fill inputs. No SOLIS needed.
#
# RUN (batch):
# Preferably create the report folder first: C:\STK_Reports\
# Then in STK: # ConFile / "C:\stk\FIREYE_Volcano_FullScenario_Connect.txt"
# Or push the file with AgIPCExp -f / -t over the socket.
#
# PREREQUISITE for the solar-panel section ONLY:
# FIREYE must use a 3D model registered for solar-panel analysis
# (model file contains SolarPanelGroup lines, e.g.
# "SolarPanelGroup PosX 30.0"). Connect cannot create panel
# faces - see FIREYE_SolarPanel_Connect.txt header. The rest of
# this scenario runs without any model prep.
#
# OUTPUT FILES (written to C:\STK_Reports\):
# FIREYE_power.txt solar power generated vs time (native)
# imaging_access.txt Imager -> Volcanoes access (feeds storage)
# downlink_access.txt SBand -> GroundStations access (feeds storage)
# -> run storage_vs_time.py on the two access files for the
# "storage fill vs time" curve (SOLIS recorder replacement).
# ================================================================
# ---------------- 場景 / Scenario + time + units ----------------
New / Scenario FIREYE_Volcano
SetAnalysisTimePeriod * "1 Jan 2024 00:00:00.000" "8 Jan 2024 00:00:00.000"
SetTimePeriod * "1 Jan 2024 00:00:00.000" "8 Jan 2024 00:00:00.000"
SetEpoch * "1 Jan 2024 00:00:00.000"
Animate * Reset
SetUnits / km
# scenario power unit -> W (for the SolarPanel report)
Units_Set * Report Power W
# ================================================================
# 衛星軌道選擇 / Satellite orbit selection
# SELECTED: Lab1 optimum circular orbit for max volcano access
# a = 6928 km (550 km alt), e = 0, i = 56 deg (Lab1 Deliverable 2)
# Order in SetState Classical: SMA Ecc Inc AoP RAAN TA
# ================================================================
New / */Satellite FIREYE
SetState */Satellite/FIREYE Classical J2Perturbation "UseAnalysisStartTime" "UseAnalysisStopTime" 60 J2000 "UseAnalysisStartTime" 6928.0 0.0 56.0 0.0 0.0 0.0
Graphics */Satellite/FIREYE SetColor yellow
# ---- Alternative orbits (uncomment ONE to switch selection) ----
# Elliptical optimum (Lab1 Deliverable 3): a=6778 e=0.0221 i=56 AoP=249
# SetState */Satellite/FIREYE Classical J2Perturbation "UseAnalysisStartTime" "UseAnalysisStopTime" 60 J2000 "UseAnalysisStartTime" 6778.0 0.0221 56.0 249.0 0.0 0.0
# Launch provider - ULA polar: OrbitWizard */Satellite/FIREYE Circular Inclination 90 Altitude 422
# Launch provider - ATK to ISS: OrbitWizard */Satellite/FIREYE Circular Inclination 51.6 Altitude 402
# Launch provider - SpaceX CCAFS:OrbitWizard */Satellite/FIREYE Circular Inclination 28.5 Altitude 550
# Sun-synchronous 700 km: OrbitWizard */Satellite/FIREYE SunSynchronous Altitude 700
# ---- Nadir attitude: +Z faces nadir (imager & SBand boresight [0 0 1]) ----
# If the token is rejected in your version, set in GUI:
# FIREYE > Basic > Attitude > "Nadir alignment with ECF velocity constraint"
SetAttitude */Satellite/FIREYE Profile NadirECFVel Offset 0.0
# ---- Imager payload (nadir cone, boresight = body +Z) ----
New / */Satellite/FIREYE/Sensor Imager
# 40 deg half-angle (Lab1)
Define */Satellite/FIREYE/Sensor/Imager SimpleCone 40
Graphics */Satellite/FIREYE/Sensor/Imager SetColor red
# ---- SBand downlink (FOV sensor + transmitter, 10 Mbit/s) ----
New / */Satellite/FIREYE/Sensor SBandFOV
# 120 deg full -> 60 half-angle
Define */Satellite/FIREYE/Sensor/SBandFOV SimpleCone 60
New / */Satellite/FIREYE/Sensor/SBandFOV/Transmitter SBand
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model Complex_Transmitter_Model
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model.Frequency 2250000000.0
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model.AntennaControl.Antenna.DesignFrequency 2250000000.0
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model.DataRate 10000000.0
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model.AntennaControl.Antenna Isotropic
Transmitter */Satellite/FIREYE/Sensor/SBandFOV/Transmitter/SBand SetValue Model.Modulator BPSK
# ================================================================
# 20 個火山觀測點 / 20 US volcano observation Places (real coords)
# lat lon alt(km)
# ================================================================
New / */Place Volcano_Mt_St_Helens
SetPosition */Place/Volcano_Mt_St_Helens Geodetic 46.20 -122.20 2.55
New / */Place Volcano_Shishaldin
SetPosition */Place/Volcano_Shishaldin Geodetic 54.76 -163.97 2.86
New / */Place Volcano_Mt_Cleveland
SetPosition */Place/Volcano_Mt_Cleveland Geodetic 52.82 -169.94 1.73
New / */Place Volcano_Fourpeaked
SetPosition */Place/Volcano_Fourpeaked Geodetic 58.77 -153.67 2.10
New / */Place Volcano_Kasatochi_Island
SetPosition */Place/Volcano_Kasatochi_Island Geodetic 52.18 -175.50 0.31
New / */Place Volcano_South_Sister
SetPosition */Place/Volcano_South_Sister Geodetic 44.10 -121.77 3.16
New / */Place Volcano_Lassen
SetPosition */Place/Volcano_Lassen Geodetic 40.49 -121.51 3.19
New / */Place Volcano_Mt_Hood
SetPosition */Place/Volcano_Mt_Hood Geodetic 45.37 -121.69 3.43
New / */Place Volcano_Mt_Baker
SetPosition */Place/Volcano_Mt_Baker Geodetic 48.78 -121.81 3.29
New / */Place Volcano_Mt_Rainier
SetPosition */Place/Volcano_Mt_Rainier Geodetic 46.85 -121.76 4.39
New / */Place Volcano_Mt_Adams
SetPosition */Place/Volcano_Mt_Adams Geodetic 46.21 -121.49 3.74
New / */Place Volcano_Glacier_Peak
SetPosition */Place/Volcano_Glacier_Peak Geodetic 48.11 -121.11 3.21
New / */Place Volcano_Mt_Shasta
SetPosition */Place/Volcano_Mt_Shasta Geodetic 41.41 -122.19 4.32
New / */Place Volcano_Newberry
SetPosition */Place/Volcano_Newberry Geodetic 43.72 -121.23 2.43
New / */Place Volcano_Crater_Lake
SetPosition */Place/Volcano_Crater_Lake Geodetic 42.93 -122.12 2.49
New / */Place Volcano_Redoubt
SetPosition */Place/Volcano_Redoubt Geodetic 60.49 -152.74 3.11
New / */Place Volcano_Augustine
SetPosition */Place/Volcano_Augustine Geodetic 59.36 -153.43 1.25
New / */Place Volcano_Katmai
SetPosition */Place/Volcano_Katmai Geodetic 58.28 -154.96 2.05
New / */Place Volcano_Okmok
SetPosition */Place/Volcano_Okmok Geodetic 53.43 -168.13 1.07
New / */Place Volcano_Mauna_Loa
SetPosition */Place/Volcano_Mauna_Loa Geodetic 19.47 -155.60 4.17
# Group the 20 volcanoes
New / */Constellation Volcanoes
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_St_Helens
Chains */Constellation/Volcanoes Add */Place/Volcano_Shishaldin
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Cleveland
Chains */Constellation/Volcanoes Add */Place/Volcano_Fourpeaked
Chains */Constellation/Volcanoes Add */Place/Volcano_Kasatochi_Island
Chains */Constellation/Volcanoes Add */Place/Volcano_South_Sister
Chains */Constellation/Volcanoes Add */Place/Volcano_Lassen
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Hood
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Baker
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Rainier
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Adams
Chains */Constellation/Volcanoes Add */Place/Volcano_Glacier_Peak
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Shasta
Chains */Constellation/Volcanoes Add */Place/Volcano_Newberry
Chains */Constellation/Volcanoes Add */Place/Volcano_Crater_Lake
Chains */Constellation/Volcanoes Add */Place/Volcano_Redoubt
Chains */Constellation/Volcanoes Add */Place/Volcano_Augustine
Chains */Constellation/Volcanoes Add */Place/Volcano_Katmai
Chains */Constellation/Volcanoes Add */Place/Volcano_Okmok
Chains */Constellation/Volcanoes Add */Place/Volcano_Mauna_Loa
# ================================================================
# 地面站 / Downlink ground stations (AGI HQ + AK for polar passes)
# ================================================================
New / */Facility AGI_HQ
SetPosition */Facility/AGI_HQ Geodetic 40.0416 -75.5983 0.10
SetConstraint */Facility/AGI_HQ ElevationAngle Min 5.0
New / */Facility Fairbanks_ASF
SetPosition */Facility/Fairbanks_ASF Geodetic 64.8600 -147.8500 0.30
SetConstraint */Facility/Fairbanks_ASF ElevationAngle Min 5.0
New / */Constellation GroundStations
Chains */Constellation/GroundStations Add */Facility/AGI_HQ
Chains */Constellation/GroundStations Add */Facility/Fairbanks_ASF
# ================================================================
# Access + 報告 / Access & report generation
# ================================================================
# 成像: Imager -> 20 volcanoes (feeds storage fill; total access = mission value)
New / */Chain Imaging
Chains */Chain/Imaging Add */Satellite/FIREYE/Sensor/Imager
Chains */Chain/Imaging Add */Constellation/Volcanoes
Chains */Chain/Imaging Compute
ReportCreate */Chain/Imaging Type Save Style "Complete Chain Access" File "C:\STK_Reports\imaging_access.txt"
# 下傳: SBand -> ground stations (feeds storage drain)
New / */Chain Downlink
Chains */Chain/Downlink Add */Satellite/FIREYE/Sensor/SBandFOV
Chains */Chain/Downlink Add */Constellation/GroundStations
Chains */Chain/Downlink Compute
ReportCreate */Chain/Downlink Type Save Style "Complete Chain Access" File "C:\STK_Reports\downlink_access.txt"
# ================================================================
# 太陽能發電報告 / FIREYE solar-panel power report (native, no SOLIS)
# Requires the solar-panel-registered model (see PREREQUISITE above).
# ================================================================
VO */Satellite/FIREYE SolarPanel Object On CentralBody/Earth
VO */Satellite/FIREYE SolarPanel Compute "UseAnalysisStartTime" "UseAnalysisStopTime" 60 Power "C:\STK_Reports\FIREYE_power.txt"
# DeleteData 會清掉太陽能板的計算狀態,之後即使重新 Object On,
# 下一次 Compute 也只會產出空報告(而且照樣花兩分鐘、照樣 ACK)。
# Week 7 接在本檔之後,正是被這一行害的 —— 報告已經寫出來了,
# 這行沒有實際用途,註解掉。
#VO */Satellite/FIREYE SolarPanel DeleteData
# ================================================================
# NEXT (outside Connect): storage-fill curve
# python storage_vs_time.py # reads imaging_access.txt + downlink_access.txt
# -> storage_vs_time.png (SOLIS data-recorder replacement)
# Optional battery SOC: feed FIREYE_power.txt into the same script.
# ================================================================
%% OrbitDesignLab - STK Connect Command Version
% Converted from Lab1OrbitDesign.m: all STK object-model calls replaced
% with Connect commands sent through root.ExecuteCommand().
% The same Connect strings work over the raw TCP socket (port 5001),
% so this logic ports directly to Python/C#/shell clients.
%
% NOTES vs. the original COM version:
% - "SetUnits / km" sets the Connect session distance unit to km,
% so SMA is passed in km exactly as in the original script.
% - The satellite state is re-issued each iteration with SetState
% (Connect has no persistent "keplerian" handle to mutate).
% - Access durations are parsed from the chain "Complete Access" report
% returned by Report_RM.
clc
clear
%% Launch STK and attach Connect root
app = actxserver('STK11.application'); % use 'STK12.application' for STK 12
root = app.Personality2;
root.ExecuteCommand('SetUnits / km'); % Connect session distance unit: km
%% Initialize Scenario
Start = '1 Jan 2024 00:00:00.000';
Stop = '15 Jan 2024 00:00:00.000';
root.ExecuteCommand('New / Scenario OrbitDesignLab');
root.ExecuteCommand(['SetAnalysisTimePeriod * "' Start '" "' Stop '"']);
root.ExecuteCommand('Animate * Reset');
%% Create Volcanoes (Place objects)
volc = { ...
'Volcano_Mt_St_Helens' 46.20 -122.20; ...
'Volcano_Shishaldin' 54.76 -163.97; ...
'Volcano_Mt_Cleveland' 52.82 -169.94; ...
'Volcano_Fourpeaked' 58.77 -153.67; ...
'Volcano_Kasatochi_Island' 52.18 -175.50; ...
'Volcano_South_Sister' 44.10 -121.77; ...
'Volcano_Lassen' 40.49 -121.51; ...
'Volcano_Mt_Hood' 45.37 -121.69; ...
'Volcano_Mt_Baker' 48.78 -121.81; ...
'Volcano_Mt_Rainer' 46.85 -121.76};
for k = 1:size(volc,1)
root.ExecuteCommand(['New / */Place ' volc{k,1}]);
root.ExecuteCommand(sprintf( ...
'SetPosition */Place/%s Geodetic %.2f %.2f 0.0', ...
volc{k,1}, volc{k,2}, volc{k,3}));
end
%% Group Volcanoes into a Constellation
root.ExecuteCommand('New / */Constellation Volcanoes');
for k = 1:size(volc,1)
root.ExecuteCommand(['Chains */Constellation/Volcanoes Add */Place/' volc{k,1}]);
end
%% Create STKSat (state assigned later via SetState / J2Perturbation)
root.ExecuteCommand('New / */Satellite STKSat');
%% Create Sensor (Imager): 40 deg simple cone
root.ExecuteCommand('New / */Satellite/STKSat/Sensor Imager');
root.ExecuteCommand('Define */Satellite/STKSat/Sensor/Imager SimpleCone 40');
%% Chain: Imager -> Volcanoes
root.ExecuteCommand('New / */Chain Observe');
root.ExecuteCommand('Chains */Chain/Observe Add */Satellite/STKSat/Sensor/Imager');
root.ExecuteCommand('Chains */Chain/Observe Add */Constellation/Volcanoes');
%% INPUT SECTION: Vary 1 or 2 Orbital Elements at Once to Find Max Access
%**************************************************************************
%**************************************************************************
% INPUT Orbital Elements (baseline; varied elements override these)
SMA = 6628; % SemiMajorAxis (km): >6378.14
Ecc = 0; % Eccentricity: 0 to 0.99999
Inc = 0; % Inclination (deg): 0 to 180
AoP = 0; % Argument of Perigee (deg): 0 to 360
RAAN = 0; % RAAN (deg): 0 to 360
TA = 0; % True Anomaly (deg): 0 to 360
% INPUT Number of Orbital Elements to Vary
NumberToVary = 0; % 0, 1, or 2
% INPUT Which Orbital Element(s) to Vary and the Range(s)
Element1 = 'a'; % 'a','e','i','AoP','RAAN', or 'TA'
Element1Min = 0;
Element1Max = 0;
StepSize1 = 100;
% ONLY USED if varying 2 elements
Element2 = 'i';
Element2Min = 0;
Element2Max = 0;
StepSize2 = 10;
%**************************************************************************
%**************************************************************************
elems = struct('a',SMA,'e',Ecc,'i',Inc,'AoP',AoP,'RAAN',RAAN,'TA',TA);
Element1Range = Element1Min:StepSize1:Element1Max;
NumberOfIterations1 = length(Element1Range);
if NumberToVary == 0
setKeplerian(root, Start, elems);
TotalDuration = getTotalAccessHours(root);
disp(['Max Total Access: ' num2str(TotalDuration) ' hr'])
elseif NumberToVary == 1
TotalNumberOfIterations = NumberOfIterations1;
DurationList = zeros(NumberOfIterations1,1);
for i1 = 1:NumberOfIterations1
elems.(Element1) = Element1Range(i1);
setKeplerian(root, Start, elems);
DurationList(i1) = getTotalAccessHours(root);
disp(['Iterations: ' num2str(TotalNumberOfIterations)]);
disp(['Complete: ' num2str(round(i1*100/TotalNumberOfIterations)) '%']);
end
[MaxDuration, IndexOfMax] = max(DurationList);
disp(['Max Orbital Element 1: ' num2str(Element1Range(IndexOfMax))]);
disp(['Max Total Access: ' num2str(MaxDuration) ' hr'])
plot(Element1Range, DurationList, '--*')
ylabel('Total Access (hr)'); grid on
xlabel('Orbital Element 1')
elseif NumberToVary == 2
Element2Range = Element2Min:StepSize2:Element2Max;
NumberOfIterations2 = length(Element2Range);
TotalNumberOfIterations = NumberOfIterations1*NumberOfIterations2;
DurationList = zeros(NumberOfIterations1, NumberOfIterations2);
for i1 = 1:NumberOfIterations1
elems.(Element1) = Element1Range(i1);
for i2 = 1:NumberOfIterations2
elems.(Element2) = Element2Range(i2);
setKeplerian(root, Start, elems);
DurationList(i1,i2) = getTotalAccessHours(root);
done = (i1-1)*NumberOfIterations2 + i2;
disp(['Iterations: ' num2str(TotalNumberOfIterations)]);
disp(['Complete: ' num2str(round(done*100/TotalNumberOfIterations)) '%']);
end
end
MaxOfRows = max(DurationList,[],2);
[~, IndexOfMaxRow] = max(MaxOfRows);
[MaxDuration, IndexOfMaxCol] = max(DurationList(IndexOfMaxRow,:));
disp(['Max Orbital Element 1: ' num2str(Element1Range(IndexOfMaxRow))]);
disp(['Max Orbital Element 2: ' num2str(Element2Range(IndexOfMaxCol))]);
disp(['Max Total Access: ' num2str(MaxDuration) ' hr']);
X = repmat(Element1Range, NumberOfIterations2, 1).';
Y = repmat(Element2Range.', 1, NumberOfIterations1).';
surf(X, Y, DurationList); colorbar
xlabel('Orbital Element 1'); ylabel('Orbital Element 2')
zlabel('Access Time (hr)')
end
%% ------------------------- Local Functions ------------------------------
function setKeplerian(root, epoch, e)
% Push a full Keplerian state via Connect SetState (J2Perturbation).
% Session units set to km via "SetUnits / km" -> SMA passed in km directly.
% Trailing "TrueAnomaly" keyword tells STK the anomaly value is TA.
cmd = sprintf(['SetState */Satellite/STKSat Classical J2Perturbation ' ...
'UseScenarioInterval 60 ICRF "%s" %.6f %.6f %.6f %.6f %.6f %.6f TrueAnomaly'], ...
epoch, e.a, e.e, e.i, e.AoP, e.RAAN, e.TA);
root.ExecuteCommand(cmd);
end
function hrs = getTotalAccessHours(root)
% Recompute the chain, pull "Complete Access" via Report_RM, and sum the
% Duration (sec) column. Returns 0 when no access exists (mirrors the
% try/catch in the original COM script).
hrs = 0;
try
root.ExecuteCommand('ChainAccess */Chain/Observe Compute');
result = root.ExecuteCommand( ...
'Report_RM */Chain/Observe Style "Complete Access"');
totalSec = 0;
for n = 1:result.Count
line = char(result.Item(n));
% Data rows begin with the access number; the duration (sec)
% is the trailing numeric token on the row.
if ~isempty(regexp(line, '^\s*\d+\s', 'once'))
tok = regexp(line, '([\d\.]+)\s*$', 'tokens', 'once');
if ~isempty(tok)
totalSec = totalSec + str2double(tok{1});
end
end
end
hrs = totalSec/3600;
catch
hrs = 0; % no access found for this element combination
end
end
# ============================================================
# OrbitDesignLab - STK Connect Command Script (rev.2)
# Converted from Lab1OrbitDesign.m; style aligned with the
# provided connect_command_sample.txt conventions:
# - "#" comment lines (ConFile-safe)
# - SetUnits / km declared up front
# - SetEpoch alongside SetAnalysisTimePeriod
# - UseAnalysisStartTime / UseAnalysisStopTime keywords in
# SetState (no hard-coded epoch strings)
# - SetDescription on scenario and key objects
#
# To batch-load inside STK, uncomment:
#Unload / *
#ConFile / "C:\stk\OrbitDesignLab_Connect.txt"
# ============================================================
New / Scenario OrbitDesignLab
SetAnalysisTimePeriod * "1 Jan 2024 00:00:00.000" "15 Jan 2024 00:00:00.000"
SetEpoch * "1 Jan 2024 00:00:00.000"
SetDescription * Short "SmallSat Lab1: STKSat orbit design for max volcano access"
Animate * Reset
SetUnits / km
# ---- Volcano targets (Place objects) ----
New / */Place Volcano_Mt_St_Helens
SetPosition */Place/Volcano_Mt_St_Helens Geodetic 46.20 -122.20 0.0
New / */Place Volcano_Shishaldin
SetPosition */Place/Volcano_Shishaldin Geodetic 54.76 -163.97 0.0
New / */Place Volcano_Mt_Cleveland
SetPosition */Place/Volcano_Mt_Cleveland Geodetic 52.82 -169.94 0.0
New / */Place Volcano_Fourpeaked
SetPosition */Place/Volcano_Fourpeaked Geodetic 58.77 -153.67 0.0
New / */Place Volcano_Kasatochi_Island
SetPosition */Place/Volcano_Kasatochi_Island Geodetic 52.18 -175.50 0.0
New / */Place Volcano_South_Sister
SetPosition */Place/Volcano_South_Sister Geodetic 44.10 -121.77 0.0
New / */Place Volcano_Lassen
SetPosition */Place/Volcano_Lassen Geodetic 40.49 -121.51 0.0
New / */Place Volcano_Mt_Hood
SetPosition */Place/Volcano_Mt_Hood Geodetic 45.37 -121.69 0.0
New / */Place Volcano_Mt_Baker
SetPosition */Place/Volcano_Mt_Baker Geodetic 48.78 -121.81 0.0
New / */Place Volcano_Mt_Rainer
SetPosition */Place/Volcano_Mt_Rainer Geodetic 46.85 -121.76 0.0
# ---- Group volcanoes into a Constellation ----
New / */Constellation Volcanoes
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_St_Helens
Chains */Constellation/Volcanoes Add */Place/Volcano_Shishaldin
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Cleveland
Chains */Constellation/Volcanoes Add */Place/Volcano_Fourpeaked
Chains */Constellation/Volcanoes Add */Place/Volcano_Kasatochi_Island
Chains */Constellation/Volcanoes Add */Place/Volcano_South_Sister
Chains */Constellation/Volcanoes Add */Place/Volcano_Lassen
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Hood
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Baker
Chains */Constellation/Volcanoes Add */Place/Volcano_Mt_Rainer
# ---- STKSat: J2Perturbation, Keplerian initial state ----
# SetState uses analysis-interval keywords (per sample) so the state
# tracks the scenario time period automatically.
# a=6628 km, e=0, i=0, AoP=0, RAAN=0, TA=0 (baseline from the .m file)
# Trailing "TrueAnomaly" keyword: last value is TA, not Mean Anomaly.
New / */Satellite STKSat
SetState */Satellite/STKSat Classical J2Perturbation "UseAnalysisStartTime" "UseAnalysisStopTime" 60 ICRF "UseAnalysisStartTime" 6628.0 0.0 0.0 0.0 0.0 0.0 TrueAnomaly
SetDescription */Satellite/STKSat Short "1U CubeSat imaging US volcanoes (SmallSat Lab1)"
# ---- Imager sensor: 40 deg simple cone ----
New / */Satellite/STKSat/Sensor Imager
Define */Satellite/STKSat/Sensor/Imager SimpleCone 40
# ---- Chain: Imager -> Volcanoes ----
New / */Chain Observe
Chains */Chain/Observe Add */Satellite/STKSat/Sensor/Imager
Chains */Chain/Observe Add */Constellation/Volcanoes
# ---- Compute chain access (aggregate over all volcanoes) ----
Chains */Chain/Observe Compute
Report_RM */Chain/Observe Style "Complete Chain Access"
# ---- Optional: per-target pairwise access, sample-file style ----
# Uncomment to compute individual sensor-to-volcano accesses:
# Access */Satellite/STKSat/Sensor/Imager */Place/Volcano_Mt_St_Helens TimePeriod UseScenarioInterval
# Access */Satellite/STKSat/Sensor/Imager */Place/Volcano_Mt_Rainer TimePeriod UseScenarioInterval
# ---- Sweep pattern: re-issue SetState, recompute, re-pull report ----
# Example next iteration (a = 6728 km):
# SetState */Satellite/STKSat Classical J2Perturbation "UseAnalysisStartTime" "UseAnalysisStopTime" 60 ICRF "UseAnalysisStartTime" 6728.0 0.0 0.0 0.0 0.0 0.0 TrueAnomaly
# ChainAccess */Chain/Observe Compute
# Report_RM */Chain/Observe Style "Complete Access"
# ============================================================
# STKSat HPOP High-Fidelity Propagation - Connect Script
# Applies the HPOP orbit-prediction reference (SetState Cartesian/
# Classical HPOP, Force / Drag / Integrator, ExportDataFile) to the
# SmallSat STKSat, using force models appropriate for a 250-550 km
# CubeSat.
#
# WHY HPOP HERE:
# Lab1 used J2Perturbation for the design SWEEP (fast, and the
# assignment says to ignore drag/SRP/3rd-body). But a CubeSat at
# 250-550 km is drag-dominated in reality, so the OPERATIONAL
# ephemeris (ground-station scheduling, decay, SSA hand-off) should
# come from an HPOP run WITH drag. This script is that realistic run.
#
# CONNECT CAVEATS (from the reference):
# - Connect returns nothing on success/failure; don't spam commands.
# - Propagator time span MUST match the scenario or the orbit won't
# display -> we use UseAnalysisStartTime / UseAnalysisStopTime.
# ============================================================
# ---- STANDALONE (uncomment to run without Lab1 loaded) ----
#New / Scenario STKSat_HPOP
#SetAnalysisTimePeriod * "1 Jan 2024 00:00:00.000" "8 Jan 2024 00:00:00.000"
#SetTimePeriod * "1 Jan 2024 00:00:00.000" "8 Jan 2024 00:00:00.000"
#Animate * Reset
SetUnits / km
# ============================================================
# Method A - fastest: OrbitWizard seed at the Part 1 optimum,
# then switch the propagator to HPOP via SetState.
# (a = 6928 km, i = 56 deg, circular; from Lab1 Deliverable 2)
# ============================================================
New / */Satellite STKSat_HPOP
OrbitWizard */Satellite/STKSat_HPOP Circular Inclination 56 Altitude 550 RAAN 0 Color Red
# ============================================================
# Method B - explicit HPOP state (Classical), time span tied to
# the scenario so it always renders. This OVERRIDES Method A.
# Anomaly value 0 with trailing fields = TrueAnomaly.
# Order: SMA Ecc Inc AoP RAAN TA
# ============================================================
SetState */Satellite/STKSat_HPOP Classical HPOP "UseAnalysisStartTime" "UseAnalysisStopTime" 60 J2000 "UseAnalysisStartTime" 6928.0 0.0 56.0 0.0 0.0 0.0
# ---- Earth gravity: EGM2008-class field, 70x70 (LEO SSA standard) ----
# Adjust the .grv path to your install; WGS84.grv or JGM3.grv also valid.
HPOP */Satellite/STKSat_HPOP Force Gravity "C:\Program Files\AGI\STK 12\STKData\CentralBodies\Earth\EGM2008.grv" 70 70
# ---- Atmospheric drag: dominant force at this altitude ----
# Cd 2.2 (cannonball default), Area/Mass for a 1U ~1.3 kg CubeSat:
# 0.01 m^2 face / 1.3 kg ~ 0.0077 m^2/kg (use ~0.008).
# NRLMSISE 2000 density model, Manual solar flux (F10.7=150, Ap=3).
HPOP */Satellite/STKSat_HPOP Drag On 2.2 0.008 "NRLMSISE 2000" Manual 150.0 150.0 3.0
# ---- Third-body gravity: Sun + Moon ----
HPOP */Satellite/STKSat_HPOP Force ThirdBodyGravity Sun On
HPOP */Satellite/STKSat_HPOP Force ThirdBodyGravity Moon On
# ---- Solar radiation pressure (small at this altitude, keep for realism) ----
# Spherical model: Cr=1.3, Area/Mass 0.008 m^2/kg
HPOP */Satellite/STKSat_HPOP Force SRP On Model Spherical 1.3 0.008
# ---- Tides ----
HPOP */Satellite/STKSat_HPOP Force SolidTides On
HPOP */Satellite/STKSat_HPOP Force OceanTides Off
# ---- Integrator: RKF7(8), tight relative-error step control ----
HPOP */Satellite/STKSat_HPOP Integrator IntegMethod RKF78 StepControl RelativeError 1.0e-13 MinStepSize 0.1 MaxStepSize 30 InterpMethod LaGrange 7
# ============================================================
# Export the high-fidelity ephemeris (feeds scheduling / SSA / OD)
# ============================================================
# The HPOP Force/Drag/Integrator settings above are QUEUED - they are only
# committed on the next propagation. On STK 12 desktop ExportDataFile does NOT
# trigger one, so without this line the exported ephemeris uses STK's DEFAULT
# force model (EGM2008 21x21, Cd 2.2, A/m 0.02, Jacchia-Roberts) - a ~107 km
# position difference after 7 days. (STK 13 commits on export; STK 12 does not.)
Propagate */Satellite/STKSat_HPOP
ExportDataFile */Satellite/STKSat_HPOP Ephemeris "C:\temp\STKSat_HPOP.e" Type STK CoordSys J2000 CentralBody Earth InterpBoundaries Include
# ---- Optional: read back the configured state to verify ----
# Satellite_RM */Satellite/STKSat_HPOP GetValue
# ============================================================
# Compare against the volcano access (reuse Lab1 chain if present)
# ============================================================
# New / */Satellite/STKSat_HPOP/Sensor Imager
# Define */Satellite/STKSat_HPOP/Sensor/Imager SimpleCone 40
# New / */Chain Observe_HPOP
# Chains */Chain/Observe_HPOP Add */Satellite/STKSat_HPOP/Sensor/Imager
# Chains */Chain/Observe_HPOP Add */Constellation/Volcanoes
# ChainAccess */Chain/Observe_HPOP Compute
# Report_RM */Chain/Observe_HPOP Style "Complete Access"
# ============================================================
# VolcanoUAV - STK Aviator (MissionModeler) Connect Script
# Applies the UAVMission sample conventions to the SmallSat
# volcano-observation scenario: a UAV patrols the Cascade
# volcano cluster as an airborne imaging platform that
# complements STKSat.
#
# USAGE:
# - Run AFTER OrbitDesignLab_Connect.txt so the volcano
# Place objects already exist, OR uncomment the
# "STANDALONE" block below to create them here.
#
# Reference (Aviator Connect library):
# https://help.agi.com/stk/12.9.1/index.htm#../Subsystems/connectCmds/Content/amm/
#
# UNIT NOTE: Aviator values carry explicit units (deg / ft / km),
# so SetUnits is not required here; altitudes follow aviation
# convention (ft), turn diameters in km.
# ============================================================
# ---- STANDALONE block (uncomment if not chaining after Lab1) ----
#New / Scenario OrbitDesignLab
#SetAnalysisTimePeriod * "1 Jan 2024 00:00:00.000" "15 Jan 2024 00:00:00.000"
#New / */Place Volcano_Mt_St_Helens
#SetPosition */Place/Volcano_Mt_St_Helens Geodetic 46.20 -122.20 0.0
#New / */Place Volcano_Mt_Rainer
#SetPosition */Place/Volcano_Mt_Rainer Geodetic 46.85 -121.76 0.0
#New / */Place Volcano_Mt_Baker
#SetPosition */Place/Volcano_Mt_Baker Geodetic 48.78 -121.81 0.0
#New / */Place Volcano_Mt_Hood
#SetPosition */Place/Volcano_Mt_Hood Geodetic 45.37 -121.69 0.0
#New / */Place Volcano_South_Sister
#SetPosition */Place/Volcano_South_Sister Geodetic 44.10 -121.77 0.0
# ============================================================
# UAV aircraft + MissionModeler propagator
# ============================================================
New / */Aircraft VolcanoUAV
# Load a visualization model (path may vary by install)
VO */Aircraft/VolcanoUAV Model File "C:\Program Files\AGI\STK 12\STKData\VO\Models\Air\rq-4a_globalhawk.glb"
SetPropagator */Aircraft/VolcanoUAV MissionModeler
MissionModeler */Aircraft/VolcanoUAV Aircraft Choose "Basic UAV"
# ------------------------------------------------------------
# Procedure 1 - Takeoff from Portland Intl (PDX) 45.5898, -122.5951
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add AsFirst SiteType Runway ProcedureType "Takeoff"
# SetTime addresses procedure 1, so it must come AFTER that procedure exists
MissionModeler */Aircraft/VolcanoUAV Procedure SetTime 1 Start "1 Jan 2024 12:00:00.00" UTCG
MissionModeler */Aircraft/VolcanoUAV Site 1 SetValue Latitude 45.5898 deg
MissionModeler */Aircraft/VolcanoUAV Site 1 SetValue Longitude -122.5951 deg
# Heading: last bool True = magnetic, False = true heading
MissionModeler */Aircraft/VolcanoUAV Site 1 SetValue Heading 10.0 deg true
MissionModeler */Aircraft/VolcanoUAV Procedure 1 SetValue RunwayAltitudeOffset 31 ft
MissionModeler */Aircraft/VolcanoUAV SendNtfUpdate
# ------------------------------------------------------------
# Procedure 2 - Enroute to Mt Hood 45.37, -121.69
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 1 SiteType Waypoint ProcedureType "Basic Point to Point"
MissionModeler */Aircraft/VolcanoUAV Site 2 SetValue Latitude 45.37 deg
MissionModeler */Aircraft/VolcanoUAV Site 2 SetValue Longitude -121.69 deg
MissionModeler */Aircraft/VolcanoUAV Procedure 2 SetValue Altitude False True 24000 ft
# ------------------------------------------------------------
# Procedure 3 - Enroute to Mt St Helens 46.20, -122.20
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 2 SiteType Waypoint ProcedureType "Basic Point to Point"
MissionModeler */Aircraft/VolcanoUAV Site 3 SetValue Latitude 46.20 deg
MissionModeler */Aircraft/VolcanoUAV Site 3 SetValue Longitude -122.20 deg
MissionModeler */Aircraft/VolcanoUAV Procedure 3 SetValue Altitude False True 24000 ft
# ------------------------------------------------------------
# Procedure 4 - Enroute to Mt Rainier 46.85, -121.76
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 3 SiteType Waypoint ProcedureType "Basic Point to Point"
MissionModeler */Aircraft/VolcanoUAV Site 4 SetValue Latitude 46.85 deg
MissionModeler */Aircraft/VolcanoUAV Site 4 SetValue Longitude -121.76 deg
MissionModeler */Aircraft/VolcanoUAV Procedure 4 SetValue Altitude False True 24000 ft
# ------------------------------------------------------------
# Procedure 5 - Loiter over Mt Rainier for extended imaging
# Holding-Circular uses "End of previous procedure" (no new Site)
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add AfterLast 0 SiteType "End of previous procedure" ProcedureType "Holding - Circular"
MissionModeler */Aircraft/VolcanoUAV Procedure 5 SetValue Diameter 40 km
MissionModeler */Aircraft/VolcanoUAV Procedure 5 SetValue Turns 3
MissionModeler */Aircraft/VolcanoUAV Procedure 5 SetValue RequestedAltitude false 21450 ft
MissionModeler */Aircraft/VolcanoUAV SendNtfUpdate
# ------------------------------------------------------------
# Procedure 6 - Enroute to Mt Baker 48.78, -121.81 (northern turn)
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 5 SiteType Waypoint ProcedureType "Basic Point to Point"
MissionModeler */Aircraft/VolcanoUAV Site 6 SetValue Latitude 48.78 deg
MissionModeler */Aircraft/VolcanoUAV Site 6 SetValue Longitude -121.81 deg
# NOTE: target Procedure 6 (the sample mislabels this as Procedure 3)
MissionModeler */Aircraft/VolcanoUAV Procedure 6 SetValue Altitude False True 24000 ft
# ------------------------------------------------------------
# Procedure 7 - Return leg toward PDX approach 45.75, -122.40
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 6 SiteType Waypoint ProcedureType "Basic Point to Point"
MissionModeler */Aircraft/VolcanoUAV Site 7 SetValue Latitude 45.75 deg
MissionModeler */Aircraft/VolcanoUAV Site 7 SetValue Longitude -122.40 deg
MissionModeler */Aircraft/VolcanoUAV Procedure 7 SetValue Altitude False True 21450 ft
# ------------------------------------------------------------
# Procedure 8 - Landing at Portland Intl (PDX)
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV Procedure Add After 7 SiteType Runway ProcedureType "Landing"
MissionModeler */Aircraft/VolcanoUAV Procedure 8 SetValue ApproachAltitude 1280 ft
MissionModeler */Aircraft/VolcanoUAV Procedure 8 SetValue Glideslope 3.0 deg
MissionModeler */Aircraft/VolcanoUAV Site 8 SetValue Latitude 45.5898 deg
MissionModeler */Aircraft/VolcanoUAV Site 8 SetValue Longitude -122.5951 deg
MissionModeler */Aircraft/VolcanoUAV Site 8 SetValue Heading 190.0 deg true
# ------------------------------------------------------------
# Build the mission
# ------------------------------------------------------------
MissionModeler */Aircraft/VolcanoUAV ConfigureAll
MissionModeler */Aircraft/VolcanoUAV CalculateAll
MissionModeler */Aircraft/VolcanoUAV SendNtfUpdate
# ============================================================
# Nadir imaging sensor + access to the Cascade volcanoes
# ============================================================
New / */Aircraft/VolcanoUAV/Sensor Imager
Define */Aircraft/VolcanoUAV/Sensor/Imager SimpleCone 40
# Aggregate access via a chain (mirrors the orbit-lab workflow)
New / */Chain UAV_Observe
Chains */Chain/UAV_Observe Add */Aircraft/VolcanoUAV/Sensor/Imager
Chains */Chain/UAV_Observe Add */Constellation/Volcanoes
Chains */Chain/UAV_Observe Compute
Report_RM */Chain/UAV_Observe Style "Complete Chain Access"
# Or per-target pairwise access (sample-file style):
# Access */Aircraft/VolcanoUAV/Sensor/Imager */Place/Volcano_Mt_Rainer TimePeriod UseScenarioInterval
# Access */Aircraft/VolcanoUAV/Sensor/Imager */Place/Volcano_Mt_St_Helens TimePeriod UseScenarioInterval
# -*- coding: utf-8 -*-
# ============================================================
# fireye_volcano_python.py
# 學習 Python 控制 STK —— 用你已熟的 FIREYE 火山任務,
# 從 Connect 指令(字串)過渡到 STK Python 物件模型(agi.stk12)。
#
# 對照你先前的 Connect 腳本 FIREYE_Volcano_FullScenario_Connect.txt:
# 同一個場景(FIREYE 衛星 + 20 火山 + access + coverage),
# 這裡改用物件模型 API 寫,並在每段標注「學了什麼」。
#
# 三種連線方式(三個上傳範例各示範一種),下面用旗標切換:
# 1. STKEngine (北科大/TASA/Aviator 範例) - 無介面、跨平台、批次
# 2. STKDesktop.StartApplication - 開新的 STK 桌面 GUI
# 3. STKDesktop.AttachToApplication - 接已開好的 STK(中興高中 notebook 用這個)
#
# 需求: STK 12.1+, pip install "C:\Program Files\AGI\STK 12\bin\
# AgPythonAPI\agi.stk12-<ver>-py3-none-any.whl"
# ------------------------------------------------------------
# 這個檔在 week04_orbit/ 與 week13_automation/ **各有一份,必須逐位元相同**。
# 改一邊要記得改另一邊 —— tools/check_duplicates.py 會盯著,分岔就會叫。
#
# 為什麼不收進 tools/(storage_vs_time.py 就是那樣合併的)?
# 因為這一支沒有 __main__ 保護,**import 就會接上 STK 並建出整個場景**,
# week13_verify.py 正是靠這個(load_lab() 之後直接用 m.root)。
# 它是週次的實驗腳本,不是 helper。Week 13 是擁有者;Week 4 的實驗單
# 把它列為該週現成檔案,所以兩邊都留。
# ============================================================
import os
import time
# --- 物件模型的列舉型別都在 stkobjects;工具型別在 stkutil ---
from agi.stk12.stkobjects import (
AgESTKObjectType, # eSatellite / eFacility / ePlace / eConstellation ...
AgEVePropagatorType, # ePropagatorTwoBody / eJ2Perturbation / eHPOP ...
AgEClassicalSizeShape, # eSizeShapeSemimajorAxis ...
AgEClassicalLocation, # eLocationTrueAnomaly ...
AgEOrientationAscNode, # eAscNodeRAAN ...
AgECvBounds, # coverage grid bounds
AgECvResolution,
AgEFmDefinitionType, # eFmAccessDuration ...
AgEFmCompute, # eAverage / eMaximum / eMinimum
)
from agi.stk12.stkutil import AgEOrbitStateType
# ------------------------------------------------------------
# 連線方式(改這裡切換 1/2/3)
# ------------------------------------------------------------
MODE = os.environ.get("STK_MODE", "engine") # "engine" | "desktop" | "attach"
# 用環境變數覆寫,方便驗證段與 CI 指定連線方式:
# set STK_MODE=attach -> 接已開好的 STK 桌面版(不佔 Engine 授權)
t0 = time.time()
def connect_stk(mode):
"""回傳 (stk, root)。這是三個上傳範例最核心的差異所在。"""
if mode == "engine":
# 北科大 / TASA / Aviator 範例:無介面、可在 Linux/伺服器跑
from agi.stk12.stkengine import STKEngine
stk = STKEngine.StartApplication(noGraphics=True)
root = stk.NewObjectRoot() # Engine 用 NewObjectRoot()
elif mode == "desktop":
# 開一個新的 STK 桌面視窗
from agi.stk12.stkdesktop import STKDesktop
stk = STKDesktop.StartApplication(visible=True, userControl=True)
root = stk.Root # Desktop 用 .Root
elif mode == "attach":
# 中興高中 notebook 用法:接「已經開好」的 STK(用環境變數 STK_PID)
from agi.stk12.stkdesktop import STKDesktop
# STK 啟動時會設 STK_PID;沒有就接目前唯一的那個實例
pid = os.getenv("STK_PID")
stk = (STKDesktop.AttachToApplication(pid=int(pid)) if pid
else STKDesktop.AttachToApplication())
root = stk.Root
else:
raise ValueError(mode)
return stk, root
stk, root = connect_stk(MODE)
# 設定顯示單位(UnitPreferences 是全域偏好,影響 DataProvider 回傳的單位)
root.UnitPreferences.SetCurrentUnit("DateFormat", "UTCG")
# ============================================================
# Section 1 — 場景 / Scenario
# 學了什麼:Children.New(型別, 名稱) 是「建立任何 STK 物件」的通用工法。
# ============================================================
root.NewScenario("FIREYE_Volcano_Py")
scenario = root.CurrentScenario
scenario.SetTimePeriod("1 Jan 2024 00:00:00.000", "8 Jan 2024 00:00:00.000")
if MODE != "engine":
root.Rewind() # GUI 才需要;Engine noGraphics 沒有動畫可復位
# ============================================================
# Section 2 — FIREYE 衛星(物件模型設定軌道)
# 學了什麼:設軌道有兩條路
# (A) 物件模型:ConvertTo(eOrbitStateClassical) 再逐欄設 -> 型別安全、可讀
# (B) 混合法:root.ExecuteCommand('SetState ...') 送 Connect 字串 -> 快
# 這裡示範 (A),對應 Lab1 最佳圓軌道 a=6928, e=0, i=56。
# ============================================================
sat = scenario.Children.New(AgESTKObjectType.eSatellite, "FIREYE")
sat.SetPropagatorType(AgEVePropagatorType.ePropagatorJ2Perturbation) # Lab1 用 J2
prop = sat.Propagator
orbit = prop.InitialState.Representation
kep = orbit.ConvertTo(AgEOrbitStateType.eOrbitStateClassical) # 轉成古典六根數介面
kep.SizeShapeType = AgEClassicalSizeShape.eSizeShapeSemimajorAxis
kep.SizeShape.SemiMajorAxis = 6928.0 # km (物件模型預設 km,和 Connect 的公尺不同!)
kep.SizeShape.Eccentricity = 0.0
kep.Orientation.Inclination = 56.0 # deg
kep.Orientation.ArgOfPerigee = 0.0
kep.Orientation.AscNodeType = AgEOrientationAscNode.eAscNodeRAAN
kep.Orientation.AscNode.Value = 0.0
kep.LocationType = AgEClassicalLocation.eLocationTrueAnomaly
kep.Location.Value = 0.0
orbit.Assign(kep) # 把設好的六根數指回衛星
prop.Propagate() # 產生星曆(忘了這步就沒有軌道)
# nadir 姿態 + imager 感測器:這類幾何設定用 Connect 字串最省事(混合法)
# 正確語法是 Profile + NadirECFVel,且 Offset 參數必填
# ("Basic Nadir ECFVelocity" 會被 STK 拒絕)
root.ExecuteCommand("SetAttitude */Satellite/FIREYE Profile NadirECFVel Offset 0.0")
root.ExecuteCommand("New / */Satellite/FIREYE/Sensor Imager")
root.ExecuteCommand("Define */Satellite/FIREYE/Sensor/Imager SimpleCone 40")
# ============================================================
# Section 3 — 20 個火山觀測點(用迴圈建物件,Python 的強項)
# 學了什麼:資料驅動建模 —— 把座標放進 list,用迴圈建,遠比 Connect
# 逐行 New/SetPosition 精簡。AssignGeodetic(lat, lon, alt_km)。
# ============================================================
VOLCANOES = [
("Mt_St_Helens", 46.20, -122.20, 2.55), ("Shishaldin", 54.76, -163.97, 2.86),
("Mt_Cleveland", 52.82, -169.94, 1.73), ("Fourpeaked", 58.77, -153.67, 2.10),
("Kasatochi", 52.18, -175.50, 0.31), ("South_Sister", 44.10, -121.77, 3.16),
("Lassen", 40.49, -121.51, 3.19), ("Mt_Hood", 45.37, -121.69, 3.43),
("Mt_Baker", 48.78, -121.81, 3.29), ("Mt_Rainier", 46.85, -121.76, 4.39),
("Mt_Adams", 46.21, -121.49, 3.74), ("Glacier_Peak",48.11, -121.11, 3.21),
("Mt_Shasta", 41.41, -122.19, 4.32), ("Newberry", 43.72, -121.23, 2.43),
("Crater_Lake", 42.93, -122.12, 2.49), ("Redoubt", 60.49, -152.74, 3.11),
("Augustine", 59.36, -153.43, 1.25), ("Katmai", 58.28, -154.96, 2.05),
("Okmok", 53.43, -168.13, 1.07), ("Mauna_Loa", 19.47, -155.60, 4.17),
]
constellation = scenario.Children.New(AgESTKObjectType.eConstellation, "Volcanoes")
root.BeginUpdate() # 批次包起來:大量建物件時關閉逐一重繪,速度快非常多
for name, lat, lon, alt in VOLCANOES:
place = scenario.Children.New(AgESTKObjectType.ePlace, f"Volcano_{name}")
place.Position.AssignGeodetic(lat, lon, alt) # 度, 度, km
constellation.Objects.AddObject(place) # 加進星群
root.EndUpdate()
# ============================================================
# Section 4 — Access + DataProvider 取數
# 學了什麼:DataProvider 是「從 STK 取結構化結果」的正規途徑,
# 比解析 Report 文字可靠。IntervalFromPath 用於有起訖的區間資料。
# ============================================================
root.UnitPreferences.SetCurrentUnit("Time", "Sec") # 讓 Duration 以秒回傳
imager = sat.Children.Item("Imager") # 取回剛用 Connect 建的感測器
chain = scenario.Children.New(AgESTKObjectType.eChain, "Imaging")
chain.Objects.AddObject(imager)
chain.Objects.AddObject(constellation)
chain.ComputeAccess()
dp = chain.DataProviders.GetDataPrvIntervalFromPath("Complete Access")
res = dp.Exec(scenario.StartTime, scenario.StopTime)
durs = res.DataSets.GetDataSetByName("Duration").GetValues()
total_hr = sum(durs) / 3600.0
print(f"總成像 access(20 火山): {total_hr:.2f} hr, {len(durs)} 個通聯")
# ============================================================
# Section 5 — Coverage + Figure of Merit(區域覆蓋,勝過逐點 access)
# 學了什麼:CoverageDefinition 對「一塊區域」算覆蓋,FigureOfMerit
# 把結果彙整成 Min/Max/Average。這是把「20 點 access」升級成
# 「美西區域覆蓋率/重訪」的正規做法(對應你 xlsx 筆記的第 8 節)。
# ============================================================
cov = scenario.Children.New(AgESTKObjectType.eCoverageDefinition, "USWest_Cov")
grid = cov.Grid
grid.BoundsType = AgECvBounds.eBoundsLatLonRegion # 用經緯度框一塊區域
b = grid.Bounds
b.MinLatitude, b.MaxLatitude = 18.0, 62.0 # 夏威夷到阿拉斯加
b.MinLongitude, b.MaxLongitude = -180.0, -120.0
grid.ResolutionType = AgECvResolution.eResolutionDistance
grid.Resolution.Distance = 150 # km 格點
cov.AssetList.Add("Constellation/Volcanoes") # 這裡也可改指派 Satellite/FIREYE
# 用衛星當 asset 更合理:對火山覆蓋要看衛星,不是火山自己
cov.AssetList.RemoveAll()
cov.AssetList.Add("Satellite/FIREYE")
cov.ComputeAccesses()
fom = cov.Children.New(AgESTKObjectType.eFigureOfMerit, "AccessDuration")
fom.SetDefinitionType(AgEFmDefinitionType.eFmAccessDuration)
fom.Definition.SetComputeType(AgEFmCompute.eAverage)
fdp = fom.DataProviders.GetDataPrvFixedFromPath("Overall Value")
fres = fdp.Exec()
vmin = fres.DataSets.GetDataSetByName("Minimum").GetValues()[0]
vmax = fres.DataSets.GetDataSetByName("Maximum").GetValues()[0]
vavg = fres.DataSets.GetDataSetByName("Average").GetValues()[0]
print(f"區域覆蓋 access duration min/avg/max = {vmin:.1f} / {vavg:.1f} / {vmax:.1f} sec")
print(f"\n完成,耗時 {time.time()-t0:.1f} sec")
# ============================================================
# 收尾:Engine 要 ShutDown;Desktop 若是你開的可留著看
# ============================================================
if MODE == "engine":
root.CloseScenario()
stk.ShutDown()
print("STK Engine 已關閉。")
# ============================================================
# 對照速查:Connect 字串 <-> Python 物件模型
# ------------------------------------------------------------
# New / */Satellite FIREYE
# -> scenario.Children.New(AgESTKObjectType.eSatellite, "FIREYE")
# SetPosition ... Geodetic lat lon alt
# -> place.Position.AssignGeodetic(lat, lon, alt)
# Chains ... Compute + Report_RM Style "Complete Chain Access"
# -> chain.ComputeAccess() + DataProviders.GetDataPrvIntervalFromPath(...)
# 單位:Connect 距離預設「公尺」需 SetUnits/km;
# 物件模型多數屬性預設「公里」。單位系統不同,別混淆!
# 何時用哪個:結構/計算/取數 -> 物件模型;
# 一次性幾何設定或物件模型沒對應的 -> root.ExecuteCommand(Connect)
# ============================================================
# -*- coding: utf-8 -*-
# ================================================================
# Week 4 驗證段 — 軌道設計、完整場景與 HPOP 受力模型
#
# 本週最隱蔽的坑是 HPOP:Force / Drag / Integrator 的設定是**延後提交**的,
# STK 12 的 ExportDataFile 不會觸發傳播,少了 Propagate 就等於整組設定沒生效,
# 而且 0 個 NACK。匯出的星曆會用 STK 預設模型,7 天後位置差約 107 km。
#
# 執行:python week04_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
import stk_paths
from agi.stk12.stkdesktop import STKDesktop
from stk_verify import run_script # noqa: E402
def main():
app = STKDesktop.AttachToApplication()
root = app.Root
v = Verifier(root)
# ---- (A) Lab1 軌道設計:掃描的「起點」軌道 ----
if root.CurrentScenario is not None:
root.CloseScenario()
run_script(root, os.path.join(HERE, "OrbitDesignLab_Connect.txt"), echo_fail=False)
v.object_exists("*/Satellite/STKSat")
v.cone_angle("*/Satellite/STKSat/Sensor/Imager", 40.0)
sc = root.CurrentScenario
nplace = sum(1 for i in range(sc.Children.Count)
if sc.Children.Item(i).ClassName == "Place")
v.expect("Lab1 火山觀測點數", nplace, 10, tol=0, unit=" 個") # FullScenario 才是 20 個
v.expect_true("Lab1 基準是掃描起點(i=0 赤道軌道),不是結論", True,
"Week 11 若直接沿用它會完全沒有 access,見 week11_verify.py")
# ---- (B) HPOP:受力模型必須真的提交 ----
run_script(root, os.path.join(HERE, "STKSat_HPOP_Connect.txt"), echo_fail=False)
fm = root.GetObjectFromPath("*/Satellite/STKSat_HPOP").Propagator.ForceModel
v.expect("重力階數", fm.CentralBodyGravity.MaxDegree, 70, tol=0, unit=" 階")
v.expect("重力次數", fm.CentralBodyGravity.MaxOrder, 70, tol=0, unit=" 次")
v.expect("阻力係數 Cd", fm.Drag.DragModel.Cd, 2.2, tol=0.01)
v.expect("面積質量比", fm.Drag.DragModel.AreaMassRatio, 0.008, tol=1e-6, unit=" m^2/kg")
v.expect_true("重力檔為 EGM2008",
"EGM2008" in fm.CentralBodyGravity.File, fm.CentralBodyGravity.File[-24:])
v.expect_true("腳本裡有 Propagate(HPOP 設定才會提交)",
any(l.strip().startswith("Propagate ")
for l in open(os.path.join(HERE, "STKSat_HPOP_Connect.txt"),
encoding="utf-8")),
"STK 12 的 ExportDataFile 不會觸發傳播")
# ---- (C) 完整 FIREYE 場景:chain access 與報告 ----
if root.CurrentScenario is not None:
root.CloseScenario()
ack, nack = run_script(root, os.path.join(HERE,
"FIREYE_Volcano_FullScenario_Connect.txt"), echo_fail=False)
v.expect_true("完整場景沒有 NACK", nack == 0, f"{ack} ACK / {nack} NACK")
sat = root.GetObjectFromPath("*/Satellite/FIREYE")
v.expect_true("FIREYE 姿態為 NadirECFVel", int(sat.Attitude.Basic.ProfileType) == 8,
f"ProfileType = {int(sat.Attitude.Basic.ProfileType)}")
# 路徑由 tools/stk_paths.py 統一 —— 原本三個 Python 檔各寫死一份
for f, label, lo in ((stk_paths.report("imaging_access.txt"), "成像報告", 50),
(stk_paths.report("downlink_access.txt"), "下傳報告", 30)):
n = sum(1 for l in open(f, encoding="utf-8", errors="ignore")
if l.strip()[:1].isdigit() and len(l.split()) > 6) if os.path.exists(f) else 0
v.expect_true(f"{label}有足夠通聯({lo} 次以上)", n >= lo, f"{n} 次")
ok = v.report("Week 4 驗證段")
return 0 if ok else 1
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)