獨立章節 1 — 地面雙基地雷達:干擾、雜波與航跡穩定度
radar_bistatic/RadarBistatic_Lab_Handout.md
實驗單 — 地面雙基地雷達:干擾、雜波與航跡穩定度
獨立章節,不屬於 16 週立方衛星課程。
先讀 RadarBistatic_Primer.md,再做本實驗。
參考:AGI 訓練單元 Radar Tracking and Jamming(單基地版)
0. 情境
台灣海峽的雙基地防空雷達:
| 物件 | 位置 | 說明 |
|---|---|---|
TxSite |
24.90°N, 120.85°E | 發射站,100 kW 峰值、4 m 拋物面、S 頻段 3 GHz |
RxSite |
24.30°N, 120.45°E | 接收站,靜默,4 m 拋物面。基線 78 km |
Track01 |
北→南沿海峽 | 真值航跡,FL300 (9144 m)、250 m/s、RCS 10 m² |
JamAir |
24.9→23.7°N, 118.9°E | 空中支援干擾機,10 km 高,20 W,對準接收站 |
分析窗 30 分鐘,取樣 5 秒(等於雷達更新率)。
為什麼用真值航跡:模擬相對真實世界最大的優勢,就是真值已知。
所有「追得準不準」的問題才有絕對的基準可比。
1. 執行步驟
1. 開啟 STK 12 桌面版(本章沒有繪圖指令,無介面 Engine 也可以)
2. python bistatic_build.py 建場景
3. python bistatic_tracking.py 四情境分析 + 圖
4. python bistatic_sweep.py 干擾功率與雜波係數掃描 + 圖
5. python bistatic_verify.py 驗證段(離開碼 0 = 全部通過)
bistatic_build.py 會先送 bistatic_radar_Connect.txt(72 條指令),
再用物件模型補上 Connect 做不到的三件事。
2. Part A — 雙基地幾何(佔 15%)
要交的東西:距離和、雙基地角、距離乘積隨時間的變化。
要回答的問題:
- 為什麼雙基地的等訊號強度輪廓是 Cassini 卵形線而不是圓?
- 距離乘積
Rt·Rr在本場景變化多少 dB?這對「哪一段航路最容易掉」有什麼預測? - 把接收站往西移 20 km(改
SetPosition */Place/RxSite),
基線變長還是變短?訊號變好還是變差?先預測,再跑。
基準值:Rt 111–216 km、Rr 59–197 km、距離和 186–360 km、
雙基地角 3.3–42.8°、距離乘積差 12.3 dB。
3. Part B — 偵測與四情境比較(佔 25%)
STK 只提供三個 PDet 元素:乾淨、w/Jamming、w/J+C。
沒有「只有雜波」。所以腳本會把干擾與雜波逐一開關,重算四次。
要回答的問題:
- 為什麼不能只跑「全開」那一組,再把退化都算到雜波頭上?
S/T Integrated J/S有 267/280 個有效樣本,其餘 13 個是 −9999.99。
那 13 拍代表什麼?(提示:干擾機的航路與可見性)- 把
PulseIntegrationType改回0(Goal SNR)重跑,PDet 會怎麼變?
為什麼那個結果是假的?
基準值(20 W 干擾、雜波係數 0.05):
| 情境 | PDet 平均 |
|---|---|
| 乾淨 | 1.0000 |
| 只有雜波 | 0.9980 |
| 只有干擾 | 0.8587 |
| 干擾+雜波 | 0.8540 |
4. Part C — 航跡連續性(佔 20%)
判準:PDet ≥ 0.8 視為這一拍有偵測;連續中斷超過 30 秒(外推上限)視為航跡丟失。
基準值:
| 情境 | 偵測率 | 最長中斷 | 中斷次數 | 航跡 |
|---|---|---|---|---|
| 乾淨 | 100.0% | 0 s | 0 | 維持 |
| 只有雜波 | 100.0% | 0 s | 0 | 維持 |
| 只有干擾 | 83.2% | 55 s | 7 | 丟失 |
| 干擾+雜波 | 82.9% | 55 s | 7 | 丟失 |
要回答的問題:
- 偵測率 83% 聽起來還不錯,但航跡卻丟了。為什麼「平均值」在這裡是誤導的?
- 把外推上限從 30 秒放寬到 60 秒,航跡就保得住嗎?代價是什麼?
(提示:外推期間你相信的是模型,不是量測 —— 目標若在此時轉彎會怎樣?) - 中斷發生在航路的哪一段?對照 Part A 的幾何,說明為什麼是那裡。
5. Part D — 追蹤誤差 vs 真值(佔 25%,本章重點)
用 STK 的 SNR 產生量測雜訊,跑 alpha-beta 濾波器,與真值逐點比對。
基準值:
| 情境 | 命中率 | 航跡存活 | 重起 | RMS 誤差 | P95 誤差 |
|---|---|---|---|---|---|
| 乾淨 | 100.0% | 100.0% | 0 | 189 m | 375 m |
| 只有雜波 | 99.6% | 100.0% | 0 | 196 m | 398 m |
| 只有干擾 | 86.1% | 95.7% | 3 | 918 m | 986 m |
| 干擾+雜波 | 85.7% | 95.7% | 3 | 920 m | 996 m |
要回答的問題:
- 偵測率只掉 17 個百分點,RMS 誤差卻變成 4.9 倍。用 2.8 節的公式解釋。
- 本場景的定位誤差主要來自距離量測還是角度量測?把過程寫出來。
(提示:4 m 天線在 3 GHz 的波束寬是 1.75°,150 km 處 1° 是多少公尺?) - 把
alpha從 0.5 改成 0.2(更信模型),誤差怎麼變?
在乾淨與有干擾兩種情境下,變化方向一樣嗎?為什麼? - 如果把接收天線換成 8 m,定位精度會改善幾倍?先用公式預測,再改參數驗證。
6. Part E — 掃描與設計決策(佔 15%)
bistatic_sweep.py 產生兩條曲線。
干擾機功率掃描(雜波關閉):
| 功率 | 0.1 W | 1 W | 5 W | 10 W | 50 W | 200 W | 2000 W |
|---|---|---|---|---|---|---|---|
| 偵測率 | 100% | 98.2% | 93.9% | 89.6% | 61.4% | 30.0% | 10.4% |
| 最長中斷 | 0 s | 10 s | 30 s | 40 s | 160 s | 205 s | 225 s |
雜波反射係數掃描(干擾關閉):
| 係數 | 0.01 | 0.05 | 0.5 | 5 | 50 | 5000 |
|---|---|---|---|---|---|---|
| 偵測率 | 100% | 100% | 86.1% | 70.0% | 42.5% | 6.8% |
要回答的問題:
- 打穿門檻落在 5–10 W 之間。一部 100 kW 的雷達被 10 W 打穿,
這個 4 個數量級的落差從哪裡來? - 雜波在預設係數 0.05 下完全不影響偵測。
這是實驗失敗還是有意義的結果? 你要怎麼把它寫進報告? - 兩條曲線哪一段平坦、哪一段陡降?平原與懸崖分別代表什麼設計意義?
- 你要替這部雷達提一個抗干擾改良案。列出三個選項
(例如:旁瓣抑制、跳頻、增加接收站),各自能買到多少 dB?各自的代價?
7. 驗證段 — 三層靜默失敗
bistatic_verify.py,62 項全部 PASS。本章的雷達模組有三層靜默失敗:
| 層 | 症狀 | 怎麼抓 |
|---|---|---|
| 1 | 指令被拒絕 | NACK 計數 |
| 2 | ACK 了卻沒生效 | Radar_RM ... GetValue 回讀比對 |
| 3 | ACK、回讀正確、輸出還是空的 | 檢查資料提供者有沒有真的吐出數字 |
第 3 層是本章新發現的:雜波幾何設成 Smooth_Oblate_Earth,
Connect ACK、回讀一字不差,但雙基地接收端的 S/T Clutter Power
全部是 −9999.99。只有 Single_Point 算得出來。
驗證段把這個「已知會壞的設定」也寫成一個檢查項 ——
如果哪天 STK 修好了,那一項會變成 FAIL,我們才會知道。
要回答的問題:
- 第 2 層與第 3 層的差別是什麼?各舉一個本章的例子。
SetPosition ... 10.0與SetHeightAboveGround 10.0,
哪一個真的把物件抬起來?哪一個的回讀值是對的?
這件事對「只靠回讀做驗證」有什麼啟示?- 自己加一個檢查項:確認干擾機的航路涵蓋整個分析窗
(提示:本章一開始的原型就因為干擾機航路只有 100 秒,
導致 6 個取樣點裡只有 1 個算得出干擾)。
8. 評分
| 項目 | 比重 |
|---|---|
| Part A 幾何理解 | 15% |
| Part B 四情境設計與解讀 | 25% |
| Part C 航跡連續性 | 20% |
| Part D 追蹤誤差分析 | 25% |
| Part E 掃描與設計決策 | 15% |
加分:把 alpha-beta 換成等速模型的 Kalman 濾波器,
用 STK 給的 SNR 逐拍計算量測共變異數,比較兩者在有干擾時的表現差異。
9. 檔案
| 檔案 | 內容 |
|---|---|
RadarBistatic_Primer.md |
基礎知識導論 |
bistatic_radar_Connect.txt |
Connect 腳本(72 條) |
bistatic_build.py |
建場景 + Connect 做不到的三件事 |
bistatic_tracking.py |
四情境分析(Part A–D) |
bistatic_sweep.py |
干擾功率與雜波係數掃描 |
bistatic_verify.py |
驗證段(62 項) |
bistatic_tracking.png |
四宮格結果圖 |
bistatic_sweep.png |
兩條掃描曲線 |
RadarBistatic_Primer.md
基礎知識導論 — 地面雙基地雷達、干擾與雜波
本章是獨立章節,不屬於 16 週立方衛星課程。
它借用同一套方法論(STK 主導、Python 補位、ACK 不等於生效),
但主題換成地面防空雷達。
1. 這一章在解什麼問題
一部雷達「偵測得到目標」與「穩定追蹤得住目標」是兩件事。
給定一條已知真值的航跡(這是模擬相對於真實世界的最大優勢:真值是已知的),
問三個層次的問題:
- 能量夠嗎 — 訊號雜訊比高不高?
- 看得連續嗎 — 偵測有沒有中斷?中斷多久?濾波器外推得過去嗎?
- 看到的位置準嗎 — 濾波器輸出與真值差多少?
多數雷達分析停在第 1 層。使用者真正在意的是第 3 層。
參考範例是 AGI 官方訓練單元
Radar Tracking and Jamming,
但那份是單基地。本章改成雙基地,並加上雜波。
2. 核心觀念
2.1 雙基地雷達:發射與接收分開站
目標 T
/ \
Rt / \ Rr
/ \
發射站 -------- 接收站
基線 L
- 雙基地距離和
R = Rt + Rr。等距離和的軌跡是一個以兩站為焦點的橢球。 - 雙基地角
beta= 目標處 Rt 與 Rr 的夾角。beta = 0退化成單基地。 beta -> 180°是前向散射 (forward scatter) 區,散射截面暴增,
但都卜勒與距離解析度同時崩潰。
2.2 雙基地雷達方程式
Pr = Pt * Gt * Gr * λ² * σ_b / ( (4π)³ * Rt² * Rr² * L_sys )
跟單基地最大的差別:
| 單基地 | 雙基地 | |
|---|---|---|
| 距離項 | R⁴ |
(Rt · Rr)² |
| 等訊號輪廓 | 圓 | Cassini 卵形線 Rt·Rr = 常數 |
| RCS | 單基地 RCS σ |
雙基地 RCS σ_b(beta),與角度有關 |
Cassini 卵形線是本章的核心幾何直覺。
當 Rt·Rr 常數夠小,卵形線會分裂成兩個各自繞著發射站與接收站的封閉區域;
常數變大時兩葉合併成一個花生形,再變大變成一個大卵形。
也就是說:雙基地雷達的覆蓋不是一個圓,而且在基線附近有一段特別敏感的區域。
本章實測:目標飛過時距離乘積 Rt·Rr 從 7.5e3 變到 3.1e4 km²,
對訊號的影響達 12.3 dB —— 一條直線航路上,訊號強度就會差 16 倍。
2.3 為什麼要做雙基地
| 動機 | 說明 |
|---|---|
| 反匿蹤 | 匿蹤外形是把能量朝特定方向反射掉。接收站不在發射站旁邊,就可能收到那些能量 |
| 接收站被動 | 接收站不輻射,不會被反輻射飛彈鎖定 |
| 抗干擾 | 干擾機通常對著它「看得到」的輻射源,而接收站是靜默的 |
| 利用他人的照射源 | 被動雷達(用電視台、手機基地台當照射源) |
代價:時間與相位同步困難、幾何複雜、資源排程困難。
2.4 偵測:從 SNR 到 PDet
單一脈衝的訊號雜訊比 SNR1 通常不夠,要脈衝積分:
SNR_int ≈ SNR1 + 10*log10(N) (理想同調積分)
非同調積分的增益低一些(約 N^0.7–N^0.8)。
偵測門檻由虛警機率 Pfa 與積分脈衝數共同決定:N 愈多,積分增益愈大,
但門檻也愈高。兩者是拉鋸,不是單向。
STK 的陷阱:
PulseIntegration預設策略是 Goal SNR ——
它會一直積到達成設定的目標 SNR 為止。結果是S/T Integrated SNR
永遠等於那個目標值,而偵測門檻隨著脈衝數(可能積到 512 個)一路升高,
PDet 反而被壓死在 1e-3 以下。
要看真實的偵測表現,必須改成 Fixed Pulse Number。
而這個切換 Connect 做不到,只能用物件模型的
Waveform.PulseIntegrationType。
2.5 干擾 (Jamming)
干擾的度量是 J/S(干擾對訊號比):
J/S = [ Pj * Gj * Gr(θj) * Rt² * Rr² * 4π ] / [ Pt * Gt * Gr(0) * σ * Rr² ]
關鍵不對稱:
- 雷達訊號走來回(
Rt去、Rr回),受Rt²Rr²衰減 - 干擾訊號只走單程(干擾機 → 接收站)
所以干擾機的功率優勢是巨大的。本章實測:一部 20 W 的空中干擾機,
足以讓一部 100 kW 的雙基地雷達偵測率從 100% 掉到 83%,
並產生 55 秒的追蹤中斷 —— 功率差 5000 倍,結果是干擾機贏。
再往上掃:
| 干擾機功率 | 偵測率 | 最長中斷 |
|---|---|---|
| 0.1 W | 100.0% | 0 s |
| 1 W | 98.2% | 10 s |
| 5 W | 93.9% | 30 s |
| 10 W | 89.6% | 40 s ← 打穿門檻 |
| 50 W | 61.4% | 160 s |
| 2000 W | 10.4% | 225 s |
燒穿距離 (burn-through range) 是這件事的另一面:靠得夠近,
訊號的 1/R⁴ 終究會贏過干擾的 1/R²。設計上要問的不是「會不會被干擾」,
而是「在什麼距離之內我方還贏得了」。
干擾進入接收機的路徑是天線的旁瓣(除非干擾機剛好在主瓣裡)。
所以旁瓣抑制 (sidelobe cancellation) 是抗干擾最重要的硬體手段。
2.6 雜波 (Clutter)
雜波是不想要的真實回波:地面、海面、雨、鳥、風機。與雜訊的差別:
| 雜訊 | 雜波 | |
|---|---|---|
| 來源 | 接收機本身 | 環境的真實散射 |
| 隨發射功率 | 不變 | 等比例增加 |
| 對策 | 加功率、加積分 | 都卜勒濾波、MTI、CFAR |
加功率對雜波完全無效,因為訊號與雜波一起變大。這是雜波和雜訊最重要的差別。
雜波強度由雜波反射係數 σ⁰(每單位面積的 RCS)與被照亮的雜波面積決定。
面積又與距離解析度、波束寬、掠射角有關 —— 所以低掠射角、寬波束、
長脈衝的雷達雜波最嚴重。
本章的目標在 9 km 高空、60–200 km 距離,天線仰角只有 1–2 度,
主瓣確實會掃到地面。但實測的 C/N 最高只有 −1.6 dB,
雜波幾乎不影響偵測(偵測率仍 100%)。掃描顯示要到反射係數 0.5 以上才開始咬人:
| 雜波反射係數 | 偵測率 | 最長中斷 |
|---|---|---|
| 0.01 / 0.05 | 100.0% | 0 s |
| 0.5 | 86.1% | 110 s |
| 5 | 70.0% | 135 s |
| 5000 | 6.8% | 390 s |
這是誠實的結果,不是失敗的實驗:對這個幾何(高空目標、中距離),
干擾是主要威脅,雜波不是。雜波成為主角要等到低空目標與岸際地形 ——
那是 TIREM 章節的主題。
2.7 追蹤:偵測不等於航跡
一條航跡要活著,需要:
- 關聯 (association) — 這一拍的量測屬於哪一條航跡
- 濾波 (filtering) — 用量測修正狀態估計
- 外推 (coasting) — 沒有量測時靠模型撐住
- 起始與終結 — 何時開一條新航跡、何時放棄
本章用最簡單的 alpha-beta 濾波器(等速模型的常增益近似):
預測 x⁻ = x + v·dt
更新 x = x⁻ + α·(z − x⁻)
v = v + (β/dt)·(z − x⁻)
α、β 決定「信量測」還是「信模型」。
連續外推超過門檻(本章取 30 秒)就宣告航跡丟失,下一個量測要重新起始。
2.8 量測精度隨 SNR 惡化
這是最容易被忽略的一點。干擾與雜波不只讓你看不到,
還讓看得到的那些拍變不準:
距離誤差 σ_R = c / ( 2·B·sqrt(2·SNR) ) B = 1/脈寬
角度誤差 σ_θ = θ_3dB / ( 1.6·sqrt(2·SNR) )
橫向位置誤差 = σ_θ × 距離,在遠距離時遠大於距離誤差。
本章的 4 m 天線在 3 GHz 波束寬 1.75°,在 150 km 處 1° 就是 2.6 km ——
所以雙基地雷達的定位精度幾乎完全由角度誤差決定。
本章實測(alpha-beta 濾波 vs STK 真值):
| 情境 | 偵測率 | 航跡存活 | RMS 位置誤差 |
|---|---|---|---|
| 乾淨 | 100.0% | 100.0% | 189 m |
| 只有雜波 | 100.0% | 100.0% | 196 m |
| 只有干擾(20 W) | 83.2% | 95.7% | 918 m |
| 干擾+雜波 | 82.9% | 95.7% | 920 m |
誤差變成 4.9 倍,而偵測率只掉了 17 個百分點。
只看偵測率會嚴重低估干擾的實際危害。
3. 名詞與符號
| 符號 | 意義 | 單位 | 本章值 |
|---|---|---|---|
Rt / Rr |
發射站/接收站到目標的距離 | km | 111–216 / 59–197 |
R |
雙基地距離和 Rt+Rr |
km | 186–360 |
L |
基線長度 | km | 78 |
beta |
雙基地角 | ° | 3.3–42.8 |
σ |
雷達截面積 | m² / dBsm | 10 m² (10 dBsm) |
σ⁰ |
雜波反射係數 | — | 0.05 |
SNR1 |
單脈衝訊雜比 | dB | — |
Pfa |
虛警機率 | — | 1e-6 |
PDet |
偵測機率 | — | 判準 ≥ 0.8 |
J/S |
干擾訊號比 | dB | −66 至 +21 |
C/N |
雜波雜訊比 | dB | −91 至 −1.6 |
θ_3dB |
天線半功率波束寬 | ° | 1.75 |
| PRF | 脈衝重複頻率 | Hz | 2000 |
4. 觀念 → 本章實驗對照
| 觀念 | 出現在哪裡 |
|---|---|
| 雙基地拓撲、波形、天線 | bistatic_radar_Connect.txt |
| Connect 做不到的三件事 | bistatic_build.py |
| Cassini 幾何、距離乘積 | bistatic_tracking.py Part A |
| PDet 四情境 | 同上 Part B |
| 航跡連續性 | 同上 Part C |
| alpha-beta 濾波 vs 真值 | 同上 Part D |
| 干擾功率與雜波係數掃描 | bistatic_sweep.py |
| 三層靜默失敗的檢查 | bistatic_verify.py(62 項全 PASS) |
5. 本章踩到的坑(全部是實測,不是教科書上的)
雷達模組的靜默失敗分三層,一層比一層難抓:
第 1 層:指令被拒絕(有 NACK,最好抓)
Radar ... SetValue Model "Bistatic Transmitter"會 NACK。
GUI 與物件模型用有空格的名稱,Connect 用底線版Bistatic_Transmitter。
同理Bistatic_Receiver、Single_Point、Constant_Coefficient。- 雜波屬性樹在
Model.ClutterEnabled = false時整棵是(Read-only)。
要先開啟才寫得進係數,順序顛倒就 NACK。
第 2 層:ACK 了卻沒生效(回讀抓得到)
- 天線設計頻率會被沖掉。 先設
Model.AntennaControl.Antenna Parabolic
再設DesignFrequency;順序反了,頻率會回到 14.5 GHz 預設值,沒有錯誤訊息。
(本教材 Week 2 與 Week 11 踩過同一個坑,雷達這裡一模一樣。) SetPosition */Place/X Geodetic lat lon 10.0抬不高 Place 物件。
指令 ACK,Position.QueryPlanetodeticArray()回讀顯示 10.0 km,
但幾何上它仍貼在地面,對 148 km 外的地面站完全沒有 access。
要抬高必須用SetHeightAboveGround——而那個指令設完之後,
回讀反而顯示 0.0。
回讀本身會騙你:一個顯示 10 km 的物件在地面,一個顯示 0 km 的在 10 km 高空。
本章因此改用 Aircraft 物件當空中干擾機。
第 3 層:ACK、回讀正確、輸出仍然是空的(最難抓)
- 雜波幾何用
Smooth_Oblate_Earth,雙基地接收端算不出雜波。
Connect ACK、Radar_RM回讀一字不差、GUI 也顯示正確,
但S/T Clutter Power全部是 −9999.99(STK 的「沒有計算」哨兵值)。
只有Single_Point這個模型在雙基地下算得出來。
Range_Over_CFAR_Cells同樣算不出來。
要抓到這一層,驗證必須一路做到「資料提供者有沒有真的吐出數字」。
回讀設定值是不夠的。
- STK 的哨兵值有兩種:
-9999.99(功率、比值類)與3000
(S/T S1/C這類「無限大」的比值)。不過濾就算平均,
你會得到一個看起來很正常、但完全沒有意義的數字。
其他
Model.Receiver.LNABandwidth改了不影響 SNR。 雜訊功率確實跟著變
(30 MHz → 1 MHz,雜訊從 −129 掉到 −144 dBW),但訊號功率同步變化,
SNR 不動。STK 在這裡是自洽的 —— 不要用它來「調高 SNR」。- 改設定後一定要
ClearAllAccess /。 否則資料提供者回的是上一組設定的結果,
數字看起來完全正常。 - 雜波係數在 Connect 是線性值(0.05),在物件模型是 dB(−13.01)。
同一個量、兩個介面、兩種單位。
6. 延伸閱讀
官方範例與說明
- AGI 訓練單元:Radar Tracking and Jamming
— 本章的參考範例(單基地版),含完整 GUI 操作步驟。 - Ansys STK 線上說明 — Radar 模組。
本機:C:\Program Files\AGI\STK 12\Help\Content\comm\CommRadar.htm
(雙基地說明在同資料夾的BistaticTransmitter.htm/BistaticReceiver.htm,
雜波在RadarClutter.htm)。 - Connect 指令的完整參考在本機:
C:\Program Files\AGI\STK 12\Help\Programming\Subsystems\connectCmds\Content\
(cmd_Radar.htm、cmd_RadarClutter.htm、cmd_RCS.htm、cmd_PointTargeted.htm…)。
這份離線文件比線上搜尋好用很多。 - STK Developer Kit 說明 — 物件模型 API。
雷達原理
- Wikipedia: Bistatic radar
— 雙基地幾何、Cassini 卵形線、前向散射。 - Wikipedia: Radar cross-section
- Wikipedia: Radar jamming and deception
- Wikipedia: Constant false alarm rate
— CFAR 是雜波環境下維持虛警率的標準手段。 - MIT Lincoln Laboratory:Introduction to Radar Systems 公開課程
— 完整的雷達系統入門課,投影片免費。 - 教科書:Skolnik, Introduction to Radar Systems(本章的量測精度公式出處);
Willis, Bistatic Radar(雙基地的標準參考書);
Richards, Fundamentals of Radar Signal Processing(偵測與 CFAR)。
追蹤與資料融合
- Wikipedia: Alpha beta filter
- Wikipedia: Kalman filter
- 教科書:Bar-Shalom, Estimation with Applications to Tracking and Navigation
(航跡起始、關聯、IMM 的標準參考書)。
工具
- NumPy · SciPy ·
matplotlib
7. 課前自我檢核
- 雙基地雷達的等訊號強度輪廓為什麼不是圓?畫出
Rt·Rr = 常數大致的樣子。 - 為什麼一部 20 W 的干擾機打得贏一部 100 kW 的雷達?寫出兩者的距離依賴關係。
- 加大發射功率對「雜訊」與「雜波」的效果分別是什麼?為什麼不同?
- 偵測率從 100% 掉到 83%,定位誤差卻變成 4.9 倍。這兩件事為什麼不成比例?
- 一個設定「Connect ACK 了、
Radar_RM回讀也正確」。它可能還是沒生效嗎?
本章的哪一個例子證明了這件事?你要再檢查什麼才算數?
圖

