Week 5 — 結構與機構
week05_fea/基礎知識導論
Week 5 基礎知識導論 — 結構、發射載荷與 FEA
衛星在軌道上幾乎不受力。它一生中最危險的 10 分鐘,是在火箭上。
結構設計的驅動條件幾乎全部來自發射,不是來自任務。
1. 這一週在解什麼問題
一片 CubeSat 太陽能板(或側板)在發射環境中會不會壞?
判準不是「應力有沒有超過降伏強度」,而是三件事:
- 第一模態頻率夠不夠高(避免與載具主頻共振)
- 隨機振動的響應加速度會不會超過元件的容許值
- 準靜態載荷下有沒有結構破壞或永久變形
本週用 CalculiX(開源 FEA)做模態分析,用 Python 做隨機振動響應。
2. 核心觀念
2.1 發射環境的四種載荷
| 載荷類型 | 來源 | 頻率範圍 | 設計影響 |
|---|---|---|---|
| 準靜態 (quasi-static) | 加速度、推力 | 約 0 Hz | 強度、螺栓 |
| 正弦振動 (sine) | 引擎、POGO、分離 | 5–100 Hz | 低階模態 |
| 隨機振動 (random) | 氣動噪音、燃燒 | 20–2000 Hz | 元件疲勞、焊點 |
| 衝擊 (shock) | 分離火工品 | 100–10000 Hz | 脆性元件、晶體 |
對 CubeSat 而言,隨機振動通常最嚴苛 —— 元件多、板薄、質量小。
2.2 為什麼要求第一模態 > 100 Hz
發射載具會給一個最低頻率要求(stiffness requirement)。理由是:
把衛星的結構模態推到載具主要激振頻率之上,就能避免耦合共振,
也讓載具的耦合載荷分析 (CLA) 可以把衛星當成剛體處理。
典型 CubeSat dispenser 的要求是第一模態 > 100 Hz(依 dispenser 規格書而定)。
這是剛度需求,不是強度需求。 加厚板子有效,換成更強的材料不一定有效 ——
模態頻率大致正比於 sqrt(E/ρ) 與幾何,跟強度無關。
2.3 模態分析在算什麼
自由振動的特徵值問題:
( K - ω² M ) φ = 0
K剛度矩陣、M質量矩陣、ω自然角頻率、φ模態振形- 解出來得到一組
(f_n, φ_n),f_n = ω_n / 2π
單自由度直覺:
f_n = (1 / 2π) * sqrt( k / m )
要提高 f_n:加剛度(加厚、加肋、改支撐位置)或減質量。
其中「改支撐位置」往往最便宜 —— 一片四角固定的板,把固定點往內移一點,
第一模態可能就上升好幾十 Hz。
2.4 隨機振動與 GEVS
隨機振動的輸入不是一條時間曲線,而是一個功率譜密度 (PSD / ASD),單位 g²/Hz。
NASA GSFC-STD-7000 (GEVS) 是最常被引用的通用環境驗證規格。
總體均方根加速度:
GRMS = sqrt( ∫ ASD(f) df ) 對整個頻帶積分
單自由度系統對白噪音輸入的響應,用 Miles 方程估:
GRMS_response = sqrt( (π/2) * f_n * Q * ASD(f_n) )
Q是品質因數(放大倍率),Q = 1 / (2ζ)。結構阻尼比ζ常取 0.02–0.05,Q約 10–25。- 設計慣例取 3σ 當設計載荷:
a_design = 3 * GRMS_response。
Miles 方程的重要推論:響應正比於 sqrt(f_n)。
把模態頻率推高雖然滿足了剛度需求,卻會讓隨機振動響應變大。
這是本週最重要的工程張力 —— 兩個需求方向相反。
2.5 FEA 的可信度來自哪裡
FEA 給的數字看起來很精確,但它的可信度只來自三件事:
- 邊界條件對不對 — 固定端是真的完全固定嗎?螺栓是點約束還是面約束?
- 網格夠不夠細 — 做網格收斂測試:加密一倍,答案變多少?
- 有沒有對照 — 與解析解(簡支/固支板公式)、與實驗、或與另一個求解器對照。
本週的作法就是第 3 點:Python 解析模型 vs CalculiX 3D,兩邊互相驗證。
兩個都對不代表對,但兩個差很多一定有一個錯 —— 這是非常有效的除錯手段。
2.6 本週的教學設計:基準案例故意是 FAIL
基準板的第一模態約 71 Hz < 100 Hz,是 FAIL。
這不是錯誤,是刻意的:設計迭代的價值,只有在你先看到失敗時才學得到。
Part B 要你改厚度 / 材料 / 支撐,把它推過 100 Hz,
並觀察隨機振動響應同時怎麼變。
3. 名詞與符號
| 符號 | 意義 | 單位 | 典型值 |
|---|---|---|---|
f_n |
自然頻率 | Hz | 需求 > 100 |
K, M |
剛度、質量矩陣 | — | — |
φ |
模態振形 | — | — |
ζ |
阻尼比 | — | 0.02–0.05 |
Q |
品質因數 = 1/(2ζ) | — | 10–25 |
ASD |
加速度譜密度 | g²/Hz | GEVS 定義 |
GRMS |
均方根加速度 | g | — |
E |
楊氏模數 | GPa | 鋁 6061:69 |
ρ |
密度 | kg/m³ | 鋁 6061:2700 |
σ_y |
降伏強度 | MPa | 鋁 6061-T6:約 276 |
4. 觀念 → 本週實驗對照
| 觀念 | 出現在本週的哪裡 |
|---|---|
| 模態分析 | cubesat_panel_modal.inp(CalculiX deck) |
| 解析 vs FEA 對照 | week5_fea_launch_vibe.py Part C |
| GEVS 隨機振動 | 同上,ASD 曲線與 GRMS 積分 |
| Miles 方程 | 同上,3σ 響應加速度 |
| 基準 FAIL → 迭代 PASS | Part A(71 Hz)→ Part B |
| 把手冊數字變成斷言 | week05_verify.py |
5. 常見誤解
- 「換更強的材料就能提高模態頻率。」 — 頻率與
sqrt(E/ρ)及幾何有關,跟強度無關。
鋁換鋼,E上升約 3 倍但ρ也上升約 2.9 倍,sqrt(E/ρ)幾乎沒變。 - 「模態頻率愈高愈好。」 — 見 2.4,
f_n上升會讓隨機振動響應上升。要同時看兩邊。 - 「隨機振動可以用等效正弦來算。」 — 兩者的物理與判準都不同,不能互換。
- 「FEA 跑得出來就是對的。」 — 沒有做網格收斂與邊界條件檢查的 FEA,是彩色的猜測。
- 「衛星在軌道上要承受很大的力。」 — 軌道上是微重力,主要載荷是熱循環與微小攝動力。
結構設計幾乎完全由發射決定。
6. 延伸閱讀
環境規格(本週數字的來源)
- NASA GSFC-STD-7000 (GEVS)
— 通用環境驗證規格,隨機振動 ASD 曲線的出處。 - NASA-STD-7009(模型與模擬的可信度標準)
— 討論「你的 FEA 憑什麼可信」的正式標準,對 2.5 節很有幫助。 - CubeSat Design Specification — dispenser 介面與結構要求。
- ECSS 標準總表 — ECSS-E-ST-32 系列(結構)。
FEA 工具
- CalculiX 官方網站 — 開源有限元素求解器,本週使用。
- CalculiX 使用手冊(PDF)
—*FREQUENCY、*BOUNDARY、材料卡的完整語法都在這裡。 - SciPy 說明文件 — 特徵值問題與數值積分。
發射載具介面(真實規格書長什麼樣)
7. 課前自我檢核
- 為什麼發射載具要求最低模態頻率,而不是要求最高強度?
- 板厚加倍,第一模態頻率大約變成幾倍?(提示:剛度與質量各自怎麼變)
- 依 Miles 方程,
f_n從 71 Hz 提高到 120 Hz,3σ 響應加速度大約變成幾倍? - 你的 FEA 邊界條件是「四角固定」。真實螺栓連接跟這個假設差在哪?結果會偏保守還是偏樂觀?
- 若模態需求與振動響應需求互相衝突,你會用哪些設計手段同時滿足?
Week5_FEA_Lab_Handout.md
Week 5 實驗單 — 結構 FEA + 發射隨機振動
課程:CubeSat 設計與實作 · Week 5 結構與機構
工具:CalculiX(正牌 3D FEA)+ Python 梁 FEA(可跑的縮階模型)
檔案:week5_fea_launch_vibe.py、cubesat_panel_modal.inp
預計時數:2–3 小時
1. 為什麼這週用 FEA + 為什麼雙軌
CubeSat 要通過發射,結構關卡有兩條:
1. 模態(剛度):第一自然頻率必須高於發射載具/分離機構的要求(常見 ≥ 100 Hz),避免與火箭低頻振動共振。
2. 強度(隨機振動):承受發射的隨機振動 PSD,響應加速度(GRMS)與應力不能超限——這是焊點/PCB 失效的主因。
STK 完全不做結構分析,所以這週用 FEA。我們用雙軌(同 Week 8 哲學):
- CalculiX = 業界開源正牌工具,3D 磚元模態分析(cubesat_panel_modal.inp)。
- Python 梁 FEA = 保證可跑的 1D 縮階模型,即時算出自然頻率、模態振型、隨機響應並視覺化。
兩者算同一片懸臂鋁面板,可對比「1D 快速估算 vs 3D 精確解」的第一模態差異——這正是真實工程裡先用縮階模型抓量級、再用 3D FEA 驗證的流程。
2. 學習目標
- 用梁單元組裝全域剛度/質量矩陣,解廣義特徵值
Kφ = ω²Mφ得自然頻率與振型。 - 判讀模態振型(第一彎曲、第二彎曲…)。
- 把發射隨機振動 PSD(GEVS 鑑定級)當基礎激振,算傳遞率與響應 PSD。
- 用兩種方法算 GRMS:全模態積分 vs Miles 方程(單模態),並解釋差異。
- 判斷結構是否通過第一模態 ≥ 發射要求,不過就設計迭代(加厚/加肋/換材料)。
3. 發射頻譜(FEA 輸入)
用 NASA GSFC-STD-7000 GEVS 鑑定級隨機振動(整體 ~14.1 GRMS),分段定義(g²/Hz):
| 頻段 (Hz) | PSD |
|---|---|
| 20–50 | +6 dB/oct(0.026 → 0.16) |
| 50–800 | 0.16(平段) |
| 800–2000 | −6 dB/oct(0.16 → 0.026) |
程式會驗證輸入 GRMS ≈ 14.1(對頻譜積分開根號),這是檢查 PSD 實作正確的 sanity check。真實專案要換成你的發射載具 payload user's guide 裡的 PSD(Falcon 9 / PSLV / Electron 各不同)。
4. 操作步驟
Part A — 跑基準案例(30 min)
python week5_fea_launch_vibe.py。- 看主控台:前 5 個自然頻率、pass/fail、輸入/響應/Miles GRMS。
- 看
week5_fea_launch_vibe.png四張圖:
- 模態振型:Mode 1 第一彎曲(尖端最大)、Mode 2/3 高階。
- 輸入 vs 響應 PSD:響應在各共振頻率放大(尖峰),peak 高度 ~ Q²。
- 自然頻率 vs 要求:紅線是發射要求,低於線的模態標紅。
- 傳遞率:共振處達 ~Q(≈1/2ζ),反共振處掉深谷。
基準案例會 FAIL(2 mm 薄板 + 60 g 元件,第一模態 ~71 Hz < 100 Hz)——這是刻意的,讓你在 Part B 修好它。
Part B — 設計迭代讓它 PASS(45 min)
改檔案上方參數,目標讓第一模態 ≥ 100 Hz,記錄每次的 f1:
| 手段 | 變數 | 試法 | 物理 |
|---|---|---|---|
| 加厚面板 | T |
2 → 3 → 4 mm | f ∝ √(EI) ∝ t^{1.5};最有效 |
| 減尖端質量 | M_TIP |
60 → 30 g | 元件輕量化/移近固定端 |
| 縮短懸臂 | L |
98 → 70 mm | f ∝ 1/L²;加中間支撐等效縮短 |
| 換材料 | E,RHO |
鈦/鎂/CFRP | 看比剛度 E/ρ |
思考題:為什麼加厚比減質量有效得多?(比較 f 對 t 與對 M_tip 的敏感度。)
Part C — 跑正牌 CalculiX(45 min,需安裝 ccx)
- 安裝 CalculiX(
ccx求解器 +cgx前後處理,或用 PrePoMax GUI)。 - 執行:
ccx cubesat_panel_modal。 - 檢視:
cgx cubesat_panel_modal.frd,或匯出 ParaView 看 3D 模態振型。 - 對比:CalculiX 的 3D 第一模態 vs Python 梁 FEA 的 f1。差多少?為什麼 1D 梁模型會高估/低估?(提示:剪切變形、板的雙向彎曲、泊松效應。)
Part D(選作)— 應力與 Miles
用 Miles 方程 GRMS = √(π/2 · f1 · Q · W(f1)) 手算第一模態的 3σ 加速度,乘上等效質量估慣性力與根部應力,對比材料降伏強度算安全裕度(margin of safety)。
5. 繳交與評分
| 項目 | 配分 |
|---|---|
| 基準案例執行 + 四圖判讀 | 25% |
| Part B 設計迭代(達成 PASS + 數據表) | 30% |
| Part C CalculiX 3D 對比 | 30% |
| 討論(頻率敏感度、1D vs 3D 差異、Miles vs 全解) | 15% |
| Part D 應力/裕度 | +10% |
6. 技術說明
- Python 梁 FEA 是真 FEA:2 節點 Euler-Bernoulli 梁單元,一致質量矩陣,組裝全域 K/M、施加固定端邊界、
scipy.linalg.eigh解廣義特徵值。不是查表公式。 - 隨機響應假設:輕阻尼、模態獨立、各模態以 SDOF 絕對加速度傳遞率響應、模態疊加。這是太空件隨機振動的標準一階做法,程式內有註明。
- 面板當等效梁的近似:1D 梁忽略板的雙向彎曲與剪切變形,對薄板第一彎曲模態尚可,高階模態誤差變大——所以才要 Part C 的 3D CalculiX 驗證。
- 沒裝 ccx 也能完成 A/B/D:Python 那軌不依賴 CalculiX;Part C 才需要 ccx。
圖

