Week 13 — 軟體與自動化
week13_automation/基礎知識導論
Week 13 基礎知識導論 — 自動化:Connect 字串 vs 物件模型
手動點選做出來的分析,沒有人能重現,包括三個月後的你自己。
這一週把整條流程變成程式碼:可版本控制、可重跑、可驗證。
1. 這一週在解什麼問題
前十二週累積了大量 STK 操作。現在要問:
- 怎麼把「建場景 → 設軌道 → 排 access → 產報表」變成一支可執行的程式?
- Connect 字串與物件模型該怎麼分工?
- 為什麼同一個屬性,兩種介面的單位不一樣?
- 怎麼確認程式真的做了你以為它做的事?
2. 核心觀念
2.1 STK 的兩套 API
| Connect | 物件模型 (Object Model) | |
|---|---|---|
| 形式 | 送字串指令 | 呼叫物件的方法與屬性 |
| 回傳 | ACK / NACK,或文字結果 | 型別化的物件 |
| 探索性 | 差(要查指令手冊) | 好(IDE 有自動完成) |
| 錯誤訊息 | 通常只有「失敗」 | 有例外型別與訊息 |
| 適合 | 一次性的批次設定、腳本移植 | 需要讀值、迴圈、條件判斷的流程 |
實務上是混用:用 Connect 快速建構,用物件模型讀值與判斷。
本課的驗證段就是這個模式 —— Connect 設定,物件模型或 *_RM ... GetValue 回讀。
2.2 單位陷阱
這是本課反覆出現的坑:
| 介面 | 距離預設 |
|---|---|
| Connect 指令 | 公尺 |
| 物件模型多數屬性 | 公里 |
而且 Connect 的 SetUnits 只管距離,而且已經是過時指令。
要改報表單位得用:
Units_Set * Report Power W
防禦作法:在腳本開頭明確設定單位,並在驗證段用已知答案檢查一次
(例如設 550 km 高度,讀回來確認是 550 而不是 550000)。
2.3 Desktop vs Engine
| STK Desktop | STK Engine(無介面) | |
|---|---|---|
| 圖形 | 有 2D/3D 視窗 | 無 |
| 適合 | 教學、除錯、視覺驗證 | 批次運算、伺服器、CI |
| 連線 | STKDesktop.AttachToApplication() |
STKEngine.StartApplication() |
本教材實測:有 18 條指令在 Engine 下會被拒絕(STK 12 與 13 表現一致):
Animate、Graphics ... SetColor、VO ... SolarPanel、OrbitWizard ... Color、
ReportCreate ... Type Display 等。全部都是圖形相關的指令。
所以這 18 個 NACK 是 engine-vs-desktop 的差異,不是版本差異。
本課需要 STK 的 notebook 都必須用桌面版。
2.4 兩個真實的 COM 陷阱
陷阱一:app 被回收
root = STKDesktop.AttachToApplication().Root # 錯
這樣寫,app 物件立刻被垃圾回收,Root 的 COM 介面變成空指標。
之後每一條指令都會拋:
'NoneType' object has no attribute '_getVtblEntry'
正確寫法是保留 app 的參考:
app = STKDesktop.AttachToApplication()
root = app.Root
陷阱二:工作目錄被改掉
啟動 STK Engine 會改變 process 的工作目錄。
之後任何 os.getcwd() 算出來的相對路徑都會錯。
本教材的 notebook 因此在最前面加了一個 bootstrap cell,
把週次資料夾釘在 WEEK_DIR,不再事後依賴 getcwd()。
2.5 資料提供者 (Data Provider)
報表與圖表背後的統一取數介面。相較於「產生報表檔再 parse」,
Data Provider 直接把數值陣列給你:
物件 → DataProviders["某個提供者"] → Exec(起, 迄, 步長) → DataSets → GetValues()
好處:不經過檔案系統、沒有格式解析問題、不會出現「空檔案也算成功」的靜默失敗。
什麼時候還是要用報表檔:需要跟別人交換、需要留存證據、
或該資料只有報表樣式提供(如 "Link Budget - Detailed")。
2.6 自動化的三個層次
層次 1 可重跑 腳本化,同樣輸入得到同樣輸出
層次 2 可驗證 程式自己檢查結果對不對(驗證段)
層次 3 可組合 一週的輸出直接餵給下一週,不經人手
本課的 16 週正是層次 3 的示範:
Week 4 的 access 報告 → Week 12 的資料預算;
Week 4 的日照時間線 → Week 6 的熱模型;
Week 8 的姿態 .a 檔 → STK 3D。
每經過一次人手複製貼上,就多一個無法追溯的錯誤來源。
2.7 版本控制與可重現性
STK 場景檔(.sc、.sa 等)是二進位或半結構化的,diff 不好看。
正確作法是版控「產生場景的腳本」,而不是場景本身。
這正是本 repo 的組織方式:每一週都是
.txt(Connect 腳本)+ .py(Python Lab)+ _verify.py(驗證段),
全部是純文字,可 diff、可 review、可回溯。
3. 名詞與符號
| 名詞 | 意義 |
|---|---|
| Connect | STK 的字串指令介面 |
| Object Model | STK 的型別化 API |
agi.stk12 / ansys.stk.core |
Python 綁定套件(STK 12 / 13) |
| Data Provider | 統一的取數介面 |
| ACK / NACK | 指令被接受 / 被拒絕 |
*_RM ... GetValue |
Connect 的回讀指令族 |
| Desktop / Engine | 有介面 / 無介面 |
WEEK_DIR |
本教材 notebook 釘住路徑用的變數 |
4. 觀念 → 本週實驗對照
| 觀念 | 出現在本週的哪裡 |
|---|---|
| 物件模型建場景 | fireye_volcano_python.py |
| Connect 與物件模型混用 | 同上 |
| 單位陷阱 | 同上的註解與斷言 |
| 驗證段 | week13_verify.py(6 項) |
week13_verify.py不收進 notebook。
它會把實驗腳本再跑一次;notebook 的 kernel 裡已經有一個 STK 連線,
同一個 process 再連一次(Engine + Desktop 混用)會讓 COM 斷線。
請在終端機單獨執行:python week13_automation/week13_verify.py。
這件事本身就是一個教學點:自動化要考慮執行環境的狀態,不只是程式邏輯。
5. 常見誤解
- 「寫成一行比較漂亮。」 —
STKDesktop.AttachToApplication().Root就是這樣壞掉的。
COM 的生命週期不是 Python 的表達式作用域。 - 「物件模型比 Connect 好,應該全部改寫。」 — 兩者各有適用場合。
大量一次性設定用 Connect 更短,需要讀值判斷用物件模型更安全。 - 「Engine 跟 Desktop 只差在有沒有視窗。」 — 有 18 條指令行為不同。
在 Engine 上開發、在 Desktop 上交付,會遇到驚喜。 - 「腳本跑完沒報錯就是對的。」 — 全課主軸。ACK 不等於生效。
- 「把 .sc 檔存進 git 就是版本控制了。」 — 二進位檔無法 review。
要版控產生它的腳本。
6. 延伸閱讀
STK 程式介面
- STK Developer Kit 說明
— Connect 指令與物件模型的完整 API 參考。 - STK Developer Kit 範例
— 官方的多語言範例(Python / MATLAB / C# / Java)。 - Ansys STK 線上說明
- 本機資源:
C:\Program Files\AGI\STK 12\CodeSamples\— 官方程式碼範例C:\Program Files\AGI\STK 12\Help\LinkedDocuments\STK_Astrogator_OM_Tutorial.pdfC:\Program Files\AGI\STK 12\Help\Programming\— 程式介面說明
可重現性與軟體工程
- NASA-STD-7009(模型與模擬的可信度標準)
— 「你的模擬憑什麼可信」的正式標準,含可重現性要求。 - NASA Lessons Learned Information System
— 搜尋 software / verification,看真實任務的軟體教訓。
替代與交叉驗證工具
- NASA GMAT — 免費,有 Python 介面,可對答案。
- Orekit — 開源高精度軌道力學函式庫(Java,有 Python 綁定)。
- Skyfield — 純 Python 的天體位置計算。
- Astropy — 時間系統與座標轉換的標準工具。
7. 課前自我檢核
STKDesktop.AttachToApplication().Root為什麼會壞?錯誤訊息長什麼樣?- 同一個高度值,Connect 與物件模型分別要填什麼數字?怎麼自動檢查你沒搞錯?
- 哪些指令在 STK Engine 下會失敗?它們有什麼共同點?
- Data Provider 相較於「產生報表檔再解析」,避免了哪一類的靜默失敗?
- 你要把整個 FIREYE 分析交給另一個人重現。你會交出哪些檔案?為什麼不交場景檔?
程式碼
# -*- 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 13 驗證段 — 物件模型自動化建出來的場景是否完整
#
# 本週教「Connect 字串 vs 物件模型」的分工。物件模型的錯誤會直接拋例外,
# 比 Connect 好抓;但腳本裡混用的那幾行 Connect 字串仍然是靜默的 ——
# 本教材原本就有一行 SetAttitude 語法錯誤藏在這裡,ACK 卻沒生效。
#
# 執行:python week13_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():
# 用 attach 接已開好的 STK 桌面版:不佔 Engine 授權,也不會和別的驗證段搶
os.environ.setdefault("STK_MODE", "attach")
# 驗證段會把實驗腳本再跑一次。若 notebook 前面的 cell 已經建過同名場景,
# 這裡不先關掉就會撞名(STK 不允許同名場景並存)。
from agi.stk12.stkdesktop import STKDesktop
_app = STKDesktop.AttachToApplication()
if _app.Root.CurrentScenario is not None:
_app.Root.CloseScenario()
m = load_lab(os.path.join(HERE, "fireye_volcano_python.py"))
v = Verifier(m.root)
sc = m.root.CurrentScenario
v.expect_true("場景已建立", sc is not None,
sc.InstanceName if sc is not None else "無")
kinds = {}
for i in range(sc.Children.Count):
k = sc.Children.Item(i).ClassName
kinds[k] = kinds.get(k, 0) + 1
v.expect("衛星數", kinds.get("Satellite", 0), 1, tol=0, unit=" 顆")
v.expect("火山觀測點數", kinds.get("Place", 0), 20, tol=0, unit=" 個")
v.object_exists("*/Satellite/FIREYE/Sensor/Imager")
v.cone_angle("*/Satellite/FIREYE/Sensor/Imager", 40.0)
# 這一行原本是 "SetAttitude ... Basic Nadir ECFVelocity",語法錯、ACK、沒生效。
# 混用 Connect 字串時,物件模型的例外保護就失效了 —— 只能靠回讀。
sat = m.root.GetObjectFromPath("*/Satellite/FIREYE")
v.expect_true("姿態為 NadirECFVel(混用的 Connect 字串有生效)",
int(sat.Attitude.Basic.ProfileType) == 8,
f"ProfileType = {int(sat.Attitude.Basic.ProfileType)}")
ok = v.report("Week 13 驗證段")
return 0 if ok else 1
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)