radar_bistatic/bistatic_anim.gif

radar_bistatic/bistatic_sweep.png
![]()
radar_bistatic/bistatic_tracking.png
程式碼
# -*- coding: utf-8 -*-
# ==============================================================================
# 雙基地雷達 — 展示動畫
#
# 設計目標:**投影出來三公尺外看得懂**,不是資訊完整。
# 所以只講一件事:同一條航跡、同一部雷達,打開 20 W 干擾機之後
# 偵測開始中斷、alpha-beta 濾波的估計誤差擴大。
#
# 兩格:
# 上 平面航跡 —— 真值航跡、目前位置、兩種情境的濾波估計
# 下 偵測機率對時間 —— 乾淨 vs 干擾,加偵測門檻與游標
#
# 資料只抓一次(兩個情境各一趟),之後純離線畫圖。
#
# 執行:python bistatic_animate.py --pid <PID>
# ==============================================================================
import os
import sys
import matplotlib
import numpy as np
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, os.pardir, "tools"))
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
from cjk import use_cjk # noqa: E402
use_cjk()
from matplotlib.animation import FuncAnimation, PillowWriter # noqa: E402
import bistatic_build as B # noqa: E402
import bistatic_tracking as T # noqa: E402
OUT = os.path.join(HERE, "bistatic_anim.gif")
FPS = 4
# 簡報動畫的長度目標是 10-15 秒。全解析度是 280 格 = 70 秒,太長。
# 只抽稀「游標的落點」,曲線與航跡仍然用全解析度畫。
MAX_FRAMES = 56
def _pid():
if "--pid" in sys.argv:
i = sys.argv.index("--pid")
if i + 1 < len(sys.argv):
return int(sys.argv[i + 1])
return None
def gather(root):
"""兩個情境:乾淨、只有干擾。各抓一次。"""
out = {}
for label, jam, clut in (("clean", False, False),
("jammed", True, False)):
T.configure(root, jam, clut)
st = T.fetch(root, "Radar SearchTrack", T.ST_ELEMENTS)
key = ("S/T Integrated PDet w/Jamming" if jam
else "S/T Integrated PDet")
pdet = T.clean(st.get(key, st["S/T Integrated PDet"]))
pdet = np.where(np.isfinite(pdet), pdet,
T.clean(st["S/T Integrated PDet"]))
out[label] = dict(t=st["Time"], pdet=pdet,
snr=T.clean(st["S/T Integrated SNR"]))
t_tr, xyz = T.truth_track(root)
out["truth"] = dict(t=t_tr, xyz=xyz)
return out
def render(data, out=OUT):
tr = data["truth"]
xyz = tr["xyz"]
# 用地固座標的水平投影當「平面圖」—— 展示用,不需要投影法的嚴謹
x = (xyz[:, 0] - xyz[0, 0]) / 1000.0
y = (xyz[:, 1] - xyz[0, 1]) / 1000.0
tc, pc = data["clean"]["t"], data["clean"]["pdet"]
tj, pj = data["jammed"]["t"], data["jammed"]["pdet"]
n = min(len(x), len(tc), len(tj))
x, y = x[:n], y[:n]
tc, pc, pj = tc[:n], pc[:n], pj[:n]
fig = plt.figure(figsize=(11, 7.2))
gs = fig.add_gridspec(2, 1, height_ratios=[1.25, 1.0], hspace=0.32,
left=0.10, right=0.97, top=0.86, bottom=0.10)
ax_m, ax_p = fig.add_subplot(gs[0]), fig.add_subplot(gs[1])
ax_m.plot(x, y, lw=1.8, color="0.75", label="truth track")
seen, = ax_m.plot([], [], lw=3.0, color="#1E6F6B")
lost, = ax_m.plot([], [], "x", ms=11, mew=2.6, color="#AC3427",
linestyle="none")
now, = ax_m.plot([], [], "o", ms=14, color="#1B5E9E", zorder=5)
# 平面航跡圖:x 與 y 都是公里,不等比例會讓航向角失真
ax_m.set_aspect("equal", adjustable="datalim")
ax_m.set_xlabel("east (km)", fontsize=11)
ax_m.set_ylabel("north (km)", fontsize=11)
ax_m.grid(alpha=.25)
ax_m.tick_params(labelsize=10)
ax_m.legend(fontsize=10, loc="upper left")
ax_p.plot(tc / 60.0, pc, lw=2.4, color="#1E6F6B", label="clean")
ax_p.plot(tc / 60.0, pj, lw=2.4, color="#AC3427",
label="with 20 W jammer")
ax_p.axhline(T.PDET_MIN, ls="--", lw=1.3, color="0.4",
label="detection threshold %.1f" % T.PDET_MIN)
cur, = ax_p.plot([], [], "o", ms=11, color="#AC3427", zorder=5)
vline = ax_p.axvline(tc[0] / 60.0, color="0.5", lw=1.2, ls=":")
ax_p.set_xlim(tc[0] / 60.0, tc[-1] / 60.0)
ax_p.set_ylim(-0.05, 1.08)
ax_p.set_xlabel("time (min)", fontsize=11)
ax_p.set_ylabel("integrated PDet", fontsize=11)
# 右下角:左下正好是兩次偵測中斷的位置,圖例會蓋住整章的重點
ax_p.legend(fontsize=10, loc="lower right")
ax_p.grid(alpha=.25)
ax_p.tick_params(labelsize=10)
title = fig.text(0.5, 0.955, "", ha="center", fontsize=17, weight="bold")
read = fig.text(0.5, 0.905, "", ha="center", fontsize=13, color="#333333")
def frame(i):
seen.set_data(x[:i + 1], y[:i + 1])
bad = [j for j in range(i + 1)
if np.isfinite(pj[j]) and pj[j] < T.PDET_MIN]
lost.set_data(x[bad], y[bad])
now.set_data([x[i]], [y[i]])
cur.set_data([tc[i] / 60.0], [pj[i] if np.isfinite(pj[i]) else 0.0])
vline.set_xdata([tc[i] / 60.0, tc[i] / 60.0])
title.set_text("雙基地雷達:20 W 干擾機如何打斷航跡")
state = ("偵測中斷" if (np.isfinite(pj[i]) and pj[i] < T.PDET_MIN)
else "追蹤中")
read.set_text("t = %5.1f min 乾淨 PDet %.2f 干擾 PDet %.2f %s"
% (tc[i] / 60.0,
pc[i] if np.isfinite(pc[i]) else float("nan"),
pj[i] if np.isfinite(pj[i]) else float("nan"),
state))
return seen, lost, now, cur, vline, title, read
# 抽稀成 MAX_FRAMES 格。用 linspace 取索引而不是固定步長,
# 這樣頭尾一定會被取到 —— 固定步長常常把最後一格漏掉。
idx = np.unique(np.linspace(0, n - 1, min(MAX_FRAMES, n)).astype(int))
anim = FuncAnimation(fig, lambda k: frame(idx[k]), frames=len(idx),
blit=False)
anim.save(out, writer=PillowWriter(fps=FPS))
plt.close(fig)
return out, len(idx)
def main():
app, root = B.build(jammer_power_w=20.0)[:2]
print("\n--- 抓資料(乾淨 / 干擾 各一趟)---")
data = gather(root)
print(" 真值航跡 %d 點" % len(data["truth"]["xyz"]))
print("\n--- 畫動畫 ---")
out, n = render(data)
print(" %d 格 @ %d fps -> %s (%.0f KB)"
% (n, FPS, out, os.path.getsize(out) / 1024.0))
return 0
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
# ==============================================================================
# 地面雙基地雷達 — 場景建構
#
# 分工:
# bistatic_radar_Connect.txt Connect 做得到的一切(建物件、波形、天線、雜波)
# 本檔 Connect 做不到的三件事,用物件模型補上
#
# Connect 做不到的三件事(STK 12.10 實測):
# 1. Model.BistaticTransmitterSelectionStrategy.BistaticTransmitters.ObjectList
# —— 讀得到(GetValue OK),寫不進去。雙基地系統的核心連結只能用物件模型。
# 2. Model.Jamming.Jammers.ObjectList
# —— 同上。Enabled 可以用 Connect 設,清單內容不行。
# 3. Model.Mode.Waveform.PulseIntegration 的策略切換
# —— 各種名稱都 NACK;要用 Waveform.PulseIntegrationType 這個列舉。
#
# 這正是本章的第一個教學點:**Connect 與物件模型不是二選一,是分工。**
# 只用 Connect 建不出一套會動的雙基地雷達。
#
# 執行:
# 1. 開啟 STK 12 桌面版(無介面的 Engine 也可以,本章沒有用到繪圖指令)
# 2. python bistatic_build.py
# ==============================================================================
import os
import sys
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
sys.path.insert(0, os.path.join(HERE, os.pardir, "tools"))
from agi.stk12.stkdesktop import STKDesktop # noqa: E402
SCRIPT = os.path.join(HERE, "bistatic_radar_Connect.txt")
TXR = "*/Place/TxSite/Sensor/TxServo/Radar/TxRadar"
RXR = "*/Place/RxSite/Sensor/RxServo/Radar/RxRadar"
JMR = "*/Aircraft/JamAir/Sensor/JamServo/Radar/Jammer"
TGT = "*/Aircraft/Track01"
# 脈衝積分策略的列舉值(AgERadarPulseIntegrationType)
PULSE_INTEGRATION_GOAL_SNR = 0
PULSE_INTEGRATION_FIXED_PULSE_NUMBER = 1
PULSES = 20 # 每次偵測積分的脈衝數
def connect(root, path, echo_fail=True):
"""逐行送出 Connect 腳本,回報 ACK / NACK。"""
ok = fail = 0
for n, line in enumerate(open(path, encoding="utf-8", errors="replace"), 1):
cmd = line.strip()
if not cmd or cmd.startswith("#"):
continue
try:
root.ExecuteCommand(cmd)
ok += 1
except Exception:
fail += 1
if echo_fail:
print(" NACK L%-4d %s" % (n, cmd[:100]))
print("%s: %d 條 ACK, %d 條 NACK" % (os.path.basename(path), ok, fail))
return ok, fail
def build(jammer_power_w=None, pulses=PULSES):
"""建好整個場景。回傳 (app, root) —— app 一定要留住,否則 COM 介面會失效。"""
app = STKDesktop.AttachToApplication()
root = app.Root
if root.CurrentScenario is not None:
root.CloseScenario()
ack, nack = connect(root, SCRIPT)
rx = root.GetObjectFromPath(RXR)
tx = root.GetObjectFromPath(TXR)
jm = root.GetObjectFromPath(JMR)
# (1) 雙基地連結:告訴接收機「照亮目標的是哪一具發射機」。
# 沒有這一步,接收端的 Search/Track 完全算不出訊號。
rx.Model.BistaticTransmitters.RemoveAll()
rx.Model.BistaticTransmitters.AddObject(tx)
# (2) 干擾清單
rx.Model.Jamming.Enabled = True
rx.Model.Jamming.Jammers.RemoveAll()
rx.Model.Jamming.Jammers.AddObject(jm)
# (3) 脈衝積分:預設的 Goal SNR 會「積到達成目標 SNR 為止」,
# 於是 Integrated SNR 永遠等於那個目標值,PDet 反而被偵測門檻壓死。
# 要看真實的偵測表現,必須改成固定脈衝數。
wf = rx.Model.Mode.Waveform
wf.PulseIntegrationType = PULSE_INTEGRATION_FIXED_PULSE_NUMBER
wf.PulseIntegration.PulseNumber = pulses
if jammer_power_w is not None:
root.ExecuteCommand("Radar %s SetValue Model.Transmitter.Power %g W"
% (JMR, jammer_power_w))
# 設定改過之後一定要清掉舊的 access 快取,否則後面取到的是舊結果。
root.ExecuteCommand("ClearAllAccess /")
return app, root, ack, nack
def main():
app, root, ack, nack = build()
rx = root.GetObjectFromPath(RXR)
print("\n雙基地連結 BistaticTransmitters =", rx.Model.BistaticTransmitters.Count)
print("干擾清單 Jammers =", rx.Model.Jamming.Jammers.Count)
print("脈衝積分 type=%s / %d 個脈衝"
% (rx.Model.Mode.Waveform.PulseIntegrationType,
rx.Model.Mode.Waveform.PulseIntegration.PulseNumber))
acc = rx.GetAccessToObject(root.GetObjectFromPath(TGT))
acc.ComputeAccess()
n = acc.ComputedAccessIntervalTimes.Count
print("接收雷達對目標的 access 區間數 =", n)
return 0
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
"""獨立章節 1 的簡報內容。純資料,不 import STK。"""
DECK = dict(
eyebrow="獨立章節 1 · STK Radar(雙基地)",
title="二十瓦打斷十萬瓦",
subtitle="地面雙基地雷達:干擾、雜波與航跡穩定度",
thesis="一部 20 W 的空中干擾機,足以讓 100 kW 的雙基地雷達失去航跡 —— "
"因為干擾走單程,雷達走雙程。",
scenario=[
("發射站 / 接收站", "分離 78 km 基線,雙基地構型"),
("雷達", "100 kW 發射,單基地 RCS 10 m²(10 dBsm)"),
("目標", "9 km 高空飛行器,已知真值航跡"),
("干擾機", "空中平台,0.1 W – 2000 W 掃描"),
("雜波", "Single Point 幾何,反射係數 0.01 – 5000 掃描"),
("追蹤", "alpha-beta 濾波,量測 σ 由 SNR 推導(Skolnik 一階)"),
],
focus=[
"雙基地的等訊號輪廓是 Cassini 卵形線,不是圓 —— 距離項是 (Rt·Rr)²。",
"干擾走單程、雷達走雙程,所以干擾機的功率優勢是巨大的。",
"雜波與雜訊完全不同:加發射功率對雜波無效,訊號與雜波等比例增加。",
"航跡穩定度才是工程判準 —— 偵測率高不代表航跡活得下來。",
],
animation="bistatic_anim.gif",
animation_title="動畫:干擾如何吃掉航跡",
animation_caption="目標沿真值航跡飛行;干擾開啟後偵測中斷、"
"alpha-beta 濾波的估計誤差擴大",
figures=[
("bistatic_tracking.png",
"航跡穩定度 —— 四種構型(乾淨 / 只有雜波 / 只有干擾 / 干擾+雜波)"),
("bistatic_sweep.png",
"掃描 —— 干擾功率與雜波反射係數各自的門檻"),
],
results_caption="全部為 STK 12.10.0 實測",
results=[
("情境", "偵測率", "航跡存活", "RMS 位置誤差"),
("乾淨", "100.0%", "100.0%", "189 m"),
("只有雜波", "100.0%", "100.0%", "196 m"),
("只有干擾(20 W)", "83.2%", "95.7%", "918 m"),
("干擾 + 雜波", "82.9%", "95.7%", "920 m"),
("", "", "", ""),
("干擾功率掃描", "偵測率", "最長中斷", ""),
("1 W", "98.2%", "10 s", ""),
("10 W", "89.6%", "40 s", "← 打穿門檻"),
("50 W", "61.4%", "160 s", ""),
("2000 W", "10.4%", "225 s", ""),
],
findings=[
"20 W 干擾機讓 RMS 定位誤差從 189 m 變成 918 m —— **4.9 倍**,"
"而偵測率只從 100% 掉到 83%。偵測率是誤導性的指標。",
"雜波在這個構型下幾乎無害:主瓣確實掃到地面,但 C/N 最高只有 −1.6 dB。"
"目標在 9 km 高空,掠射角大,雜波面積小。",
"雙基地的雜波幾何只有 Single_Point 算得出來 —— Smooth_Oblate_Earth "
"在雙基地構型下靜默地什麼都不產出,已寫成一項反向驗證檢查。",
"Bistatic Transmitter 的 Connect 指令要寫成 Bistatic_Transmitter(底線),"
"而干擾機與雙基地發射機清單只能用物件模型 AddObject。",
"Goal SNR 的脈衝積分會壓垮 PDet —— 要用 Fixed Pulse Number。",
],
verify=(62, 0, 0),
verify_note=[
"62 項涵蓋:Connect 設過的每個值回讀比對、雙基地與干擾機清單的結構、"
"Cassini 幾何的物理判準,以及一項反向檢查 ——",
"「Smooth_Oblate_Earth 在雙基地下算不出雜波」寫成預期成立的斷言;"
"哪天 STK 修好了,那一項會 FAIL,提醒我們重寫結論。",
],
run=[
"python bistatic_build.py --pid <PID> 建場景(72 條 Connect,0 NACK)",
"python bistatic_tracking.py --pid <PID> Part A–D + 航跡圖",
"python bistatic_animate.py --pid <PID> 展示動畫",
"python bistatic_sweep.py --pid <PID> 干擾與雜波掃描(數分鐘)",
"python bistatic_verify.py --pid <PID> 驗證段(62 項)",
],
files=[
("bistatic_radar_Connect.txt", "Connect 腳本(72 條,0 NACK)"),
("bistatic_build.py", "雙基地與干擾機清單(物件模型)"),
("bistatic_tracking.py", "Part A–D:幾何、干擾、雜波、航跡穩定度"),
("bistatic_animate.py", "展示動畫"),
("bistatic_sweep.py", "干擾功率與雜波係數掃描"),
("bistatic_verify.py", "驗證段(62 項全 PASS)"),
("RadarBistatic_Primer.md", "基礎知識導論"),
("RadarBistatic_Lab_Handout.md", "實驗單"),
],
)
# ==============================================================================
# 地面雙基地雷達 — 場景建構 Connect 腳本
#
# 這支腳本負責「Connect 做得到」的部分:建物件、擺位置、設波形、設天線、
# 開雜波。三件 Connect 做不到的事留給 bistatic_build.py 用物件模型補:
#
# 1. 把發射雷達加進接收雷達的 BistaticTransmitters 清單
# 2. 把干擾機加進接收雷達的 Jammers 清單
# 3. 把脈衝積分策略從 Goal SNR 改成 Fixed Pulse Number
#
# 這三件事在 STK 12.10 都沒有可用的 Connect 屬性路徑(實測 NACK),
# 詳見 RadarBistatic_Primer.md 第 5 節。
#
# 執行:python bistatic_build.py(它會先送這支腳本,再補上述三件事)
# ==============================================================================
# ---- 場景 --------------------------------------------------------------------
New / Scenario BistaticTrack
SetAnalysisTimePeriod * "1 Jun 2024 02:00:00.000" "1 Jun 2024 02:30:00.000"
SetUnits / km
# ---- 真值航跡 ----------------------------------------------------------------
# 大圓航路 + 明確航點 = 完全確定的真值。之後所有「追得準不準」都是拿這條當基準。
New / */Aircraft Track01
SetPropagator */Aircraft/Track01 GreatArc
Waypoints */Aircraft/Track01 Clear
AddWaypoint */Aircraft/Track01 DetTimeAccFromVel 26.00 119.90 9.144 0.25
AddWaypoint */Aircraft/Track01 DetTimeAccFromVel 24.60 119.70 9.144 0.25
AddWaypoint */Aircraft/Track01 DetTimeAccFromVel 23.00 120.40 9.144 0.25
Propagate */Aircraft/Track01
# 目標 RCS:10 m^2 (10 dBsm),中型機的量級。
# 注意 Inherit 要先關掉,否則物件會沿用場景層級的值,你設的數字不會生效。
RCS */Aircraft/Track01 SetValue Inherit false
RCS */Aircraft/Track01 SetValue Model.FrequencyBandList[0].ComputeTypeStrategy.ConstantValue 10.0 sqm
# ---- 干擾機:空中支援干擾(stand-off jammer)---------------------------------
# 為什麼用 Aircraft 而不用 Place:
# Place 想抬高只能用 SetHeightAboveGround。用 SetPosition ... 10.0 設高度會
# ACK、回讀也顯示 10 km,但幾何上仍然貼在地面,對地面站完全沒有 access。
# 這是本章最典型的「ACK 不等於生效,而且回讀還會騙你」案例。
New / */Aircraft JamAir
SetPropagator */Aircraft/JamAir GreatArc
Waypoints */Aircraft/JamAir Clear
AddWaypoint */Aircraft/JamAir DetTimeAccFromVel 24.90 118.90 10.0 0.20
AddWaypoint */Aircraft/JamAir DetTimeAccFromVel 23.70 118.90 10.0 0.20
AddWaypoint */Aircraft/JamAir DetTimeAccFromVel 24.90 118.90 10.0 0.20
Propagate */Aircraft/JamAir
# ---- 兩個地面站 --------------------------------------------------------------
New / */Place TxSite
SetPosition */Place/TxSite Geodetic 24.90 120.85 0.0
New / */Place RxSite
SetPosition */Place/RxSite Geodetic 24.30 120.45 0.0
# ---- 伺服機構:用 Sensor 當天線的轉台 ----------------------------------------
# Radar 物件的天線指向父物件的 Z 體軸。把 Radar 掛在一個「Targeted」的
# Sensor 底下,天線就會跟著轉台鎖定目標。
# 錐角只影響可見性判定,不影響指向;這裡取 3 度(略大於天線波束)。
New / */Place/TxSite/Sensor TxServo
Define */Place/TxSite/Sensor/TxServo SimpleCone 3.0
Point */Place/TxSite/Sensor/TxServo Targeted Add Aircraft/Track01
Point */Place/TxSite/Sensor/TxServo Targeted Tracking Aircraft/Track01
Location */Place/TxSite/Sensor/TxServo Fixed Cartesian 0.0 0.0 -15.0
New / */Place/RxSite/Sensor RxServo
Define */Place/RxSite/Sensor/RxServo SimpleCone 3.0
Point */Place/RxSite/Sensor/RxServo Targeted Add Aircraft/Track01
Point */Place/RxSite/Sensor/RxServo Targeted Tracking Aircraft/Track01
Location */Place/RxSite/Sensor/RxServo Fixed Cartesian 0.0 0.0 -15.0
New / */Aircraft/JamAir/Sensor JamServo
Define */Aircraft/JamAir/Sensor/JamServo SimpleCone 10.0
Point */Aircraft/JamAir/Sensor/JamServo Targeted Add Place/RxSite
Point */Aircraft/JamAir/Sensor/JamServo Targeted Tracking Place/RxSite
# ---- 三具雷達 ----------------------------------------------------------------
# Connect 的型別名稱是底線版:Bistatic_Transmitter / Bistatic_Receiver。
# GUI 與物件模型用的是有空格的 "Bistatic Transmitter",Connect 送空格版會 NACK。
New / */Place/TxSite/Sensor/TxServo/Radar TxRadar
New / */Place/RxSite/Sensor/RxServo/Radar RxRadar
New / */Aircraft/JamAir/Sensor/JamServo/Radar Jammer
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model Bistatic_Transmitter
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model Bistatic_Receiver
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model Monostatic
# ---- 天線 --------------------------------------------------------------------
# 順序很重要:先設天線型別,再設設計頻率。
# 反過來寫,設定頻率會被換型別的動作沖回 14.5 GHz 預設值,而且不會有任何錯誤訊息。
# (這個坑在本教材的 Week 2 與 Week 11 都踩過,雷達這裡一模一樣。)
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.AntennaControl.Antenna Parabolic
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.AntennaControl.Antenna.InputType "Use Diameter"
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.AntennaControl.Antenna.Diameter 4.0 m
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.AntennaControl.Antenna.DesignFrequency 3.0 GHz
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.AntennaControl.Antenna Parabolic
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.AntennaControl.Antenna.InputType "Use Diameter"
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.AntennaControl.Antenna.Diameter 4.0 m
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.AntennaControl.Antenna.DesignFrequency 3.0 GHz
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.AntennaControl.Antenna Parabolic
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.AntennaControl.Antenna.InputType "Use Diameter"
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.AntennaControl.Antenna.Diameter 1.00 m
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.AntennaControl.Antenna.DesignFrequency 3.0 GHz
# ---- 發射端:功率與波形 ------------------------------------------------------
# 雙基地系統的脈衝定義(PRF / 脈寬 / 脈衝數)在發射端,
# 積分與偵測門檻(PulseIntegration / Pfa)在接收端。兩邊不要設錯地方。
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.Transmitter.Power 100000 W
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.Transmitter.Frequency 3.0 GHz
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.Mode.Waveform.PulseDefinition.PRF 2000 Hz
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.Mode.Waveform.PulseDefinition.PulseWidth 1e-6 sec
Radar */Place/TxSite/Sensor/TxServo/Radar/TxRadar SetValue Model.Mode.Waveform.PulseDefinition.NumberOfPulses 20
# ---- 接收端 ------------------------------------------------------------------
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Receiver.Frequency 3.0 GHz
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Receiver.ConstantSystemNoiseTemp 290 K
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Mode.Waveform.ProbabilityOfDetection.ProbabilityFalseAlarm 1e-6
# ---- 干擾機發射參數 ----------------------------------------------------------
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.Transmitter.Power 20 W
Radar */Aircraft/JamAir/Sensor/JamServo/Radar/Jammer SetValue Model.Transmitter.Frequency 3.0 GHz
# ---- 干擾與雜波的開關(清單內容要用物件模型加)------------------------------
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Jamming.Enabled true
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.ClutterEnabled true
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Clutter.PointProviderList[0].PointProvider Single_Point
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Clutter.PointProviderList[0].PointProvider.ScatteringPointModel Constant_Coefficient
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Clutter.PointProviderList[0].PointProvider.ScatteringPointModel.ClutterCoefficient 0.05
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Mode.ClutterFilters.EnableMainLobeClutter true
Radar */Place/RxSite/Sensor/RxServo/Radar/RxRadar SetValue Model.Mode.ClutterFilters.EnableSideLobeClutter true
# -*- coding: utf-8 -*-
# ==============================================================================
# 地面雙基地雷達 — 兩個掃描:干擾機功率、雜波反射係數
#
# bistatic_tracking.py 回答「這一組參數下追不追得住」。
# 本檔回答設計者真正要問的兩個問題:
#
# 1. 干擾機要多大功率才能打斷航跡?(反過來說:雷達要多少餘裕才撐得住)
# 2. 雜波反射係數要多大才會開始咬人?
#
# 這兩條曲線的形狀比單點結果重要:平原代表設計耐受,懸崖代表脆弱。
#
# 執行(需先跑過 bistatic_build.py):
# python bistatic_sweep.py
# ==============================================================================
import os
import sys
import matplotlib
import numpy as np
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
sys.path.insert(0, HERE)
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
from agi.stk12.stkdesktop import STKDesktop # noqa: E402
from bistatic_tracking import ( # noqa: E402
RXR, STEP, PDET_MIN, ST_ELEMENTS, clean, configure, continuity, fetch)
JMR = "*/Aircraft/JamAir/Sensor/JamServo/Radar/Jammer"
CLUT = ("Radar %s SetValue Model.Clutter.PointProviderList[0]"
".PointProvider.ScatteringPointModel.ClutterCoefficient %g")
JAM_POWERS = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 200.0, 2000.0] # W
CLUT_COEFFS = [0.01, 0.05, 0.5, 5.0, 50.0, 500.0, 5000.0] # 線性反射係數
def measure(root, pdet_element):
st = fetch(root, "Radar SearchTrack", ST_ELEMENTS)
p = clean(st[pdet_element])
rate, longest, ngap = continuity(p)
return rate, longest, ngap, np.nanmean(p)
def sweep_jammer(root):
print("=" * 74)
print("掃描 1 干擾機功率(雜波關閉,單獨看干擾)")
print("=" * 74)
print(" %10s %10s %13s %10s %s"
% ("功率 (W)", "偵測率", "最長中斷(s)", "中斷次數", "航跡"))
rows = []
for p in JAM_POWERS:
root.ExecuteCommand("Radar %s SetValue Model.Transmitter.Power %g W" % (JMR, p))
configure(root, True, False)
rate, longest, ngap, mean = measure(root, "S/T Integrated PDet w/Jamming")
rows.append((p, rate, longest, ngap))
print(" %10g %9.1f%% %13.0f %10d %s"
% (p, rate * 100, longest, ngap, "維持" if longest <= 30 else "丟失"))
return rows
def sweep_clutter(root):
print()
print("=" * 74)
print("掃描 2 雜波反射係數(干擾關閉,單獨看雜波)")
print("=" * 74)
print(" %10s %10s %13s %10s %s"
% ("反射係數", "偵測率", "最長中斷(s)", "中斷次數", "航跡"))
rows = []
for c in CLUT_COEFFS:
# 順序不能顛倒:ClutterEnabled 是 false 的時候,整棵雜波屬性樹都是
# (Read-only),SetValue 會 NACK。要先開啟才寫得進去。
configure(root, False, True)
root.ExecuteCommand(CLUT % (RXR, c))
root.ExecuteCommand("ClearAllAccess /")
rate, longest, ngap, mean = measure(root, "S/T Integrated PDet w/J+C")
rows.append((c, rate, longest, ngap))
print(" %10g %9.1f%% %13.0f %10d %s"
% (c, rate * 100, longest, ngap, "維持" if longest <= 30 else "丟失"))
return rows
def plot(jam, clut, path):
fig, ax = plt.subplots(1, 2, figsize=(12, 4.4))
for a, rows, xlabel, title in [
(ax[0], jam, "jammer power (W)", "jammer power sweep"),
(ax[1], clut, "clutter reflection coefficient", "clutter sweep")]:
x = [r[0] for r in rows]
a.semilogx(x, [r[1] * 100 for r in rows], "o-", lw=1.8, color="C0")
a.set_xlabel(xlabel)
a.set_ylabel("detection rate (%)", color="C0")
a.set_ylim(-5, 105)
a.axhline(90, ls="--", c="k", lw=1)
a2 = a.twinx()
a2.semilogx(x, [max(r[2], 1) for r in rows], "s--", lw=1.4, color="C3")
a2.set_yscale("log")
a2.set_ylabel("longest gap (s)", color="C3")
a.set_title(title)
a.grid(alpha=.3, which="both")
fig.tight_layout()
fig.savefig(path, dpi=130)
print("\n圖已存到", path)
def threshold(rows, floor=0.9):
"""找出偵測率跌破 floor 的第一個值,回傳 (前一個安全值, 第一個失敗值)。"""
prev = None
for x, rate, _, _ in rows:
if rate < floor:
return prev, x
prev = x
return prev, None
def main():
app = STKDesktop.AttachToApplication()
root = app.Root
if root.CurrentScenario is None:
print("沒有開啟的場景。請先執行 python bistatic_build.py")
return 1
jam = sweep_jammer(root)
root.ExecuteCommand("Radar %s SetValue Model.Transmitter.Power 2000 W" % JMR)
clut = sweep_clutter(root)
configure(root, True, True) # 還原成預設設定
root.ExecuteCommand(CLUT % (RXR, 0.05))
root.ExecuteCommand("ClearAllAccess /")
plot(jam, clut, os.path.join(HERE, "bistatic_sweep.png"))
print()
print("=" * 74)
print("結論")
print("=" * 74)
ok, bad = threshold(jam)
print(" 干擾機:%s W 還撐得住,%s W 就打穿了(偵測率 < 90%%)。"
% (ok, bad if bad is not None else "掃描範圍內都沒有"))
ok, bad = threshold(clut)
print(" 雜波 :反射係數 %s 還撐得住,%s 就開始咬人。"
% (ok, bad if bad is not None else "掃描範圍內都沒有"))
print()
print(" 兩條曲線的形狀比單點數字重要:")
print(" 平坦的一段代表設計對該參數不敏感,陡降的一段代表那裡是懸崖。")
return 0
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
# ==============================================================================
# 地面雙基地雷達 — 能不能穩定追蹤已知真值航跡?
#
# 「看得到」跟「追得住」是兩件事。本檔把它拆成四段:
#
# Part A 幾何:雙基地距離和、雙基地角、距離乘積(Cassini 卵形線)
# Part B 偵測:四種情境的 PDet
# Part C 航跡連續性:偵測率、最長中斷、中斷次數
# Part D 追蹤濾波:用 STK 的 SNR 產生量測雜訊,跑 alpha-beta 濾波器,
# 與 STK 給的真值逐點比對,算出真正的定位誤差
#
# Part D 才是本章的重點。前三段回答「訊號夠不夠」,
# Part D 回答使用者真正在意的事:**這條航跡的位置資訊可不可信。**
#
# 執行:
# 1. 開啟 STK 12 桌面版
# 2. python bistatic_build.py (先建場景)
# 3. python bistatic_tracking.py
# ==============================================================================
import math
import os
import sys
import matplotlib
import numpy as np
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, os.pardir, "tools"))
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
from agi.stk12.stkdesktop import STKDesktop # noqa: E402
RXR = "*/Place/RxSite/Sensor/RxServo/Radar/RxRadar"
TGT = "*/Aircraft/Track01"
STEP = 5.0 # 取樣間隔 (s) —— 也就是雷達更新率
PDET_MIN = 0.8 # 判定「這一拍有偵測」的門檻
COAST_MAX = 30.0 # 濾波器最多允許外推多久 (s),超過視為航跡丟失
C_LIGHT = 299792458.0
SEED = 20240601 # 固定亂數種子,量測雜訊可重現
# STK 用 -9999.99 / 3000 這類哨兵值表示「這個量沒有計算」。
# 不濾掉的話,平均值會被汙染成無意義的數字 —— 而且不會有任何錯誤訊息。
SENTINEL_LOW = -9000.0
SENTINEL_HIGH = 2999.0
# 四個情境。STK 只提供「乾淨 / 加干擾 / 加干擾+雜波」三個 PDet 元素,
# 沒有「只有雜波」。要把雜波的貢獻單獨拆出來,只能真的把干擾關掉再算一次 ——
# 這就是為什麼下面要逐一切換設定重新計算,而不是一次取數就好。
CASES = [
# 顯示名稱 畫圖用 干擾 雜波 PDet 元素 SNR 元素
("乾淨", "clean", False, False, "S/T Integrated PDet",
"S/T Integrated SNR"),
("只有雜波", "clutter", False, True, "S/T Integrated PDet w/J+C",
"S/T Integrated S/(N+J+C)"),
("只有干擾", "jamming", True, False, "S/T Integrated PDet w/Jamming",
"S/T Integrated S/(N+J)"),
("干擾+雜波", "jam+clut", True, True, "S/T Integrated PDet w/J+C",
"S/T Integrated S/(N+J+C)"),
]
GEO_ELEMENTS = ["Time", "Range Sum", "Bistatic Angle", "Rcvr Range", "Xmtr Range"]
ST_ELEMENTS = ["Time", "S/T Integrated SNR", "S/T Integ. Det. Thresh.",
"S/T Integrated J/S", "S/T C/N",
"S/T Integrated S/(N+J)", "S/T Integrated S/(N+J+C)",
"S/T Integrated PDet", "S/T Integrated PDet w/Jamming",
"S/T Integrated PDet w/J+C"]
# ------------------------------------------------------------------------------
# 取數
# ------------------------------------------------------------------------------
def clean(a):
"""把哨兵值換成 NaN。"""
a = np.asarray(a, dtype=float).copy()
a[(a < SENTINEL_LOW) | (a > SENTINEL_HIGH)] = np.nan
return a
def fetch(root, dp_name, elements, step=STEP):
"""從 Access 的資料提供者取一組元素,回傳 {元素名: numpy 陣列}。"""
rx = root.GetObjectFromPath(RXR)
acc = rx.GetAccessToObject(root.GetObjectFromPath(TGT))
acc.ComputeAccess()
sc = root.CurrentScenario
res = acc.DataProviders.Item(dp_name).ExecElements(
sc.StartTime, sc.StopTime, step, elements)
out = {}
for i in range(res.DataSets.Count):
d = res.DataSets.Item(i)
out[d.ElementName] = np.array(
[v if isinstance(v, (int, float)) else np.nan for v in d.GetValues()],
dtype=float)
return out
def configure(root, jamming, clutter):
"""切換干擾與雜波的開關,並清掉舊的 access 快取。
不清快取的話,拿到的會是上一個設定的結果 —— 而且數字看起來完全正常。
"""
rx = root.GetObjectFromPath(RXR)
rx.Model.Jamming.Enabled = bool(jamming)
root.ExecuteCommand("Radar %s SetValue Model.ClutterEnabled %s"
% (RXR, "true" if clutter else "false"))
root.ExecuteCommand("ClearAllAccess /")
def truth_track(root, step=STEP):
"""從 STK 取目標的真值航跡(地固座標,公尺)。"""
tgt = root.GetObjectFromPath(TGT)
sc = root.CurrentScenario
dp = tgt.DataProviders.Item("Cartesian Position").Group.Item("Fixed")
res = dp.ExecElements(sc.StartTime, sc.StopTime, step, ["Time", "x", "y", "z"])
g = {}
for i in range(res.DataSets.Count):
d = res.DataSets.Item(i)
g[d.ElementName] = np.array(
[v if isinstance(v, (int, float)) else np.nan for v in d.GetValues()],
dtype=float)
# Connect 單位設成 km,Cartesian Position 也回公里 -> 換成公尺
return g["Time"], np.vstack([g["x"], g["y"], g["z"]]).T * 1000.0
def collect(root):
"""逐一切換四個情境並取數。回傳 (t, geo, per_case)。"""
geo = None
per = {}
for name, _, jam, clut, pdet_el, snr_el in CASES:
configure(root, jam, clut)
st = fetch(root, "Radar SearchTrack", ST_ELEMENTS)
if geo is None:
geo = fetch(root, "Radar Geometry", GEO_ELEMENTS)
snr = clean(st.get(snr_el, st["S/T Integrated SNR"]))
# 情境關掉的那一項,對應的 SNR 元素會是空的 -> 退回乾淨 SNR
snr = np.where(np.isfinite(snr), snr, clean(st["S/T Integrated SNR"]))
per[name] = dict(pdet=clean(st[pdet_el]), snr=snr,
thr=clean(st["S/T Integ. Det. Thresh."]),
js=clean(st["S/T Integrated J/S"]),
cn=clean(st["S/T C/N"]))
return st["Time"], geo, per
# ------------------------------------------------------------------------------
# Part A 幾何
# ------------------------------------------------------------------------------
def part_a(geo):
print("=" * 74)
print("Part A 雙基地幾何")
print("=" * 74)
rs, br = clean(geo["Range Sum"]), clean(geo["Bistatic Angle"])
rr, rt = clean(geo["Rcvr Range"]), clean(geo["Xmtr Range"])
print(" 發射站到目標 Rt : %7.1f - %7.1f km" % (np.nanmin(rt), np.nanmax(rt)))
print(" 目標到接收站 Rr : %7.1f - %7.1f km" % (np.nanmin(rr), np.nanmax(rr)))
print(" 雙基地距離和 R : %7.1f - %7.1f km" % (np.nanmin(rs), np.nanmax(rs)))
print(" 雙基地角 beta : %7.2f - %7.2f deg" % (np.nanmin(br), np.nanmax(br)))
print()
print(" 單基地雷達的作用距離由 R^4 決定,雙基地由乘積 (Rt*Rr)^2 決定。")
print(" 等訊號強度的軌跡是 Cassini 卵形線 Rt*Rr = 常數,不是圓。")
prod = rt * rr
print(" 距離乘積 Rt*Rr : %.3g - %.3g km^2,對訊號的影響達 %.1f dB"
% (np.nanmin(prod), np.nanmax(prod),
20 * math.log10(np.nanmax(prod) / np.nanmin(prod))))
return rs, br, rt, rr
# ------------------------------------------------------------------------------
# Part B 偵測
# ------------------------------------------------------------------------------
def part_b(per):
print()
print("=" * 74)
print("Part B 偵測機率(四個情境各自重新計算)")
print("=" * 74)
jc = per["干擾+雜波"]
print(" 乾淨 SNR : %6.2f - %6.2f dB (偵測門檻 %.2f dB)"
% (np.nanmin(per["乾淨"]["snr"]), np.nanmax(per["乾淨"]["snr"]),
np.nanmedian(per["乾淨"]["thr"])))
for label, key in [("干擾訊號比 J/S", "js"), ("雜波雜訊比 C/N", "cn")]:
v = jc[key]
if np.isfinite(v).any():
print(" %-15s : %6.2f - %6.2f dB 有效樣本 %d/%d"
% (label, np.nanmin(v), np.nanmax(v), np.isfinite(v).sum(), v.size))
else:
print(" %-15s : 全部無資料 —— 這一項根本沒有接上,不是「影響很小」" % label)
print()
for name, _, _, _, _, _ in CASES:
p = per[name]["pdet"]
print(" PDet(%-10s): %.4f - %.4f 平均 %.4f"
% (name, np.nanmin(p), np.nanmax(p), np.nanmean(p)))
# ------------------------------------------------------------------------------
# Part C 航跡連續性
# ------------------------------------------------------------------------------
def continuity(pdet, thresh=PDET_MIN):
"""回傳 (偵測率, 最長連續中斷秒數, 中斷次數)。"""
ok = np.isfinite(pdet) & (pdet >= thresh)
gaps, run = [], 0
for v in ok:
if v:
if run:
gaps.append(run)
run = 0
else:
run += 1
if run:
gaps.append(run)
longest = max(gaps) * STEP if gaps else 0.0
return (ok.mean() if ok.size else 0.0), longest, len(gaps)
def part_c(per):
print()
print("=" * 74)
print("Part C 航跡連續性(PDet >= %.1f 視為這一拍有偵測)" % PDET_MIN)
print("=" * 74)
print(" %-11s %9s %13s %9s %s"
% ("情境", "偵測率", "最長中斷(s)", "中斷次數", "航跡是否維持"))
for name, _, _, _, _, _ in CASES:
rate, longest, ngap = continuity(per[name]["pdet"])
held = longest <= COAST_MAX
per[name].update(rate=rate, longest=longest, ngap=ngap)
print(" %-11s %8.1f%% %13.0f %9d %s"
% (name, rate * 100, longest, ngap,
"維持" if held else "丟失(超過 %.0f s 外推上限)" % COAST_MAX))
# ------------------------------------------------------------------------------
# Part D 追蹤濾波器 vs 真值
# ------------------------------------------------------------------------------
def measurement_sigma(snr_db, beamwidth_rad, rng_m, pulse_width=1e-6):
"""由 SNR 推出量測誤差標準差(Skolnik 的一階式)。
距離 sigma_R = c / (2 * B * sqrt(2*SNR)),B = 1/脈寬
角度 sigma_th = theta_3dB / (1.6 * sqrt(2*SNR))
重點不是係數多精確,而是 **量測精度隨 SNR 惡化**:
干擾與雜波不只讓你看不到,還會讓看得到的那些拍變不準。
"""
snr = np.clip(np.power(10.0, np.asarray(snr_db, float) / 10.0), 1e-3, None)
sig_r = C_LIGHT / (2.0 * (1.0 / pulse_width) * np.sqrt(2.0 * snr))
sig_cross = (beamwidth_rad / (1.6 * np.sqrt(2.0 * snr))) * rng_m
return sig_r, sig_cross
def alpha_beta(t, meas, hit, alpha=0.5, beta=0.1, coast_max=COAST_MAX):
"""三維 alpha-beta 濾波器。
沒有量測時外推 (coast);連續外推超過 coast_max 秒就宣告航跡丟失,
下一個有效量測重新起始。回傳 (估計, 航跡是否存活, 重起次數)。
"""
n = len(t)
est = np.full((n, 3), np.nan)
alive = np.zeros(n, dtype=bool)
x = v = None
coast, restarts, started = 0.0, 0, False
for k in range(n):
dt = (t[k] - t[k - 1]) if k else 0.0
if x is not None:
x = x + v * dt
if hit[k] and np.isfinite(meas[k]).all():
if x is None:
x, v = meas[k].copy(), np.zeros(3)
if started:
restarts += 1
started = True
else:
resid = meas[k] - x
x = x + alpha * resid
if dt > 0:
v = v + (beta / dt) * resid
coast = 0.0
else:
coast += dt
if coast > coast_max:
x = v = None # 航跡丟失
if x is not None:
est[k] = x
alive[k] = True
return est, alive, restarts
def part_d(root, t, per, geo):
print()
print("=" * 74)
print("Part D 追蹤濾波器 vs 真值航跡")
print("=" * 74)
t_truth, truth = truth_track(root)
n = min(len(t), len(t_truth))
t, truth = np.asarray(t[:n], float), truth[:n]
rng = clean(geo["Rcvr Range"])[:n] * 1000.0
lam = C_LIGHT / 3.0e9
beamwidth = math.radians(70.0 * lam / 4.0) # 拋物面 4 m 天線的 3 dB 波束寬
print(" 接收天線 3 dB 波束寬 = %.3f deg,量測誤差由此換算" % math.degrees(beamwidth))
print()
print(" %-11s %8s %10s %7s %11s %11s"
% ("情境", "命中率", "航跡存活", "重起", "RMS 誤差", "P95 誤差"))
for name, _, _, _, _, _ in CASES:
d = per[name]
rs = np.random.RandomState(SEED) # 每個情境用同一組雜訊種子
snr = d["snr"][:n]
sig_r, sig_x = measurement_sigma(snr, beamwidth, rng)
sigma = np.sqrt((sig_r ** 2 + 2 * sig_x ** 2) / 3.0) # 等效各軸誤差
pd = d["pdet"][:n]
hit = np.isfinite(pd) & (rs.rand(n) < np.nan_to_num(pd))
meas = truth + np.nan_to_num(rs.randn(n, 3) * sigma[:, None])
est, alive, restarts = alpha_beta(t, meas, hit)
err = np.linalg.norm(est - truth, axis=1)
e = err[np.isfinite(err)]
d.update(hit=hit.mean(), alive=alive.mean(), restarts=restarts,
rms=np.sqrt(np.mean(e ** 2)) if e.size else np.nan,
p95=np.percentile(e, 95) if e.size else np.nan,
err=err, sigma=sigma)
print(" %-11s %7.1f%% %9.1f%% %7d %9.0f m %9.0f m"
% (name, d["hit"] * 100, d["alive"] * 100, restarts, d["rms"], d["p95"]))
return t, truth
# ------------------------------------------------------------------------------
def plot(t, geo, per, path):
fig, ax = plt.subplots(2, 2, figsize=(13.5, 8.5))
n = len(t)
rs = clean(geo["Range Sum"])[:n]
br = clean(geo["Bistatic Angle"])[:n]
ascii_name = {c[0]: c[1] for c in CASES}
a = ax[0][0]
a.plot(t / 60.0, rs, lw=1.6, color="C0")
a.set_xlabel("time (min)")
a.set_ylabel("bistatic range sum (km)", color="C0")
a2 = a.twinx()
a2.plot(t / 60.0, br, lw=1.6, color="C3")
a2.set_ylabel("bistatic angle (deg)", color="C3")
a.set_title("A. bistatic geometry")
a.grid(alpha=.3)
a = ax[0][1]
for i, (name, _, _, _, _, _) in enumerate(CASES):
a.plot(t / 60.0, per[name]["pdet"][:n], lw=1.4,
label=ascii_name[name], color="C%d" % i)
a.axhline(PDET_MIN, ls="--", c="k", lw=1)
a.set_xlabel("time (min)")
a.set_ylabel("integrated PDet")
a.set_ylim(-0.05, 1.08)
a.set_title("B. detection probability (PDet=%.1f dashed)" % PDET_MIN)
a.legend(fontsize=8, ncol=2)
a.grid(alpha=.3)
a = ax[1][0]
for i, (name, _, _, _, _, _) in enumerate(CASES):
p = per[name]["pdet"][:n]
ok = np.isfinite(p) & (p >= PDET_MIN)
a.fill_between(t / 60.0, i, i + ok * 0.8, step="mid",
color="C%d" % i, alpha=.85, linewidth=0)
a.set_yticks([i + 0.4 for i in range(len(CASES))])
a.set_yticklabels([ascii_name[c[0]] for c in CASES], fontsize=9)
a.set_xlabel("time (min)")
a.set_title("C. track continuity (filled = detected)")
a.grid(alpha=.3, axis="x")
a = ax[1][1]
for i, (name, _, _, _, _, _) in enumerate(CASES):
a.plot(t / 60.0, per[name]["err"][:n], lw=1.2,
label="%s RMS %.0f m" % (ascii_name[name], per[name]["rms"]),
color="C%d" % i)
a.set_yscale("log")
a.set_xlabel("time (min)")
a.set_ylabel("position error vs truth (m)")
a.set_title("D. tracking error against known truth")
a.legend(fontsize=8)
a.grid(alpha=.3, which="both")
fig.tight_layout()
fig.savefig(path, dpi=130)
print("\n圖已存到", path)
def main():
app = STKDesktop.AttachToApplication() # 保留 app,否則 COM 介面會失效
root = app.Root
if root.CurrentScenario is None:
print("沒有開啟的場景。請先執行 python bistatic_build.py")
return 1
t, geo, per = collect(root)
part_a(geo)
part_b(per)
part_c(per)
t2, truth = part_d(root, t, per, geo)
plot(t2, geo, per, os.path.join(HERE, "bistatic_tracking.png"))
# 收尾:把場景還原成「干擾與雜波都開」,免得下一支腳本拿到半調子的設定
configure(root, True, True)
print()
print("=" * 74)
print("結論")
print("=" * 74)
a, c, j, jc = [per[k[0]] for k in CASES]
print(" 乾淨 偵測率 %5.1f%%,航跡不斷,定位誤差 RMS %.0f m。"
% (a["rate"] * 100, a["rms"]))
print(" 只有雜波 偵測率 %5.1f%%,RMS %.0f m。"
% (c["rate"] * 100, c["rms"]))
print(" 只有干擾 偵測率 %5.1f%%,最長中斷 %.0f s,RMS %.0f m。"
% (j["rate"] * 100, j["longest"], j["rms"]))
print(" 干擾+雜波 偵測率 %5.1f%%,航跡重起 %d 次,RMS %.0f m。"
% (jc["rate"] * 100, jc["restarts"], jc["rms"]))
print()
print(" 把四個情境分開算,才知道退化是誰造成的。只跑「全開」那一組,")
print(" 你會把干擾的效果算到雜波頭上 —— 兩者的對策完全不同。")
return 0
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
# ================================================================
# 雙基地雷達章 — 驗證段
#
# 這一章的驗證段特別重要,因為雷達模組有三種不同層次的靜默失敗:
#
# 第 1 層 指令被拒絕 —— 會有 NACK,最好抓
# 第 2 層 指令 ACK 但沒生效 —— 回讀就抓得到
# 第 3 層 指令 ACK、回讀也正確、輸出還是空的
# —— 只有一路檢查到「資料提供者有沒有真的吐出數字」才抓得到
#
# 第 3 層是本章的新發現:把雜波幾何設成 Smooth Oblate Earth,
# Connect ACK、Radar_RM 回讀顯示得一清二楚,但雙基地接收端的
# S/T Clutter Power 全部是 -9999.99(STK 的「沒有計算」哨兵值)。
# 只有 Single Point 這個模型算得出來。
#
# 執行:
# 1. 開啟 STK 12 桌面版
# 2. python bistatic_verify.py
# 離開碼 0 = 全部通過,1 = 有項目沒生效
# ================================================================
import os
import sys
import numpy as np
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, os.pardir, "tools"))
from agi.stk12.stkdesktop import STKDesktop # noqa: E402
from stk_verify import Verifier # noqa: E402
import bistatic_build as B # noqa: E402
from bistatic_tracking import (ST_ELEMENTS, clean, configure, # noqa: E402
continuity, fetch)
SCRIPT = os.path.join(HERE, "bistatic_radar_Connect.txt")
TXR, RXR, JMR, TGT = B.TXR, B.RXR, B.JMR, B.TGT
# 實測基準(20 W 干擾機、雜波係數 0.05、100 kW 發射、4 m 天線)
BASE = dict(clean_rate=1.00, jam_rate=0.832, jam_gap=55.0,
clean_rms=189.0, jam_rms=918.0)
def rm(root, path, prop):
"""Radar_RM GetValue 的單行取值。"""
try:
r = root.ExecuteCommand("Radar_RM %s GetValue %s" % (path, prop))
for i in range(r.Count):
for line in str(r.Item(i)).split("\n"):
if "=" in line and line.split("=", 1)[0].strip() == prop:
return line.split("=", 1)[1].replace("(Read-only)", "").strip()
except Exception:
return None
return None
def main():
app = STKDesktop.AttachToApplication() # 保留 app,否則 COM 介面會失效
root = app.Root
print("--- 建場景 ---")
B.build()
v = Verifier(root)
# ---- (1) 自動:把 Connect 腳本設過的每一個值讀回來比對 ----------------
v.auto_setvalues(SCRIPT)
# ---- (2) 結構:物件與雙基地拓撲 --------------------------------------
for p in ["*/Place/TxSite", "*/Place/RxSite", TGT, "*/Aircraft/JamAir",
TXR, RXR, JMR]:
v.object_exists(p)
v.position("*/Place/TxSite", 24.90, 120.85)
v.position("*/Place/RxSite", 24.30, 120.45)
v.expect_true("TxRadar 是 Bistatic_Transmitter",
rm(root, TXR, "Model") == "Bistatic_Transmitter",
str(rm(root, TXR, "Model")))
v.expect_true("RxRadar 是 Bistatic_Receiver",
rm(root, RXR, "Model") == "Bistatic_Receiver",
str(rm(root, RXR, "Model")))
rx = root.GetObjectFromPath(RXR)
# Connect 寫不進這兩個清單,只能用物件模型加。這裡確認真的加進去了。
v.expect_true("雙基地連結:接收端掛著 1 具發射機",
rx.Model.BistaticTransmitters.Count == 1,
"Count = %d" % rx.Model.BistaticTransmitters.Count)
v.expect_true("干擾清單:接收端掛著 1 具干擾機",
rx.Model.Jamming.Jammers.Count == 1,
"Count = %d" % rx.Model.Jamming.Jammers.Count)
v.expect_true("脈衝積分已改成 Fixed Pulse Number",
int(rx.Model.Mode.Waveform.PulseIntegrationType) ==
B.PULSE_INTEGRATION_FIXED_PULSE_NUMBER,
"type = %s" % rx.Model.Mode.Waveform.PulseIntegrationType)
v.expect("積分脈衝數", rx.Model.Mode.Waveform.PulseIntegration.PulseNumber,
B.PULSES)
# ---- (3) 天線設計頻率沒有被沖掉 --------------------------------------
# 先設天線型別、後設設計頻率。順序反了會被沖回 14.5 GHz 預設值,
# 而且完全沒有錯誤訊息 —— 本教材在 Week 2 與 Week 11 都踩過同一個坑。
for name, p in [("TxRadar", TXR), ("RxRadar", RXR), ("Jammer", JMR)]:
f = rm(root, p, "Model.AntennaControl.Antenna.DesignFrequency")
hz = float(f.split()[0]) if f else float("nan")
v.expect("%s 天線設計頻率" % name, hz / 1e9, 3.0, tol=1e-3, unit="GHz")
# ---- (4) 干擾與雜波真的「算得出數字」,不是只有設定值正確 -------------
# 這是第 3 層失敗的檢查點。STK 用 -9999.99 表示「這個量沒有計算」,
# 只看設定值永遠看不出來。
configure(root, True, True)
st = fetch(root, "Radar SearchTrack", ST_ELEMENTS)
js, cn = clean(st["S/T Integrated J/S"]), clean(st["S/T C/N"])
v.expect_true("干擾真的進到接收機(J/S 有數值)", np.isfinite(js).any(),
"有效樣本 %d/%d" % (np.isfinite(js).sum(), js.size))
v.expect_true("雜波真的進到接收機(C/N 有數值)", np.isfinite(cn).any(),
"有效樣本 %d/%d;若全為 -9999 表示雜波幾何模型不被"
"雙基地接收端支援" % (np.isfinite(cn).sum(), cn.size))
# 反向確認:換成 Smooth Oblate Earth 應該就算不出雜波了。
# 把「已知會壞的設定」也寫成檢查項,下次 STK 改版修好了才會被發現。
root.ExecuteCommand(
"Radar %s SetValue Model.Clutter.PointProviderList[0]"
".PointProvider Smooth_Oblate_Earth" % RXR)
root.ExecuteCommand("ClearAllAccess /")
st2 = fetch(root, "Radar SearchTrack", ST_ELEMENTS)
cn2 = clean(st2["S/T C/N"])
v.custom("Smooth Oblate Earth 在雙基地下算不出雜波(已知限制)",
not np.isfinite(cn2).any(),
"有效樣本 %d/%d —— 若這一項變成 FAIL,代表 STK 已修正此限制"
% (np.isfinite(cn2).sum(), cn2.size))
root.ExecuteCommand(
"Radar %s SetValue Model.Clutter.PointProviderList[0]"
".PointProvider Single_Point" % RXR)
root.ExecuteCommand("ClearAllAccess /")
# ---- (5) 實驗單寫死的基準數字,變成可執行的斷言 -----------------------
configure(root, False, False)
p_clean = clean(fetch(root, "Radar SearchTrack", ST_ELEMENTS)
["S/T Integrated PDet"])
r_clean, g_clean, _ = continuity(p_clean)
v.expect("乾淨情境偵測率", r_clean, BASE["clean_rate"], tol=0.02)
v.expect_true("乾淨情境航跡不中斷", g_clean == 0.0, "最長中斷 %.0f s" % g_clean)
configure(root, True, False)
p_jam = clean(fetch(root, "Radar SearchTrack", ST_ELEMENTS)
["S/T Integrated PDet w/Jamming"])
r_jam, g_jam, n_jam = continuity(p_jam)
v.expect("20 W 干擾下偵測率", r_jam, BASE["jam_rate"], tol=0.05)
v.expect("20 W 干擾下最長中斷", g_jam, BASE["jam_gap"], tol=15.0, unit="s")
v.expect_true("20 W 干擾足以打斷航跡(最長中斷 > 30 s 外推上限)",
g_jam > 30.0, "%.0f s / %d 次中斷" % (g_jam, n_jam))
configure(root, True, True) # 還原
# ---- (6) 幾何:雙基地距離和必定大於基線 --------------------------------
geo = fetch(root, "Radar Geometry",
["Time", "Range Sum", "Bistatic Angle", "Rcvr Range", "Xmtr Range"])
rs = clean(geo["Range Sum"])
rr, rt = clean(geo["Rcvr Range"]), clean(geo["Xmtr Range"])
v.expect_true("距離和 = Rt + Rr(STK 的定義自洽)",
np.nanmax(np.abs(rs - (rr + rt))) < 1e-3,
"最大差 %.2e km" % np.nanmax(np.abs(rs - (rr + rt))))
baseline = 78.0 # 兩站間距約 78 km
v.expect_true("距離和永遠大於基線長度(橢圓的基本性質)",
np.nanmin(rs) > baseline,
"最小距離和 %.1f km vs 基線 %.0f km" % (np.nanmin(rs), baseline))
ok = v.report("雙基地雷達 驗證段")
print("\n提醒:本章的第 4 組檢查是這一章的重點 —— 設定值正確不代表算得出數字。")
return 0 if ok else 1
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)