week05_fea/week5_fea_launch_vibe.png
程式碼
** Week5 CubeSat panel - CalculiX modal analysis
** Same cantilever panel as the Python beam FEA (compare f1).
*NODE
1, 0.000000, 0.000000, 0.000000
2, 0.009800, 0.000000, 0.000000
3, 0.019600, 0.000000, 0.000000
4, 0.029400, 0.000000, 0.000000
5, 0.039200, 0.000000, 0.000000
6, 0.049000, 0.000000, 0.000000
7, 0.058800, 0.000000, 0.000000
8, 0.068600, 0.000000, 0.000000
9, 0.078400, 0.000000, 0.000000
10, 0.088200, 0.000000, 0.000000
11, 0.098000, 0.000000, 0.000000
12, 0.000000, 0.009800, 0.000000
13, 0.009800, 0.009800, 0.000000
14, 0.019600, 0.009800, 0.000000
15, 0.029400, 0.009800, 0.000000
16, 0.039200, 0.009800, 0.000000
17, 0.049000, 0.009800, 0.000000
18, 0.058800, 0.009800, 0.000000
19, 0.068600, 0.009800, 0.000000
20, 0.078400, 0.009800, 0.000000
21, 0.088200, 0.009800, 0.000000
22, 0.098000, 0.009800, 0.000000
23, 0.000000, 0.019600, 0.000000
24, 0.009800, 0.019600, 0.000000
25, 0.019600, 0.019600, 0.000000
26, 0.029400, 0.019600, 0.000000
27, 0.039200, 0.019600, 0.000000
28, 0.049000, 0.019600, 0.000000
29, 0.058800, 0.019600, 0.000000
30, 0.068600, 0.019600, 0.000000
31, 0.078400, 0.019600, 0.000000
32, 0.088200, 0.019600, 0.000000
33, 0.098000, 0.019600, 0.000000
34, 0.000000, 0.029400, 0.000000
35, 0.009800, 0.029400, 0.000000
36, 0.019600, 0.029400, 0.000000
37, 0.029400, 0.029400, 0.000000
38, 0.039200, 0.029400, 0.000000
39, 0.049000, 0.029400, 0.000000
40, 0.058800, 0.029400, 0.000000
41, 0.068600, 0.029400, 0.000000
42, 0.078400, 0.029400, 0.000000
43, 0.088200, 0.029400, 0.000000
44, 0.098000, 0.029400, 0.000000
45, 0.000000, 0.039200, 0.000000
46, 0.009800, 0.039200, 0.000000
47, 0.019600, 0.039200, 0.000000
48, 0.029400, 0.039200, 0.000000
49, 0.039200, 0.039200, 0.000000
50, 0.049000, 0.039200, 0.000000
51, 0.058800, 0.039200, 0.000000
52, 0.068600, 0.039200, 0.000000
53, 0.078400, 0.039200, 0.000000
54, 0.088200, 0.039200, 0.000000
55, 0.098000, 0.039200, 0.000000
56, 0.000000, 0.049000, 0.000000
57, 0.009800, 0.049000, 0.000000
58, 0.019600, 0.049000, 0.000000
59, 0.029400, 0.049000, 0.000000
60, 0.039200, 0.049000, 0.000000
61, 0.049000, 0.049000, 0.000000
62, 0.058800, 0.049000, 0.000000
63, 0.068600, 0.049000, 0.000000
64, 0.078400, 0.049000, 0.000000
65, 0.088200, 0.049000, 0.000000
66, 0.098000, 0.049000, 0.000000
67, 0.000000, 0.058800, 0.000000
68, 0.009800, 0.058800, 0.000000
69, 0.019600, 0.058800, 0.000000
70, 0.029400, 0.058800, 0.000000
71, 0.039200, 0.058800, 0.000000
72, 0.049000, 0.058800, 0.000000
73, 0.058800, 0.058800, 0.000000
74, 0.068600, 0.058800, 0.000000
75, 0.078400, 0.058800, 0.000000
76, 0.088200, 0.058800, 0.000000
77, 0.098000, 0.058800, 0.000000
78, 0.000000, 0.068600, 0.000000
79, 0.009800, 0.068600, 0.000000
80, 0.019600, 0.068600, 0.000000
81, 0.029400, 0.068600, 0.000000
82, 0.039200, 0.068600, 0.000000
83, 0.049000, 0.068600, 0.000000
84, 0.058800, 0.068600, 0.000000
85, 0.068600, 0.068600, 0.000000
86, 0.078400, 0.068600, 0.000000
87, 0.088200, 0.068600, 0.000000
88, 0.098000, 0.068600, 0.000000
89, 0.000000, 0.078400, 0.000000
90, 0.009800, 0.078400, 0.000000
91, 0.019600, 0.078400, 0.000000
92, 0.029400, 0.078400, 0.000000
93, 0.039200, 0.078400, 0.000000
94, 0.049000, 0.078400, 0.000000
95, 0.058800, 0.078400, 0.000000
96, 0.068600, 0.078400, 0.000000
97, 0.078400, 0.078400, 0.000000
98, 0.088200, 0.078400, 0.000000
99, 0.098000, 0.078400, 0.000000
100, 0.000000, 0.088200, 0.000000
101, 0.009800, 0.088200, 0.000000
102, 0.019600, 0.088200, 0.000000
103, 0.029400, 0.088200, 0.000000
104, 0.039200, 0.088200, 0.000000
105, 0.049000, 0.088200, 0.000000
106, 0.058800, 0.088200, 0.000000
107, 0.068600, 0.088200, 0.000000
108, 0.078400, 0.088200, 0.000000
109, 0.088200, 0.088200, 0.000000
110, 0.098000, 0.088200, 0.000000
111, 0.000000, 0.098000, 0.000000
112, 0.009800, 0.098000, 0.000000
113, 0.019600, 0.098000, 0.000000
114, 0.029400, 0.098000, 0.000000
115, 0.039200, 0.098000, 0.000000
116, 0.049000, 0.098000, 0.000000
117, 0.058800, 0.098000, 0.000000
118, 0.068600, 0.098000, 0.000000
119, 0.078400, 0.098000, 0.000000
120, 0.088200, 0.098000, 0.000000
121, 0.098000, 0.098000, 0.000000
122, 0.000000, 0.000000, 0.002000
123, 0.009800, 0.000000, 0.002000
124, 0.019600, 0.000000, 0.002000
125, 0.029400, 0.000000, 0.002000
126, 0.039200, 0.000000, 0.002000
127, 0.049000, 0.000000, 0.002000
128, 0.058800, 0.000000, 0.002000
129, 0.068600, 0.000000, 0.002000
130, 0.078400, 0.000000, 0.002000
131, 0.088200, 0.000000, 0.002000
132, 0.098000, 0.000000, 0.002000
133, 0.000000, 0.009800, 0.002000
134, 0.009800, 0.009800, 0.002000
135, 0.019600, 0.009800, 0.002000
136, 0.029400, 0.009800, 0.002000
137, 0.039200, 0.009800, 0.002000
138, 0.049000, 0.009800, 0.002000
139, 0.058800, 0.009800, 0.002000
140, 0.068600, 0.009800, 0.002000
141, 0.078400, 0.009800, 0.002000
142, 0.088200, 0.009800, 0.002000
143, 0.098000, 0.009800, 0.002000
144, 0.000000, 0.019600, 0.002000
145, 0.009800, 0.019600, 0.002000
146, 0.019600, 0.019600, 0.002000
147, 0.029400, 0.019600, 0.002000
148, 0.039200, 0.019600, 0.002000
149, 0.049000, 0.019600, 0.002000
150, 0.058800, 0.019600, 0.002000
151, 0.068600, 0.019600, 0.002000
152, 0.078400, 0.019600, 0.002000
153, 0.088200, 0.019600, 0.002000
154, 0.098000, 0.019600, 0.002000
155, 0.000000, 0.029400, 0.002000
156, 0.009800, 0.029400, 0.002000
157, 0.019600, 0.029400, 0.002000
158, 0.029400, 0.029400, 0.002000
159, 0.039200, 0.029400, 0.002000
160, 0.049000, 0.029400, 0.002000
161, 0.058800, 0.029400, 0.002000
162, 0.068600, 0.029400, 0.002000
163, 0.078400, 0.029400, 0.002000
164, 0.088200, 0.029400, 0.002000
165, 0.098000, 0.029400, 0.002000
166, 0.000000, 0.039200, 0.002000
167, 0.009800, 0.039200, 0.002000
168, 0.019600, 0.039200, 0.002000
169, 0.029400, 0.039200, 0.002000
170, 0.039200, 0.039200, 0.002000
171, 0.049000, 0.039200, 0.002000
172, 0.058800, 0.039200, 0.002000
173, 0.068600, 0.039200, 0.002000
174, 0.078400, 0.039200, 0.002000
175, 0.088200, 0.039200, 0.002000
176, 0.098000, 0.039200, 0.002000
177, 0.000000, 0.049000, 0.002000
178, 0.009800, 0.049000, 0.002000
179, 0.019600, 0.049000, 0.002000
180, 0.029400, 0.049000, 0.002000
181, 0.039200, 0.049000, 0.002000
182, 0.049000, 0.049000, 0.002000
183, 0.058800, 0.049000, 0.002000
184, 0.068600, 0.049000, 0.002000
185, 0.078400, 0.049000, 0.002000
186, 0.088200, 0.049000, 0.002000
187, 0.098000, 0.049000, 0.002000
188, 0.000000, 0.058800, 0.002000
189, 0.009800, 0.058800, 0.002000
190, 0.019600, 0.058800, 0.002000
191, 0.029400, 0.058800, 0.002000
192, 0.039200, 0.058800, 0.002000
193, 0.049000, 0.058800, 0.002000
194, 0.058800, 0.058800, 0.002000
195, 0.068600, 0.058800, 0.002000
196, 0.078400, 0.058800, 0.002000
197, 0.088200, 0.058800, 0.002000
198, 0.098000, 0.058800, 0.002000
199, 0.000000, 0.068600, 0.002000
200, 0.009800, 0.068600, 0.002000
201, 0.019600, 0.068600, 0.002000
202, 0.029400, 0.068600, 0.002000
203, 0.039200, 0.068600, 0.002000
204, 0.049000, 0.068600, 0.002000
205, 0.058800, 0.068600, 0.002000
206, 0.068600, 0.068600, 0.002000
207, 0.078400, 0.068600, 0.002000
208, 0.088200, 0.068600, 0.002000
209, 0.098000, 0.068600, 0.002000
210, 0.000000, 0.078400, 0.002000
211, 0.009800, 0.078400, 0.002000
212, 0.019600, 0.078400, 0.002000
213, 0.029400, 0.078400, 0.002000
214, 0.039200, 0.078400, 0.002000
215, 0.049000, 0.078400, 0.002000
216, 0.058800, 0.078400, 0.002000
217, 0.068600, 0.078400, 0.002000
218, 0.078400, 0.078400, 0.002000
219, 0.088200, 0.078400, 0.002000
220, 0.098000, 0.078400, 0.002000
221, 0.000000, 0.088200, 0.002000
222, 0.009800, 0.088200, 0.002000
223, 0.019600, 0.088200, 0.002000
224, 0.029400, 0.088200, 0.002000
225, 0.039200, 0.088200, 0.002000
226, 0.049000, 0.088200, 0.002000
227, 0.058800, 0.088200, 0.002000
228, 0.068600, 0.088200, 0.002000
229, 0.078400, 0.088200, 0.002000
230, 0.088200, 0.088200, 0.002000
231, 0.098000, 0.088200, 0.002000
232, 0.000000, 0.098000, 0.002000
233, 0.009800, 0.098000, 0.002000
234, 0.019600, 0.098000, 0.002000
235, 0.029400, 0.098000, 0.002000
236, 0.039200, 0.098000, 0.002000
237, 0.049000, 0.098000, 0.002000
238, 0.058800, 0.098000, 0.002000
239, 0.068600, 0.098000, 0.002000
240, 0.078400, 0.098000, 0.002000
241, 0.088200, 0.098000, 0.002000
242, 0.098000, 0.098000, 0.002000
*ELEMENT, TYPE=C3D8, ELSET=PANEL
1, 1,2,13,12,122,123,134,133
2, 2,3,14,13,123,124,135,134
3, 3,4,15,14,124,125,136,135
4, 4,5,16,15,125,126,137,136
5, 5,6,17,16,126,127,138,137
6, 6,7,18,17,127,128,139,138
7, 7,8,19,18,128,129,140,139
8, 8,9,20,19,129,130,141,140
9, 9,10,21,20,130,131,142,141
10, 10,11,22,21,131,132,143,142
11, 12,13,24,23,133,134,145,144
12, 13,14,25,24,134,135,146,145
13, 14,15,26,25,135,136,147,146
14, 15,16,27,26,136,137,148,147
15, 16,17,28,27,137,138,149,148
16, 17,18,29,28,138,139,150,149
17, 18,19,30,29,139,140,151,150
18, 19,20,31,30,140,141,152,151
19, 20,21,32,31,141,142,153,152
20, 21,22,33,32,142,143,154,153
21, 23,24,35,34,144,145,156,155
22, 24,25,36,35,145,146,157,156
23, 25,26,37,36,146,147,158,157
24, 26,27,38,37,147,148,159,158
25, 27,28,39,38,148,149,160,159
26, 28,29,40,39,149,150,161,160
27, 29,30,41,40,150,151,162,161
28, 30,31,42,41,151,152,163,162
29, 31,32,43,42,152,153,164,163
30, 32,33,44,43,153,154,165,164
31, 34,35,46,45,155,156,167,166
32, 35,36,47,46,156,157,168,167
33, 36,37,48,47,157,158,169,168
34, 37,38,49,48,158,159,170,169
35, 38,39,50,49,159,160,171,170
36, 39,40,51,50,160,161,172,171
37, 40,41,52,51,161,162,173,172
38, 41,42,53,52,162,163,174,173
39, 42,43,54,53,163,164,175,174
40, 43,44,55,54,164,165,176,175
41, 45,46,57,56,166,167,178,177
42, 46,47,58,57,167,168,179,178
43, 47,48,59,58,168,169,180,179
44, 48,49,60,59,169,170,181,180
45, 49,50,61,60,170,171,182,181
46, 50,51,62,61,171,172,183,182
47, 51,52,63,62,172,173,184,183
48, 52,53,64,63,173,174,185,184
49, 53,54,65,64,174,175,186,185
50, 54,55,66,65,175,176,187,186
51, 56,57,68,67,177,178,189,188
52, 57,58,69,68,178,179,190,189
53, 58,59,70,69,179,180,191,190
54, 59,60,71,70,180,181,192,191
55, 60,61,72,71,181,182,193,192
56, 61,62,73,72,182,183,194,193
57, 62,63,74,73,183,184,195,194
58, 63,64,75,74,184,185,196,195
59, 64,65,76,75,185,186,197,196
60, 65,66,77,76,186,187,198,197
61, 67,68,79,78,188,189,200,199
62, 68,69,80,79,189,190,201,200
63, 69,70,81,80,190,191,202,201
64, 70,71,82,81,191,192,203,202
65, 71,72,83,82,192,193,204,203
66, 72,73,84,83,193,194,205,204
67, 73,74,85,84,194,195,206,205
68, 74,75,86,85,195,196,207,206
69, 75,76,87,86,196,197,208,207
70, 76,77,88,87,197,198,209,208
71, 78,79,90,89,199,200,211,210
72, 79,80,91,90,200,201,212,211
73, 80,81,92,91,201,202,213,212
74, 81,82,93,92,202,203,214,213
75, 82,83,94,93,203,204,215,214
76, 83,84,95,94,204,205,216,215
77, 84,85,96,95,205,206,217,216
78, 85,86,97,96,206,207,218,217
79, 86,87,98,97,207,208,219,218
80, 87,88,99,98,208,209,220,219
81, 89,90,101,100,210,211,222,221
82, 90,91,102,101,211,212,223,222
83, 91,92,103,102,212,213,224,223
84, 92,93,104,103,213,214,225,224
85, 93,94,105,104,214,215,226,225
86, 94,95,106,105,215,216,227,226
87, 95,96,107,106,216,217,228,227
88, 96,97,108,107,217,218,229,228
89, 97,98,109,108,218,219,230,229
90, 98,99,110,109,219,220,231,230
91, 100,101,112,111,221,222,233,232
92, 101,102,113,112,222,223,234,233
93, 102,103,114,113,223,224,235,234
94, 103,104,115,114,224,225,236,235
95, 104,105,116,115,225,226,237,236
96, 105,106,117,116,226,227,238,237
97, 106,107,118,117,227,228,239,238
98, 107,108,119,118,228,229,240,239
99, 108,109,120,119,229,230,241,240
100, 109,110,121,120,230,231,242,241
*NSET, NSET=FIXED
1,12,23,34,45,56,67,78,
89,100,111,122,133,144,155,166,
177,188,199,210,221,232,
*MATERIAL, NAME=AL6061
*ELASTIC
69000000000, 0.33
*DENSITY
2700.0
*SOLID SECTION, ELSET=PANEL, MATERIAL=AL6061
*BOUNDARY
FIXED, 1, 3
*STEP
*FREQUENCY
5
*NODE FILE
U
*END STEP
# -*- coding: utf-8 -*-
# ================================================================
# Week 5 驗證段 — 模態與隨機振動的基準結果
#
# 本週刻意設計成「基準 FAIL → Part B 迭代到 PASS」。那個 FAIL 是教學的起點,
# 所以第一模態 71 Hz 這個數字必須穩定 —— 一旦漂掉,整個 Lab 的敘事就斷了。
#
# 執行:python week05_verify.py
# 離開碼 0 = 全部通過,1 = 有項目對不上
# ================================================================
import os
import sys
# notebook 的 kernel 沒有 __file__。而且啟動 STK Engine 會改變 process 的
# 工作目錄,所以不能事後才靠 os.getcwd() —— notebook 開頭的 bootstrap cell
# 會先把週次目錄釘在 WEEK_DIR。
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
REPO = os.path.abspath(os.path.join(HERE, os.pardir))
sys.path.insert(0, os.path.join(REPO, "tools"))
from stk_verify import Verifier, load_lab # noqa: E402
def main():
import numpy as np
m = load_lab(os.path.join(HERE, "week5_fea_launch_vibe.py"))
v = Verifier(None)
freqs, phi, free, Mff, _ndof = m.solve_modal()
v.expect("第一模態", freqs[0], 71.0, tol=1.0, unit=" Hz")
v.expect_true("基準應為 FAIL(f1 < 100 Hz)", freqs[0] < 100.0,
f"f1 = {freqs[0]:.1f} Hz,發射要求 100 Hz")
v.expect_true("模態頻率單調遞增",
all(freqs[i] < freqs[i + 1] for i in range(4)),
f"前五模態 {[round(float(f), 1) for f in freqs[:5]]}")
f = np.logspace(np.log10(20), np.log10(2000), 500)
v.expect("GEVS 輸入 GRMS", m.grms(f, m.gevs_psd(f)), 14.1, tol=0.5, unit=" g")
_, _, _, _, grms_in, grms_out, grms_miles = m.random_response(freqs, phi, free, Mff)
v.expect("尖端響應 GRMS", grms_out, 19.8, tol=1.0, unit=" g")
v.expect_true("Miles 單模態近似 <= 全解", grms_miles <= grms_out,
f"Miles {grms_miles:.1f} g vs 全解 {grms_out:.1f} g")
ok = v.report("Week 5 驗證段")
return 0 if ok else 1
if __name__ == "__main__":
_rc = main()
if "ipykernel" not in sys.modules:
raise SystemExit(_rc)
# -*- coding: utf-8 -*-
# ================================================================
# Week 5 Lab - 結構 FEA + 發射隨機振動響應
# CubeSat 面板/支架的模態分析(自然頻率 vs 發射要求)+ 用發射頻譜
# (GEVS 鑑定級隨機振動 PSD)做基礎激振,算傳遞率、響應 PSD、GRMS。
#
# 雙軌設計(同 Week 8 哲學):
# (1) CalculiX = 正牌 3D FEA:本檔會產生 cubesat_panel_modal.inp,
# 在有 ccx 的機器上跑 `ccx cubesat_panel_modal` 得 3D 模態。
# (2) Python 梁 FEA = 保證可跑的縮階模型:Euler-Bernoulli 梁單元,
# 組裝全域 K/M、解廣義特徵值 -> 自然頻率 + 模態振型 + 隨機響應。
# 兩者算「同一片懸臂面板」,可對比 1D 縮階 vs 3D 全解的第一模態。
#
# 發射頻譜:NASA GSFC-STD-7000 GEVS 鑑定級隨機振動(~14.1 GRMS)。
#
# 輸出:week5_fea_launch_vibe.png + cubesat_panel_modal.inp + 主控台摘要
# ================================================================
import numpy as np
from scipy.linalg import eigh
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import os
# 產出要落在**週次目錄**,不是 process 的工作目錄。每一支 weekNN_verify.py
# 開頭都有這個慣用法並註明「啟動 STK Engine 會改變 process 的工作目錄」,
# 但 lab 這一側先前沒有套用 —— 結果庫根目錄散落著九個產出,
# 其中一份 week15_launch.png 還是過期的。notebook 的 kernel 沒有 __file__,
# 所以留 WEEK_DIR(notebook 開頭的 bootstrap cell 會設)當後備。
HERE = (os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals()
else globals().get("WEEK_DIR") or os.path.abspath(os.getcwd()))
G = 9.80665 # m/s^2
# ----------------------------------------------------------------
# 0. 結構參數(CubeSat 側面板,懸臂於分離機構)—— 學生可調
# ----------------------------------------------------------------
E = 69.0e9 # Al 6061 楊氏模數 (Pa)
RHO = 2700.0 # 鋁密度 (kg/m^3)
L = 0.098 # 懸臂長度 (m) ~ 1U 面板邊
B = 0.098 # 面板寬 (m)
T = 0.0020 # 面板厚 (m) <- Part B 要改這個看頻率變化
M_TIP = 0.060 # 面板上元件的集中質量 (kg) (拉低第一模態,較真實)
ZETA = 0.03 # 模態阻尼比 (Q = 1/2zeta ~ 16.7)
F_REQ = 100.0 # 發射要求:第一自然頻率須 >= 此值 (Hz)
N_ELEM = 16 # 梁單元數
N_MODES_SHOW = 3 # 畫前幾個振型
# 導出截面性質(把面板當等效懸臂梁,矩形截面)
A_SEC = B * T # 截面積 (m^2)
I_SEC = B * T**3 / 12.0 # 慣性矩 (m^4)
RHO_A = RHO * A_SEC # 線密度 (kg/m)
# ----------------------------------------------------------------
# 1. 梁單元 FEA:組裝 K, M(Euler-Bernoulli,每節點 2 DOF: v, theta)
# ----------------------------------------------------------------
def beam_KM():
ne = N_ELEM
Le = L / ne
ndof = 2 * (ne + 1)
K = np.zeros((ndof, ndof))
M = np.zeros((ndof, ndof))
EI = E * I_SEC
# 單元剛度矩陣
ke = EI / Le**3 * np.array([
[ 12, 6*Le, -12, 6*Le],
[ 6*Le, 4*Le**2,-6*Le, 2*Le**2],
[-12, -6*Le, 12, -6*Le],
[ 6*Le, 2*Le**2,-6*Le, 4*Le**2]])
# 一致質量矩陣
me = RHO_A * Le / 420.0 * np.array([
[156, 22*Le, 54, -13*Le],
[22*Le, 4*Le**2, 13*Le,-3*Le**2],
[54, 13*Le, 156, -22*Le],
[-13*Le,-3*Le**2,-22*Le, 4*Le**2]])
for e in range(ne):
dofs = [2*e, 2*e+1, 2*e+2, 2*e+3]
for i in range(4):
for j in range(4):
K[dofs[i], dofs[j]] += ke[i, j]
M[dofs[i], dofs[j]] += me[i, j]
# 尖端集中質量(加在最後一個節點的平移 DOF)
M[2*ne, 2*ne] += M_TIP
return K, M, ndof
def solve_modal():
K, M, ndof = beam_KM()
# 邊界條件:節點 0 固定(v0=theta0=0)-> 移除 DOF 0,1
free = list(range(2, ndof))
Kff = K[np.ix_(free, free)]
Mff = M[np.ix_(free, free)]
# 廣義特徵值 K phi = w^2 M phi(質量正規化)
w2, phi = eigh(Kff, Mff)
w2 = np.clip(w2, 0, None)
freqs = np.sqrt(w2) / (2*np.pi) # Hz
return freqs, phi, free, Mff, ndof
# ----------------------------------------------------------------
# 2. 發射頻譜:GEVS 鑑定級隨機振動 PSD (g^2/Hz)
# ----------------------------------------------------------------
def gevs_psd(f):
"""NASA GSFC GEVS qualification random vibration, ~14.1 GRMS."""
f = np.asarray(f, dtype=float)
W = np.zeros_like(f)
# 20-50 Hz: +6 dB/oct (PSD ∝ f^2), 0.026 @20 -> 0.16 @50
m1 = (f >= 20) & (f < 50)
W[m1] = 0.026 * (f[m1]/20.0)**2
# 50-800 Hz: flat 0.16
m2 = (f >= 50) & (f < 800)
W[m2] = 0.16
# 800-2000 Hz: -6 dB/oct (PSD ∝ f^-2), 0.16 @800 -> 0.026 @2000
m3 = (f >= 800) & (f <= 2000)
W[m3] = 0.16 * (800.0/f[m3])**2
return W
def grms(f, psd):
return np.sqrt(np.trapezoid(psd, f))
# ----------------------------------------------------------------
# 3. 基礎激振隨機響應(模態疊加,SDOF 絕對加速度傳遞率)
# 假設:輕阻尼、模態獨立、各模態以 SDOF 絕對加速度傳遞率響應。
# ----------------------------------------------------------------
def sdof_abs_transmissibility(f, fn, zeta):
r = f / fn
num = 1.0 + (2*zeta*r)**2
den = (1.0 - r**2)**2 + (2*zeta*r)**2
return np.sqrt(num/den)
def random_response(freqs, phi, free, Mff):
# 基礎平移影響向量 r_f:自由 DOF 中平移分量=1、旋轉分量=0
rf = np.zeros(len(free))
for k, dof in enumerate(free):
if dof % 2 == 0: # 偶數 DOF = 平移 v
rf[k] = 1.0
# 模態參與因子(phi 已質量正規化 -> phi^T M phi = 1)
Gamma = phi.T @ (Mff @ rf)
# 尖端平移 DOF 在自由集合中的索引(最後一個節點的 v)
tip_dof = free.index(2*N_ELEM)
phi_tip = phi[tip_dof, :]
fgrid = np.linspace(20, 2000, 4000)
Win = gevs_psd(fgrid)
# 尖端絕對加速度傳遞率(模態加權和)
T_tip = np.zeros_like(fgrid)
for i in range(len(freqs)):
if freqs[i] < 1.0: # 跳過近零剛體/數值模態
continue
Ti = sdof_abs_transmissibility(fgrid, freqs[i], ZETA)
T_tip += (Gamma[i]*phi_tip[i]) * Ti
T_tip = np.abs(T_tip)
Sresp = (T_tip**2) * Win # 尖端響應 PSD (g^2/Hz)
grms_in = grms(fgrid, Win)
grms_out = grms(fgrid, Sresp)
# Miles 方程(僅第一模態)交叉驗證
f1 = freqs[freqs > 1][0]
Q = 1.0/(2*ZETA)
grms_miles = np.sqrt((np.pi/2.0)*f1*Q*float(gevs_psd(f1)))
return fgrid, Win, Sresp, T_tip, grms_in, grms_out, grms_miles
# ----------------------------------------------------------------
# 4. 產生 CalculiX 3D 模態 .inp(正牌工具路徑)
# ----------------------------------------------------------------
def write_calculix_inp(path=None, nx=10, ny=10, nz=1):
"""同一片懸臂面板的 3D C3D8 磚元網格,*FREQUENCY 取 5 模態。
在有 CalculiX 的機器: ccx cubesat_panel_modal
檢視:cgx cubesat_panel_modal.frd 或匯出 ParaView。
path 給 None 就寫到週次目錄(預設值不能寫成相對檔名,那會跟著
process 的工作目錄跑)。"""
path = path or os.path.join(HERE, "cubesat_panel_modal.inp")
xs = np.linspace(0, L, nx+1)
ys = np.linspace(0, B, ny+1)
zs = np.linspace(0, T, nz+1)
def nid(i, j, k): return 1 + i + (nx+1)*j + (nx+1)*(ny+1)*k
lines = []
lines.append("** Week5 CubeSat panel - CalculiX modal analysis")
lines.append("** Same cantilever panel as the Python beam FEA (compare f1).")
lines.append("*NODE")
for k, z in enumerate(zs):
for j, y in enumerate(ys):
for i, x in enumerate(xs):
lines.append(f"{nid(i,j,k)}, {x:.6f}, {y:.6f}, {z:.6f}")
lines.append("*ELEMENT, TYPE=C3D8, ELSET=PANEL")
eid = 1
for k in range(nz):
for j in range(ny):
for i in range(nx):
n1=nid(i,j,k); n2=nid(i+1,j,k); n3=nid(i+1,j+1,k); n4=nid(i,j+1,k)
n5=nid(i,j,k+1); n6=nid(i+1,j,k+1); n7=nid(i+1,j+1,k+1); n8=nid(i,j+1,k+1)
lines.append(f"{eid}, {n1},{n2},{n3},{n4},{n5},{n6},{n7},{n8}")
eid += 1
# 固定端:x=0 面所有節點
lines.append("*NSET, NSET=FIXED")
fixed=[nid(0,j,k) for k in range(nz+1) for j in range(ny+1)]
for c in range(0, len(fixed), 8):
lines.append(",".join(str(n) for n in fixed[c:c+8]) + ",")
lines.append("*MATERIAL, NAME=AL6061")
lines.append("*ELASTIC")
lines.append(f"{E:.0f}, 0.33")
lines.append("*DENSITY")
lines.append(f"{RHO:.1f}")
lines.append("*SOLID SECTION, ELSET=PANEL, MATERIAL=AL6061")
lines.append("*BOUNDARY")
lines.append("FIXED, 1, 3") # 固定端三向位移=0
lines.append("*STEP")
lines.append("*FREQUENCY")
lines.append("5") # 取前 5 個自然頻率
lines.append("*NODE FILE")
lines.append("U")
lines.append("*END STEP")
with open(path, "w") as f:
f.write("\n".join(lines) + "\n")
nnode=(nx+1)*(ny+1)*(nz+1); nelem=nx*ny*nz
return path, nnode, nelem
# ----------------------------------------------------------------
# 5. 執行 + 視覺化
# ----------------------------------------------------------------
def main():
freqs, phi, free, Mff, ndof = solve_modal()
fnz = freqs[freqs > 1.0]
f1 = fnz[0]
passed = f1 >= F_REQ
print("=== 模態分析(Python 梁 FEA)===")
print(f"截面 {B*1e3:.0f}x{T*1e3:.1f} mm, L={L*1e3:.0f} mm, 尖端質量 {M_TIP*1e3:.0f} g")
for i in range(min(5, len(fnz))):
print(f" Mode {i+1}: {fnz[i]:8.1f} Hz")
print(f"發射要求 f1 >= {F_REQ:.0f} Hz -> 第一模態 {f1:.1f} Hz ==> {'PASS' if passed else 'FAIL'}")
fgrid, Win, Sresp, Ttip, g_in, g_out, g_miles = random_response(freqs, phi, free, Mff)
print("\n=== 發射隨機振動(GEVS 鑑定級)===")
print(f"輸入 GRMS : {g_in:5.1f} g (GEVS 標稱 ~14.1)")
print(f"尖端響應 GRMS : {g_out:5.1f} g (含各模態放大)")
print(f"Miles(第一模態): {g_miles:5.1f} g (單模態近似,應 <= 全解)")
inp, nn, ne = write_calculix_inp()
print(f"\n已產生 CalculiX deck: {inp} ({nn} nodes, {ne} C3D8)")
print(" 在有 ccx 的機器執行: ccx cubesat_panel_modal")
# ---------- 圖 ----------
fig = plt.figure(figsize=(12, 9))
gs = fig.add_gridspec(2, 2)
# (1) 前 3 個模態振型
ax1 = fig.add_subplot(gs[0, 0])
xn = np.linspace(0, L, N_ELEM+1)*1e3
colors = ["tab:blue", "tab:orange", "tab:green"]
for m in range(min(N_MODES_SHOW, len(fnz))):
full = np.zeros(ndof)
full[free] = phi[:, m]
v = full[0::2] # 平移分量
v = v/np.max(np.abs(v))
ax1.plot(xn, v, color=colors[m], lw=2, label=f"Mode {m+1}: {fnz[m]:.0f} Hz")
ax1.axhline(0, color="gray", lw=0.6)
ax1.set_xlabel("Position along cantilever (mm)"); ax1.set_ylabel("Normalized deflection")
ax1.set_title("Mode Shapes"); ax1.legend(fontsize=8); ax1.grid(alpha=0.3)
# (2) 輸入 PSD vs 尖端響應 PSD(log-log)
ax2 = fig.add_subplot(gs[0, 1])
ax2.loglog(fgrid, Win, color="black", lw=1.5, label=f"Input GEVS ({g_in:.1f} GRMS)")
ax2.loglog(fgrid, Sresp, color="tab:red", lw=1.2, label=f"Tip response ({g_out:.1f} GRMS)")
for fr in fnz[:3]:
ax2.axvline(fr, color="gray", ls=":", lw=0.8)
ax2.set_xlabel("Frequency (Hz)"); ax2.set_ylabel("PSD (g$^2$/Hz)")
ax2.set_title("Launch random vibration: input vs response"); ax2.legend(fontsize=8); ax2.grid(alpha=0.3, which="both")
# (3) 自然頻率 vs 發射要求
ax3 = fig.add_subplot(gs[1, 0])
idx = np.arange(1, min(5, len(fnz))+1)
bars = ax3.bar(idx, fnz[:len(idx)],
color=["tab:green" if v >= F_REQ else "tab:red" for v in fnz[:len(idx)]])
ax3.axhline(F_REQ, color="red", ls="--", lw=1.5, label=f"Launch req {F_REQ:.0f} Hz")
ax3.set_xlabel("Mode number"); ax3.set_ylabel("Natural frequency (Hz)")
ax3.set_title(f"Natural freq vs requirement ({'PASS' if passed else 'FAIL'})")
ax3.set_xticks(idx); ax3.legend(fontsize=8); ax3.grid(alpha=0.3, axis="y")
# (4) 尖端傳遞率
ax4 = fig.add_subplot(gs[1, 1])
ax4.semilogy(fgrid, Ttip, color="tab:purple", lw=1.2)
Q = 1.0/(2*ZETA)
ax4.axhline(Q, color="gray", ls=":", lw=1, label=f"Q ≈ {Q:.0f} (zeta={ZETA})")
for fr in fnz[:3]:
ax4.axvline(fr, color="gray", ls=":", lw=0.6)
ax4.set_xlabel("Frequency (Hz)"); ax4.set_ylabel("Tip/base acceleration transmissibility")
ax4.set_title("Transmissibility"); ax4.legend(fontsize=8); ax4.grid(alpha=0.3, which="both")
fig.suptitle("Week 5 - CubeSat panel FEA + launch random vibration", fontsize=13, y=0.995)
fig.tight_layout(rect=[0, 0, 1, 0.98])
fig.savefig(os.path.join(HERE, "week5_fea_launch_vibe.png"), dpi=130)
print("\nWrote week5_fea_launch_vibe.png")
if __name__ == "__main__":
main()