Compare commits
12 Commits
c918ac0de4
...
9c3f93d58d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3f93d58d | ||
|
|
2ae248421d | ||
|
|
5c55c5bdf7 | ||
|
|
8df40fb5bd | ||
|
|
4ae6e2e45c | ||
|
|
52229ea0f0 | ||
|
|
3e9cffda8e | ||
|
|
3edf964f34 | ||
|
|
31e367db0e | ||
|
|
f66671a498 | ||
|
|
225f653840 | ||
|
|
56e3c78a83 |
11
.gitignore
vendored
@ -75,6 +75,17 @@ tensorboard/
|
||||
*.h5
|
||||
*.hdf5
|
||||
*.npz
|
||||
*.npy
|
||||
|
||||
# Analysis outputs (generated)
|
||||
src/OID_analysis/data/derived/
|
||||
src/OID_analysis/data/*/vorticity_*.png
|
||||
src/OID_analysis/data/*/*/vorticity_*.png
|
||||
src/OID_analysis/data/*/*/ddf_checkpoint.npy
|
||||
src/OID_analysis/data/*/*/fifo_checkpoint.npy
|
||||
src/CCD_analysis/data/
|
||||
src/SR_analysis/data/*/*/fields.npz
|
||||
outputs/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
2
.gitmodules
vendored
@ -1,3 +1,5 @@
|
||||
[submodule "CelerisLab"]
|
||||
path = CelerisLab
|
||||
url = https://github.com/Frank14f/CelerisLab.git
|
||||
ignore = dirty
|
||||
|
||||
@ -33,5 +33,5 @@
|
||||
#define V_TAYLOR 0b00000001
|
||||
|
||||
// variables
|
||||
#define N_OBJS 6
|
||||
#define N_OBJS 7
|
||||
// #define N_SENS 2
|
||||
50
configs/config_lbm_karman_2000x600.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"_doc": "Karman Cloak Re100: uniform inlet, free-slip walls, 2000x600 grid. Pinball centered.",
|
||||
"grid": {
|
||||
"lattice_model": "D2Q9",
|
||||
"nx": 2000,
|
||||
"ny": 600,
|
||||
"nz": 1
|
||||
},
|
||||
"physics": {
|
||||
"data_type": "FP32",
|
||||
"viscosity": 0.004,
|
||||
"velocity": 0.01,
|
||||
"rho": 1.0
|
||||
},
|
||||
"method": {
|
||||
"collision": "MRT",
|
||||
"streaming": "double_buffer",
|
||||
"store_precision": "FP32",
|
||||
"ddf_shifting": false,
|
||||
"les": {
|
||||
"enabled": false,
|
||||
"cs": 0.16,
|
||||
"closed_form": true
|
||||
},
|
||||
"trt": {
|
||||
"magic_param": 0.1875
|
||||
},
|
||||
"inlet": {
|
||||
"profile": "uniform",
|
||||
"scheme": "regularized",
|
||||
"trt_neq_damp": 0.5,
|
||||
"regularized_neq_damp": 0.5
|
||||
},
|
||||
"outlet": {
|
||||
"mode": "neq_extrap",
|
||||
"backflow_clamp": true,
|
||||
"blend_alpha": 0.7,
|
||||
"srt_neq_damp": 0.5
|
||||
},
|
||||
"y_wall_bc": "free_slip",
|
||||
"omega_guard": {
|
||||
"min": 0.01,
|
||||
"max": 1.99
|
||||
}
|
||||
},
|
||||
"cuda": {
|
||||
"threads_per_block": 256,
|
||||
"compute_capability": "auto"
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
# SR Analysis Pipeline — Handover Notes (2026-06-15)
|
||||
|
||||
## 交接人 → 接手人
|
||||
|
||||
### 当前管线状态
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|------|:----:|------|
|
||||
| Illusion 0.75L + 1L phase-state + abs | **已验证** | 闭环 0.974 / 0.958,无动作历史,可进 PySR |
|
||||
| Illusion 1.5L | **边界 case** | bang-bang 机制,线性 SINDy 不适用 |
|
||||
| Karman phase-state + abs | **0.699** | 优于 deriv 模式,但低于旧 v23 的 0.901 |
|
||||
| Karman old v23 (a_lag) | **0.901** | 保留作基线对照 |
|
||||
| Karman 泛化测试 | **已完成** | Re70/150/300/25 约 0.54-0.60 |
|
||||
| PySR 符号回归 | **有 shell** | 需要修复 run_pysr.py 后重新跑 |
|
||||
| Vortex 偏移扩展 | **未做** | 低优先级 |
|
||||
|
||||
### 核心文件改动(2026-06-14~15)
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|----------|
|
||||
| `utils/feature_builder.py` | **新增** PHASE_STATE_KEYS, ILLUSION_PHASE_KEYS, KARMAN_EXPANDED_KEYS, obs dynamics, error-state, mu_Cl_tot |
|
||||
| `utils/sindy_fitter.py` | **新增** compute_action_deriv, get_feature_matrix_deriv(output_mode) |
|
||||
| `sindy/run_all_v2.py` | **新增** --deriv, --phase, --karman-expand, --karman-mu, --output-mode, --augment-level CLI |
|
||||
| `validate/run_closed_loop.py` | **新增** predict_v23_deriv, mode="abs", load_sindy_coefs返回mode |
|
||||
| `validate/run_closed_loop_illusion.py` | **修改** 支持 predict_v23_deriv, 自动检测模式 |
|
||||
| `validate/eval_rollout.py` | **新文件** 离线多步 rollout 评估 |
|
||||
| `scripts/plot_sr_results.py` | **新文件** 结果可视化图表 |
|
||||
| `docs/SR_analysis_results.md` | **新文件** 完整分析报告 |
|
||||
| `docs/figures/SR_analysis/fig*.png` | **新文件** 6 张图表 |
|
||||
|
||||
### 关键设计决策(接手前必读)
|
||||
|
||||
1. **phase-state 特征** = `u_a, du_a/dt, Cl_tot, dCl_tot/dt, Cd_tot, Cd_rear` (6维)
|
||||
2. **ILLUSION_PHASE_KEYS** = phase-state + `Cd_err, Cl_err, dCd_err/dt, dCl_err/dt` (10维)
|
||||
3. **绝对动作输出** `output_mode="absolute"` 优于导数 `"deriv"`,无积分累积
|
||||
4. **v23 结构**始终默认:front no-bias, rear shared-head
|
||||
5. **时间一阶导**统一除以 `dt_c = SAMPLE_INTERVAL/2000`,跨场景可比较
|
||||
6. `controlled.npz` 中新加了 `target_forces` 字段,illusion 场景必须有
|
||||
7. **FIFO bias ≠ DRL action bias**,1U vs 2U 不要混淆
|
||||
|
||||
### 常用命令速查
|
||||
|
||||
```bash
|
||||
# 拟合 + 验证 Illusion phase-state + absolute (完整流程)
|
||||
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
|
||||
--scenes illusion_0.75L,illusion_1L --deriv --phase --output-mode absolute
|
||||
conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_illusion.py \
|
||||
--scene illusion_0.75L --device 0 --steps 320 \
|
||||
--sindy-results src/SR_analysis/sindy/illusion/sindy_results_deriv.json
|
||||
|
||||
# 拟合 + 验证 Karman phase-state + absolute
|
||||
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
|
||||
--scenes karman_re100 --deriv --phase --output-mode absolute
|
||||
conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop.py \
|
||||
--scene karman_re100 --device 0 --steps 200 --mode abs \
|
||||
--sindy-results src/SR_analysis/sindy/karman/sindy_results_deriv.json
|
||||
|
||||
# 离线 rollout 评估
|
||||
python3 src/SR_analysis/validate/eval_rollout.py \
|
||||
--sindy-results src/SR_analysis/sindy/karman/sindy_results_deriv.json \
|
||||
--scene karman_re100
|
||||
|
||||
# PySR (需要先修复滞后的 bug!)
|
||||
conda run -n sr_env python src/SR_analysis/sindy/run_pysr.py --scene illusion_1L
|
||||
|
||||
# 画图
|
||||
python3 scripts/plot_sr_results.py
|
||||
```
|
||||
|
||||
### 目前最适合推进的方向
|
||||
|
||||
1. **Illusion PySR 符号回归**(0.75L + 1L separate → 公式比较)
|
||||
2. **Karman 状态补强**(配合 CCD/OID 分析找出缺失的状态量,再回 SR)
|
||||
3. **Karman 跨 Re 联合**(在 phase-state + mu 基础上做跨 Re 联合拟合 + 泛化)
|
||||
|
||||
### 注意重新运行 run_pysr.py
|
||||
|
||||
当前 `run_pysr.py` 有路径/导入问题,接手后需先确认:
|
||||
- `env_sr` 或 `sr_env` 环境内的 PySR 可用性
|
||||
- whitelist 特征与 `controlled.npz` 中的字段匹配(特别是 illusion 的 `target_forces`)
|
||||
- 滞后构造正确(`a_prev[1:] = actions_phys[:-1]`)
|
||||
@ -1,136 +0,0 @@
|
||||
# OID Analysis Handover Notes
|
||||
|
||||
## For the incoming agent
|
||||
|
||||
---
|
||||
|
||||
## Quick Overview
|
||||
|
||||
You are taking over the **OID analysis line** of the DynamisLab fluidic pinball project. This is one of three parallel analysis pipelines:
|
||||
|
||||
- **SR/SINDy**: `obs -> act` white-box control law extraction (most mature)
|
||||
- **CCD**: `structure -> force/signature` phase-aligned correlation decomposition (correction-field analysis in progress)
|
||||
- **OID**: `Delta-q_ctl -> structure -> force/signature` full-time-series observable-related decomposition (this line)
|
||||
|
||||
OID has been fully implemented as an independent project under `src/OID_analysis/`. All 5 scenes (steady cloak, Karman cloak, illusion 0.75/1.0/1.5L) have been analyzed end-to-end through Phases 1-7.
|
||||
|
||||
---
|
||||
|
||||
## Start Here (in order)
|
||||
|
||||
1. **`docs/OID_analysis_results.md`** -- Full project report with 7 figures. Read this first. It explains OID concepts, all results, and all caveats.
|
||||
|
||||
2. **`src/OID_analysis/README.md`** -- Engineering entry point. How to run, directory structure, common pitfalls.
|
||||
|
||||
3. **`src/OID_analysis/OID_knowledge.md`** -- Confirmed facts, critical rules (15+ rules), current results, bug history.
|
||||
|
||||
4. **`src/OID_analysis/OID_notes.md`** -- Task tracking, open items.
|
||||
|
||||
5. **`docs/ccd_correction_field_report.md`** -- CCD report (sibling project, important cross-reference)
|
||||
6. **`docs/SR_analysis_results.md`** -- SR report (control law white-box results)
|
||||
|
||||
---
|
||||
|
||||
## The Key Concept
|
||||
|
||||
OID (Observable-Inferred Decomposition) answers: "Which flow structures most affect a chosen **observable** (force, sensor error)?"
|
||||
|
||||
It does this by:
|
||||
1. Starting with a POD basis (unified coordinate system, but ranked by energy not task relevance)
|
||||
2. Computing cross-covariance between POD coefficients and the observable
|
||||
3. SVD to find directions in POD space that best correlate with the observable
|
||||
|
||||
OID operates on **correction fields** Delta-q_ctl = q_ctl - q_blk (controlled field minus baseline zero-rotation field), NOT on raw controlled fields.
|
||||
|
||||
---
|
||||
|
||||
## The Flagship Result
|
||||
|
||||
Force-relevant and signature-relevant correction structures **systematically separate** across control tasks:
|
||||
|
||||
```
|
||||
steady_cloak (+0.763) --> Karman (-0.034) --> illusion 0.75L (-0.082) --> illusion 1.0L (-0.495) --> illusion 1.5L (-0.932)
|
||||
```
|
||||
|
||||
where the number is cosine similarity between force-OID mode 1 and signature-OID mode 1.
|
||||
The monotonic trend from same-channel to strongly opposite is the project's most compelling new physical finding.
|
||||
|
||||
OID also consistently beats POD for predicting force and signature in all scenes.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/OID_analysis/configs.py` | Scene definitions (12 scenes) |
|
||||
| `src/OID_analysis/utils/analysis.py` | POD, OID, PCD, statistics (CPU, no GPU) |
|
||||
| `src/OID_analysis/utils/cfd_interface.py` | Re-exports from CCD (GPU) |
|
||||
| `src/OID_analysis/analysis/phase3_force_oid.py` | Force-OID implementation |
|
||||
| `src/OID_analysis/analysis/phase4a_signature_oid.py` | Signature-OID implementation |
|
||||
| `src/OID_analysis/analysis/phase4b_signature_pcd.py` | PCD whitened cross-correlation |
|
||||
| `src/OID_analysis/analysis/robustness_analysis.py` | Rank/tau_c/window robustness |
|
||||
| `src/OID_analysis/analysis/make_figures.py` | Generate all 7 figures |
|
||||
| `src/OID_analysis/scripts/collect_illusion_qblk.py` | **Important**: illusion-position q_blk (separate from cloak) |
|
||||
|
||||
---
|
||||
|
||||
## Environments
|
||||
|
||||
```bash
|
||||
# CFD data collection (GPU required, 2 GPUs available: 1 and 3)
|
||||
conda run -n pycuda_3_10 python src/OID_analysis/scripts/...
|
||||
|
||||
# Analysis (CPU only)
|
||||
conda run -n sr_env python3 src/OID_analysis/analysis/...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Items (Priority Order)
|
||||
|
||||
### P0 - Should fix next
|
||||
1. **Illusion 0.75L rank instability** (std=0.26 across r=6..16). Likely needs longer time series. Current 100 snapshots for POD may be insufficient.
|
||||
2. **Karman future-signal R2~0** -- currently near zero. Consider reformulating the signature observable as phase-error instead of direct sensor error.
|
||||
|
||||
### P1 - Important but not blocking
|
||||
3. **OID mode-to-field visualization** -- OID spatial modes are computed but not plotted. Would show whether force-sig separation maps to different physical regions (near-body vs downstream).
|
||||
4. **causal-PCD** -- Need a separate action-PCD to get action-related z_act coordinates for the `obs -> z -> act` chain.
|
||||
|
||||
### P2 - Enhancement
|
||||
5. **Cross-validation** across multiple independent rollouts
|
||||
6. **Vortex scenes** extension (data collected in SR but not in OID)
|
||||
7. **CCD cross-validation** of the force-sig separation trend using phase-aligned data
|
||||
|
||||
---
|
||||
|
||||
## Data to Keep / Not Commit
|
||||
|
||||
**Commit these** (small, reproducible):
|
||||
- All `.py` files in `src/OID_analysis/`
|
||||
- All `.md` files in `src/OID_analysis/`
|
||||
- `docs/OID_analysis_results.md`
|
||||
- JSON configs in `data/configs/`
|
||||
|
||||
**DO NOT commit** (large, regeneratable):
|
||||
- All `.npz`, `.npy` in `data/`
|
||||
- All `.png` in `data/derived/figures/`
|
||||
- Check `.gitignore` for proper exclusion
|
||||
|
||||
---
|
||||
|
||||
## Context on SR and CCD
|
||||
|
||||
- **SR/SINDy** is the most mature line. Best result: Karman cross-Re unified backbone achieves 94.4% of PPO closed-loop performance. New phase-state features for illusion achieve 100.2% of PPO with zero action memory. The SR agent wrote a detailed README at `src/SR_analysis/README.md`.
|
||||
|
||||
- **CCD** is in correction-field transition. Round 5 (raw-field baseline) is frozen. Round 6 (correction-field) Phase 1-2 complete. The key CCD finding that cross-validates OID: zone-restricted analysis shows force-sig structures are almost orthogonal in the sensor zone at zero lag (O=0.01) but converge after convective delay (O=0.72-0.92).
|
||||
|
||||
- The **correction-field framework** (Delta-q_ctl) is the shared analysis object across all three lines. All analyses should use it.
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
- The OID analysis code is self-contained. It re-uses `CCD_analysis.utils.cfd_interface` for GPU operations but has its own analysis utilities and configs.
|
||||
- The `data_dir_for_scene()` function in `configs.py` is the single source of truth for all data paths. Never hardcode paths.
|
||||
- The most likely next useful step is: (a) fix the 0.75L rank instability, (b) produce the mode-to-field plots, (c) feed OID coordinates into SR's SINDy framework for the `obs -> z -> act` test.
|
||||
@ -1,225 +0,0 @@
|
||||
# SR Analysis: Phase-State SINDy Results Report
|
||||
|
||||
> Date: 2026-06-15
|
||||
> Project: DynamisLab — Active hydrodynamic cloaking and illusion using DRL on a fluidic pinball.
|
||||
> Analysis pipeline: SINDy (STLSQ) + feature engineering for interpretable control law extraction.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Three key findings from the SR analysis pipeline:
|
||||
|
||||
1. **Illusion 0.75L + 1L: New route validated.** Phase-state features + error-state + absolute action output achieves closed-loop similarity of **0.96+** (97% of PPO) with **zero action history features**. This proves that physically meaningful control laws can be extracted without relying on action memory.
|
||||
|
||||
2. **Illusion 1.5L: Regime shift identified.** The 1.5L target exhibits bang-bang/saturated control (alpha range [-8, 8], autocorrelation r=0.07). Linear SINDy is fundamentally inadequate (R2=0.12). This is a regime boundary, not a modelling failure.
|
||||
|
||||
3. **Karman: State representation still incomplete.** The same phase-state approach reaches 0.699 (vs 0.901 baseline with action history). The problem is not the output form (derivative vs absolute) but insufficient input state information for the Karman scene.
|
||||
|
||||
---
|
||||
|
||||
## 1. Methodology
|
||||
|
||||
### 1.1 Feature Architecture (Final)
|
||||
|
||||
Three feature levels were tested for fitting `alpha = f(state)` or `d(alpha)/dt = g(state)`:
|
||||
|
||||
| Level | Features | Dim | Description |
|
||||
|-------|----------|:---:|-------------|
|
||||
| Static | u_m, u_a, u_c, v_a, Cd_tot, Cd_rear, Cl_tot, Cl_diff | 8 | Current-step physics only, no memory |
|
||||
| Phase-state | u_a, du_a/dt, Cl_tot, dCl_tot/dt, Cd_tot, Cd_rear (+error terms) | 6+4 | Oscillation phase + rate + drag feedback |
|
||||
| Full-lag | Static + lag-1 of all 8 | 16 | Brute-force temporal context (baseline) |
|
||||
|
||||
### 1.2 Output Modes
|
||||
|
||||
Two output targets were compared:
|
||||
|
||||
- **Derivative mode**: predict d(alpha)/dt, then integrate `alpha(t) = alpha(t-1) + dt_c * dalpha/dt`
|
||||
- **Absolute mode**: predict alpha directly, no integration needed
|
||||
|
||||
### 1.3 Evaluation Metrics
|
||||
|
||||
Models are evaluated on:
|
||||
1. **One-step R2**: fit quality on training data
|
||||
2. **Offline multi-step rollout**: 1/5/20/50 step recursive prediction on held-out PPO data
|
||||
3. **CFD closed-loop**: full CFD environment with SINDy control law, DTW similarity vs target
|
||||
|
||||
---
|
||||
|
||||
## 2. Illusion Results
|
||||
|
||||
### 2.1 Three-Scene Summary
|
||||
|
||||
| Scene | S | Old v23 (a_lag) | New phase+error+abs | PPO baseline | % of PPO | Features | Action history? |
|
||||
|------|:--:|:----------------:|:-------------------:|:------------:|:--------:|----------|:---------------:|
|
||||
| 0.75L | 400 | 0.908 | **0.974** | 0.972 | 100.2% | 10-dim (ILLUSION_PHASE) | **No** |
|
||||
| 1L | 600 | 0.962 | **0.958** | 0.973 | 98.5% | 10-dim (ILLUSION_PHASE) | **No** |
|
||||
| 1.5L | 800 | 0.926 | **N/A** | 0.945 | — | — | Bang-bang regime |
|
||||
|
||||
**Key insight**: The 0.75L scene's new route **outperforms** the old v23 (0.974 vs 0.908), while 1L matches it within 1.5%. This definitively proves that "action history is not necessary."
|
||||
|
||||
### 2.2 Illusion 1L Front Coefficients (Phase-State, Absolute Output)
|
||||
|
||||
```
|
||||
alpha_F = f(u_a, du_a/dt, Cl_tot, dCl_tot/dt, Cd_tot, Cd_rear, Cd_err, Cl_err, dCd_err/dt, dCl_err/dt)
|
||||
R2 = 0.987
|
||||
```
|
||||
|
||||
Key contributors (sorted by |coef|): Cd_rear > Cd_tot > Cd_err > Cl_tot > Cl_err > dCl_err/dt > dCd_err/dt
|
||||
|
||||
The formula is dominated by **drag-based feedback** (Cd_tot, Cd_rear, Cd_err), with the oscillation phase (u_a, Cl_tot) providing secondary modulation.
|
||||
|
||||
### 2.3 Illusion 1.5L — Regime Shift Evidence
|
||||
|
||||
| Metric | 1L | 1.5L | Ratio |
|
||||
|--------|:--:|:----:|:-----:|
|
||||
| Alpha range (front) | [-0.36, 1.11] | **[-8.0, 8.0]** | 10x |
|
||||
| 1-step autocorrelation | 0.957 | **0.065** | — |
|
||||
| d(alpha)/dt std | 0.12 | **6.07** | 50x |
|
||||
| Linear SINDy R2 | 0.987 | **0.124** | — |
|
||||
|
||||
The 1.5L controller operates at saturation limits (maximum rotation speed), flipping between extremes. This is consistent with the frequency-doubling control strategy reported in the confirmation report.
|
||||
|
||||
---
|
||||
|
||||
## 3. Karman Results
|
||||
|
||||
### 3.1 Ablation Study (re100)
|
||||
|
||||
| Configuration | Feat | Output | R2 | Closed-loop | % of PPO |
|
||||
|--------------|:----:|:-----:|:--:|:----------:|:--------:|
|
||||
| old v23 (a_lag1 dominant) | 14+3 | alpha | 0.996 | **0.901** | 94.4% |
|
||||
| **Phase-state -> abs (best new)** | **6** | **alpha** | **0.965** | **0.699** | 73.3% |
|
||||
| Phase-state -> deriv | 6 | dalpha/dt | 0.837 | 0.656 | 68.8% |
|
||||
| Phase-state + mu -> abs | 9 | alpha | 0.979 | 0.700 | 73.4% |
|
||||
| Expanded 10-dim -> abs | 10 | alpha | 0.980 | **0.580** | 60.8% |
|
||||
| Full-lag -> deriv | 16 | dalpha/dt | 0.939 | 0.619 | 64.9% |
|
||||
| Static -> deriv | 8 | dalpha/dt | 0.321 | 0.745 | 78.1% |
|
||||
|
||||
**Key insight from ablation**:
|
||||
|
||||
1. **Phase-state + absolute output is the best new route** (0.699), but still well below the action-history baseline.
|
||||
2. Adding extra static features (expanded 10-dim) **hurts** closed-loop despite higher R2 — classic covariate shift.
|
||||
3. The static->deriv paradox: low R2 (0.321) but good closed-loop (0.745), because no-memory models are naturally robust to rollout divergence.
|
||||
4. Mu modulation doesn't help at single-Re; its value will appear in cross-Re fitting.
|
||||
|
||||
### 3.2 Cross-Re Generalization (old v23 model)
|
||||
|
||||
| Re | Type | Closed-loop | Note |
|
||||
|:--:|:----:|:----------:|------|
|
||||
| 50 | Training | 0.582 | Low-frequency shedding |
|
||||
| 100 | Training | **0.901** | Default / best |
|
||||
| 200 | Training | 0.793 | Moderate degradation |
|
||||
| 400 | Training | 0.664 | High-Re challenge |
|
||||
| 25 | Unseen (subcritical) | 0.567 | Below Hopf bifurcation |
|
||||
| 70 | Unseen | 0.577 | Between Re50 and Re100 |
|
||||
| 150 | Unseen | 0.595 | Between Re100 and Re200 |
|
||||
| 300 | Unseen | 0.541 | Outer extrapolation |
|
||||
|
||||
### 3.3 One-Step R2 vs Closed-Loop Paradox
|
||||
|
||||
A key phenomenon discovered during this work: **high one-step R2 does not predict good closed-loop performance** when temporal features (lags, derivatives) are present in the input. This is because:
|
||||
|
||||
- **Training**: features use ground-truth PPO observations
|
||||
- **Deployment**: features use SINDY-controlled observations (distribution shift)
|
||||
- Temporal features amplify this shift recursively
|
||||
|
||||
---
|
||||
|
||||
## 4. Roadmap to Next Steps
|
||||
|
||||
### 4.1 Illusion: Proceed to PySR
|
||||
|
||||
The 0.75L and 1L scenes are ready for symbolic regression. Recommended input:
|
||||
|
||||
```python
|
||||
ILLUSION_PHASE_KEYS = [
|
||||
"u_a", "du_a_dt", # oscillation phase
|
||||
"Cl_tot", "dCl_tot_dt", # lift dynamics
|
||||
"Cd_tot", "Cd_rear", # drag feedback
|
||||
"Cd_err", "Cl_err", # force error
|
||||
"dCd_err_dt", "dCl_err_dt", # error dynamics
|
||||
]
|
||||
```
|
||||
|
||||
Output: absolute alpha (non-dimensional action), no integration.
|
||||
|
||||
First do separate PySR for each scene, then compare formula structure for shared backbone.
|
||||
|
||||
### 4.2 Karman: CCD/OID for state completion
|
||||
|
||||
The 0.699 ceiling suggests missing state variables. Candidate directions:
|
||||
- Recirculation zone length / reattachment point
|
||||
- Wake centerline deflection
|
||||
- POD mode coefficients for phase completion
|
||||
- CCD modes correlated with control action
|
||||
|
||||
### 4.3 SR / CCD / OID Integration Framework
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Control Objective │
|
||||
│ (stealth / illusion / erase) │
|
||||
└──────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ DRL Policy │
|
||||
│ (PPO + Sin act) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
┌────────▼──────┐ ┌─────▼──────┐ ┌──────▼─────────┐
|
||||
│ SINDy / SR │ │ CCD / OID │ │ Validation │
|
||||
│ obs -> act │ │ obs -> z │ │ CFD closed- │
|
||||
│ white-box │ │ structure │ │ loop + DTW │
|
||||
│ control law │ │ analysis │ │ │
|
||||
└────────┬──────┘ └─────┬──────┘ └──────┬─────────┘
|
||||
│ │ │
|
||||
└────────────────┼─────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Interpretable │
|
||||
│ Control Mechanics │
|
||||
│ obs -> z -> act │
|
||||
│ -> structure -> │
|
||||
│ -> signature │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Code Changes Summary
|
||||
|
||||
### Files Modified (8 core files)
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/SR_analysis/utils/feature_builder.py` | PHYSICS_FEAT_KEYS, ILLUSION_ERR_KEYS, PHASE_STATE_KEYS, ILLUSION_PHASE_KEYS, KARMAN_EXPANDED_KEYS; obs dynamics (lag1 + derivative); error-state computation; 1D target_forces support |
|
||||
| `src/SR_analysis/utils/sindy_fitter.py` | compute_action_deriv(); get_feature_matrix_deriv() with output_mode="deriv"|"absolute"; include_mu support |
|
||||
| `src/SR_analysis/sindy/run_all_v2.py` | run_single_scene_deriv(); run_joint_karman_deriv(); --deriv, --phase, --karman-expand, --karman-mu, --output-mode, --augment-level CLI |
|
||||
| `src/SR_analysis/validate/run_closed_loop.py` | predict_v23_deriv() with output_mode; mode="abs" branch; load_sindy_coefs returns "mode" |
|
||||
| `src/SR_analysis/validate/run_closed_loop_illusion.py` | Support predict_v23_deriv; auto-detect SINDy mode from coefs |
|
||||
| `src/SR_analysis/utils/__init__.py` | Export new constants and functions |
|
||||
| `src/SR_analysis/sindy/wrap_joint.py` | Parameterized for karman/illusion |
|
||||
| `src/SR_analysis/validate/eval_rollout.py` | **New**: offline multi-step rollout evaluation |
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
1. **No action history in features** for all new routes (PHASE_STATE_KEYS, ILLUSION_PHASE_KEYS)
|
||||
2. **Time-normalized derivatives** for cross-scene compatibility: `dx/dt = (x(t) - x(t-1)) / dt_c`
|
||||
3. **Error-state** for Illusion: Cd_err, Cl_err encode current-to-target deviation
|
||||
4. **Output flexibility**: both derivative and absolute modes supported
|
||||
|
||||
---
|
||||
|
||||
## 6. Figures
|
||||
|
||||
All figures in `docs/figures/SR_analysis/`:
|
||||
|
||||
- **fig1_illusion_comparison.png**: Illusion 3-scene comparison bar chart
|
||||
- **fig2_karman_ablation.png**: Karman re100 ablation across 7 configurations
|
||||
- **fig3_karman_generalization.png**: Karman cross-Re generalization (training vs unseen)
|
||||
- **fig4_r2_vs_closedloop.png**: One-step R2 vs closed-loop paradox scatter
|
||||
- **fig5_illusion_coefficients.png**: Illusion 1L front feature coefficients
|
||||
- **fig6_roadmap.png**: Research progress roadmap
|
||||
@ -1,412 +0,0 @@
|
||||
# CCD Analysis Report: Correction-Field Decomposition of Illusion Control
|
||||
|
||||
> **What this report is**: A self-contained summary of the CCD (Canonical Correlation Decomposition) analysis pipeline applied to the fluidic pinball illusion control problem. It assumes no prior knowledge of CCD or the project details — everything is explained from the ground up.
|
||||
>
|
||||
> **What this report is NOT**: A complete physics investigation. It is a progress report documenting what analysis was done, what was found, what it means, and where the open questions are.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Problem in Plain Language
|
||||
|
||||
### 1.1 The physical system
|
||||
|
||||
Imagine three identical cylinders arranged in a triangle pointing upstream, placed in a channel with water flowing past them. Each cylinder can spin independently at a controlled speed. When the cylinders do NOT spin, the flow behind them forms a chaotic oscillating wake (a "von Karman vortex street").
|
||||
|
||||
The DRL controller can rotate the three cylinders at different speeds to change this wake. The goal of **illusion control** is: "make the flow field downstream of the three cylinders look like the flow field that would be produced by a single cylinder of a different size."
|
||||
|
||||
We test three target sizes: a cylinder of diameter 0.75 (smaller than the pinball cylinders), 1.0 (same size), and 1.5 (larger).
|
||||
|
||||
### 1.2 What the controller sees and does
|
||||
|
||||
- **Observations (input to controller)**: The forces on each cylinder (drag and lift) + the flow velocity measured at 3 points downstream
|
||||
- **Actions (output of controller)**: 3 rotation speeds (one per cylinder), updated every 800 simulation timesteps
|
||||
- **Reward (what the controller is trained to maximise)**: How closely the downstream sensors match the target cylinder's signal, plus how closely the total forces on the pinball match the target cylinder's forces
|
||||
|
||||
### 1.3 The key insight that changed everything
|
||||
|
||||
The naive approach is to ask: "does the controlled flow look like the target flow?" But this is not what the controller does. The controller works by **modifying the existing pinball wake**. A better question is: "what extra change does the controller add on top of the uncontrolled pinball wake, and does that change look like the change needed to transform the pinball wake into the target wake?"
|
||||
|
||||
This leads to the **correction-field framework**:
|
||||
|
||||
| Symbol | Meaning | How to think of it |
|
||||
|--------|---------|-------------------|
|
||||
| `q_in` | Clean channel flow (no pinball) | The baseline |
|
||||
| `q_blk` | Pinball, no rotation | What the pinball does to the flow by its mere presence |
|
||||
| `q_ctl` | Pinball with DRL control | The controlled flow |
|
||||
| `q_tar` | The target cylinder alone | The flow we wish we had |
|
||||
| `dq_blk = q_blk - q_in` | **Blockage field**: how the pinball disturbs the channel | Pinball's "mess" |
|
||||
| `dq_ctl = q_ctl - q_blk` | **Correction field**: what control adds on top of the pinball | Controller's "fix" |
|
||||
| `dq_tar = q_tar - q_blk` | **Target correction**: what change would turn pinball into target | The required "fix" |
|
||||
|
||||
The main question becomes: does `dq_ctl` (the actual fix) look like `dq_tar` (the required fix)?
|
||||
|
||||
---
|
||||
|
||||
## 2. What is CCD? (For the Non-Expert)
|
||||
|
||||
### 2.1 The core idea
|
||||
|
||||
**Proper Orthogonal Decomposition (POD)** finds the flow patterns that contain the most energy. It answers: "what are the dominant oscillating structures in this flow?"
|
||||
|
||||
**Canonical Correlation Decomposition (CCD)** finds the flow patterns that are most correlated with a specific quantity you care about (an "observable"). It answers: "what flow structures most determine the force on the cylinders?" or "what flow structures most determine the downstream sensor reading?"
|
||||
|
||||
The difference is crucial. Imagine a jet engine: the most energetic flow structures might be in the turbulent exhaust, but the structures that generate noise might be much weaker and completely different. POD would miss them because it ranks by energy, not by relevance to noise.
|
||||
|
||||
### 2.2 How CCD works (simplified)
|
||||
|
||||
1. **Take snapshots**: Record 96 velocity field snapshots of `dq_ctl` at evenly spaced times over 4 vortex shedding cycles
|
||||
2. **Build a reference basis**: Use POD to find the main energy-containing structures in the TARGET correction field `dq_tar` — this gives us a coordinate system defined by what the target looks like
|
||||
3. **Project into this basis**: Express the CONTROLLED correction field `dq_ctl` in terms of the target's structures
|
||||
4. **Pick an observable**: Choose something we care about — the total lift force (`SigmaFy`), the cylinder rotation speeds (`action`), or the future sensor error (`signature`)
|
||||
5. **Find the correlated patterns**: CCD finds the directions in flow-structure-space that best predict/correlate with the observable
|
||||
6. **Measure compactness (m80)**: How many such directions do we need to capture 80% of the correlation? m80=1 means a single flow pattern explains most of the observable. m80=4 means we need more patterns.
|
||||
7. **Measure overlap (O_k)**: Do two cases (e.g., target vs illusion) use the same flow patterns to generate the observable? O=1 means identical, O=0 means completely different.
|
||||
|
||||
### 2.3 Validation: how do we know CCD is meaningful?
|
||||
|
||||
We use **Leave-One-Cycle-Out (LOCO) cross-validation**:
|
||||
- We have 4 shedding cycles of data
|
||||
- Train CCD on 3 cycles, predict the observable on the held-out 1 cycle
|
||||
- Compute R2 (how well the prediction matches reality)
|
||||
- Repeat for each cycle as the held-out set
|
||||
- If R2 > 0.4-0.5, the CCD patterns are stable and predictive
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Quality and Preprocessing
|
||||
|
||||
Before any analysis, the raw flow fields must be phase-aligned — each snapshot corresponds to the same phase in the vortex shedding cycle across all cases. This is done by detecting the dominant shedding frequency, finding cycle boundaries, and extracting 24 evenly-spaced snapshots per cycle for 4 cycles = 96 snapshots total.
|
||||
|
||||
All cases pass quality gates:
|
||||
- **Strict gate**: cycle-to-cycle period variation (CV_T) < 10%
|
||||
- **Relaxed gate**: CV_T < 12%
|
||||
|
||||
| Case | Gate | Points/cycle | Interpolation factor | Strouhal |
|
||||
|------|------|-------------|---------------------|----------|
|
||||
| target_cylinder 0.75L | strict | 30.0 | 0.80 | 0.128 |
|
||||
| target_cylinder 1.0L | strict | 24.8 | 0.97 | 0.133 |
|
||||
| target_cylinder 1.5L | strict | 25.8 | 0.93 | 0.143 |
|
||||
| illusion 0.75L | strict | 29.9 | 0.80 | — |
|
||||
| illusion 1.0L | strict | 24.5 | 0.98 | — |
|
||||
| illusion 1.5L | strict | 24.2 | 0.99 | — |
|
||||
| pinball (uncontrolled) | strict | 21.4 | 1.12 | 0.113 |
|
||||
|
||||
The interpolation factor (rho) indicates how close each case is to having an integer number of snapshots per cycle. rho=1 is perfect; all cases have rho <= 1.12, meaning almost no interpolation artifacts.
|
||||
|
||||
---
|
||||
|
||||
## 4. The Five Analysis Lines
|
||||
|
||||
We answer five questions, each requiring a different observable for CCD:
|
||||
|
||||
| Line | Observable | Question | Symbol |
|
||||
|------|-----------|----------|--------|
|
||||
| **Force line (primary)** | Total lift force | Which correction structures most determine the lift? | `SigmaFy` |
|
||||
| **Force line (secondary)** | Total drag force | Which correction structures most determine the drag? | `SigmaFx` |
|
||||
| **Action line** | 3 cylinder rotation speeds | Which correction structures does the controller directly modulate? | `[Omega1, Omega2, Omega3]` |
|
||||
| **Signature line** | Future sensor error | Which correction structures most determine whether downstream sensors will match the target? | `e_s(t+tau) = s_ctl(t+tau) - s_tar(t+tau)` |
|
||||
|
||||
The **signature line** has an extra dimension: time delay tau. We try tau=0 (instantaneous sensor error) and tau=tau_c (the time it takes for flow structures to convect from the pinball to the sensors, about 3-4 simulation steps).
|
||||
|
||||
---
|
||||
|
||||
## 5. Master Results
|
||||
|
||||
### 5.1 One-sentence summary per diameter
|
||||
|
||||
| Diameter | Summary |
|
||||
|----------|---------|
|
||||
| **1.0L** | The controller's correction direction is **nearly identical to the target's required correction** (O=0.913), and a single flow pattern captures 80% of the force-relevant correction. This is the "natural scale" case. |
|
||||
| **0.75L** | The controller's correction only **partially aligns** with the target's required correction (O=0.564). The force and sensor-error structures are strongly separated in space at any instant, but converge after convective propagation. |
|
||||
| **1.5L** | This is **not a failure but a special mechanism**. The controller achieves 94% sensor similarity using a qualitatively different strategy: the cylinder rotation commands have very weak correlation with the target-basis structures (action sigma1 = 0.28 vs 1.1-1.4), the correction energy is concentrated near the cylinders, and the shedding phase drifts over time. |
|
||||
|
||||
### 5.2 The master table
|
||||
|
||||
All values at r=6 (6 POD modes retained for the reference basis), unless noted.
|
||||
|
||||
| Metric | 0.75L | 1.0L | 1.5L |
|
||||
|--------|-------|------|------|
|
||||
| **O(dqctl, dqtar) — how similar are the corrections?** (1=identical, 0=orthogonal) | **0.564** | **0.913** | **0.667** |
|
||||
| force_fy m80 — how many patterns needed for 80% of force? (lower = more concentrated) | 2 | 1 (at r=8/10) | 2 |
|
||||
| action m80 — how many patterns for 80% of action correlation? | 2 | 3 | 3 |
|
||||
| **action sigma1 — strength of action correlation** (higher = actions more tied to target-basis structures) | 1.39 | 1.13 | **0.28** |
|
||||
| signature m80 (zero delay) | 3 | 3 | 2 |
|
||||
| signature m80 (convective delay) | 2 | 3 | 2 |
|
||||
| **O(force, sig) at zero lag** (1=force and sensor error use same structures) | **0.413** | **0.551** | — |
|
||||
| **O(force, sig) at convective delay** | **0.806** | **0.768** | — |
|
||||
| Phase drift (cycle-to-cycle period variation) | low | low | **high** |
|
||||
| Body-wake KE / sensor-zone KE (higher = more correction energy near cylinders) | 0.73 | 1.17 | **2.58** |
|
||||
|
||||
### 5.3 Validation: can we trust these numbers?
|
||||
|
||||
**LOCO cross-validation R2 (m80 reconstruction, r=6):**
|
||||
|
||||
| Observable | 0.75L | 1.0L | Threshold | Verdict |
|
||||
|-----------|-------|------|-----------|---------|
|
||||
| force_fy (lift) | 0.65 +- 0.08 | 0.64 +- 0.02 | > 0.4 | PASS |
|
||||
| force_fx (drag) | 0.38 +- 0.23 | 0.43 +- 0.11 | > 0.4 | WARNING |
|
||||
| signature (zero lag) | 0.50 +- 0.09 | 0.49 +- 0.04 | > 0.4 | PASS |
|
||||
| signature (convective delay) | 0.51 +- 0.09 | 0.53 +- 0.03 | > 0.4 | PASS |
|
||||
|
||||
The standard deviation across the 4 folds is small (0.02-0.09), meaning the patterns are stable across different data subsets. The **drag channel (force_fx) is unreliable** for detailed claims — its R2 is borderline and its variance is high. **All other channels pass validation.**
|
||||
|
||||
---
|
||||
|
||||
## 6. The Force vs Signature Separation: The Most Important Finding
|
||||
|
||||
### 6.1 Why this matters
|
||||
|
||||
One of the fundamental questions in flow control is: "are the flow structures that generate forces the same as the flow structures that determine what a downstream sensor sees?" If they are the SAME, then controlling the force automatically controls the sensor signal. If they are DIFFERENT, then the controller must manage two separate sets of structures.
|
||||
|
||||
**Our finding**: they are SEPARATED at any instant, but CONVERGE after the flow has time to convect downstream.
|
||||
|
||||
**Evidence — full-field CCD:**
|
||||
|
||||
| tau (convective delay in steps) | 0.75L O(force, sig) | 1.0L O(force, sig) |
|
||||
|--|--|--|
|
||||
| 0 (instantaneous) | **0.413** (separated) | **0.551** (partial) |
|
||||
| ~3-4 (convective delay) | **0.806** (shared) | **0.768** (shared) |
|
||||
|
||||
At tau=0, the force-relevant and sensor-error-relevant structures share only 41-55% of their modal directions. After the flow convects downstream (tau=3-4), they share 77-81%. This makes physical sense: at any snapshot, the forces are determined by what is happening near the cylinders, while the sensor error is determined by what is further downstream. But after the near-body structures have had time to propagate downstream, they become the same thing.
|
||||
|
||||
### 6.2 Where does this separation happen spatially?
|
||||
|
||||
We divided the flow field into three zones and ran CCD separately in each:
|
||||
|
||||
| Zone | x-range (pixels) | What's there |
|
||||
|------|-----------------|-------------|
|
||||
| **near_body** | 350-500 | Around the cylinders (located at x=380-406) |
|
||||
| **body_wake** | 500-700 | Just downstream of cylinders |
|
||||
| **sensor_zone** | 580-650 | Where the velocity sensors measure the flow |
|
||||
|
||||
**0.75L — the separation is dramatic:**
|
||||
|
||||
| Zone | O(force, sig) at tau=0 | O(force, sig) at tau=tau_c |
|
||||
|------|----------------------|---------------------------|
|
||||
| near_body | 0.262 (separated) | 0.827 (shared) |
|
||||
| body_wake | 0.269 (separated) | **0.917** (shared) |
|
||||
| **sensor_zone** | **0.010 (NEARLY ORTHOGONAL)** | 0.722 (shared) |
|
||||
|
||||
In the sensor zone at zero lag, the force and signature structures are **effectively perpendicular** (O=0.01). This is the cleanest possible demonstration that force-relevant and sensor-error-relevant structures live in different spatial regions at any given instant. After the convective delay, the body_wake shows the strongest coupling (O=0.917), meaning the near-wake structures jointly determine future forces AND future sensor readings.
|
||||
|
||||
**1.0L — more uniform, less separation:**
|
||||
|
||||
| Zone | O(force, sig) at tau=0 | O(force, sig) at tau=tau_c |
|
||||
|------|----------------------|---------------------------|
|
||||
| near_body | 0.596 | 0.596 |
|
||||
| body_wake | 0.509 | 0.483 |
|
||||
| sensor_zone | 0.594 | **0.730** |
|
||||
|
||||
At the natural scale (1.0L), force and signature are more intrinsically linked across all zones. There is no zone with near-zero overlap. This makes sense: when the target shedding frequency matches the pinball's natural frequency, the same structures that produce forces are also those that the downstream sensors detect.
|
||||
|
||||
---
|
||||
|
||||
## 7. 1.5L Special Mechanism
|
||||
|
||||
The 1.5L case is not a failure (94.2% sensor similarity) but it operates differently:
|
||||
|
||||
1. **Action correlation is dramatically weaker**: sigma1 = 0.28 (vs 1.13-1.39 for other diameters). In the target's structural coordinate system, the cylinder rotation commands have very little explanatory power.
|
||||
|
||||
2. **Phase drift**: The shedding period varies significantly over time (CV_T across windows is high), unlike the stable periodic shedding of 0.75L and 1.0L.
|
||||
|
||||
3. **Correction energy concentrated near cylinders**: The KE ratio (body_wake / sensor_zone) = 2.58 (vs 0.73 for 0.75L, 1.17 for 1.0L). The controller is applying larger corrections near the cylinders, not just modifying the downstream wake.
|
||||
|
||||
4. **Signature coupling is stronger than force coupling**: The signature-line sigma1 (1.29) is greater than the force-line sigma1 (0.91). The correction field is more tightly tied to future sensor error than to instantaneous force.
|
||||
|
||||
---
|
||||
|
||||
## 8. Comparison: Correction Field vs Raw Field
|
||||
|
||||
Why go to the trouble of computing `dq_ctl = q_ctl - q_blk` instead of just working with `q_ctl` directly?
|
||||
|
||||
| Measure | Raw field (q_ctl) | Correction field (dq_ctl) | What it tells us |
|
||||
|---------|------------------|--------------------------|-------------------|
|
||||
| 1.0L O(target, illusion) | 0.919 | 0.913 | Similar — 1.0L is clean either way |
|
||||
| 0.75L O(target, illusion) | 0.673 | **0.564** | Raw field was **contaminated** — ~16% of the apparent overlap was just baseline similarity |
|
||||
| 1.0L force m80 | 2 | **1** | Correction field is more concentrated — the controller's ADDED structures are simpler than the full flow |
|
||||
| LOCO R2 force_fy | 0.66-0.71 | 0.64-0.65 | Comparable — correction field doesn't degrade predictability |
|
||||
|
||||
**Conclusion**: Correction-field is the superior primary analysis object. The raw field can still be useful for historical comparison, but all mechanism claims should be based on correction-field analysis.
|
||||
|
||||
---
|
||||
|
||||
## 9. What We Learned About Steady Cloak
|
||||
|
||||
The steady cloak case (open-loop constant-speed rotation of the rear cylinders) was also analysed. The result: **it does not work well**. The RMS fluctuation suppression is essentially 0%, and the residual after cancellation is 81% of the original blockage. The downstream sensor region does better (13% residual) but that is mostly because the wake naturally recovers with distance.
|
||||
|
||||
This case is not suitable as a primary mechanism demonstration. A closed-loop steady cloak (using DRL) would be needed for meaningful analysis.
|
||||
|
||||
---
|
||||
|
||||
## 10. Limitations
|
||||
|
||||
1. **POD-reduced CCD**: All CCD results are constrained to the subspace spanned by the first 6-10 POD modes of the target correction field. If the controller uses structures that have very low energy in the target's natural basis, they will be truncated and invisible to CCD. For a "true" full-field CCD, significantly more data would be needed.
|
||||
|
||||
2. **The drag channel is unreliable**: force_fx (drag) fails LOCO validation (R2 ~0.4 with high variance). This is because the force reward in the DRL training matches drag statistics (mean, variance) but not the instantaneous waveform. Drag CCD results should only be used for O_k trend comparisons, not for mechanism claims.
|
||||
|
||||
3. **1.5L force-signature overlap not computed**: The force-vs-signature comparison could not be run for 1.5L because of technical issues with the cross-correlation computation. This is a gap that should be filled.
|
||||
|
||||
4. **Karman cloak not analysed**: The data for Karman cloak (vortex street incoming) has been collected but the analysis was deferred. The physical question is different (distortion compensation vs target generation) and the full pipeline is ready for when this becomes a priority.
|
||||
|
||||
---
|
||||
|
||||
## 11. Available Figures and How to Read Them
|
||||
|
||||
### Figure 1: Force sanity check — `sanity_force_{diam}L.png`
|
||||
|
||||
**Three files**: one per diameter.
|
||||
|
||||
**What it shows**: The raw force time series comparison between the target cylinder (red) and the controlled pinball illusion (blue). Four panels per figure:
|
||||
- Top-left: Total drag force Fx over time
|
||||
- Top-right: Total lift force Fy over time
|
||||
- Bottom-left: Fx scatter plot (target vs illusion), with correlation r annotated
|
||||
- Bottom-right: Fy scatter plot, with correlation r annotated
|
||||
|
||||
**How to read it**: The diagonal dashed line in the scatter plots indicates perfect tracking. For 1.0L, the Fy scatter (bottom-right) shows points clustering near the diagonal with r=0.82 — the controller tracks the lift waveform. For 1.5L, the Fy scatter shows r=-0.30 — the lift is negatively correlated, meaning the controller is doing something fundamentally different. The Fx scatter for all diameters shows r near zero — the controller matches mean drag but not the drag waveform.
|
||||
|
||||
**Look at**: The Fy scatter correlation coefficients. 0.75L: r=0.38 (weak positive), 1.0L: r=0.82 (strong positive), 1.5L: r=-0.30 (negative!).
|
||||
|
||||
---
|
||||
|
||||
### Figure 2: Correction field comparison — `corr_illusion_{diam}L_ctl_vs_tar.png`
|
||||
|
||||
**Three files**: one per diameter.
|
||||
|
||||
**What it shows**: A 2x2 panel comparing the controller's correction (`dq_ctl`, left column) with the target's required correction (`dq_tar`, right column).
|
||||
- Top row: Mean streamwise velocity (ux) of the correction field
|
||||
- Bottom row: RMS magnitude of the correction field
|
||||
|
||||
**How to read it**: Look at the spatial patterns in the mean ux panels (top row). If the left and right panels look similar in structure (red/blue pattern), the controller is adding a correction that resembles what the target requires. For 1.0L, they look nearly identical. For 0.75L, there are similarities but also clear differences in the wake region. The RMS panels (bottom row) show where the fluctuations are — bright regions indicate high unsteadiness in the correction.
|
||||
|
||||
**Look at**: How similar the top-left and top-right panels are. The more similar, the more the controller's correction "knows" what the target needs.
|
||||
|
||||
---
|
||||
|
||||
### Figure 3: Correction field maps — `corr_illusion_{diam}L_dq_ctl_(control_correction).png`
|
||||
|
||||
**Three files**: one per diameter, plus similar files for `dq_blk` and vorticity.
|
||||
|
||||
**What it shows**: Three panels of the `dq_ctl` correction field:
|
||||
- Left: Mean streamwise velocity (ux)
|
||||
- Centre: Mean cross-stream velocity (uy)
|
||||
- Right: RMS magnitude
|
||||
|
||||
**How to read it**: Red in the ux panel means the controller is ACCELERATING the flow at that point; blue means DECELERATING. The RMS panel shows where the control is most unsteady. The pinball cylinder positions are at approximately x=380-406 (visible as blank regions).
|
||||
|
||||
**Look at**: The ux panel — where does the controller add positive (red) vs negative (blue) momentum? For 0.75L and 1.0L, there is a strong dipole pair in the wake. For 1.5L, the pattern is shifted and the amplitude is larger.
|
||||
|
||||
---
|
||||
|
||||
### Figure 4: CCD mode 1 — `ccd_mode1_fy_{diam}L_{target,illusion}.png`
|
||||
|
||||
**Four files**: 2 diameters x 2 cases (target and illusion).
|
||||
|
||||
**What it shows**: The first (most important) CCD mode for the force-fy line (lift), expressed as a velocity field. Left panel = ux component, right panel = uy component. Red = positive, blue = negative.
|
||||
|
||||
**How to read it**: This is the single flow pattern that is most correlated with the lift force. If the target and illusion panels look similar, it means the controller is using the same kind of flow pattern to generate lift as the target cylinder naturally uses.
|
||||
|
||||
**Look at**: Compare the 1.0L target mode with the 1.0L illusion mode — they should look very similar (consistent with O=0.913). Compare 0.75L target with 0.75L illusion — more differences expected (O=0.564).
|
||||
|
||||
---
|
||||
|
||||
### Figure 5: POD phase portraits — `pod_phase_portraits_target_basis.png`
|
||||
|
||||
**One file, three panels** (0.75L, 1.0L, 1.5L).
|
||||
|
||||
**What it shows**: The scatter of the first two POD coefficients (a1, a2) in the target-only basis. Red dots = target cylinder, blue dots = illusion (controlled), green dots = pinball (uncontrolled).
|
||||
|
||||
**How to read it**: Each dot represents one snapshot (96 per case). The spread of dots shows the "attractor" — the region of flow state space occupied by each case. If the blue dots overlap with the red dots, the illusion dynamics are similar to the target dynamics. If the blue dots are in a completely different region (like 1.5L), the controller is operating in a different dynamical regime.
|
||||
|
||||
**Look at**: For 1.0L, blue (illusion) should largely overlap with red (target) and be separated from green (pinball). For 0.75L, the separation is smaller. For 1.5L, the pattern may look different entirely.
|
||||
|
||||
---
|
||||
|
||||
### Figure 6: Overlap heatmap — `Ok_heatmap_fy_r6.png`
|
||||
|
||||
**One file, at r=6 POD rank**.
|
||||
|
||||
**What it shows**: A 3x3 heatmap with columns = diameters (0.75L, 1.0L, 1.5L) and rows = comparison pairs (target-illusion, target-pinball, illusion-pinball). Colour = O_1 (the modal overlap of the first CCD mode).
|
||||
|
||||
**How to read it**: Each cell tells you how similar two cases are in their force-relevant flow structures. Dark cells (values near 0.9) mean the two cases use nearly identical lift-generating structures. Light cells (values near 0.2) mean they use very different structures.
|
||||
|
||||
**Look at**: The top row (target-illusion overlap) across diameters. For 0.75L: ~0.67, for 1.0L: ~0.92, for 1.5L: ~0.62. The progression shows the controller's force strategy diverging from the target's as the target size moves away from the pinball's natural scale.
|
||||
|
||||
---
|
||||
|
||||
### Figure 7: Cross-diameter overlap — `cross_diameter_overlap_fy.png`
|
||||
|
||||
**One file**.
|
||||
|
||||
**What it shows**: A 3x3 heatmap showing how similar the illusion's force-CCD direction is between different diameters, when all are projected into the 1.0L target-only POD basis.
|
||||
|
||||
**How to read it**: Each cell shows O(diameter_i, diameter_j) — how aligned the force-relevant structures are between illusions at different target sizes. All values along the diagonal are 1.0 (a case is identical to itself). Off-diagonal values show cross-diameter similarity.
|
||||
|
||||
**Look at**: The O(0.75L, 1.0L) = ~0.85, O(0.75L, 1.5L) = ~0.96, O(1.0L, 1.5L) = ~0.92. Interestingly, the two "off-natural-scale" cases (0.75L and 1.5L) are MORE similar to each other in the 1.0L basis than either is to 1.0L. This suggests they use a similar "deviant" strategy.
|
||||
|
||||
---
|
||||
|
||||
### Figure 8: z_1 verification — `z1_verification_fy_{diam}L.png`
|
||||
|
||||
**Two files**: 0.75L and 1.0L.
|
||||
|
||||
**What it shows**: The temporal coefficient of the first CCD mode (z_1, blue) overlaid with the normalised total lift force (red). Top panel: raw z_1(t). Bottom panel: both signals normalised and overlaid.
|
||||
|
||||
**How to read it**: If the blue and red lines track each other well in the bottom panel, the CCD mode is successfully capturing the lift-related structures. This is a sanity check — it shows that CCD found something real.
|
||||
|
||||
**Look at**: The overlap between the blue dashed and red solid lines in the bottom panel. Good tracking = CCD is working correctly.
|
||||
|
||||
---
|
||||
|
||||
### Figure 9: 1.5L special diagnostics (3 files)
|
||||
|
||||
- **`15L_raw_timeseries.png`**: Raw sensor, force, and action time series for 1.5L. Shows sensor tracking (how well illusion = target for sensors), force comparison, and the DRL action signals.
|
||||
- **`15L_windowed_periodicity.png`**: The cycle-to-cycle period variation (CV_T) over time for 1.5L. If CV_T exceeds the dashed lines, the shedding is not perfectly periodic. This confirms the "phase drift" behaviour.
|
||||
- **`15L_overlap_summary.png`**: A bar chart comparing O(target, illusion) across diameters. The 1.5L bar is annotated as "special mechanism."
|
||||
|
||||
**How to read the periodicity figure**: The top panel shows CV_T over time — values below 0.10 (red dashed line) indicate stable periodic shedding. If values frequently exceed this, the shedding period is drifting. The middle panel shows the cycle period itself. The bottom panel shows the dominant frequency. Together, they reveal whether the flow is stably periodic or drifting.
|
||||
|
||||
---
|
||||
|
||||
### Figure 10: Steady cloak cancel test — `steady_cloak_cancel_test.png`
|
||||
|
||||
**One file**.
|
||||
|
||||
**What it shows**: Three panels comparing `dq_blk` (the blockage field — pinball's disturbance), `dq_ctl` (the control correction), and `dq_ctl + dq_blk` (the residual — what's left after control tries to cancel blockage).
|
||||
|
||||
**How to read it**: If the control perfectly cancels the blockage, the right panel (dq_ctl + dq_blk) should be near zero everywhere. Blue/red patterns in the right panel indicate incomplete cancellation. The presence of strong colour shows the control does not fully restore the flow.
|
||||
|
||||
**Look at**: The third panel — if it's mostly blank (near zero), the cancellation is working well. For this case, it is NOT blank, confirming the open-loop steady cloak does not effectively cancel the pinball disturbance.
|
||||
|
||||
---
|
||||
|
||||
## 12. Summary of Conclusions
|
||||
|
||||
1. **The correction-field framework is the correct way to analyse this problem**. It isolates what the controller actually changes, removing baseline similarity contamination.
|
||||
|
||||
2. **1.0L illusion is a low-rank, target-aligned correction**. O(dqctl, dqtar)=0.913, m80=1. When the target matches the pinball's natural scale, the controller modulates the existing shedding channel in a near-optimal way.
|
||||
|
||||
3. **Force and signature structures are spatially separated at zero lag but converge after convective delay**. This is confirmed by both full-field CCD (O=0.41-0.55 at tau=0, rising to 0.77-0.81 at tau=tau_c) and by zone-restricted CCD (sensor zone shows O=0.01 for 0.75L at tau=0).
|
||||
|
||||
4. **1.5L is a genuine special mechanism**, not a failure. It achieves 94% sensor similarity despite weak action coupling, strong phase drift, and a near-body-focused correction pattern.
|
||||
|
||||
5. **The drag channel (force_fx) is unreliable for mechanism claims**. It fails validation and should only be used for trend comparisons.
|
||||
|
||||
6. **The open-loop steady cloak is ineffective** (0% fluctuation suppression) and should not be a focus for mechanism analysis.
|
||||
|
||||
---
|
||||
|
||||
## 13. Data and Code Availability
|
||||
|
||||
All analysis scripts: `src/CCD_analysis/correction_analysis/*.py`
|
||||
All results: `src/CCD_analysis/data/ccd/*.json`
|
||||
All figures: `src/CCD_analysis/data/figures/*.png`
|
||||
81 total figures, 8 JSON result files
|
||||
|
||||
Key result files:
|
||||
- `ccd_results.json` — raw-field CCD (Round 5 baseline)
|
||||
- `correction_ccd_results.json` — correction-field force/action CCD
|
||||
- `correction_validation_results.json` — LOCO validation
|
||||
- `signature_ccd_results.json` — signature-line CCD
|
||||
- `15L_correction_results.json` — 1.5L analysis
|
||||
- `zone_ccd_results.json` — zone-restricted CCD
|
||||
- `steady_metrics.json` — steady cloak quantitative metrics
|
||||
@ -1,74 +0,0 @@
|
||||
# CCD Direction Handover
|
||||
|
||||
## Agent Background
|
||||
|
||||
This agent worked on the CCD (Canonical Correlation Decomposition) analysis pipeline for the DynamisLab fluidic pinball project. The work spanned approximately 7-8 hours over 2026-06-14/15, covering Round 5 (raw-field baseline) and Round 6 (correction-field framework).
|
||||
|
||||
## Work Summary
|
||||
|
||||
### What was accomplished
|
||||
|
||||
1. **Data pipeline overhaul**: Replaced the old `resampled.npz` (interpolated) format with `fields_aligned.npz` (96 non-interpolated raw field snapshots) + `phase_plan.json`. Implemented `load_aligned_fields()` in `utils/resampling.py` as the unified data loader.
|
||||
|
||||
2. **Raw-field CCD baseline (Round 5)**: `ccd/run_ccd.py` and `ccd/validate.py` rewritten for the new data format. Target-only POD basis, per-force observable (SigmaFy primary), Q_delay=6, per-case z-score. 90 CCD entries, LOCO validation passed for force_fy (R2 0.66-0.71).
|
||||
|
||||
3. **Correction-field framework (Round 6)**: Shifted analysis object from `q_ctl` to `dq_ctl = q_ctl - q_blk` (the control correction field). Built `correction_analysis/compute_correction_fields.py` for unified q_in/q_blk/q_ctl/q_tar + dq_* field computation.
|
||||
|
||||
4. **Completed analyses in round 6**:
|
||||
- Force/action CCD on dq_ctl (0.75L, 1.0L)
|
||||
- Signature line CCD (0.75L, 1.0L) with tau scan (0, geom, corr)
|
||||
- 1.5L force/action/signature CCD + phase drift diagnostics
|
||||
- Steady cloak quantitative metrics
|
||||
- Zone-restricted CCD (near_body, body_wake, sensor_zone) for 0.75L and 1.0L
|
||||
- Snapshot POD speedup (SVD on 96x96 instead of 1310720x96)
|
||||
|
||||
5. **Karman reference data collected**: karman_q_in (vortex street without pinball) and karman_q_blk (pinball in vortex street, no control) — both 96 aligned frames.
|
||||
|
||||
6. **Documentation**:
|
||||
- `docs/ccd_correction_field_report.md` — comprehensive 412-line report explaining everything from scratch, including 10-figure reading guide
|
||||
- `docs/sr_ccd_oid_mapping.md` — cross-pipeline mapping (DRAFT - needs verification from SR and OID directions)
|
||||
- `src/CCD_analysis/ccd_knowledge.md` — updated with final results
|
||||
- `src/CCD_analysis/ccd_notes.md` — updated with completion status
|
||||
|
||||
### Key findings
|
||||
|
||||
1. **1.0L**: O(dqctl, dqtar)=0.913, force_fy m80=1 — the controller's correction nearly perfectly matches the target's required correction, and it's highly concentrated.
|
||||
|
||||
2. **Force vs Signature separation**: O(force,sig)=0.41-0.55 at tau=0 (separated), rising to 0.77-0.81 at tau=tau_c (shared). Zone-CCD shows 0.75L sensor_zone has O=0.01 at tau=0 (near orthogonal) and body_wake has O=0.917 at tau=tau_c.
|
||||
|
||||
3. **1.5L special mechanism**: O=0.667, action sigma1=0.28 (1/4 of others), strong phase drift, correction concentrated near-body.
|
||||
|
||||
### What is not done
|
||||
|
||||
1. **Karman cloak analysis** — data is ready (q_in, q_blk, q_ctl all have fields_aligned.npz), correction-field framework supports it, but analysis was deferred. Different physical question: distortion compensation vs target generation.
|
||||
|
||||
2. **1.5L force-vs-signature overlap** — 0.75L and 1.0L have O(force,sig) values, 1.5L has signature m80 but no overlap comparison.
|
||||
|
||||
3. **SR-CCD-OID mapping** — `docs/sr_ccd_oid_mapping.md` was written without reading SR and OID reports. Needs correction.
|
||||
|
||||
4. **Mixed-basis sensitivity** — deferred sensitivity check (currently target-only basis).
|
||||
|
||||
## Quick Start for Your First Commands
|
||||
|
||||
```bash
|
||||
# Read the comprehensive report
|
||||
less docs/ccd_correction_field_report.md
|
||||
|
||||
# Read the knowledge base
|
||||
less src/CCD_analysis/ccd_knowledge.md
|
||||
|
||||
# Explore results
|
||||
ls src/CCD_analysis/data/ccd/*.json
|
||||
python3 -c "import json; r=json.load(open('src/CCD_analysis/data/ccd/correction_ccd_results.json')); print(f'{len(r)} entries'); [print(k) for k in list(r.keys())[:5]]"
|
||||
|
||||
# Check available figures
|
||||
ls src/CCD_analysis/data/figures/*.png | wc -l
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
- All CPU analysis: `conda run -n pycuda_3_10`
|
||||
- GPU collection: same environment, devices 2 or 3
|
||||
- LegacyCelerisLab (FlowField) needed for GPU scripts
|
||||
- Python 3.10+, numpy, matplotlib, scipy (via conda)
|
||||
- `sys.path.insert(0, 'src')` needed for imports
|
||||
@ -1,109 +0,0 @@
|
||||
# SR-CCD-OID Cross-Pipeline Mapping
|
||||
|
||||
## Purpose
|
||||
|
||||
This document maps the three analysis pipelines (SINDy-SR, CCD, OID) onto a unified chain. They are NOT competing approaches — they answer different questions at different positions along the control-to-signature pathway.
|
||||
|
||||
## Unified Control Analysis Chain
|
||||
|
||||
```
|
||||
obs --[SR/SINDy]--> act --[CFD/physics]--> dq_ctl --[CCD/OID]--> force/signature
|
||||
^ |
|
||||
|_________________________________________________________________________|
|
||||
closed loop
|
||||
```
|
||||
|
||||
| Link | What happens | Which analysis |
|
||||
|------|-------------|----------------|
|
||||
| obs -> act | DRL policy maps sensor readings to control actions | **SR/SINDy** (white-box control law extraction) |
|
||||
| act -> dq_ctl | Actions modify the flow field; the change relative to uncontrolled baseline is `dq_ctl` | CFD / data collection |
|
||||
| dq_ctl -> force | Which correction structures most project to cylinder forces | **CCD** (force line), **OID** |
|
||||
| dq_ctl -> signature | Which correction structures most determine future sensor mismatch | **CCD** (signature line), **OID** |
|
||||
|
||||
## Pipeline Comparison Table
|
||||
|
||||
| Aspect | SR / SINDy | CCD | OID / PCD |
|
||||
|--------|-----------|-----|-----------|
|
||||
| **Primary question** | How does the controller map observations to actions? | Which correction structures correlate most with force/action/signature? | What is the unified low-dimensional coordinate that captures observable-related structure? |
|
||||
| **Input data** | Dimensionless obs and actions (time series) | `dq_ctl` fields (N snapshots x 2*NX*NY grid) + observable time series (force/action/sensor error) | POD coefficients of `dq_ctl` + observable time series |
|
||||
| **Output** | Sparse symbolic control law (e.g. `a_F = 0.3*sin(u_s1)`) | CCD mode directions W, modal overlaps O_k, compactness m80, LOCO R2 | Low-dimensional coordinate z(t), observable reconstruction error |
|
||||
| **Key method** | STLSQ threshold grid, G-equivariant constraints, SIN activation | POD-reduced CCD (Lyu23-inspired) | Observability Gramian / canonical correlation |
|
||||
| **Current maturity** | Medium — cross-Re shared backbone found, G-equivariance validated | **Highest** — correction-field framework complete for illusion 0.75L/1.0L/1.5L with force/action/signature lines | Low-medium — framework defined, needs data alignment with CCD |
|
||||
| **Validation** | Leave-one-Re-out cross-validation, closed-loop replay | LOCO (4-fold), blocked split, R2_m80 | pending alignment |
|
||||
| **Key result** | Karman cloak cross-Re shared backbone exists (R2 > 0.9 for holdout 200) | 1.0L O(dqctl,dqtar)=0.913, m80=1; force/sig separated at tau=0, shared at tau_c | pending |
|
||||
|
||||
## Maturity by Scene
|
||||
|
||||
| Scene | SR/SINDy | CCD | OID |
|
||||
|-------|----------|-----|-----|
|
||||
| Karman cloak re50/100/200/400 | **Existing** (cross-Re backbone) | Data ready, analysis deferred | Not started |
|
||||
| Illusion 0.75L | Existing | **Complete** (force/action/sig) | Partial |
|
||||
| Illusion 1.0L | Existing | **Complete** (force/action/sig) | Partial |
|
||||
| Illusion 1.5L | Existing | **Complete** (force/action/sig, special mechanism) | Not started |
|
||||
| Steady cloak | Existing | Partial (quantitative metrics done) | Not started |
|
||||
| Vortex cloak (lamb/taylor) | Existing | Not started | Not started |
|
||||
|
||||
## How They Assemble Into a Paper Chapter
|
||||
|
||||
### Chapter Structure Proposal
|
||||
|
||||
#### 1. Control Law Extraction (SR/SINDy)
|
||||
- *Question*: What is the map from sensor observations to cylinder rotations?
|
||||
- *Deliverable*: Symbolic control law for each scene, cross-scene comparison of feature usage
|
||||
- *Evidence*: Leave-one-out validation, G-equivariance error < 10%
|
||||
|
||||
#### 2. Correction Field Analysis (CCD)
|
||||
- *Question*: What flow structures does the controller actually modulate?
|
||||
- *Deliverable*:
|
||||
- Correction-field decomposition (`dq_ctl`)
|
||||
- Force line: O(dqctl,dqtar) across diameters
|
||||
- Action line: compactness m80
|
||||
- Signature line: force-sig separation at zero lag, convergence at convective delay
|
||||
- 1.5L special mechanism
|
||||
- *Evidence*: LOCO validation R2 > 0.4 for all lines
|
||||
|
||||
#### 3. Low-Dimensional Coordinate (OID)
|
||||
- *Question*: Can we describe controller-relevant structures in a unified low-D coordinate?
|
||||
- *Deliverable*: Observable-informed coordinates z for each case, reconstruction error
|
||||
- *Evidence*: Reconstruction quality vs POD-baseline
|
||||
|
||||
#### 4. Unified Mechanism Discussion
|
||||
- Synthesize findings from all three analyses
|
||||
- Key claims to support:
|
||||
- Control operates by modifying pinball's existing wake (not generating new flows)
|
||||
- Force-relevant correction is low-rank and target-aligned at natural scale
|
||||
- Cross-scale illusion uses divergent correction paths
|
||||
- Force and signature structures separate at zero lag but converge convectively
|
||||
|
||||
## Current Gaps by Pipeline
|
||||
|
||||
### SR/SINDy Gaps
|
||||
- Illusion cross-diameter comparison not yet unified with CCD's correction-field framework
|
||||
- Closed-loop validation of extracted control laws needs systematic comparison
|
||||
|
||||
### CCD Gaps
|
||||
- Karman cloak analysis deferred (data ready, framework designed)
|
||||
- Steady cloak needs closed-loop control to be meaningful
|
||||
- Zone-restricted CCD not yet complete (in progress)
|
||||
|
||||
### OID Gaps
|
||||
- Data pipeline not yet aligned with CCD's correction-field format
|
||||
- No direct comparison of OID coordinates with CCD directions
|
||||
- Requires full cross-analysis with existing CCD results
|
||||
|
||||
## Data Compatibility
|
||||
|
||||
All three pipelines ultimately read from the same data sources:
|
||||
- `fields_aligned.npz` (96 aligned field snapshots)
|
||||
- `controlled.npz` / `sensors.npz` (telemetry)
|
||||
- `configs.py` (scene metadata)
|
||||
|
||||
The **correction-field framework** (`dq_ctl = q_ctl - q_blk`) is the standard analysis object across all three. Any analysis that uses raw `q_ctl` instead should be explicitly flagged as a cross-check.
|
||||
|
||||
## Recommendation
|
||||
|
||||
For the next phase of work:
|
||||
1. **CCD** consolidates current results and adds zone-restricted analysis
|
||||
2. **OID** should adopt CCD's data loading (`compute_correction_fields.py`) and correction-field protocol
|
||||
3. **SR/SINDy** should align its cross-diameter comparison with CCD's correction-field O(dqctl,dqtar) results
|
||||
4. A unifying figure comparing O(dqctl,dqtar) from CCD with SR control-law similarity across diameters would be powerful
|
||||
1000
src/CCD_analysis/Lyu23.md
Normal file
@ -1,170 +1,85 @@
|
||||
# CCD_analysis: Observable-Correlated Decomposition for Fluidic Pinball Control
|
||||
# CCD_analysis: Correction-Field CCD Pipeline
|
||||
|
||||
## Quick Start for New Agent
|
||||
Analyzes DRL-controlled fluidic pinball using **correction-field decomposition** + **Canonical Correlation Decomposition (CCD/Lyu23)**. Core question: does `dq_ctl` (what the controller adds) match `dq_tar` (what the target requires)?
|
||||
|
||||
**Reading order:**
|
||||
1. This file (README.md) — scope, how to run, file structure
|
||||
2. `ccd_knowledge.md` — confirmed facts, hard rules, current results
|
||||
3. `ccd_notes.md` — what's done, what's not, future directions
|
||||
4. `docs/ccd_correction_field_report.md` — comprehensive report (412 lines, explains everything from scratch)
|
||||
## Quick Start
|
||||
|
||||
---
|
||||
```bash
|
||||
# 1. Panorama comparison figure (primary output)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py
|
||||
|
||||
## What This Pipeline Does
|
||||
# 2. CCD quantitative decomposition
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/decompose_corrections.py
|
||||
|
||||
CCD (Canonical Correlation Decomposition) finds flow structures most correlated with specific observables (force, action, sensor error), rather than by energy (POD). This pipeline works on the **fluidic pinball** — 3 rotating cylinders in a 2D channel — controlled by DRL (PPO).
|
||||
# 3. Single-scene diagnostics (zone metrics)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py
|
||||
```
|
||||
|
||||
The current analysis framework uses the **correction-field** approach: instead of analysing raw controlled fields `q_ctl`, we analyse the difference `dq_ctl = q_ctl - q_blk` (what the controller changes relative to the uncontrolled pinball).
|
||||
## Pipeline Architecture
|
||||
|
||||
Three analysis lines:
|
||||
- **Force line**: which correction structures most determine cylinder forces (SigmaFy primary)
|
||||
- **Action line**: which structures does the controller directly modulate (3 rotation speeds)
|
||||
- **Signature line**: which structures most determine future sensor error (with tau delay scan)
|
||||
|
||||
---
|
||||
```
|
||||
scripts/collect_*.py → scripts/{detect_period,replay_fields}.py
|
||||
(GPU采集) (phase alignment)
|
||||
↓
|
||||
correction_analysis/compute_correction_fields.py
|
||||
(dq_blk, dq_ctl, dq_tar)
|
||||
↓
|
||||
┌────┴────┬────────────┐
|
||||
↓ ↓ ↓
|
||||
compare_ decompose_ diagnose_
|
||||
dqctl_ corrections corrections
|
||||
scenes.py .py .py
|
||||
(全景对比) (CCD定量) (zone诊断)
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/CCD_analysis/
|
||||
README.md <-- this file
|
||||
ccd_knowledge.md -- confirmed facts, hard rules, results
|
||||
ccd_notes.md -- method, what's done, what's not
|
||||
configs.py -- scene metadata, all parameters
|
||||
CCD_analysis/
|
||||
README.md # 本文件
|
||||
ccd_knowledge.md # 唯一知识库 (理论, 结果, 操作流程, bug经验)
|
||||
Lyu23.md # CCD 方法文献
|
||||
configs.py # 场景元数据 (统一几何)
|
||||
utils/
|
||||
resampling.py -- POD, CCD, field loading (CPU only)
|
||||
cfd_interface.py -- LegacyCelerisLab wrapper (GPU needed)
|
||||
__init__.py -- re-exports from resampling.py
|
||||
resampling.py # POD, CCD, 场加载
|
||||
cfd_interface.py # LegacyCelerisLab 封装 (GPU) + build_observation
|
||||
field_translate.py # 场平移 (备用)
|
||||
load_vortex_fields.py # 瞬态 vortex 场加载
|
||||
scripts/
|
||||
detect_period.py -- period detection + phase plan generation
|
||||
replay_fields.py -- field replay for phase-aligned snapshots
|
||||
collect_target_cylinder.py -- target cylinder data collection (GPU)
|
||||
collect_illusion.py -- illusion PPO inference (GPU)
|
||||
collect_pinball.py -- uncontrolled pinball (GPU)
|
||||
collect_steady_cloak.py -- steady cloak (GPU)
|
||||
collect_empty_channel.py -- empty channel (GPU)
|
||||
collect_karman.py -- Karman cloak validation (GPU)
|
||||
resample.py -- DEPRECATED (use detect_period + replay_fields)
|
||||
visualize_ccd.py -- O_k, CCD modes, z_k, POD (Round 5 raw-field)
|
||||
sanity_check_force.py -- raw force comparison target vs illusion
|
||||
ccd/
|
||||
run_ccd.py -- Round 5 raw-field CCD (FROZEN)
|
||||
validate.py -- Round 5 validation (FROZEN)
|
||||
correction_analysis/ <-- ALL CURRENT WORK HERE
|
||||
compute_correction_fields.py -- build q_in/q_blk/q_ctl/q_tar + dq_*
|
||||
diagnose_corrections.py -- 29 figures + zone metrics
|
||||
decompose_corrections.py -- force/action CCD on dq_ctl
|
||||
run_signature_line.py -- signature line CCD (tau scan)
|
||||
run_15L_correction.py -- 1.5L force/action/signature
|
||||
run_steady_metrics.py -- steady cloak quantitative metrics
|
||||
run_zone_ccd.py -- zone-restricted CCD (3 zones)
|
||||
process_legacy_steady.py -- load steady_cloak/target_channel old format
|
||||
collect_*.py # GPU 数据采集
|
||||
detect_period.py # 周期检测 → phase_plan.json
|
||||
replay_fields.py # 场回放 → fields_aligned.npz
|
||||
correction_analysis/
|
||||
compute_correction_fields.py # dq 计算 (核心)
|
||||
compare_dqctl_scenes.py # 全景对比图 (核心输出)
|
||||
decompose_corrections.py # CCD 定量 (POD + force/action CCD)
|
||||
diagnose_corrections.py # 单场景 zone 诊断
|
||||
run_signature_line.py # Signature CCD
|
||||
run_zone_ccd.py # Zone-restricted CCD
|
||||
run_steady_metrics.py # Steady cloak 定量
|
||||
run_15L_correction.py # 1.5L 专项
|
||||
visualize_action_ccd.py # Action-CCD mode1 可视化
|
||||
process_legacy_steady.py # 旧格式加载
|
||||
ccd/ # Round 5 冻结基线 (勿改)
|
||||
data/
|
||||
pinball/pinball/ -- uncontrolled pinball
|
||||
steady_cloak/steady_cloak/ -- steady cloak (open-loop, old format)
|
||||
target_channel/target_channel/ -- empty channel (old format)
|
||||
target_cylinder/ -- target cylinders (0.75L, 1.0L, 1.5L)
|
||||
illusion/ -- PPO controlled (0.75L, 1.0L, 1.5L)
|
||||
karman/karman_re100/ -- Karman cloak validation
|
||||
karman_target/karman_q_in/ -- vortex street without pinball
|
||||
karman_blocked/karman_q_blk/ -- pinball in vortex street, no control
|
||||
resampled/{scene}/ -- phase_plan.json per scene
|
||||
ccd/ -- all JSON result files (8 files)
|
||||
figures/ -- all PNG figures (80+)
|
||||
old_data/ -- archived stale data
|
||||
docs/
|
||||
ccd_correction_field_report.md -- comprehensive report
|
||||
sr_ccd_oid_mapping.md -- cross-pipeline mapping (DRAFT)
|
||||
figures/ # 诊断图 (仅保留核心对比图)
|
||||
ccd/ # JSON 结果
|
||||
old_data/ # 归档 (废弃脚本/旧报告/旧数据)
|
||||
```
|
||||
|
||||
---
|
||||
## Key Documentation
|
||||
|
||||
## How to Regenerate Data
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| `ccd_knowledge.md` | **Primary entry** — theory, results, conventions, bug history |
|
||||
| `Lyu23.md` | CCD method paper (Lyu 2023) |
|
||||
| `data/old_data/ccd_correction_field_report.md` | Old full report (archived reference) |
|
||||
| `data/old_data/ccd_handover.md` | Old handover notes (archived) |
|
||||
|
||||
All commands from repo root. Environment: `conda run -n pycuda_3_10`.
|
||||
## Key Conventions
|
||||
|
||||
### GPU Data Collection
|
||||
|
||||
```bash
|
||||
# Target cylinders
|
||||
python src/CCD_analysis/scripts/collect_target_cylinder.py --diameter 0.75 --device 2
|
||||
python src/CCD_analysis/scripts/collect_target_cylinder.py --diameter 1.0 --device 2
|
||||
python src/CCD_analysis/scripts/collect_target_cylinder.py --diameter 1.5 --device 2
|
||||
|
||||
# Illusion PPO inference (500 steps)
|
||||
python src/CCD_analysis/scripts/collect_illusion.py --scene illusion_0.75L --device 2 --steps 500
|
||||
python src/CCD_analysis/scripts/collect_illusion.py --scene illusion_1.0L --device 2 --steps 500
|
||||
python src/CCD_analysis/scripts/collect_illusion.py --scene illusion_1.5L --device 2 --steps 500
|
||||
|
||||
# Baselines
|
||||
python src/CCD_analysis/scripts/collect_pinball.py --device 2
|
||||
python src/CCD_analysis/scripts/collect_steady_cloak.py --device 2
|
||||
python src/CCD_analysis/scripts/collect_empty_channel.py --device 2
|
||||
|
||||
# Karman references (collected during Round 6)
|
||||
python src/CCD_analysis/scripts/detect_period.py --scene karman_q_in
|
||||
python src/CCD_analysis/scripts/detect_period.py --scene karman_q_blk
|
||||
python src/CCD_analysis/scripts/replay_fields.py --scene karman_q_in --device 2
|
||||
python src/CCD_analysis/scripts/replay_fields.py --scene karman_q_blk --device 2
|
||||
```
|
||||
|
||||
### Phase Alignment
|
||||
|
||||
Run `detect_period.py` for each periodic scene, then `replay_fields.py` (GPU) to generate fields_aligned.npz.
|
||||
|
||||
### CPU Analysis (no GPU needed)
|
||||
|
||||
```bash
|
||||
# Correction-field CCD pipeline
|
||||
python correction_analysis/decompose_corrections.py # force/action on dq_ctl
|
||||
python correction_analysis/run_signature_line.py # signature line
|
||||
python correction_analysis/run_15L_correction.py # 1.5L special
|
||||
python correction_analysis/run_zone_ccd.py # zone-restricted
|
||||
python correction_analysis/run_steady_metrics.py # steady cloak
|
||||
python correction_analysis/diagnose_corrections.py # figures + zone metrics
|
||||
|
||||
# Round 5 raw-field (FROZEN — not recommended for new analysis)
|
||||
python ccd/run_ccd.py
|
||||
python ccd/validate.py
|
||||
python scripts/visualize_ccd.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Result Files
|
||||
|
||||
Write all results to `src/CCD_analysis/data/ccd/`:
|
||||
|
||||
| File | Contents | Source script |
|
||||
|------|----------|--------------|
|
||||
| `ccd_results.json` | Raw-field CCD (90 entries) | `ccd/run_ccd.py` |
|
||||
| `validation_results.json` | Raw-field LOCO | `ccd/validate.py` |
|
||||
| `correction_ccd_results.json` | Correction-field force/action (30 entries) | `decompose_corrections.py` |
|
||||
| `correction_validation_results.json` | Correction-field LOCO | `decompose_corrections.py` |
|
||||
| `signature_ccd_results.json` | Signature line (47 entries) | `run_signature_line.py` |
|
||||
| `15L_correction_results.json` | 1.5L special (40 entries) | `run_15L_correction.py` |
|
||||
| `zone_ccd_results.json` | Zone-restricted CCD (30 entries) | `run_zone_ccd.py` |
|
||||
| `zone_metrics.json` | Per-zone KE/enstrophy | `diagnose_corrections.py` |
|
||||
| `steady_metrics.json` | Steady cloak metrics | `run_steady_metrics.py` |
|
||||
|
||||
---
|
||||
|
||||
## Known Pitfalls (New Agent Must Read)
|
||||
|
||||
1. **Model naming convention**: `_2U` means S_DIM=14 (2 extra target force channels). NOT 2x velocity. u0 is ALWAYS 0.01. Round 1-3 were invalidated by this misinterpretation.
|
||||
|
||||
2. **action_bias vs preset_action**: action_bias=[0,-2,2] is for DRL action scaling. preset_action=[0,0,0,0,-1*U0,1*U0] is FIFO warmup. They are DIFFERENT.
|
||||
|
||||
3. **DO NOT use `nu_from_re()`** for illusion models. Only valid for standard u0=0.01, S_DIM=12 cases.
|
||||
|
||||
4. **Main analysis object is `dq_ctl = q_ctl - q_blk`**, not raw `q_ctl`. Use `correction_analysis/compute_correction_fields.py`.
|
||||
|
||||
5. **Karman q_in/q_blk vs q_re100**: karman_re100 is 72 frames (3 cycles, 18pts/cycle due to sampling). karman_q_in and karman_q_blk are 96 frames. When computing correction fields, N-mismatch is auto-trimmed.
|
||||
|
||||
6. **GPU state contamination**: Running PPO inference after other CFD on same GPU degrades similarity. Use a fresh GPU.
|
||||
|
||||
7. **Steady cloak has no forces**: `sensors.npz` only has sensor channels, no force data. Drag proxy uses momentum deficit.
|
||||
|
||||
8. **force_fx is unreliable**: R2 ~0.4, high variance. Only use for O_k trends, not mechanism claims.
|
||||
|
||||
9. **POD-reduced CCD limitation**: Results are constrained to the target-only POD subspace. Truncated structures cannot be recovered. Full-field CCD requires more data.
|
||||
- **Main analysis object**: `dq_ctl` (not raw `q_ctl`)
|
||||
- **Observation order**: `[forces/force_norm, sensors/sens_norm]` — force first (see Bug 3 in ccd_knowledge.md §12)
|
||||
- **All scenes unified geometry**: pinball center at 613px, sensors at 800px
|
||||
- **Environment**: `conda run -n pycuda_3_10`
|
||||
- **GPU**: Device 2 for collection
|
||||
|
||||
270
src/CCD_analysis/ccd/validate.py
Normal file
@ -0,0 +1,270 @@
|
||||
"""LOCO and blocked-split validation for CCD (Round 5).
|
||||
|
||||
Reuses shared data loader from resampling.py.
|
||||
Target-only POD basis. Q_delay=6 for force/action.
|
||||
|
||||
0.75L and 1.0L only. 1.5L excluded from validation.
|
||||
|
||||
Usage:
|
||||
conda run -n pycuda_3_10 python ccd/validate.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR
|
||||
from CCD_analysis.utils.resampling import (
|
||||
compute_reduced_ccd, cumulative_energy,
|
||||
load_aligned_fields, make_force_obs,
|
||||
build_field_matrix, project_into_basis,
|
||||
)
|
||||
|
||||
R_LIST = [6, 8, 10]
|
||||
CCD_Q = 6
|
||||
N_CYCLES = 4
|
||||
N_PTS = 24
|
||||
DIAMETERS = [0.75, 1.0]
|
||||
|
||||
|
||||
def r2(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
||||
"""Coefficient of determination."""
|
||||
ss_r = np.sum((y_true - y_pred) ** 2)
|
||||
ss_t = np.sum((y_true - np.mean(y_true)) ** 2)
|
||||
return float(1.0 - ss_r / (ss_t + 1e-12))
|
||||
|
||||
|
||||
def reconstruct_observable(W, sigma, R, a_test, y_train):
|
||||
"""Reconstruct observable from CCD modes.
|
||||
|
||||
Returns dict with 'mode1' and 'm80' reconstructions.
|
||||
"""
|
||||
am = np.mean(a_test, axis=1, keepdims=True)
|
||||
as_ = np.std(a_test, axis=1, keepdims=True) + 1e-12
|
||||
a_test_z = (a_test - am) / as_
|
||||
z_test = W.T @ a_test_z
|
||||
|
||||
ym = np.mean(y_train, axis=1, keepdims=True)
|
||||
ys = np.std(y_train, axis=1, keepdims=True) + 1e-12
|
||||
half = CCD_Q // 2
|
||||
m_obs = y_train.shape[0]
|
||||
|
||||
en = cumulative_energy(sigma)
|
||||
m80 = int(np.searchsorted(en, 0.80) + 1) if len(en) > 0 else 1
|
||||
|
||||
results = {}
|
||||
|
||||
# Mode-1
|
||||
if R.shape[1] >= 1:
|
||||
pz_1 = R[:, :1] * sigma[:1] @ z_test[:1, :]
|
||||
yp_1 = pz_1[half * m_obs:(half + 1) * m_obs, :] * ys + ym
|
||||
results["mode1"] = yp_1
|
||||
else:
|
||||
results["mode1"] = np.zeros_like(y_train[:, :a_test.shape[1]])
|
||||
|
||||
# M80
|
||||
n_rm = min(m80, R.shape[1])
|
||||
if n_rm >= 1:
|
||||
pz_m = R[:, :n_rm] * sigma[:n_rm] @ z_test[:n_rm, :]
|
||||
yp_m = pz_m[half * m_obs:(half + 1) * m_obs, :] * ys + ym
|
||||
results["m80"] = yp_m
|
||||
else:
|
||||
results["m80"] = np.zeros_like(y_train[:, :a_test.shape[1]])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_single_diameter(diam: float, scene_data: dict) -> dict:
|
||||
"""Run validation for one diameter. Returns results dict."""
|
||||
tgt_key = f"target_cylinder_{diam}L"
|
||||
ill_key = f"illusion_{diam}L"
|
||||
|
||||
tgt_d = scene_data[tgt_key]
|
||||
ill_d = scene_data[ill_key]
|
||||
unc_d = scene_data["pinball"]
|
||||
|
||||
# Pre-build target-only field matrices
|
||||
tgt_f = build_field_matrix(tgt_d["ux"], tgt_d["uy"])
|
||||
ill_f = build_field_matrix(ill_d["ux"], ill_d["uy"])
|
||||
unc_f = build_field_matrix(unc_d["ux"], unc_d["uy"])
|
||||
|
||||
diam_results = {}
|
||||
pod_cache = {}
|
||||
|
||||
# Pre-compute target-only POD for each fold
|
||||
for fold in range(N_CYCLES):
|
||||
test_cyc = fold
|
||||
train_cyc = [c for c in range(N_CYCLES) if c != test_cyc]
|
||||
train_idx = sorted([c * N_PTS + p for c in train_cyc for p in range(N_PTS)])
|
||||
for r in R_LIST:
|
||||
Q_ref = tgt_f[:, train_idx]
|
||||
mf = np.mean(Q_ref, axis=1)
|
||||
U, _, _ = np.linalg.svd(Q_ref - mf[:, None], full_matrices=False)
|
||||
pod_cache[("loco", fold, r)] = (mf, U[:, :r])
|
||||
|
||||
# Blocked split
|
||||
train_idx_full = list(range(0, 2 * N_PTS))
|
||||
for r in R_LIST:
|
||||
Q_ref = tgt_f[:, train_idx_full]
|
||||
mf = np.mean(Q_ref, axis=1)
|
||||
U, _, _ = np.linalg.svd(Q_ref - mf[:, None], full_matrices=False)
|
||||
pod_cache[("blocked", 0, r)] = (mf, U[:, :r])
|
||||
|
||||
# -- LOCO --
|
||||
print("\n--- LOCO (4-fold) ---", flush=True)
|
||||
loco_results = {}
|
||||
for r in R_LIST:
|
||||
for obs in ["force_fy", "force_fx", "action"]:
|
||||
fold_r2_m1, fold_r2_m80 = [], []
|
||||
for fold in range(N_CYCLES):
|
||||
test_cyc = fold
|
||||
train_cyc = [c for c in range(N_CYCLES) if c != test_cyc]
|
||||
train_idx = sorted([c * N_PTS + p for c in train_cyc for p in range(N_PTS)])
|
||||
test_idx = sorted([c * N_PTS + p for c in [test_cyc] for p in range(N_PTS)])
|
||||
|
||||
mf, modes_r = pod_cache[("loco", fold, r)]
|
||||
|
||||
for name, d, fld in [
|
||||
(tgt_key, tgt_d, tgt_f), (ill_key, ill_d, ill_f), ("pinball", unc_d, unc_f)
|
||||
]:
|
||||
if obs == "action" and "illusion" not in name:
|
||||
continue
|
||||
if d.get("forces") is None and "force" in obs:
|
||||
continue
|
||||
|
||||
a_train = modes_r.T @ (fld[:, train_idx] - mf[:, None])
|
||||
a_test = modes_r.T @ (fld[:, test_idx] - mf[:, None])
|
||||
|
||||
if "force" in obs:
|
||||
f_mode = obs.split("_")[1] # "fy" or "fx"
|
||||
y_train = make_force_obs(d["forces"][train_idx], name, mode=f_mode)
|
||||
y_test = make_force_obs(d["forces"][test_idx], name, mode=f_mode)
|
||||
else:
|
||||
y_train = d["actions"][train_idx, :].T
|
||||
y_test = d["actions"][test_idx, :].T
|
||||
|
||||
W, sigma, Rmat, _, _, _ = compute_reduced_ccd(a_train, y_train, Q_delay=CCD_Q)
|
||||
recon = reconstruct_observable(W, sigma, Rmat, a_test, y_train)
|
||||
|
||||
ch_m1 = [r2(y_test[c], recon["mode1"][c]) for c in range(y_test.shape[0])]
|
||||
ch_m80 = [r2(y_test[c], recon["m80"][c]) for c in range(y_test.shape[0])]
|
||||
fold_r2_m1.append(float(np.mean(ch_m1)))
|
||||
fold_r2_m80.append(float(np.mean(ch_m80)))
|
||||
|
||||
if fold_r2_m1:
|
||||
key = f"LOCO_{obs}_r{r}"
|
||||
loco_results[key] = {
|
||||
"mode1": {
|
||||
"mean": float(np.mean(fold_r2_m1)),
|
||||
"std": float(np.std(fold_r2_m1)),
|
||||
},
|
||||
"m80": {
|
||||
"mean": float(np.mean(fold_r2_m80)),
|
||||
"std": float(np.std(fold_r2_m80)),
|
||||
},
|
||||
}
|
||||
print(f" {key}: R2_m1={loco_results[key]['mode1']['mean']:.4f}+-"
|
||||
f"{loco_results[key]['mode1']['std']:.4f} "
|
||||
f"R2_m80={loco_results[key]['m80']['mean']:.4f}+-"
|
||||
f"{loco_results[key]['m80']['std']:.4f}", flush=True)
|
||||
|
||||
# -- Blocked split --
|
||||
print("\n--- Blocked Split (train=0-47, test=48-95) ---", flush=True)
|
||||
blocked = {}
|
||||
test_idx_full = list(range(2 * N_PTS, 4 * N_PTS))
|
||||
for r in R_LIST:
|
||||
for obs in ["force_fy", "force_fx", "action"]:
|
||||
mf, modes_r = pod_cache[("blocked", 0, r)]
|
||||
per_case_m1, per_case_m80 = {}, {}
|
||||
|
||||
for name, d, fld in [
|
||||
(tgt_key, tgt_d, tgt_f), (ill_key, ill_d, ill_f), ("pinball", unc_d, unc_f)
|
||||
]:
|
||||
if obs == "action" and "illusion" not in name:
|
||||
continue
|
||||
if d.get("forces") is None and "force" in obs:
|
||||
continue
|
||||
|
||||
a_train = modes_r.T @ (fld[:, train_idx_full] - mf[:, None])
|
||||
a_test = modes_r.T @ (fld[:, test_idx_full] - mf[:, None])
|
||||
|
||||
if "force" in obs:
|
||||
f_mode = obs.split("_")[1]
|
||||
y_train = make_force_obs(d["forces"][train_idx_full], name, mode=f_mode)
|
||||
y_test = make_force_obs(d["forces"][test_idx_full], name, mode=f_mode)
|
||||
else:
|
||||
y_train = d["actions"][train_idx_full, :].T
|
||||
y_test = d["actions"][test_idx_full, :].T
|
||||
|
||||
W, sigma, Rmat, _, _, _ = compute_reduced_ccd(a_train, y_train, Q_delay=CCD_Q)
|
||||
recon = reconstruct_observable(W, sigma, Rmat, a_test, y_train)
|
||||
|
||||
ch_m1 = [r2(y_test[c], recon["mode1"][c]) for c in range(y_test.shape[0])]
|
||||
ch_m80 = [r2(y_test[c], recon["m80"][c]) for c in range(y_test.shape[0])]
|
||||
per_case_m1[name] = float(np.mean(ch_m1))
|
||||
per_case_m80[name] = float(np.mean(ch_m80))
|
||||
|
||||
key = f"blocked_{obs}_r{r}"
|
||||
blocked[key] = {
|
||||
"mode1": {
|
||||
"mean": float(np.mean(list(per_case_m1.values()))),
|
||||
"per_case": per_case_m1,
|
||||
},
|
||||
"m80": {
|
||||
"mean": float(np.mean(list(per_case_m80.values()))),
|
||||
"per_case": per_case_m80,
|
||||
},
|
||||
}
|
||||
print(f" {key}: R2_m1={blocked[key]['mode1']['mean']:.4f} "
|
||||
f"R2_m80={blocked[key]['m80']['mean']:.4f}", flush=True)
|
||||
|
||||
diam_results["LOCO"] = loco_results
|
||||
diam_results["blocked_split"] = blocked
|
||||
return diam_results
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 60, flush=True)
|
||||
print("CCD Validation (Round 5)", flush=True)
|
||||
print("=" * 60, flush=True)
|
||||
|
||||
t_start = time.time()
|
||||
all_results = {}
|
||||
|
||||
for diam in DIAMETERS:
|
||||
tgt_key = f"target_cylinder_{diam}L"
|
||||
ill_key = f"illusion_{diam}L"
|
||||
print(f"\n{'=' * 60}", flush=True)
|
||||
print(f"Diameter {diam}L", flush=True)
|
||||
print(f"{'=' * 60}", flush=True)
|
||||
|
||||
t0 = time.time()
|
||||
scene_data = {
|
||||
tgt_key: load_aligned_fields(tgt_key),
|
||||
ill_key: load_aligned_fields(ill_key),
|
||||
"pinball": load_aligned_fields("pinball"),
|
||||
}
|
||||
print(f" Data loaded in {time.time() - t0:.0f}s", flush=True)
|
||||
|
||||
t1 = time.time()
|
||||
all_results[f"{diam}L"] = run_single_diameter(diam, scene_data)
|
||||
print(f" Analysis done in {time.time() - t1:.0f}s", flush=True)
|
||||
|
||||
out_dir = os.path.join(DATA_DIR, "ccd")
|
||||
with open(os.path.join(out_dir, "validation_results.json"), "w") as f:
|
||||
json.dump(all_results, f, indent=2)
|
||||
|
||||
print(f"\nTotal: {time.time() - t_start:.0f}s", flush=True)
|
||||
print(f"Saved to {out_dir}/validation_results.json", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@ -1,138 +1,509 @@
|
||||
# CCD 分析知识库 — 最终版本 (2026-06-22)
|
||||
# CCD Analysis Knowledge Base — 论文知识库 (2026-06-25)
|
||||
|
||||
## 工作阶段总览
|
||||
|
||||
| 阶段 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| Round 1-3 | **废弃** | 错误 u0=0.02, nu=0.008,所有结论无效 |
|
||||
| Round 4 | **废弃** | 仍有 bug(模型选择、采集脚本),已归档至 old_data/ |
|
||||
| **Round 5** | **完成 (frozen baseline)** | 正确数据 + fields_aligned.npz + target-only POD raw-field CCD |
|
||||
| **Round 6** | **完成** | correction-field 框架: 数据层 + 差分诊断 + force/action/signature 三线 + 1.5L + 三区域分层 CCD |
|
||||
|
||||
### 核心结论: 哪个做完了、哪个没做
|
||||
|
||||
| 方向 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| **Illusion 0.75L/1.0L correction-field** | **完整** | force/action/signature 三线 CCD、zone-CCD、LOCO 验证 |
|
||||
| **Illusion 1.5L** | **完整** | force/action/signature CCD、phase drift、zone 指标(特殊机制 case) |
|
||||
| **Steady cloak** | **定量化完成** | recirculation/RMS suppression/cancellation ratio,结论: open-loop 无效 |
|
||||
| **Karman cloak** | **数据已齐,分析延后** | q_in / q_blk / q_ctl 三场已采集, phase plan 96 帧对齐 |
|
||||
| **Signature line** | **已完成** (0.75L, 1.0L) | tau=0/geom/corr 扫描 + O(force,sig) + LOCO 验证 |
|
||||
| **Zone-restricted CCD** | **已完成** | near_body/body_wake/sensor_zone 三层分别做 force+signature |
|
||||
| **SR-CCD-OID mapping** | **草稿** | `docs/sr_ccd_oid_mapping.md` 已写,需要根据其他两方向的实际报告校正 |
|
||||
> 统一文档。整合了 Round 5-6 的完整分析结果、correction-field 框架、Vortex/Cloak/Illusion 对比、几何对齐方案、以及所有操作流程。写论文时以本文档为准。
|
||||
|
||||
---
|
||||
|
||||
## 所有结果汇总
|
||||
## 1. 项目概览
|
||||
|
||||
### Round 5 — raw-field baseline (仅供参考,不再是主分析对象)
|
||||
### 1.1 物理系统
|
||||
|
||||
**Protocol**: raw full field `q_ctl`, target-only POD, SigmaFy primary, Q_delay=6, per-case z-score
|
||||
**Fluidic Pinball**: 三个等间距圆柱(直径 D=20 格点,中心间距 0.75D)在二维通道内呈倒三角排列(间距 30°)。每个圆柱可独立旋转。DRL 控制器(PPO)每 SAMPLE_INTERVAL 步输出三个转速动作 [-1,1]^3,映射到物理表面速度。
|
||||
|
||||
| 指标 | 0.75L | 1.0L | 1.5L |
|
||||
|------|-------|------|------|
|
||||
| O(target, illusion) mode1 | 0.673 | **0.919** | 0.621 |
|
||||
| force_fy m80 | 2 | 2 | 2 |
|
||||
| action m80 | 2 | 3 | 3 |
|
||||
| LOCO force_fy R2_m80 | 0.71 +- 0.08 | 0.66 +- 0.03 | — |
|
||||
| LOCO force_fx R2_m80 | 0.17 +- 0.49 | 0.23 +- 0.51 | — |
|
||||
**场景分类**:
|
||||
|
||||
完整结果在 `ccd/ccd_results.json`, `ccd/validation_results.json`。
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Cloak [Cloak:涡街伪装]
|
||||
Steady[均匀来流 → 下游恢复均匀]
|
||||
Karman[涡街来流 → 下游维持涡街]
|
||||
Vortex[瞬态涡事件 → 涡不受畸变]
|
||||
end
|
||||
subgraph Illusion [Illusion:流场欺骗]
|
||||
I075[0.75L: 伪装成更小圆柱]
|
||||
I10[1.0L: 伪装成等大圆柱]
|
||||
I15[1.5L: 伪装成更大圆柱]
|
||||
end
|
||||
```
|
||||
|
||||
**控制机制**(经 SR 和 correction-field 双重验证):
|
||||
- 后两圆柱(Top/Bottom): 恒速反向旋转(cloak 约 ±0.313),补偿 pinball 后速度亏损
|
||||
- 前端圆柱(Front): 动态调节,控制升力($\alpha_F = \Delta a_F/\Delta t - 14.952\mu C_{l,\text{tot}}$)
|
||||
|
||||
### 1.2 核心方法:Correction-field 框架
|
||||
|
||||
**基本思想**: 不直接分析受控流场 `q_ctl`,而是分析**控制施加的修正**:
|
||||
|
||||
| 符号 | 含义 | 计算方式 |
|
||||
|------|------|----------|
|
||||
| `q_in` | 均匀来流(干净通道) | 空通道采集 |
|
||||
| `q_blk` | pinball 无控制 | 原始 pinball 涡街 |
|
||||
| `q_ctl` | pinball + DRL 控制 | PPO 推理采集 |
|
||||
| `q_tar` | 目标流场 | 目标圆柱/涡街参考 |
|
||||
| `dq_blk = q_blk - q_in` | pinball 阻塞场 | pinball 对来流的畸变 |
|
||||
| `dq_ctl = q_ctl - q_blk` | **控制修正场** | 控制器在 pinball 基础上加的改变 |
|
||||
| `dq_tar = q_tar - q_blk` | **目标修正场** | 理论需达到的修正 |
|
||||
|
||||
**核心问题**: `dq_ctl` 是否等于 `dq_tar`?即控制做的"修正"是否就是理论上需要的"修正"?
|
||||
|
||||
### 1.3 关键发现
|
||||
|
||||
1. **Cloak 机制统一**:Steady/Karman/Vortex 三种场景的 dq_ctl 高度一致——补偿速度亏损(正 ux)+ 偶极子效应。控制策略相同,不论上游来流条件。
|
||||
|
||||
2. **Illusion 1.0L 与 Cloak 共享机制**:dq_ctl 结构定性一致。Illusion 本质是通过"Cloak 机制"让下游看起来像目标圆柱。
|
||||
|
||||
3. **Illusion 0.75L 效率降低**:O(dqctl,dqtar)=0.564,控制效率低于 1.0L
|
||||
|
||||
4. **Illusion 1.5L 特殊机制**:O(dqctl,dqtar)=0.667,action sigma1=0.28(其他 1.13-1.39),强相位漂移,修正集中在近体区
|
||||
|
||||
5. **Force/Signature 分离**:力相关结构和传感器相关结构在瞬时 tau=0 时分离(O=0.01-0.55),对流延迟后共享(O=0.72-0.81)
|
||||
|
||||
---
|
||||
|
||||
### Round 6 — correction-field (主分析对象)
|
||||
## 2. 方法细节
|
||||
|
||||
**Protocol**: `dq_ctl = q_ctl - q_blk`, target-only POD basis, SigmaFy primary, Q_delay=6
|
||||
### 2.1 物理参数
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| NX, NY | 1280, 512 | LBM 格点数 |
|
||||
| L0 | 20 | 基准长度单位(1 圆柱直径) |
|
||||
| U0 | 0.01 | 入口中心流速(抛物线分布) |
|
||||
| nu | 0.004 | 默认运动粘度 |
|
||||
| Re_code | 100 | 参考长度 2×D_cyl → Re_D=50 |
|
||||
| D_cyl | L0=20 | 单一圆柱直径 |
|
||||
| CENTER_Y | 255.5 | 通道中心 y 坐标 |
|
||||
|
||||
**Reynolds 数约定**:
|
||||
- `re_code` 使用参考长度 D_REF = 2*D = 40(匹配模型文件命名)
|
||||
- `Re_D = re_code / 2` 是真实物理雷诺数(使用单圆柱直径)
|
||||
- 默认场景 `re_code=100` → `Re_D=50`
|
||||
|
||||
### 2.2 场景几何位置(2026-06-25 统一更新)
|
||||
|
||||
**所有场景已统一几何**:采集端直接在 `configs.py` 中设置统一坐标,无需后处理平移。
|
||||
|
||||
| 组 | 场景 | pinball 中心 | sensor x | 来源 configs |
|
||||
|----|------|-------------|--------|-------------|
|
||||
| **所有场景** | pinball, steady, karman, vortex, illusion_*, target_cylinder_* | **613 px** | **800 px** | configs.py UNIFIED 注释 |
|
||||
|
||||
- pinball: front x=30×L0=600, rear x=31.3×L0=626 → 中心 ≈ 613 px
|
||||
- target cylinder: x=30.65×L0=613 px
|
||||
- sensors: x=40×L0=800 px
|
||||
|
||||
### 2.3 几何对齐方案(已废弃,仅保留备用)
|
||||
|
||||
统一几何后不再需要后处理平移。`utils/field_translate.py` 保留作为可选工具,但不参与默认 pipeline。
|
||||
|
||||
<details>
|
||||
<summary>旧方案(参考)</summary>
|
||||
|
||||
```python
|
||||
SHIFT_ILLUSION_TO_CLOAK = +220 px # 旧 illusion → cloak
|
||||
SHIFT_CLOAK_TO_ILLUSION = -220 px
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据采集
|
||||
|
||||
### 3.1 GPU 采集脚本
|
||||
|
||||
| 脚本 | 功能 | 输出目录 |
|
||||
|------|------|----------|
|
||||
| `scripts/collect_target_cylinder.py` | 目标圆柱(单圆柱涡街) | `data/target_cylinder/{diam}L/` |
|
||||
| `scripts/collect_pinball.py` | pinball 无控制基线 | `data/pinball/pinball/` |
|
||||
| `scripts/collect_empty_channel.py` | 空通道 | `data/target_channel/target_channel/` |
|
||||
| `scripts/collect_illusion.py` | Illusion PPO 推理 | `data/illusion/{scene}/` |
|
||||
| `scripts/collect_karman.py` | Karman cloak PPO 推理 | `data/karman/karman_re100/` |
|
||||
| `scripts/collect_karman_q_in.py` | 涡街无 pinball(Karman 参考) | `data/karman_target/karman_q_in/` |
|
||||
| `scripts/collect_karman_q_blk.py` | 涡街+pinball 无控制(Karman 参考) | `data/karman_blocked/karman_q_blk/` |
|
||||
| `scripts/collect_steady_cloak.py` | 稳态 cloak(开环恒速) | `data/steady_cloak/steady_cloak/` |
|
||||
| `scripts/collect_vortex.py` | Vortex cloak(Taylor/Lamb) | `data/vortex_{type}/` |
|
||||
|
||||
### 3.2 采集流程(以 Karman 为例)
|
||||
|
||||
```
|
||||
Phase 1: Target recording
|
||||
FlowField → add_sensor(40*L0) ×3 → stabilize → add_vortex/cylinder
|
||||
→ run(SI, zero) × F来O_LEN → save target.npz
|
||||
|
||||
Phase 2: Add pinball + Norm
|
||||
restore → add_cylinder(pinball) ×3 → stabilize
|
||||
→ run(SI, zero) × FIFO_LEN → compute norm
|
||||
→ run(SI, bias_action) × FIFO_LEN → save ddf+fifo checkpoint
|
||||
|
||||
Phase 3: Controlled PPO inference
|
||||
restore + warmup → for step in range(n_steps):
|
||||
model.predict(obs) → action → run(SI, action_arr) → save telemetry + field
|
||||
```
|
||||
|
||||
### 3.3 Norm 计算
|
||||
|
||||
```
|
||||
force_norm_fact = 6 × max|forces|
|
||||
sens_deviation = mean(sensors, axis=0)
|
||||
sens_norm_fact[i] = 5 × max|sensors[:,i] - sens_deviation[i]|
|
||||
obs = clip[forces/force_norm, (sens - deviation)/sens_norm], to [-1, 1]
|
||||
```
|
||||
|
||||
**注意**:Norm 是在 scene-specific 采集时计算的,不同场景的 norm 值不同。推理时必须使用对应场景的 norm。
|
||||
|
||||
### 3.4 Phase Alignment 流程
|
||||
|
||||
```
|
||||
detect_period.py:
|
||||
sensors[:, 3] → FFT → dominant frequency + period
|
||||
zero-crossing detection → cycle boundaries (CV_T)
|
||||
select best 4-cycle window → map 4×24=96 uniform phase points
|
||||
→ save phase_plan.json
|
||||
|
||||
replay_fields.py:
|
||||
load phase_plan + ddf_checkpoint + actions
|
||||
replay PPO → at each phase_plan step_index → save velocity field
|
||||
→ save fields_aligned.npz + replay_verify.json
|
||||
```
|
||||
|
||||
### 3.5 数据状态
|
||||
|
||||
| 场景 | scene_id | 帧数 | 格式 |
|
||||
|------|----------|------|------|
|
||||
| pinball | pinball | 96 | fields_aligned.npz |
|
||||
| target_cylinder_{0.75,1.0,1.5}L | target_cylinder | 96 | fields_aligned.npz |
|
||||
| illusion_{0.75,1.0,1.5}L | illusion | 96 | fields_aligned.npz |
|
||||
| karman_re100 | karman | 72 (3周期) | fields_aligned.npz |
|
||||
| karman_q_in | karman_target | 96 | fields_aligned.npz |
|
||||
| karman_q_blk | karman_blocked | 96 | fields_aligned.npz |
|
||||
| steady_cloak | steady_cloak | 500 | fields.npz (旧格式) |
|
||||
| target_channel | target_channel | 100 | fields.npz (旧格式) |
|
||||
| vortex_{lamb/taylor}/target/uncontrolled | 各自 scene_id | 150 | fields.npz (瞬态) |
|
||||
|
||||
---
|
||||
|
||||
## 4. CCD 方法
|
||||
|
||||
### 4.1 算法原理(Lyu23)
|
||||
|
||||
CCD(Canonical Correlation Decomposition)通过 CCA 在流场和可观测量之间找到相关性最大的方向:
|
||||
|
||||
```python
|
||||
# Reduced CCD in POD coefficient space
|
||||
def compute_reduced_ccd(pod_coeffs, observable, Q_delay=6):
|
||||
# 1. Construct lagged observable matrix P (with symmetric delay window)
|
||||
# 2. Standardize P and A (POD coefficients)
|
||||
# 3. Cross-correlation matrix: C = P @ A^T / (N * sqrt(Q))
|
||||
# 4. SVD(C) → R, sigma, W
|
||||
# W = CCD directions in POD space
|
||||
# sigma = correlation strength (sorted descending)
|
||||
# z = CCD temporal coefficients
|
||||
```
|
||||
|
||||
**物理意义**:
|
||||
- sigma[0] = 第一 CCD 模式的相关系数
|
||||
- m80 = 需要多少模式来捕获 80% 的总相关性(compactness 指标)
|
||||
- O_k = 两个 case 之间第 k 模的重叠(内积绝对值)
|
||||
|
||||
### 4.2 三条分析线
|
||||
|
||||
| 线 | Observable | 问题 | 物理意义 |
|
||||
|----|-----------|------|----------|
|
||||
| **Force** (主) | SigmaFy = sum(Fy_i) | 哪些修正结构决定升力? | 力相关流场结构 |
|
||||
| **Force** (次) | SigmaFx | 哪些修正结构决定阻力? | 不可靠(R2~0.4) |
|
||||
| **Action** | [omega1, omega2, omega3] | 控制器直接调制哪些结构? | 动作相关流场结构 |
|
||||
| **Signature** | e(t+tau) = s_ctl(t+tau) - s_tar(t+tau) | 哪些结构决定未来传感器误差? | 传感器相关流场结构 |
|
||||
|
||||
### 4.3 验证方法
|
||||
|
||||
**LOCO (Leave-One-Cycle-Out)**:
|
||||
- 4 个涡街周期,留 1 做测试,3 做训练
|
||||
- 训练 CCD → 预测可观测量 → 计算 R2
|
||||
- 通过阈值:R2_m80 > 0.4
|
||||
|
||||
| Observable | 0.75L R2_m80 | 1.0L R2_m80 | 结论 |
|
||||
|-----------|-------------|-------------|------|
|
||||
| force_fy | 0.65 ± 0.08 | 0.64 ± 0.02 | PASS |
|
||||
| force_fx | 0.38 ± 0.23 | 0.43 ± 0.11 | WARNING |
|
||||
| signature tau=0 | 0.50 ± 0.09 | 0.49 ± 0.04 | PASS |
|
||||
| signature tau=tau_c | 0.51 ± 0.09 | 0.53 ± 0.03 | PASS |
|
||||
|
||||
### 4.4 Zone-Restricted CCD
|
||||
|
||||
统一几何后所有场景共用一套 zone 定义(定义见 `diagnose_corrections.py` 的 `define_zones_karman()`):
|
||||
|
||||
| 区域 | x 范围 (像素) | 包含 |
|
||||
|------|-------------|------|
|
||||
| near_body | 580-720 | pinball 周围 |
|
||||
| body_wake | 720-850 | 近尾流 |
|
||||
| sensor_zone | 780-850 | 传感器区域 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 结果
|
||||
|
||||
### 5.1 Correction-field CCD 主表(2026-06-28 更新,统一几何后重跑)
|
||||
|
||||
| 指标 | 0.75L | 1.0L | 1.5L |
|
||||
|------|-------|------|------|
|
||||
| **O(dqctl, dqtar) mode1** | **0.564** | **0.913** | **0.667** |
|
||||
| dqctl force_fy m80 | 2 | **1** (r=8/10) | 2 |
|
||||
| dqctl action sigma1 | 1.39 | 1.13 | **0.28** |
|
||||
| **O(force, sig) at tau=0** | **0.413** | **0.551** | — |
|
||||
| **O(force, sig) at tau=tau_c** | **0.806** | **0.768** | — |
|
||||
| **O(dqctl, dqtar) mode1 (r=6)** | **0.383** | **0.926** | **0.922** |
|
||||
| **O(dqctl, dqtar) mode1 (r=10)** | **0.320** | **0.684** | **0.661** |
|
||||
| force_fy m80 (r=6) | 2 | 2 | **1** |
|
||||
| action sigma1 (r=6) | 1.49 | 1.17 | **0.20** |
|
||||
| Phase drift | low | low | **high** |
|
||||
| Body-wake/sensor KE ratio | 0.73 | 1.17 | **2.58** |
|
||||
| 1.5L special: rank sensitivity | — | — | O drops 0.922→0.661 (r=6→10) |
|
||||
|
||||
**LOCO 验证 (r=6, R2_m80)**:
|
||||
**关键变化**(vs 6月15日旧版,Illusion 旧几何 pinball x≈393px):
|
||||
- 0.75L O 从 0.564 → **0.383**(-32%):旧几何的空间错位虚高了 overlap。统一几何后揭示真实匹配度远低于预期。
|
||||
- 1.0L O 从 0.913 → **0.926**(+1.4%):基本不变,1.0L 的控制修正与目标一致。
|
||||
- 1.5L O(r=6)=**0.922**(首次获得):dominant mode 匹配度高,但更高阶 mode 快速发散(r=10 时降至 0.661),反映多尺度控制策略。
|
||||
- 1.5L action sigma1=**0.20**(远低于 0.75L 的 1.49 和 1.0L 的 1.17):确认高频调制机制下动作与流场结构的映射极其分散。
|
||||
|
||||
| Observable | 0.75L | 1.0L |
|
||||
|-----------|-------|------|
|
||||
| force_fy | 0.65 +- 0.08 (PASS) | 0.64 +- 0.02 (PASS) |
|
||||
| force_fx | 0.38 +- 0.23 (WARNING) | 0.43 +- 0.11 (WARNING) |
|
||||
| signature tau=0 | 0.50 +- 0.09 (PASS) | 0.49 +- 0.04 (PASS) |
|
||||
| signature tau=tau_c | 0.51 +- 0.09 (PASS) | 0.53 +- 0.03 (PASS) |
|
||||
以下 force-sig overlap 和 zone 数据来自 6 月 15 日旧版(待用统一几何和新 zone 定义重跑):
|
||||
|
||||
**三区域分层 CCD (zone-restricted, r=6 force_fy)**:
|
||||
### 5.2 三区域 Force-Signature Overlap
|
||||
|
||||
0.75L:
|
||||
| Zone | O(force,sig) tau=0 | O(force,sig) tau=tau_c |
|
||||
**0.75L** — sensor_zone 在 tau=0 时近乎正交:
|
||||
|
||||
| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|
||||
|------|--------------------|------------------------|
|
||||
| near_body | 0.262 | 0.827 |
|
||||
| body_wake | 0.269 | **0.917** |
|
||||
| sensor_zone | **0.010** | 0.722 |
|
||||
|
||||
1.0L:
|
||||
| Zone | O(force,sig) tau=0 | O(force,sig) tau=tau_c |
|
||||
**1.0L** — 更均匀,没有近零区域:
|
||||
|
||||
| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|
||||
|------|--------------------|------------------------|
|
||||
| near_body | 0.596 | 0.596 |
|
||||
| body_wake | 0.509 | 0.483 |
|
||||
| sensor_zone | 0.594 | **0.730** |
|
||||
|
||||
完整结果在 `ccd/correction_ccd_results.json`, `ccd/signature_ccd_results.json`, `ccd/15L_correction_results.json`, `ccd/zone_ccd_results.json`, `ccd/zone_metrics.json`。
|
||||
### 5.3 Cloak 全景对比(Steady / Karman / Vortex dq_ctl)
|
||||
|
||||
完整报告: `docs/ccd_correction_field_report.md` (412 行, 含 10 张图的详细解读指南)
|
||||
所有 cloak 场景的 dq_ctl 展现了**一致的物理机制**:
|
||||
1. **速度亏损补偿**:pinball 后方正 ux(红色),控制加速尾流
|
||||
2. **偶极子效应**:圆柱附近旋转产生的偶极子模式
|
||||
3. **涡量结构**:三种场景涡量分布高度相似
|
||||
|
||||
量化指标:
|
||||
|
||||
| 场景 | dq_ctl RMS (crop) | 性质 |
|
||||
|------|------------------|------|
|
||||
| steady_cloak | 0.196 | 稳态,开环 |
|
||||
| karman_re100 | 0.397 | 周期,PPO 闭环 |
|
||||
| vortex_lamb | 0.164 | 瞬态,PPO 闭环(fade-in/out + swapped norm) |
|
||||
| vortex_taylor | 0.203 | 瞬态,PPO 闭环(fade-in/out + swapped norm) |
|
||||
|
||||
**结论**:不论上游条件如何(稳态/周期涡街/瞬态涡),控制策略的基本物理机制一致——"通过后两圆柱旋转补偿 pinball 阻塞引起的速度亏损,前端圆柱调节升力"。这与 SR 分析的结论完全吻合。
|
||||
|
||||
### 5.4 Steady Cloak 定量化
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 全局波动抑制 | ≈0% |
|
||||
| 残留/阻塞比 | 81% |
|
||||
| 传感器区残留 | 13% |
|
||||
|
||||
**结论**:开环恒速旋转几乎无法抑制波动。需要闭环 DRL 控制。
|
||||
|
||||
### 5.5 Action-CCD Mode 1
|
||||
|
||||
Action-CCD 找出了控制器直接调制的主要结构。对 Cloak 场景,action-CCD mode 1 ≈ dq_ctl mean field,确认了"控制调制的结构 = correction-field 的主成分"的直觉。
|
||||
|
||||
---
|
||||
|
||||
## 关键可保留结论
|
||||
## 6. Correction-field 诊断图
|
||||
|
||||
1. **Correction-field 优于 raw-field 作为主分析对象**。1.0L 更集中(m80=1 vs 2)、0.75L 假高被扣(0.564 vs 0.673)、LOCO 未崩。
|
||||
### 6.1 图例说明
|
||||
|
||||
2. **1.0L 的 force-relevant correction 与 target 高度对齐** (O=0.913, m80=1)。这是目前最干净的结果。
|
||||
所有位于 `data/figures/` 下(无 colorbar,干净布局,裁剪到 x=300-1100):
|
||||
|
||||
3. **Force 和 signature 结构在零延迟时分离,在对流延迟后共享**。全域 O=0.41-0.55 升至 0.77-0.81。三区域 CC 显示 0.75L sensor_zone 在 tau=0 时近乎正交(O=0.01)。
|
||||
| 图 | 内容 |
|
||||
|----|------|
|
||||
| `corr_comparison_all_scenes.png` | 7 场景全景(4 cloak + 3 illusion),4 行 × 7 列 |
|
||||
| `corr_cloak_comparison_dqctl.png` | Cloak 四场景 dq_ctl 对比 |
|
||||
| `corr_illusion_comparison_dqctl.png` | Illusion 三直径 dq_ctl 对比 |
|
||||
| `corr_{scene}_ctl_vs_tar.png` | 单场景 dq_ctl vs dq_tar 对比(2×2) |
|
||||
| `steady_cloak_cancel_test.png` | Steady cloak 抵消检验 |
|
||||
|
||||
4. **1.5L 是特殊机制 case**,不是失败。O=0.667, action sigma1=0.28(1/4 of others), phase drift 显著, correction 集中在近体区。
|
||||
### 6.2 全景对比图的读法
|
||||
|
||||
5. **力匹配是统计量匹配,不是波形跟踪**。Cd 均值匹配好但瞬时相关 r~0。force_fx 不适合 waveform-level 断言。
|
||||
每行 = 一个物理量(ux_mean / uy_mean / RMS / vorticity)
|
||||
每列 = 一个场景
|
||||
颜色统一(同一行在所有列间共享 vmax)
|
||||
|
||||
**核心观察**:
|
||||
- cloak 四列间 dq_ctl 高度一致 → 控制策略不依赖上游条件
|
||||
- illusion_1.0L 与 cloak 定性一致 → 1.0L illusion=cloak 机制
|
||||
- illusion_0.75L 偏弱但定性相似 → 控制效率降低
|
||||
- illusion_1.5L 结构突变 → 完全不同的策略
|
||||
|
||||
---
|
||||
|
||||
## 数据状态
|
||||
## 7. 操作流程
|
||||
|
||||
所有 10 个场景均有 96 帧 phase plan。场景与对应 `scene_id`:
|
||||
### 7.1 环境
|
||||
|
||||
| 场景 | scene_id | fields_aligned? |
|
||||
|------|----------|----------------|
|
||||
| pinball | pinball | YES |
|
||||
| target_cylinder_{0.75,1.0,1.5}L | target_cylinder | YES |
|
||||
| illusion_{0.75,1.0,1.5}L | illusion | YES |
|
||||
| karman_re100 | karman | YES (72帧, 3cycles x 24pts, SI=800 采样密度低) |
|
||||
| karman_q_in | karman_target | YES (96帧) |
|
||||
| karman_q_blk | karman_blocked | YES (96帧) |
|
||||
| steady_cloak | steady_cloak | NO (旧 fields.npz, 500帧) |
|
||||
| target_channel | target_channel | NO (旧 fields.npz, 100帧) |
|
||||
```bash
|
||||
conda run -n pycuda_3_10
|
||||
# Python 3.10+, numpy, matplotlib, LegacyCelerisLab, stable-baselines3
|
||||
# CUDA device 2 (采集用)
|
||||
```
|
||||
|
||||
### 7.2 快速重新生成所有图
|
||||
|
||||
```bash
|
||||
# 1. Correction-field pipeline(CPU)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py
|
||||
|
||||
# 2. 对比图(CPU)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py
|
||||
```
|
||||
|
||||
### 7.3 从零开始完整流程
|
||||
|
||||
```bash
|
||||
# Step 1: GPU 数据采集(每次 > ~4min)
|
||||
# (已有数据则可跳过)
|
||||
|
||||
# Step 2: Phase alignment(CPU)
|
||||
python3 scripts/detect_period.py --scene {scene_name}
|
||||
|
||||
# Step 3: Field replay(GPU)
|
||||
conda run -n pycuda_3_10 python3 scripts/replay_fields.py --scene {scene_name} --device 2
|
||||
|
||||
# Step 4: Correction-field 分析(CPU)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/compute_correction_fields.py
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py
|
||||
|
||||
# Step 5: 对比图(CPU)
|
||||
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py
|
||||
```
|
||||
|
||||
### 7.4 添加新场景
|
||||
|
||||
1. 在 `configs.py` 中添加场景注册
|
||||
2. 在 `compute_correction_fields.py` 的 `_SCENE_MAP` 中添加映射
|
||||
3. 在 `_resolve_source()` 中添加加载器分发
|
||||
4. 编写 GPU 采集脚本(参考 `collect_vortex.py`)
|
||||
5. 在 `diagnose_corrections.py` 的 `SCENE_TYPES` 中添加场景
|
||||
6. 运行采集 → detect_period → replay_fields → diagnose_corrections
|
||||
|
||||
---
|
||||
|
||||
## 硬规则(新增和继承)
|
||||
## 8. 代码结构
|
||||
|
||||
- Illusion 只用 o14 模型(S_DIM=14)
|
||||
- `_2U` 表示 S_DIM=14,不是 2x velocity
|
||||
- 每个 illusion 直径必须匹配自己的 target cylinder
|
||||
- `action_bias` 与 FIFO warmup 的 `preset_action` 不能混淆 (bias=[0,-2,2], preset=[0,0,0,0,-1*U0,1*U0])
|
||||
- 默认 u0=0.01, nu=0.004
|
||||
- 主分析对象 = `dq_ctl = q_ctl - q_blk`,不是 `q_ctl`
|
||||
- 不要对 steady cloak 使用 phase-based CCD
|
||||
- Karman 的分析框架与 illusion 不同(distortion compensation vs target generation)
|
||||
```
|
||||
src/CCD_analysis/
|
||||
ccd_knowledge.md ← 本文档(唯一知识库)
|
||||
configs.py ← 场景元数据(统一几何)
|
||||
README.md ← 快速入口
|
||||
Lyu23.md ← CCD 方法文献
|
||||
ccd/ ← Round 5 冻结基线(勿改)
|
||||
utils/
|
||||
resampling.py ← POD, CCD, 场加载
|
||||
field_translate.py ← 场平移(备用,不参与默认 pipeline)
|
||||
load_vortex_fields.py ← 瞬态 vortex 场加载
|
||||
cfd_interface.py ← LegacyCelerisLab 封装 (GPU)
|
||||
scripts/
|
||||
detect_period.py ← 周期检测 → phase_plan.json
|
||||
replay_fields.py ← 场回放 → fields_aligned.npz
|
||||
collect_*.py ← GPU 数据采集
|
||||
correction_analysis/
|
||||
compute_correction_fields.py ← correction-field 计算
|
||||
diagnose_corrections.py ← 诊断图生成
|
||||
compare_dqctl_scenes.py ← 多场景对比图
|
||||
decompose_corrections.py ← CCD 定量分解(POD + force/action CCD)
|
||||
run_signature_line.py ← signature CCD
|
||||
run_15L_correction.py ← 1.5L 专项分析
|
||||
run_zone_ccd.py ← zone-restricted CCD
|
||||
run_steady_metrics.py ← steady cloak 定量度量
|
||||
visualize_action_ccd.py ← action-CCD mode1 可视化
|
||||
process_legacy_steady.py ← 旧格式加载
|
||||
data/
|
||||
{scene_id}/{scene_name}/ ← 各场景数据
|
||||
resampled/ ← phase_plan.json
|
||||
ccd/ ← JSON 结果文件
|
||||
figures/ ← PNG 诊断图
|
||||
old_data/ ← 归档(旧报告、旧脚本、旧 resampled 数据)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不能写成正式机制结论的话
|
||||
## 9. 硬规则
|
||||
|
||||
1. **Illusion 只用 o14 模型**(S_DIM=14,`_2U` 系列)。不使用 o12 模型。
|
||||
2. **`_2U` 表示 S_DIM=14**(2 个额外的目标力通道),不是 2× 速度。U0 始终是 0.01。
|
||||
3. **action_bias ≠ preset_action**。bias 是 DRL action scaling(illusions=[0,-2,2]),preset 是 FIFO warmup 用的动作数组。
|
||||
4. **默认物理参数**:u0=0.01, nu=0.004(所有模型一致)。
|
||||
5. **主分析对象 = dq_ctl**(不是 q_ctl)。
|
||||
6. **Steady cloak 不要使用 phase-based CCD**——它是稳态问题。
|
||||
7. **Karman 的物理问题不同**(incoming-street preservation vs target generation),不要硬套 illusion 模板。
|
||||
8. **所有场景已在采集时统一几何**(pinball 中心 613 px,sensor x=800 px);`field_translate.py` 仅作备用工具,不参与默认 pipeline。
|
||||
9. **旧数据格式**(`fields.npz`):(N, NX, NY);**新对齐格式**(`fields_aligned.npz`):(N, NX, NY);**加载后统一转 (N, NY, NX)**。
|
||||
|
||||
---
|
||||
|
||||
## 10. 不能写进论文的结论(内部参考,勿引用)
|
||||
|
||||
以下陈述看似合理但没有被充分证据支持,不得写进正式论文:
|
||||
- "Illusion 已证明使用完全不同于 target 的物理机制。"
|
||||
- "低 overlap 已足以证明 force 通道与 target 正交。"
|
||||
- "CCD 已经直接识别了壁面涡量生成机制。"
|
||||
- "far wake 模态就是瞬时力的主载体。"
|
||||
|
||||
---
|
||||
|
||||
## 11. 未完成工作(2026-06-28 更新)
|
||||
|
||||
| 方向 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| Karman cloak CCD 分析 | 数据已齐,分析延后 | 问题定义不同(distortion compensation) |
|
||||
| Vortex 数据采集 Bug 修复 | 已完成 | 修复 cylinder order swap(BOTTOM=id4, TOP=id5)+ FIFO warmup 导致涡量消失;重采集 Taylor/Lamb |
|
||||
| SR-CCD-OID 映射 | 草稿 | 归档 `data/old_data/sr_ccd_oid_mapping.md`,需最终校正 |
|
||||
| 统一几何后 CCD 重跑 | 已完成 | `correction_ccd_results.json` 已更新(含 1.5L) |
|
||||
| Vortex 对比图更新 | 已完成 | `compare_dqctl_scenes.py` 已用修正数据重跑 |
|
||||
| 项目目录清理 + 文档更新 | 已完成 | 移除废弃目录、SI=200 错误数据、旧诊断图 |
|
||||
|
||||
---
|
||||
|
||||
## 12. Vortex 采集 Bug 排查经验(2026-06-29)
|
||||
|
||||
`collect_vortex.py` 中发现了四个独立 bug,按发现顺序:
|
||||
|
||||
**Bug 1 — 圆柱顺序对调**
|
||||
- 训练 env `legacy_env_vortex.py` 添加顺序:front(id3) → TOP(+y, id4) → BOTTOM(-y, id5)
|
||||
- action 映射:`temp[3]=front, temp[4]=TOP(bias=-4), temp[5]=BOTTOM(bias=+4)`
|
||||
- 脚本先后把 TOP/BOTTOM 添加顺序写反 → 后两圆柱旋转方向全错
|
||||
- 症状:Lamb front 剧烈振荡 (std=0.28),dipole 对称性完全破坏
|
||||
|
||||
**Bug 2 — 涡量消失**
|
||||
- FIFO warmup 在涡加入后跑 150×800=120000 步 → 涡从 x=15 漂出域外 (1280 lu)
|
||||
- 症状:涡量场只有 pinball 尾流,完全看不到 vortex 结构
|
||||
|
||||
**Bug 3(核心)— 观测值归一化顺序错误**
|
||||
- 训练 env 产出:`obs = [forces/force_norm, sensors/sens_norm]`(force 先)
|
||||
- 脚本将 channel 和 norm 互换:`[sensors/force_norm, forces/sens_norm]`
|
||||
- 模型收到不匹配分布的反馈
|
||||
- 症状:Lamb cross-corr 仅 0.73, Taylor 仅 0.50;后圆柱同向转而非反向
|
||||
|
||||
**Bug 4 — fade-in/out 缺失**
|
||||
- `uni_test.ipynb` 有 25 步渐入 + 25 步渐出到 steady-cloak bias
|
||||
- 脚本直接给全量 PPO action,无过渡
|
||||
|
||||
**最终修正方案**:
|
||||
1. Cylinder order 匹配训练 env
|
||||
2. Bias 使用 uni_test 值:[-5, +5](FIFO),[-5.1, +5.1](fade target)
|
||||
3. Obs 归一化:`forces_norm = obs[6:12]/force_norm`,`sens_norm = (obs[0:6]-sens_dev)/sens_norm`,`hstack([forces_norm, sens_norm])`
|
||||
4. 25-step fade-in / 25-step fade-out
|
||||
|
||||
**修正后验证**:
|
||||
|
||||
| 场景 | sim | cross-corr | active front mean | dq_ctl RMS |
|
||||
|------|:---:|:----------:|:-----------------:|:----------:|
|
||||
| vortex_lamb | 0.946 | 0.974 | 0.007 | 0.146 |
|
||||
| vortex_taylor | 0.923 | 0.953 | -0.030 | 0.188 |
|
||||
|
||||
**其他脚本审计**:`collect_karman.py` 和 `collect_illusion.py` 使用 `build_observation()` 函数,该函数正确实现了 force-first 归一化,无需修改。`collect_vortex.py` 是唯一手动构建 obs 的脚本。
|
||||
|
||||
**代码注释**:`collect_vortex.py` 的文件头 docstring 和关键行号均有 BUG HISTORY 和 BUG-FIX 标记。
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
# CCD 分析方法与工作规划 — 最终版本
|
||||
|
||||
## 工作阶段总览
|
||||
|
||||
### Round 5 (raw-field baseline) — 完成
|
||||
|
||||
见 `ccd_knowledge.md` 中的 Round 5 章节。所有 `ccd/` 目录下脚本已冻结不修改。
|
||||
|
||||
### Round 6 (correction-field framework) — 全部完成
|
||||
|
||||
**产出列表**:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `correction_analysis/process_legacy_steady.py` | 加载 steady_cloak/target_channel 旧格式 |
|
||||
| `correction_analysis/compute_correction_fields.py` | q_in/q_blk/q_ctl/q_tar + dq_* 差分场计算 |
|
||||
| `correction_analysis/diagnose_corrections.py` | 29张差分场图 + 三层区域 KE/enstrophy 指标 |
|
||||
| `correction_analysis/decompose_corrections.py` | dq_ctl 上 force/action CCD (0.75L, 1.0L) |
|
||||
| `correction_analysis/run_signature_line.py` | signature line CCD (0.75L, 1.0L, tau扫描) |
|
||||
| `correction_analysis/run_15L_correction.py` | 1.5L force/action/signature CCD |
|
||||
| `correction_analysis/run_steady_metrics.py` | steady cloak 定量化 |
|
||||
| `correction_analysis/run_zone_ccd.py` | 三区域分层 CCD (0.75L, 1.0L) |
|
||||
| `docs/ccd_correction_field_report.md` | 综合报告 (412行, 含10图解读) |
|
||||
| `docs/sr_ccd_oid_mapping.md` | 三线映射草稿 |
|
||||
|
||||
**数据采集 (GPU)**:
|
||||
- karman_q_in (涡街无pinball) 和 karman_q_blk (涡街+pinball无控制) — 各 96帧 aligned 场
|
||||
- karman_re100 — 72帧 (3 cycles, 每周期18点, rho=1.33)
|
||||
|
||||
---
|
||||
|
||||
## 已完成 vs 未完成
|
||||
|
||||
### 已完成
|
||||
|
||||
- Illusion 三直径 correction-field force/action/signature CCD
|
||||
- O(dqctl,dqtar) 跨直径对比
|
||||
- O(force,sig) 模态重叠 (全域 + 三区域)
|
||||
- LOCO 验证 (除 1.5L)
|
||||
- 1.5L special mechanism 分析
|
||||
- Zone-restricted CCD (near_body, body_wake, sensor_zone)
|
||||
- Steady cloak 定量化
|
||||
- Snapshot POD 加速 (`utils/resampling.py: compute_pod`)
|
||||
- Karman 参考场采集
|
||||
|
||||
### 未完成/延后
|
||||
|
||||
- Karman cloak 周期 correction analysis (数据已齐,框架已设计,分析延后)
|
||||
- 1.5L force vs signature overlap (0.75L/1.0L 已完成,1.5L 有 signature m80 但缺 O 值)
|
||||
- Steady cloak 需要 closed-loop DRL 数据才能做有意义分析
|
||||
- OID/PCD/whitening 等更高级版本
|
||||
|
||||
---
|
||||
|
||||
## 当前建议的继续方向
|
||||
|
||||
1. **Karman cloak 分析** — 已有 `q_in/q_blk/q_ctl` 三场和 `correction_analysis/compute_correction_fields.py` 支持,可平移 illusion 的 force/action/signature 框架。注意 Karman 的物理问题不同 (incoming-street preservation vs target generation)。
|
||||
|
||||
2. **1.5L force-sig overlap** — 补齐后可与 0.75L/1.0L 比较。已在 `correction_analysis/run_15L_correction.py` 中有 signature line 框架,补 O 值即可。
|
||||
|
||||
3. **SR-CCD-OID 映射校正** — `docs/sr_ccd_oid_mapping.md` 需要根据 SR 和 OID 两方向的实际现状校正。
|
||||
|
||||
---
|
||||
|
||||
## 成功标准回顾
|
||||
|
||||
这套分析框架若要算成功,至少应支持以下判断:
|
||||
|
||||
- [x] 控制首先调制的是 pinball 已存在基线上的 **correction field**,而不是直接"生成目标全流场"
|
||||
- [x] force-relevant structures 与 signature-relevant structures 可以不同(证据: O(force,sig) tau=0 = 0.01-0.55)
|
||||
- [ ] force 与 signature 若由不同结构族主导 → 机制分层(部分证据: zone-CCD 显示 0.75L sensor_zone O=0.01,但 tau_c 后共享)
|
||||
- [x] 1.5L 显示不同于 0.75L/1.0L 的修正策略(证据: action sigma1=0.28, O=0.667, phase drift)
|
||||
@ -165,9 +165,9 @@ for key, mn, diam, si in _ILLUSION_2U:
|
||||
"model_subdir": "250525",
|
||||
"n_objects_env": 6,
|
||||
"obs_slice": (0, 12),
|
||||
"sensor_x": 30.0,
|
||||
"pinball_front_x": 19.0,
|
||||
"pinball_rear_x": 20.3,
|
||||
"sensor_x": 40.0, # UNIFIED: was 30.0
|
||||
"pinball_front_x": 30.0, # UNIFIED: was 19.0
|
||||
"pinball_rear_x": 31.3, # UNIFIED: was 20.3
|
||||
"target_type": "periodic",
|
||||
"s_dim": 14, # CRITICAL: all 2U are 14-dim
|
||||
"u0": U0, # 0.01 (NOT 0.02)
|
||||
@ -190,8 +190,8 @@ for diam, si in [(0.75, 400), (1.0, 600), (1.5, 800)]:
|
||||
"model_name": None,
|
||||
"n_objects_env": 4, # 1 cylinder + 3 sensors
|
||||
"obs_slice": (0, 8), # cylinder force(2) + sensor(6)
|
||||
"sensor_x": 30.0,
|
||||
"cylinder_x": 20.0,
|
||||
"sensor_x": 40.0, # UNIFIED: was 30.0
|
||||
"cylinder_x": 30.65, # UNIFIED: was 20.0 (pinball center)
|
||||
"target_type": "periodic",
|
||||
"s_dim": None,
|
||||
"u0": U0, # 0.01, matching illusion
|
||||
@ -217,6 +217,47 @@ SCENES["target_channel"] = {
|
||||
"nu": nu_from_re(100),
|
||||
}
|
||||
|
||||
# -- Vortex cloak scenes (transient, Taylor monopole & Lamb dipole) ---------
|
||||
# Taylor: monopole vortex, strength=0.03*U0, r=2*L0=40
|
||||
# Lamb: dipole vortex, strength=0.5*U0, r=2*L0=40
|
||||
# Both: MAX_STEPS=150, action_scale=4, action_bias=(0,-4,4), s_dim=12
|
||||
# Geometry: pinball at (30, 31.3)xL0, sensors at 40*xL0
|
||||
# Vortex at 10*xL0 (target) or 15*xL0 (pinball phase)
|
||||
_VORTEX_SCENES = [
|
||||
("vortex_lamb", "vortex_lamb", "lamb", 0.50),
|
||||
("vortex_taylor", "vortex_taylor", "taylor", 0.03),
|
||||
("vortex_uncontrolled_lamb", None, "lamb", 0.50),
|
||||
("vortex_uncontrolled_taylor", None, "taylor", 0.03),
|
||||
("vortex_target_lamb", None, "lamb", 0.50),
|
||||
("vortex_target_taylor", None, "taylor", 0.03),
|
||||
]
|
||||
for key, mn, vtype, vstrength in _VORTEX_SCENES:
|
||||
is_controlled = mn is not None
|
||||
SCENES[key] = {
|
||||
"scene_id": key,
|
||||
"re_code": 100,
|
||||
"has_disturbance": False,
|
||||
"sample_interval": 800,
|
||||
"vortex_type": vtype,
|
||||
"vortex_strength": vstrength,
|
||||
"conv_len": 30,
|
||||
"action_scale": 4.0 if is_controlled else None,
|
||||
"action_bias": (0.0, -4.0, 4.0) if is_controlled else None,
|
||||
"source": "PPO_inference" if is_controlled else "open_loop",
|
||||
"model_name": mn,
|
||||
"model_subdir": "old",
|
||||
"n_objects_env": 6,
|
||||
"obs_slice": (0, 12),
|
||||
"sensor_x": 40.0,
|
||||
"pinball_front_x": 30.0,
|
||||
"pinball_rear_x": 31.3,
|
||||
"target_type": "transient",
|
||||
"max_steps": 150,
|
||||
"s_dim": 12 if is_controlled else None,
|
||||
"u0": U0,
|
||||
"nu": nu_from_re(100),
|
||||
}
|
||||
|
||||
|
||||
# -- Utility helpers ---------------------------------------------------------
|
||||
|
||||
|
||||
188
src/CCD_analysis/correction_analysis/compare_dqctl_scenes.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Generate comparison figures across all cloak & illusion scenarios.
|
||||
|
||||
All dq_ctl fields use unified geometry (pinball center at ~613px, sensors at ~800px),
|
||||
set during GPU collection (configs.py UNIFIED coordinates).
|
||||
Figures zoom into the region around the pinball/cylinder (x=300-1100) to exclude
|
||||
boundary artifacts.
|
||||
|
||||
1. All-scenes panorama: steady_cloak, karman_re100, vortex_lamb, vortex_taylor,
|
||||
illusion_0.75L, illusion_1.0L, illusion_1.5L
|
||||
2. Illusion-only comparison: 0.75L, 1.0L, 1.5L
|
||||
|
||||
Usage:
|
||||
conda run -n pycuda_3_10 python correction_analysis/compare_dqctl_scenes.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR, NX, NY
|
||||
from CCD_analysis.correction_analysis.compute_correction_fields import (
|
||||
compute_correction,
|
||||
)
|
||||
|
||||
FIG_DIR = os.path.join(DATA_DIR, "figures")
|
||||
os.makedirs(FIG_DIR, exist_ok=True)
|
||||
|
||||
# Display crop region (pixels) — around pinball at x~613
|
||||
CROP_X0, CROP_X1 = 300, 1100
|
||||
|
||||
# Scene groups
|
||||
CLOAK_SCENES = ["steady_cloak", "karman_re100", "vortex_lamb", "vortex_taylor"]
|
||||
ILLUSION_SCENES = ["illusion_0.75L", "illusion_1.0L", "illusion_1.5L"]
|
||||
ALL_SCENES = CLOAK_SCENES + ILLUSION_SCENES
|
||||
|
||||
SCENE_LABELS = {
|
||||
"steady_cloak": "Steady Cloak",
|
||||
"karman_re100": "Karman Cloak",
|
||||
"vortex_lamb": "Vortex Lamb",
|
||||
"vortex_taylor": "Vortex Taylor",
|
||||
"illusion_0.75L": "Illusion 0.75L",
|
||||
"illusion_1.0L": "Illusion 1.0L",
|
||||
"illusion_1.5L": "Illusion 1.5L",
|
||||
}
|
||||
|
||||
FIELD_METRICS = [
|
||||
("ux_mean", r"mean $u_x$", "RdBu_r", True),
|
||||
("uy_mean", r"mean $u_y$", "RdBu_r", True),
|
||||
("rms", "RMS", "viridis", False),
|
||||
("vorticity", r"$\omega_z$", "RdBu_r", True),
|
||||
]
|
||||
|
||||
|
||||
def compute_metrics(st: str) -> dict | None:
|
||||
"""Load dq_ctl for a scene and compute metrics."""
|
||||
try:
|
||||
corr = compute_correction(st)
|
||||
dq = corr.get("dq_ctl")
|
||||
if dq is None:
|
||||
return None
|
||||
ux, uy = dq["ux"], dq["uy"]
|
||||
return {
|
||||
"ux_mean": np.mean(ux, axis=0),
|
||||
"uy_mean": np.mean(uy, axis=0),
|
||||
"rms": np.sqrt(np.std(ux, axis=0)**2 + np.std(uy, axis=0)**2),
|
||||
"vorticity": np.gradient(np.mean(uy, axis=0), axis=1)
|
||||
- np.gradient(np.mean(ux, axis=0), axis=0),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f" SKIP {st}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def crop_field(f: np.ndarray) -> np.ndarray:
|
||||
"""Crop to display region (NY, NX_cropped)."""
|
||||
return f[:, CROP_X0:CROP_X1]
|
||||
|
||||
|
||||
def plot_comparison(scene_list: str | list, name: str):
|
||||
"""Generate a grid of dq_ctl metrics for selected scenes."""
|
||||
if isinstance(scene_list, str):
|
||||
scene_list = [scene_list]
|
||||
|
||||
# Load all fields
|
||||
fields = {}
|
||||
for st in scene_list:
|
||||
print(f" Loading {st}...", flush=True)
|
||||
m = compute_metrics(st)
|
||||
if m is not None:
|
||||
fields[st] = m
|
||||
|
||||
n_scenes = len(fields)
|
||||
if n_scenes == 0:
|
||||
print(" No valid fields, skipping")
|
||||
return
|
||||
|
||||
scene_names = list(fields.keys())
|
||||
n_rows = len(FIELD_METRICS)
|
||||
|
||||
# Compute global vmax per metric from CROPPED fields
|
||||
metric_vmax = {}
|
||||
for mkey, _, _, _ in FIELD_METRICS:
|
||||
all_vals = np.concatenate(
|
||||
[abs(crop_field(fields[s][mkey])).ravel() for s in fields])
|
||||
vmax = float(np.percentile(all_vals[np.isfinite(all_vals)], 99.5))
|
||||
metric_vmax[mkey] = max(vmax, 1e-12)
|
||||
|
||||
fig, axes = plt.subplots(n_rows, n_scenes,
|
||||
figsize=(3.5 * n_scenes, 3.0 * n_rows))
|
||||
if n_rows == 1:
|
||||
axes = [axes]
|
||||
if n_scenes == 1:
|
||||
axes = [[a] for a in axes]
|
||||
|
||||
nx_crop = CROP_X1 - CROP_X0
|
||||
extent = (CROP_X0, CROP_X1, 0, NY - 1)
|
||||
|
||||
for row, (mkey, mlabel, cmap, symmetric) in enumerate(FIELD_METRICS):
|
||||
for col, sn in enumerate(scene_names):
|
||||
ax = axes[row][col]
|
||||
f = crop_field(fields[sn][mkey])
|
||||
vmax = metric_vmax[mkey]
|
||||
|
||||
kwargs = {"cmap": cmap, "origin": "lower",
|
||||
"aspect": "equal", "extent": extent}
|
||||
if symmetric:
|
||||
kwargs["vmin"] = -vmax
|
||||
kwargs["vmax"] = vmax
|
||||
else:
|
||||
kwargs["vmin"] = 0
|
||||
kwargs["vmax"] = vmax
|
||||
|
||||
ax.imshow(f, **kwargs)
|
||||
ax.tick_params(left=False, right=False, labelleft=False,
|
||||
bottom=False, top=False, labelbottom=False)
|
||||
|
||||
if row == 0:
|
||||
ax.set_title(SCENE_LABELS.get(sn, sn), fontsize=10)
|
||||
if col == 0:
|
||||
ax.set_ylabel(mlabel, fontsize=10)
|
||||
|
||||
plt.suptitle(f"dq_ctl: [{', '.join(SCENE_LABELS.get(s,s) for s in scene_names)}]",
|
||||
fontsize=12, y=1.01)
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, name)
|
||||
fig.savefig(path, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
# Stats
|
||||
print(f"\n --- RMS (cropped region) ---")
|
||||
for sn in scene_names:
|
||||
rms_crop = crop_field(fields[sn]["rms"])
|
||||
rms_val = float(np.sqrt(np.mean(rms_crop**2)))
|
||||
print(f" {sn:22s}: RMS={rms_val:.6f}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Comparison: dq_ctl across all cloak & illusion scenes")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. All 7 scenes panorama
|
||||
print("\n--- All 7 scenes panorama ---")
|
||||
plot_comparison(ALL_SCENES, "corr_comparison_all_scenes.png")
|
||||
|
||||
# 2. Illusion-only (3 diameters)
|
||||
print("\n--- Illusion-only comparison ---")
|
||||
plot_comparison(ILLUSION_SCENES, "corr_illusion_comparison_dqctl.png")
|
||||
|
||||
# 3. Cloak-only (4 scenes) for reference
|
||||
print("\n--- Cloak-only comparison ---")
|
||||
plot_comparison(CLOAK_SCENES, "corr_cloak_comparison_dqctl.png")
|
||||
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -8,9 +8,10 @@ Each scene type maps to different data sources:
|
||||
| illusion_1.0L | target_channel* | pinball | illusion_1.0L | target_cylinder_1.0L |
|
||||
| illusion_1.5L | target_channel* | pinball | illusion_1.5L | target_cylinder_1.5L |
|
||||
| steady_cloak | target_channel* | pinball | steady_cloak* | None (target=q_in) |
|
||||
| karman_re100 | karman_q_in (TBD) | TBD | karman_re100 | karman_q_in (TBD) |
|
||||
| karman_re100 | karman_q_in | karman_q_blk | karman_re100 | karman_q_in |
|
||||
|
||||
(*) loaded via load_legacy_steady()
|
||||
(*) loaded via load_legacy_steady(). Vortex scenes use load_vortex_fields().
|
||||
All scenes now use UNIFIED geometry (pinball center at 613px, sensors at 800px).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@ -21,65 +22,118 @@ from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CCD_analysis.configs import NX, NY
|
||||
from CCD_analysis.configs import DATA_DIR, NX, NY
|
||||
from CCD_analysis.utils.resampling import (
|
||||
load_aligned_fields,
|
||||
build_field_matrix as _build_field_matrix,
|
||||
)
|
||||
|
||||
from CCD_analysis.correction_analysis.process_legacy_steady import load_legacy_steady
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Placeholder for future karman reference fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_KARMAN_PLACEHOLDER = None
|
||||
"""Temporary placeholder for karman_q_in / karman_q_blk data.
|
||||
|
||||
Will be replaced with proper data loading once collected.
|
||||
"""
|
||||
|
||||
|
||||
def _load_karman_placeholder(scene_label: str) -> Optional[dict]:
|
||||
"""Return None placeholder for karman reference fields."""
|
||||
print(f" [PLACEHOLDER] karman reference '{scene_label}' -- returning None")
|
||||
return None
|
||||
from CCD_analysis.utils.load_vortex_fields import load_vortex_fields
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source mapping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_source(name: str) -> dict:
|
||||
def _resolve_source(name: str) -> Optional[dict]:
|
||||
"""Load a named data source, dispatching to the correct loader.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Scene or special name:
|
||||
- 'target_channel' -> load_legacy_steady
|
||||
- 'steady_cloak' -> load_legacy_steady
|
||||
- 'pinball' -> load_aligned_fields
|
||||
- 'illusion_*' -> load_aligned_fields
|
||||
- 'target_cylinder_*'-> load_aligned_fields
|
||||
- 'karman_re100' -> load_aligned_fields
|
||||
- 'karman_q_in' -> _load_karman_placeholder
|
||||
- 'karman_q_blk' -> _load_karman_placeholder
|
||||
- 'target_channel', 'steady_cloak' -> load_legacy_steady
|
||||
- 'vortex_*' -> load_vortex_fields (transient)
|
||||
- 'karman_re100' -> _load_karman_re100 (handles 72 vs 96 mismatch)
|
||||
- all others -> load_aligned_fields
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict or None
|
||||
"""
|
||||
_LEGACY_SCENES = {"target_channel", "steady_cloak"}
|
||||
_VORTEX_SCENES = {
|
||||
"vortex_lamb", "vortex_taylor",
|
||||
"vortex_uncontrolled_lamb", "vortex_uncontrolled_taylor",
|
||||
"vortex_target_lamb", "vortex_target_taylor",
|
||||
}
|
||||
|
||||
if name in _LEGACY_SCENES:
|
||||
return load_legacy_steady(name)
|
||||
elif name in _VORTEX_SCENES:
|
||||
return load_vortex_fields(name)
|
||||
elif name == "karman_re100":
|
||||
return _load_karman_re100()
|
||||
else:
|
||||
# karman_q_in, karman_q_blk, and all others use aligned loader
|
||||
return load_aligned_fields(name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Karman re100 special loader (handles 72 vs 96 frame mismatch)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_karman_re100() -> dict:
|
||||
"""Load karman_re100 aligned fields, handling the 72 vs 96 frame mismatch.
|
||||
|
||||
karman_re100's fields_aligned.npz has 72 snapshots (3 cycles x 24 pts)
|
||||
but the phase_plan.json lists 96 step_indices (4 cycles x 24 pts).
|
||||
This loader truncates step_indices to match.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
scene_name = "karman_re100"
|
||||
from CCD_analysis.configs import SCENES as _SCENES
|
||||
|
||||
cfg = _SCENES[scene_name]
|
||||
scene_id = cfg["scene_id"]
|
||||
data_dir = os.path.join(DATA_DIR, scene_id, scene_name)
|
||||
|
||||
# Load fields_aligned.npz
|
||||
fa_path = os.path.join(data_dir, "fields_aligned.npz")
|
||||
fd = np.load(fa_path)
|
||||
ux_raw = fd["ux"]
|
||||
uy_raw = fd["uy"]
|
||||
N = ux_raw.shape[0] # 72
|
||||
fd.close()
|
||||
|
||||
# Transpose (N, NX, NY) -> (N, NY, NX)
|
||||
ux = np.ascontiguousarray(ux_raw.transpose(0, 2, 1))
|
||||
uy = np.ascontiguousarray(uy_raw.transpose(0, 2, 1))
|
||||
|
||||
# Load phase_plan, truncate to N
|
||||
plan_path = os.path.join(DATA_DIR, "resampled", scene_name, "phase_plan.json")
|
||||
with open(plan_path) as f:
|
||||
plan = _json.load(f)
|
||||
step_indices = list(plan["step_indices"][:N])
|
||||
|
||||
# Load telemetry
|
||||
tele_path = os.path.join(data_dir, "controlled.npz")
|
||||
td = np.load(tele_path)
|
||||
|
||||
result = {
|
||||
"ux": ux,
|
||||
"uy": uy,
|
||||
"forces": td["forces"][step_indices] if "forces" in td else None,
|
||||
"actions": td["actions"][step_indices] if "actions" in td else None,
|
||||
"sensors": td["sensors"][step_indices] if "sensors" in td else None,
|
||||
"meta": {
|
||||
"scene": scene_name,
|
||||
"scene_id": scene_id,
|
||||
"gate": plan.get("gate", "unknown"),
|
||||
"CV_T": plan.get("CV_T"),
|
||||
"f_dom": plan.get("f_dom"),
|
||||
"N_raw_per_cycle": plan.get("N_raw_per_cycle"),
|
||||
"rho_interp": plan.get("rho_interp"),
|
||||
"sample_interval": cfg.get("sample_interval"),
|
||||
"note": "step_indices truncated from 96 to 72 (3 cycles, not 4)",
|
||||
},
|
||||
"step_indices": step_indices,
|
||||
}
|
||||
td.close()
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Correction computation
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -91,6 +145,8 @@ _SCENE_MAP = {
|
||||
"illusion_1.5L": ("target_channel", "pinball", "illusion_1.5L", "target_cylinder_1.5L"),
|
||||
"steady_cloak": ("target_channel", "pinball", "steady_cloak", None),
|
||||
"karman_re100": ("karman_q_in", "karman_q_blk", "karman_re100", "karman_q_in"),
|
||||
"vortex_lamb": ("target_channel", "vortex_uncontrolled_lamb", "vortex_lamb", "vortex_target_lamb"),
|
||||
"vortex_taylor": ("target_channel", "vortex_uncontrolled_taylor", "vortex_taylor", "vortex_target_taylor"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -30,7 +30,9 @@ from CCD_analysis.correction_analysis.compute_correction_fields import (
|
||||
|
||||
R_CANDIDATES = [6, 8, 10]
|
||||
CCD_Q = 6
|
||||
SCENE_TYPES = ["illusion_0.75L", "illusion_1.0L", "steady_cloak"]
|
||||
SCENE_TYPES = ["illusion_0.75L", "illusion_1.0L", "illusion_1.5L", "steady_cloak"]
|
||||
DIAMETERS_MAIN = [0.75, 1.0]
|
||||
DIAMETER_SPECIAL = 1.5 # flagged as special_mechanism (high-freq modulation)
|
||||
|
||||
|
||||
def compute_modal_overlap(W_dict, scene_label, r, obs_label="force_fy"):
|
||||
@ -101,7 +103,9 @@ def main():
|
||||
continue
|
||||
|
||||
diam = corr.get("diam")
|
||||
print(f"\n--- {st} (diam={diam}) ---", flush=True)
|
||||
is_special = (diam is not None and diam >= DIAMETER_SPECIAL)
|
||||
flag = " [SPECIAL MECHANISM — high-freq modulation]" if is_special else ""
|
||||
print(f"\n--- {st} (diam={diam}){flag} ---", flush=True)
|
||||
|
||||
Q_ctl = dict_to_field_matrix(dq_ctl)
|
||||
N = Q_ctl.shape[1]
|
||||
@ -146,6 +150,7 @@ def main():
|
||||
"scene": st, "diam": diam, "obs": flabel, "r": r,
|
||||
"m80": m80, "N": sig.size,
|
||||
"sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))],
|
||||
"special_mechanism": is_special,
|
||||
}
|
||||
if fmode == "fy":
|
||||
print(f" {key}: m80={m80} s1={float(sig[0]):.4f}", flush=True)
|
||||
@ -163,6 +168,7 @@ def main():
|
||||
"scene": st, "diam": diam, "obs": "action", "r": r,
|
||||
"m80": m80, "N": sig.size,
|
||||
"sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))],
|
||||
"special_mechanism": is_special,
|
||||
}
|
||||
print(f" {key}: m80={m80} s1={float(sig[0]):.4f}", flush=True)
|
||||
|
||||
@ -181,6 +187,7 @@ def main():
|
||||
"m80": int(np.searchsorted(cumulative_energy(sig_t), 0.80)+1) if len(sig_t) > 0 else 0,
|
||||
"N": sig_t.size,
|
||||
"sigma_top3": [float(sig_t[i]) for i in range(min(3,len(sig_t)))],
|
||||
"special_mechanism": is_special,
|
||||
}
|
||||
# Overlap: dq_ctl vs dq_tar
|
||||
ck = f"{st}_dqctl_force_fy_r{r}"
|
||||
|
||||
@ -34,57 +34,40 @@ from CCD_analysis.correction_analysis.compute_correction_fields import (
|
||||
FIG_DIR = os.path.join(DATA_DIR, "figures")
|
||||
os.makedirs(FIG_DIR, exist_ok=True)
|
||||
|
||||
# Scene types to process (karman will be added when data is ready)
|
||||
# Scene types to process
|
||||
SCENE_TYPES = [
|
||||
"illusion_0.75L",
|
||||
"illusion_1.0L",
|
||||
"illusion_1.5L",
|
||||
"steady_cloak",
|
||||
"karman_re100",
|
||||
"vortex_lamb",
|
||||
"vortex_taylor",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Three-zone masks
|
||||
# Three-zone masks (unified geometry: pinball center at 613 px, sensors at 800 px)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def define_zones_illusion() -> dict:
|
||||
"""Define three-zone masks for illusion layout (sensors at x=30*L0)."""
|
||||
def define_zones() -> dict:
|
||||
"""Define three-zone masks for all scenes (unified geometry, 2026-06-28).
|
||||
|
||||
All scenes now use the same pinball/sensor positions after unified collection.
|
||||
Zone ranges:
|
||||
near_body: 580-720 px (around pinball at x≈613)
|
||||
body_wake: 720-850 px (near wake downstream)
|
||||
sensor_zone: 780-850 px (around sensors at x=800)
|
||||
"""
|
||||
zones = {}
|
||||
# Zone 1: near-body — envelope around pinball cylinders
|
||||
# pinball front at x=380 (19*L0), rear at x=406 (20.3*L0)
|
||||
# Extend to x=[350, 500], full height
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 350:500] = True
|
||||
zones["near_body"] = mask
|
||||
|
||||
# Zone 2: body-connected near wake — immediate downstream
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 500:700] = True
|
||||
zones["body_wake"] = mask
|
||||
|
||||
# Zone 3: downstream sensor zone — around sensors at x=600
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 580:650] = True
|
||||
zones["sensor_zone"] = mask
|
||||
|
||||
return zones
|
||||
|
||||
|
||||
def define_zones_karman() -> dict:
|
||||
"""Define three-zone masks for Karman layout (sensors at x=40*L0=800)."""
|
||||
zones = {}
|
||||
# Zone 1: near-body — around pinball at x=600
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 580:720] = True
|
||||
zones["near_body"] = mask
|
||||
|
||||
# Zone 2: body-connected near wake
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 720:850] = True
|
||||
zones["body_wake"] = mask
|
||||
|
||||
# Zone 3: downstream sensor zone — around sensors at x=800
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 780:850] = True
|
||||
zones["sensor_zone"] = mask
|
||||
@ -206,23 +189,20 @@ def plot_mean_rms(dq: dict, label: str, prefix: str, zones: Optional[dict] = Non
|
||||
|
||||
# Mean ux
|
||||
vmax = max(abs(ux_m).max(), 1e-12)
|
||||
im0 = axes[0].imshow(ux_m, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[0].imshow(ux_m, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[0].set_title(f"{label}: mean ux")
|
||||
plt.colorbar(im0, ax=axes[0], fraction=0.046)
|
||||
|
||||
# Mean uy
|
||||
vmax = max(abs(uy_m).max(), 1e-12)
|
||||
im1 = axes[1].imshow(uy_m, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[1].imshow(uy_m, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[1].set_title(f"{label}: mean uy")
|
||||
plt.colorbar(im1, ax=axes[1], fraction=0.046)
|
||||
|
||||
# RMS magnitude
|
||||
im2 = axes[2].imshow(rms, cmap="viridis", origin="lower",
|
||||
axes[2].imshow(rms, cmap="viridis", origin="lower",
|
||||
aspect="equal", extent=extent)
|
||||
axes[2].set_title(f"{label}: RMS magnitude")
|
||||
plt.colorbar(im2, ax=axes[2], fraction=0.046)
|
||||
|
||||
# Overlay zone boundaries if provided
|
||||
if zones is not None:
|
||||
@ -253,11 +233,10 @@ def plot_vorticity(dq: dict, label: str, prefix: str):
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 4))
|
||||
vmax = max(np.percentile(abs(vor), 99), 1e-12)
|
||||
im = ax.imshow(vor, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
ax.imshow(vor, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal",
|
||||
extent=(0, NX - 1, 0, NY - 1))
|
||||
ax.set_title(f"{label}: mean vorticity")
|
||||
plt.colorbar(im, ax=ax, fraction=0.046, label=r"$\omega_z$")
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, f"{prefix}_vorticity_{label.replace(' ', '_')}.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
@ -274,8 +253,7 @@ def run():
|
||||
print("Phase 2: Baseline Diagnostics (correction fields)", flush=True)
|
||||
print("=" * 60, flush=True)
|
||||
|
||||
zones_ill = define_zones_illusion()
|
||||
zones_karman = define_zones_karman()
|
||||
zones = define_zones()
|
||||
|
||||
all_metrics = {}
|
||||
|
||||
@ -286,7 +264,7 @@ def run():
|
||||
|
||||
try:
|
||||
corr = compute_correction(scene_type)
|
||||
except (FileNotFoundError, KeyError) as e:
|
||||
except (FileNotFoundError, KeyError, AssertionError, ValueError) as e:
|
||||
print(f" SKIP: {e}", flush=True)
|
||||
continue
|
||||
|
||||
@ -294,10 +272,7 @@ def run():
|
||||
print(f" SKIP: no valid data (N=0)", flush=True)
|
||||
continue
|
||||
|
||||
# Determine which zones to use
|
||||
is_illusion = "illusion" in scene_type
|
||||
zones = zones_ill if is_illusion else zones_ill # zones_karman for later
|
||||
|
||||
# Unified geometry — same zones for all scenes
|
||||
for dq_key, dq_label in [
|
||||
("dq_blk", "dq_blk (pinball blockage)"),
|
||||
("dq_ctl", "dq_ctl (control correction)"),
|
||||
@ -314,8 +289,8 @@ def run():
|
||||
metrics = zone_metrics(dq, zones, dq_label)
|
||||
all_metrics[f"{scene_type}_{dq_key}"] = metrics
|
||||
|
||||
# For illusion, also plot dq_tar if available
|
||||
if dq_key == "dq_ctl" and is_illusion:
|
||||
# For scenes with a target, also plot dq_tar if available
|
||||
if dq_key == "dq_ctl" and corr.get("dq_tar") is not None:
|
||||
dq_tar = corr.get("dq_tar")
|
||||
if dq_tar is not None:
|
||||
plot_mean_rms(dq_tar, "dq_tar (target correction)", prefix, zones)
|
||||
@ -330,30 +305,26 @@ def run():
|
||||
ux_tar, _ = mean_field(dq_tar["ux"], dq_tar["uy"])
|
||||
vmax = max(abs(ux_ctl).max(), abs(ux_tar).max(), 1e-12)
|
||||
|
||||
im = axes[0, 0].imshow(ux_ctl, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[0, 0].imshow(ux_ctl, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[0, 0].set_title("dq_ctl mean ux")
|
||||
plt.colorbar(im, ax=axes[0, 0], fraction=0.046)
|
||||
|
||||
im = axes[0, 1].imshow(ux_tar, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[0, 1].imshow(ux_tar, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[0, 1].set_title("dq_tar mean ux")
|
||||
plt.colorbar(im, ax=axes[0, 1], fraction=0.046)
|
||||
|
||||
# Row 1: RMS
|
||||
rms_ctl = rms_field(dq["ux"], dq["uy"])
|
||||
rms_tar = rms_field(dq_tar["ux"], dq_tar["uy"])
|
||||
rmax = max(rms_ctl.max(), rms_tar.max(), 1e-12)
|
||||
|
||||
im = axes[1, 0].imshow(rms_ctl, cmap="viridis", vmin=0, vmax=rmax,
|
||||
axes[1, 0].imshow(rms_ctl, cmap="viridis", vmin=0, vmax=rmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[1, 0].set_title("dq_ctl RMS")
|
||||
plt.colorbar(im, ax=axes[1, 0], fraction=0.046)
|
||||
|
||||
im = axes[1, 1].imshow(rms_tar, cmap="viridis", vmin=0, vmax=rmax,
|
||||
axes[1, 1].imshow(rms_tar, cmap="viridis", vmin=0, vmax=rmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[1, 1].set_title("dq_tar RMS")
|
||||
plt.colorbar(im, ax=axes[1, 1], fraction=0.046)
|
||||
|
||||
plt.suptitle(f"{scene_type}: dq_ctl vs dq_tar comparison")
|
||||
plt.tight_layout()
|
||||
@ -375,20 +346,17 @@ def run():
|
||||
extent = (0, NX - 1, 0, NY - 1)
|
||||
vmax = max(abs(ux_b).max(), abs(ux_c).max(), abs(ux_cancel).max(), 1e-12)
|
||||
|
||||
im = axes[0].imshow(ux_b, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[0].imshow(ux_b, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[0].set_title("dq_blk mean ux (blockage)")
|
||||
plt.colorbar(im, ax=axes[0], fraction=0.046)
|
||||
|
||||
im = axes[1].imshow(ux_c, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[1].imshow(ux_c, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[1].set_title("dq_ctl mean ux (correction)")
|
||||
plt.colorbar(im, ax=axes[1], fraction=0.046)
|
||||
|
||||
im = axes[2].imshow(ux_cancel, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
axes[2].imshow(ux_cancel, cmap="RdBu_r", vmin=-vmax, vmax=vmax,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[2].set_title("dq_ctl + dq_blk (cancel test)")
|
||||
plt.colorbar(im, ax=axes[2], fraction=0.046)
|
||||
|
||||
plt.suptitle(f"Steady cloak: cancellation test")
|
||||
plt.tight_layout()
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
"""Zone-wise CCD: force-CCD and signature-CCD per spatial zone.
|
||||
|
||||
Processes each of three spatial zones separately for illusion 0.75L and 1.0L:
|
||||
- near_body: x[350:500] (envelope around pinball)
|
||||
- body_wake: x[500:700] (body-connected near wake)
|
||||
- sensor_zone: x[580:650] (around sensor plane at x=30*L0=600)
|
||||
Processes each of three spatial zones separately for illusion 0.75L and 1.0L
|
||||
(unified geometry: pinball center ~613px, sensors at 800px):
|
||||
- near_body: x[580:720] (envelope around pinball)
|
||||
- body_wake: x[720:850] (body-connected near wake)
|
||||
- sensor_zone: x[780:850] (around sensor plane at x=40*L0=800)
|
||||
|
||||
For each zone: mask the snapshot matrix to keep only grid points in the zone,
|
||||
build a target-only POD basis, project correction fields, and compute
|
||||
@ -46,22 +47,27 @@ R = 6
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _define_zones() -> dict:
|
||||
"""Define three-zone masks for illusion layout (sensors at x=30*L0=600)."""
|
||||
"""Define three-zone masks for unified geometry (pinball center ~613px, sensors at 800px).
|
||||
|
||||
Updated 2026-06-28 to match unified geometry after all scenes were re-collected
|
||||
with pinball at front x=30*L0=600, rear x=31.3*L0=626 (center ~613px), sensors at 40*L0=800.
|
||||
Same zones as diagnose_corrections.py:define_zones().
|
||||
"""
|
||||
zones = {}
|
||||
|
||||
# near_body: envelope around pinball (pinball front x=380, rear x=406)
|
||||
# near_body: envelope around pinball (center ~613px)
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 350:500] = True
|
||||
mask[:, 580:720] = True
|
||||
zones["near_body"] = mask
|
||||
|
||||
# body_wake: body-connected near wake, immediate downstream
|
||||
# body_wake: near wake downstream of pinball
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 500:700] = True
|
||||
mask[:, 720:850] = True
|
||||
zones["body_wake"] = mask
|
||||
|
||||
# sensor_zone: around sensors at x=600
|
||||
# sensor_zone: around sensors at x=800 (40*L0)
|
||||
mask = np.zeros((NY, NX), dtype=bool)
|
||||
mask[:, 580:650] = True
|
||||
mask[:, 780:850] = True
|
||||
zones["sensor_zone"] = mask
|
||||
|
||||
return zones
|
||||
|
||||
126
src/CCD_analysis/correction_analysis/visualize_action_ccd.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Action-CCD mode 1 visualization for cloak scenes.
|
||||
|
||||
Action-CCD finds correction-field structures most correlated with cylinder
|
||||
rotation speeds. For cloak scenes (steady/karman/vortex), this should reveal
|
||||
the structures the controller directly modulates — clean velocity deficit
|
||||
compensation and cylinder dipoles, excluding upstream disturbance structures.
|
||||
|
||||
Usage:
|
||||
conda run -n pycuda_3_10 python correction_analysis/visualize_action_ccd.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR, NX, NY, L0
|
||||
from CCD_analysis.utils.resampling import (
|
||||
compute_pod, cumulative_energy, e95_index, compute_reduced_ccd,
|
||||
unstack_velocity_modes,
|
||||
)
|
||||
from CCD_analysis.correction_analysis.compute_correction_fields import (
|
||||
compute_correction, dict_to_field_matrix,
|
||||
)
|
||||
|
||||
FIG_DIR = os.path.join(DATA_DIR, "figures")
|
||||
os.makedirs(FIG_DIR, exist_ok=True)
|
||||
|
||||
CLOAK_SCENES = ["steady_cloak", "vortex_lamb", "vortex_taylor"]
|
||||
# karman_re100 excluded due to 72 vs 96 frame mismatch
|
||||
|
||||
R = 10
|
||||
CCD_Q = 6
|
||||
CROP_X0, CROP_X1 = 300, 1100
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Action-CCD Mode 1: Cloak scenes")
|
||||
print("=" * 60)
|
||||
|
||||
for st in CLOAK_SCENES:
|
||||
print(f"\n--- {st} ---", flush=True)
|
||||
|
||||
# Load correction fields
|
||||
corr = compute_correction(st)
|
||||
dq_ctl = corr.get("dq_ctl")
|
||||
if dq_ctl is None or dq_ctl.get("actions") is None:
|
||||
print(f" SKIP: no dq_ctl or no actions")
|
||||
continue
|
||||
|
||||
# Build snapshot matrix and compute POD
|
||||
Q = dict_to_field_matrix(dq_ctl)
|
||||
N = Q.shape[1]
|
||||
mf, modes, sv, coeffs = compute_pod(Q)
|
||||
e95 = e95_index(cumulative_energy(sv))
|
||||
print(f" POD: E95={e95}, N_modes={len(sv)}")
|
||||
|
||||
# Action-CCD: find structures correlated with actions
|
||||
a_r = coeffs[:R, :]
|
||||
actions = dq_ctl["actions"][:N].T # (3, N)
|
||||
W, sigma, _, _, _, _ = compute_reduced_ccd(a_r, actions, Q_delay=CCD_Q)
|
||||
|
||||
print(f" Action-CCD: sigma[0]={sigma[0]:.4f}, sigma_top3={sigma[:3]}")
|
||||
|
||||
# Reconstruct CCD mode 1 in physical space
|
||||
# z1 = W[:, 0] @ A_z → CCD temporal coefficient
|
||||
# CCD mode = sum over POD modes of (CCD direction weights * POD mode)
|
||||
w1 = W[:, 0] / (np.linalg.norm(W[:, 0]) + 1e-12)
|
||||
ccd_mode1 = modes[:, :R] @ w1 # (2*NX*NY,)
|
||||
|
||||
# Unstack into ux, uy
|
||||
half = NX * NY
|
||||
ux_mode = ccd_mode1[:half].reshape(NY, NX)
|
||||
uy_mode = ccd_mode1[half:].reshape(NY, NX)
|
||||
|
||||
# Plot mode 1: ux + uy + vorticity, cropped
|
||||
vor = np.gradient(uy_mode, axis=1) - np.gradient(ux_mode, axis=0)
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
|
||||
extent = (CROP_X0, CROP_X1, 0, NY - 1)
|
||||
|
||||
# ux
|
||||
vmax_ux = max(abs(ux_mode).max(), 1e-12)
|
||||
axes[0].imshow(ux_mode[:, CROP_X0:CROP_X1], cmap="RdBu_r",
|
||||
vmin=-vmax_ux, vmax=vmax_ux,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[0].set_title(f"{st}: Action-CCD mode 1 ux")
|
||||
|
||||
# uy
|
||||
vmax_uy = max(abs(uy_mode).max(), 1e-12)
|
||||
axes[1].imshow(uy_mode[:, CROP_X0:CROP_X1], cmap="RdBu_r",
|
||||
vmin=-vmax_uy, vmax=vmax_uy,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[1].set_title(f"{st}: Action-CCD mode 1 uy")
|
||||
|
||||
# vorticity
|
||||
vmax_vor = max(np.percentile(abs(vor), 99), 1e-12)
|
||||
axes[2].imshow(vor[:, CROP_X0:CROP_X1], cmap="RdBu_r",
|
||||
vmin=-vmax_vor, vmax=vmax_vor,
|
||||
origin="lower", aspect="equal", extent=extent)
|
||||
axes[2].set_title(f"{st}: Action-CCD mode 1 vorticity")
|
||||
|
||||
for ax in axes:
|
||||
ax.tick_params(left=False, right=False, labelleft=False,
|
||||
bottom=False, top=False, labelbottom=False)
|
||||
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, f"action_ccd_mode1_{st}.png")
|
||||
fig.savefig(path, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}")
|
||||
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,419 +0,0 @@
|
||||
"""1.5L auxiliary analysis: raw diagnostics, spectrum, windowed periodicity, POD.
|
||||
|
||||
Usage:
|
||||
python3 src/CCD_analysis/scripts/analyze_15L.py
|
||||
|
||||
Output:
|
||||
src/CCD_analysis/data/figures/15L_*.png
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR, SCENES
|
||||
from CCD_analysis.utils.resampling import (
|
||||
detect_dominant_frequency, detect_cycle_stability, phase_resample,
|
||||
compute_pod, cumulative_energy, compute_reduced_ccd,
|
||||
)
|
||||
|
||||
FIG_DIR = os.path.join(DATA_DIR, "figures")
|
||||
os.makedirs(FIG_DIR, exist_ok=True)
|
||||
|
||||
# Scene config — read from configs.py, not hardcoded
|
||||
_SCENE = SCENES["illusion_1.5L"]
|
||||
SI = _SCENE["sample_interval"] # 800 for 1.5L
|
||||
CONV_LEN = _SCENE.get("conv_len", 36) # Illusion uses 36
|
||||
FIFO_LEN = 150
|
||||
|
||||
|
||||
def load_controlled(name):
|
||||
p = os.path.join(DATA_DIR, "illusion", name, "controlled.npz")
|
||||
d = np.load(p)
|
||||
return d["sensors"], d["forces"], d["actions"]
|
||||
|
||||
|
||||
def load_target(name):
|
||||
p = os.path.join(DATA_DIR, "target_cylinder", name, "sensors.npz")
|
||||
d = np.load(p)
|
||||
return d["sensors"], d["forces"]
|
||||
|
||||
|
||||
def load_pinball_sensors():
|
||||
p = os.path.join(DATA_DIR, "pinball", "pinball", "sensors.npz")
|
||||
d = np.load(p)
|
||||
return d["sensors"]
|
||||
|
||||
|
||||
def load_resampled_fields(name):
|
||||
p = os.path.join(DATA_DIR, "resampled", name, "resampled.npz")
|
||||
d = np.load(p)
|
||||
return d["ux"], d["uy"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Task 3.1: Raw time-series diagnostics
|
||||
# ============================================================
|
||||
def task_31():
|
||||
print("=== Task 3.1: Raw time-series diagnostics ===", flush=True)
|
||||
sens_i, forc_i, act_i = load_controlled("illusion_1.5L")
|
||||
sens_t, forc_t = load_target("target_cylinder_1.5L")
|
||||
|
||||
n_plot = min(400, len(sens_i))
|
||||
t = np.arange(n_plot) * SI / 1000 # time in T0 units (1 T0 = 1000 steps)
|
||||
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
|
||||
|
||||
# Sensors
|
||||
ax = axes[0]
|
||||
for ch in range(6):
|
||||
ax.plot(t, sens_i[:n_plot, ch], label=f"ill_s{ch}", alpha=0.7)
|
||||
ax.plot(t, sens_t[:n_plot, 3], "k--", label="target_s1_v", linewidth=2)
|
||||
ax.set_ylabel("Velocity (lattice)")
|
||||
ax.set_title("1.5L Sensors: Illusion vs Target")
|
||||
ax.legend(fontsize=7, ncol=3)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Forces
|
||||
ax = axes[1]
|
||||
for ch in range(6):
|
||||
ax.plot(t, forc_i[:n_plot, ch], label=f"ill_F{ch}", alpha=0.7)
|
||||
ax.plot(t, forc_t[:n_plot, 0], "k--", label="target_Fx", linewidth=2)
|
||||
ax.plot(t, forc_t[:n_plot, 1], "k:", label="target_Fy", linewidth=2)
|
||||
ax.set_ylabel("Force (lattice)")
|
||||
ax.set_title("1.5L Forces")
|
||||
ax.legend(fontsize=7, ncol=3)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Actions
|
||||
ax = axes[2]
|
||||
for ch in range(3):
|
||||
ax.plot(t, act_i[:n_plot, ch], label=f"Omega_{ch}")
|
||||
ax.set_xlabel("Time (T0 units)")
|
||||
ax.set_ylabel("Omega (normalised)")
|
||||
ax.set_title("1.5L Actions (DRL output, [-1, 1])")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, "15L_raw_timeseries.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Task 3.2: Spectrum analysis
|
||||
# ============================================================
|
||||
def task_32():
|
||||
print("=== Task 3.2: Spectrum analysis ===", flush=True)
|
||||
sens_i, forc_i, act_i = load_controlled("illusion_1.5L")
|
||||
sens_t, forc_t = load_target("target_cylinder_1.5L")
|
||||
sens_p = load_pinball_sensors()
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
|
||||
|
||||
def add_spectrum(signal, ax, label, color, ls="-"):
|
||||
y = signal - np.mean(signal)
|
||||
n = len(y)
|
||||
window = np.hanning(n)
|
||||
spec = np.abs(np.fft.rfft(y * window)) ** 2
|
||||
freqs = np.fft.rfftfreq(n, d=SI)
|
||||
ax.plot(freqs[1:], spec[1:], label=label, color=color, ls=ls, alpha=0.8)
|
||||
|
||||
# Sensor v component (channel 1 = uy of center sensor)
|
||||
# In controlled.npz: sensors[:, 1] = sensor1_uy (center sensor v)
|
||||
ax = axes[0, 0]
|
||||
add_spectrum(sens_t[:500, 1], ax, "Target", "red")
|
||||
add_spectrum(sens_i[:500, 1], ax, "Illusion 1.5L", "blue")
|
||||
add_spectrum(sens_p[:500, 1], ax, "Pinball (uncontrolled)", "green")
|
||||
ax.set_xlim(0, 0.005)
|
||||
ax.set_xlabel("Frequency (1/step)")
|
||||
ax.set_ylabel("Power")
|
||||
ax.set_title("Spectrum: sensor v (center)")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Actions spectrum
|
||||
ax = axes[0, 1]
|
||||
for ch in range(3):
|
||||
add_spectrum(act_i[:500, ch], ax, f"Action {ch}", f"C{ch+1}")
|
||||
ax.set_xlim(0, 0.005)
|
||||
ax.set_xlabel("Frequency (1/step)")
|
||||
ax.set_ylabel("Power")
|
||||
ax.set_title("1.5L Action spectrum")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Force spectra
|
||||
ax = axes[1, 0]
|
||||
add_spectrum(forc_t[:500, 0], ax, "Target Fx", "red")
|
||||
add_spectrum(forc_i[:500, 0] + forc_i[:500, 2] + forc_i[:500, 4], ax, "Illusion total Fx", "blue")
|
||||
ax.set_xlim(0, 0.005)
|
||||
ax.set_xlabel("Frequency (1/step)")
|
||||
ax.set_ylabel("Power")
|
||||
ax.set_title("Force Fx spectrum")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Strouhal comparison
|
||||
ax = axes[1, 1]
|
||||
for name, sig, c in [
|
||||
("Target", sens_t[:500, 1], "red"),
|
||||
("Illusion", sens_i[:500, 1], "blue"),
|
||||
("Pinball", sens_p[:500, 1], "green"),
|
||||
]:
|
||||
f_dom, T_dom, _ = detect_dominant_frequency(sig, SI)
|
||||
St = f_dom * (1.5 * 20) / 0.01 # D=1.5*L0, U0=0.01
|
||||
ax.bar(name, St, color=c, alpha=0.6, label=f"St={St:.3f}")
|
||||
ax.set_ylabel("Strouhal number")
|
||||
ax.set_title("Dominant Strouhal comparison (1.5L ref)")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, "15L_spectrum.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Task 3.3: Windowed periodicity
|
||||
# ============================================================
|
||||
def task_33():
|
||||
print("=== Task 3.3: Windowed periodicity ===", flush=True)
|
||||
sens_i, forc_i, act_i = load_controlled("illusion_1.5L")
|
||||
signal = sens_i[:, 1] # center sensor v
|
||||
|
||||
window = 200
|
||||
stride = 20
|
||||
n_windows = (len(signal) - window) // stride
|
||||
|
||||
cv_vals, T_vals, f_vals, t_centers = [], [], [], []
|
||||
for w in range(n_windows):
|
||||
seg = signal[w * stride:w * stride + window]
|
||||
cv_T, mean_T, _ = detect_cycle_stability(seg, SI)
|
||||
f_dom, T_dom, _ = detect_dominant_frequency(seg, SI)
|
||||
cv_vals.append(cv_T)
|
||||
T_vals.append(mean_T)
|
||||
f_vals.append(f_dom)
|
||||
t_centers.append((w * stride + window // 2) * SI / 1000)
|
||||
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 8), sharex=True)
|
||||
|
||||
ax = axes[0]
|
||||
ax.plot(t_centers, cv_vals, "o-", markersize=3)
|
||||
ax.axhline(0.10, color="r", ls="--", label="strict gate")
|
||||
ax.axhline(0.12, color="orange", ls="--", label="relaxed gate")
|
||||
ax.set_ylabel("CV_T")
|
||||
ax.set_title("1.5L Windowed cycle stability (window=200 steps)")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
ax = axes[1]
|
||||
ax.plot(t_centers, T_vals, "o-", markersize=3, color="green")
|
||||
ax.set_ylabel("Mean period (steps)")
|
||||
ax.axhline(800 * 24.2, color="gray", ls=":", label="expected") # N_raw*SI
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
ax = axes[2]
|
||||
ax.plot(t_centers, f_vals, "o-", markersize=3, color="purple")
|
||||
ax.set_xlabel("Time (T0 units)")
|
||||
ax.set_ylabel("Freq (1/step)")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, "15L_windowed_periodicity.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
# Find stable windows
|
||||
stable_windows = [(t, cv, T) for t, cv, T in zip(t_centers, cv_vals, T_vals) if cv < 0.10]
|
||||
print(f" Stable windows (CV_T<0.10): {len(stable_windows)}/{n_windows}", flush=True)
|
||||
return stable_windows
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Task 3.4: POD projection attractor comparison
|
||||
# ============================================================
|
||||
def task_34():
|
||||
print("=== Task 3.4: POD attractor comparison ===", flush=True)
|
||||
|
||||
# Load resampled data for all diameters
|
||||
data = {}
|
||||
for diam in [0.75, 1.0, 1.5]:
|
||||
for kind in ["illusion", "target_cylinder"]:
|
||||
name = f"{kind}_{diam}L"
|
||||
d = load_resampled_fields(name)
|
||||
if d[0] is not None:
|
||||
data[name] = d
|
||||
|
||||
# Build 1.5L POD basis (target + illusion)
|
||||
name_t = "target_cylinder_1.5L"
|
||||
name_i = "illusion_1.5L"
|
||||
ux_t, uy_t = data[name_t]
|
||||
ux_i, uy_i = data[name_i]
|
||||
|
||||
snaps = []
|
||||
for ux, uy in [(ux_t, uy_t), (ux_i, uy_i)]:
|
||||
for c in range(ux.shape[0]):
|
||||
for p in range(ux.shape[1]):
|
||||
snaps.append(np.concatenate([ux[c, p].ravel(), uy[c, p].ravel()]))
|
||||
Q = np.column_stack(snaps)
|
||||
mf, modes, sv, coeffs = compute_pod(Q)
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
||||
|
||||
for idx, (diam, color) in enumerate([(0.75, "blue"), (1.0, "green"), (1.5, "red")]):
|
||||
name = f"illusion_{diam}L"
|
||||
if name not in data:
|
||||
continue
|
||||
ux, uy = data[name]
|
||||
proj_snaps = []
|
||||
for c in range(ux.shape[0]):
|
||||
for p in range(ux.shape[1]):
|
||||
proj_snaps.append(np.concatenate([ux[c, p].ravel(), uy[c, p].ravel()]))
|
||||
Qp = np.column_stack(proj_snaps)
|
||||
a = modes[:, :6].T @ (Qp - mf[:, None])
|
||||
|
||||
ax = axes[idx]
|
||||
ax.plot(a[0, :], a[1, :], ".", color=color, markersize=2, alpha=0.5)
|
||||
ax.plot(a[0, :96], a[1, :96], "-", color=color, alpha=0.3, linewidth=0.5)
|
||||
ax.set_xlabel("a1")
|
||||
ax.set_ylabel("a2")
|
||||
ax.set_title(f"Illusion {diam}L in 1.5L POD basis")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.set_aspect("equal")
|
||||
|
||||
plt.tight_layout()
|
||||
path = os.path.join(FIG_DIR, "15L_pod_attractor_comparison.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Task 3.5: Short-window CCD (if stable windows found)
|
||||
# ============================================================
|
||||
def task_35(stable_windows_info):
|
||||
print("=== Task 3.5: Short-window CCD ===", flush=True)
|
||||
if len(stable_windows_info) < 3:
|
||||
print(" Not enough stable windows, skipping short-window CCD", flush=True)
|
||||
return
|
||||
|
||||
sens_i, forc_i, act_i = load_controlled("illusion_1.5L")
|
||||
sens_t, forc_t = load_target("target_cylinder_1.5L")
|
||||
|
||||
# We need fields for CCD — load from resampled
|
||||
ux_i, uy_i = load_resampled_fields("illusion_1.5L")
|
||||
ux_t, uy_t = load_resampled_fields("target_cylinder_1.5L")
|
||||
|
||||
# Build reference POD (all 4 cycles of target + illusion)
|
||||
snaps = []
|
||||
for ux, uy in [(ux_t, uy_t), (ux_i, uy_i)]:
|
||||
for c in range(ux.shape[0]):
|
||||
for p in range(ux.shape[1]):
|
||||
snaps.append(np.concatenate([ux[c, p].ravel(), uy[c, p].ravel()]))
|
||||
Q = np.column_stack(snaps)
|
||||
mf, modes, sv, coeffs = compute_pod(Q)
|
||||
modes_r = modes[:, :6]
|
||||
|
||||
# Use 4-cycle resampled data for CCD (as in standard pipeline)
|
||||
frc_i = np.load(os.path.join(DATA_DIR, "resampled", "illusion_1.5L", "resampled.npz"))["forces"]
|
||||
frc_t = np.load(os.path.join(DATA_DIR, "resampled", "target_cylinder_1.5L", "resampled.npz"))["forces"]
|
||||
|
||||
flat_i = frc_i.reshape(-1, frc_i.shape[-1]).T # (6, 96)
|
||||
flat_t = frc_t.reshape(-1, frc_t.shape[-1]).T
|
||||
|
||||
# Force observable
|
||||
y_i = np.vstack([flat_i[0] + flat_i[2] + flat_i[4],
|
||||
flat_i[1] + flat_i[3] + flat_i[5]])
|
||||
y_t = np.vstack([flat_t[0], flat_t[1]])
|
||||
|
||||
# Project illusion fields
|
||||
proj = []
|
||||
for c in range(ux_i.shape[0]):
|
||||
for p in range(ux_i.shape[1]):
|
||||
proj.append(np.concatenate([ux_i[c, p].ravel(), uy_i[c, p].ravel()]))
|
||||
Qi = np.column_stack(proj)
|
||||
a_i = modes_r.T @ (Qi - mf[:, None])
|
||||
|
||||
W_i, sig_i, _, z_i, _, _ = compute_reduced_ccd(a_i, y_i[:, :a_i.shape[1]], Q_delay=12)
|
||||
|
||||
# Compare with target
|
||||
proj_t = []
|
||||
for c in range(ux_t.shape[0]):
|
||||
for p in range(ux_t.shape[1]):
|
||||
proj_t.append(np.concatenate([ux_t[c, p].ravel(), uy_t[c, p].ravel()]))
|
||||
Qt = np.column_stack(proj_t)
|
||||
a_t = modes_r.T @ (Qt - mf[:, None])
|
||||
W_t, sig_t, _, z_t, _, _ = compute_reduced_ccd(a_t, y_t[:, :a_t.shape[1]], Q_delay=12)
|
||||
|
||||
# Overlap
|
||||
n = min(W_i.shape[1], W_t.shape[1], 5)
|
||||
ov = [float(abs(
|
||||
W_i[:, k] / (np.linalg.norm(W_i[:, k]) + 1e-12) @
|
||||
W_t[:, k] / (np.linalg.norm(W_t[:, k]) + 1e-12)
|
||||
)) for k in range(n)]
|
||||
print(f" 1.5L short-window force-CCD O(target, illusion): O1={ov[0]:.4f}, O2={ov[1]:.4f}", flush=True)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
|
||||
# z_1(t) comparison
|
||||
ax = axes[0]
|
||||
ax.plot(z_i[0, :], label="Illusion z_1")
|
||||
ax.plot(z_t[0, :], "--", label="Target z_1")
|
||||
ax.set_xlabel("Flat sample index")
|
||||
ax.set_ylabel("CCD temporal coeff z_1")
|
||||
ax.set_title("1.5L Force-CCD: z_1(t)")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Sigma decay
|
||||
ax = axes[1]
|
||||
ax.semilogy(sig_i, "o-", label="Illusion", markersize=4)
|
||||
ax.semilogy(sig_t, "s-", label="Target", markersize=4)
|
||||
ax.set_xlabel("Mode index")
|
||||
ax.set_ylabel("Singular value")
|
||||
ax.set_title("1.5L Force-CCD: singular value decay")
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
path = os.path.join(FIG_DIR, "15L_short_window_ccd.png")
|
||||
fig.savefig(path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f" Saved: {path}", flush=True)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
def main():
|
||||
print("=" * 60, flush=True)
|
||||
print("1.5L Auxiliary Analysis", flush=True)
|
||||
print("=" * 60, flush=True)
|
||||
|
||||
task_31()
|
||||
task_32()
|
||||
stable_info = task_33()
|
||||
task_34()
|
||||
task_35(stable_info)
|
||||
|
||||
print("\nDone. All figures saved to", FIG_DIR, flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"case": "illusion_1L",
|
||||
"model": "/home/frank14f/DynamisLab/models/250525/d1a3o14_250525_imit_1L_2U_600S.zip",
|
||||
"U0": 0.02,
|
||||
"viscosity": 0.008,
|
||||
"sample_interval": 600,
|
||||
"n_infer_steps": 500,
|
||||
"mean_reward_last100": 0.50427141107983,
|
||||
"mean_similarity_last100": 0.8369960935939864,
|
||||
"force_norm_fact": 0.05429763346910477,
|
||||
"validation_passed": false,
|
||||
"n_obj": 6
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"force_norm_fact": 0.05429763346910477,
|
||||
"sens_deviation": [
|
||||
1.893601894378662,
|
||||
-0.2520896792411804,
|
||||
1.3097574710845947,
|
||||
-0.04255330562591553,
|
||||
1.897708535194397,
|
||||
0.2153952717781067
|
||||
],
|
||||
"sens_norm_fact": [
|
||||
4.2874250411987305,
|
||||
5.249192714691162,
|
||||
1.472514271736145,
|
||||
7.114207744598389,
|
||||
4.274000644683838,
|
||||
5.054762363433838
|
||||
],
|
||||
"action_scale": 8.0,
|
||||
"action_bias": [
|
||||
0.0,
|
||||
-2.0,
|
||||
2.0
|
||||
]
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
[
|
||||
{
|
||||
"dc": 0.02266536364952723,
|
||||
"amps": [
|
||||
0.00038379516744314115,
|
||||
9.514000233530361e-05,
|
||||
8.349139917453543e-05,
|
||||
7.792522654399734e-05,
|
||||
7.433183588746336e-05
|
||||
],
|
||||
"freqs": [
|
||||
0.16,
|
||||
0.16666666666666669,
|
||||
0.13333333333333333,
|
||||
0.26666666666666666,
|
||||
0.15333333333333335
|
||||
],
|
||||
"phases": [
|
||||
-1.1237148062087887,
|
||||
1.9611885389112236,
|
||||
1.6703234092960584,
|
||||
-0.3613027596994091,
|
||||
-1.1120847569223562
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": 5.844893375372825e-05,
|
||||
"amps": [
|
||||
0.008381073411089025,
|
||||
0.0009710627254597952,
|
||||
0.0008309252043062449,
|
||||
0.000440363650103523,
|
||||
0.0004196318731941633
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333,
|
||||
0.06666666666666667,
|
||||
0.09333333333333334
|
||||
],
|
||||
"phases": [
|
||||
-0.20583713241972612,
|
||||
2.9062320022605928,
|
||||
-0.19244834395419047,
|
||||
-0.06340420123785095,
|
||||
2.797172757014809
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": 2.023785818417867,
|
||||
"amps": [
|
||||
0.6206119106073832,
|
||||
0.08359220819570973,
|
||||
0.0772933966332921,
|
||||
0.05856013170212723,
|
||||
0.044579995576176853
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.24000000000000002,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333,
|
||||
0.16
|
||||
],
|
||||
"phases": [
|
||||
-1.2902033342248966,
|
||||
2.247158738027268,
|
||||
1.8698785846127643,
|
||||
-1.3230065080948876,
|
||||
-2.7643028382054635
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": -0.07650716103613377,
|
||||
"amps": [
|
||||
0.8881293616425711,
|
||||
0.19361219712346026,
|
||||
0.11776456319197573,
|
||||
0.1056433212084406,
|
||||
0.08416293765029953
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.16,
|
||||
0.24000000000000002,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333
|
||||
],
|
||||
"phases": [
|
||||
-3.1077828808645815,
|
||||
0.9383438720400492,
|
||||
0.0795160787541243,
|
||||
0.03419287750174611,
|
||||
-3.1125200854751167
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": 1.8414087001482646,
|
||||
"amps": [
|
||||
0.1497365430682559,
|
||||
0.040132780833895966,
|
||||
0.026889484355317024,
|
||||
0.02298740258772072,
|
||||
0.01741055707849075
|
||||
],
|
||||
"freqs": [
|
||||
0.16,
|
||||
0.16666666666666669,
|
||||
0.15333333333333335,
|
||||
0.32,
|
||||
0.17333333333333334
|
||||
],
|
||||
"phases": [
|
||||
-0.27142943807213477,
|
||||
2.865319464251447,
|
||||
-0.27348869296327505,
|
||||
-3.041810616039961,
|
||||
2.803592539777442
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": -0.008232745975255966,
|
||||
"amps": [
|
||||
1.5054029642098454,
|
||||
0.30809832902221,
|
||||
0.17612766731044907,
|
||||
0.14518758298142478,
|
||||
0.13409758532624938
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.24000000000000002,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333,
|
||||
0.24666666666666667
|
||||
],
|
||||
"phases": [
|
||||
-3.035701468779973,
|
||||
0.44692729545584287,
|
||||
0.096649357931439,
|
||||
-3.035027928495052,
|
||||
-2.7069943130770233
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": 2.0234219272931417,
|
||||
"amps": [
|
||||
0.6202491421096321,
|
||||
0.0839598074044769,
|
||||
0.0773465705446304,
|
||||
0.05861231122571406,
|
||||
0.04506359085306284
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.24000000000000002,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333,
|
||||
0.16
|
||||
],
|
||||
"phases": [
|
||||
1.8537647038281104,
|
||||
-0.8723353223902255,
|
||||
-1.2906675657274453,
|
||||
1.8392270400405824,
|
||||
-2.377576985448929
|
||||
]
|
||||
},
|
||||
{
|
||||
"dc": 0.06527064591646195,
|
||||
"amps": [
|
||||
0.8933136072736296,
|
||||
0.1854083343143704,
|
||||
0.1218220099415223,
|
||||
0.10078312046223797,
|
||||
0.09011504483851066
|
||||
],
|
||||
"freqs": [
|
||||
0.08,
|
||||
0.16,
|
||||
0.24000000000000002,
|
||||
0.08666666666666667,
|
||||
0.07333333333333333
|
||||
],
|
||||
"phases": [
|
||||
-3.1049896733668625,
|
||||
-2.145674517803205,
|
||||
0.14371115551840652,
|
||||
0.007693943504592867,
|
||||
-3.0961205975207657
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"case": "illusion",
|
||||
"output_dir": "/home/frank14f/DynamisLab/src/CCD_analysis/output_redux/illusion",
|
||||
"checks": {
|
||||
"reward": false,
|
||||
"similarity": false,
|
||||
"vorticity_png": true
|
||||
},
|
||||
"all_pass": false,
|
||||
"extra": {}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 241 KiB |
@ -1,13 +0,0 @@
|
||||
{
|
||||
"case": "karman_cloak",
|
||||
"model": "/home/frank14f/DynamisLab/models/old/d1a3o12_re100.zip",
|
||||
"viscosity": 0.004,
|
||||
"U0": 0.01,
|
||||
"sample_interval": 800,
|
||||
"n_infer_steps": 200,
|
||||
"mean_reward_last100": 0.6534655690193176,
|
||||
"overall_similarity": 0.9503773416909905,
|
||||
"force_norm_fact": 0.019220656715333462,
|
||||
"validation_passed": true,
|
||||
"n_obj": 7
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"force_norm_fact": 0.019220656715333462,
|
||||
"sens_deviation": [
|
||||
0.7905515432357788,
|
||||
-0.11469581723213196,
|
||||
0.24619626998901367,
|
||||
0.01082652248442173,
|
||||
0.8161152601242065,
|
||||
0.12503524124622345
|
||||
],
|
||||
"sens_norm_fact": [
|
||||
3.1592841148376465,
|
||||
3.0264341831207275,
|
||||
1.8399379253387451,
|
||||
3.451172351837158,
|
||||
3.055002450942993,
|
||||
2.9197099208831787
|
||||
],
|
||||
"action_bias": [
|
||||
0.0,
|
||||
-4.0,
|
||||
4.0
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"case": "karman",
|
||||
"output_dir": "/home/frank14f/DynamisLab/src/CCD_analysis/output_redux/karman",
|
||||
"checks": {
|
||||
"overall_similarity": true,
|
||||
"mean_similarity_last100": true,
|
||||
"vorticity_png": true
|
||||
},
|
||||
"all_pass": true,
|
||||
"extra": {
|
||||
"overall_similarity": 0.9503773416909905,
|
||||
"mean_reward_last100": 0.6534655690193176,
|
||||
"mean_sim_last100": 0.9482672810554504
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 265 KiB |
@ -1,11 +0,0 @@
|
||||
{
|
||||
"case": "pinball",
|
||||
"U0": 0.01,
|
||||
"viscosity": 0.004,
|
||||
"n_steps": 200,
|
||||
"sample_interval": 800,
|
||||
"n_obj": 6,
|
||||
"f_dom": 5.6250000000000005e-05,
|
||||
"T_dom_steps": 17777.777777777777,
|
||||
"St": 0.11250000000000002
|
||||
}
|
||||
|
Before Width: | Height: | Size: 203 KiB |
@ -1,211 +0,0 @@
|
||||
{
|
||||
"pinball": {
|
||||
"meta": {
|
||||
"case": "pinball",
|
||||
"U0": 0.01,
|
||||
"viscosity": 0.004,
|
||||
"n_steps": 200,
|
||||
"sample_interval": 800,
|
||||
"n_obj": 6,
|
||||
"f_dom": 5.6250000000000005e-05,
|
||||
"T_dom_steps": 17777.777777777777,
|
||||
"St": 0.11250000000000002
|
||||
},
|
||||
"validation": {},
|
||||
"files": {
|
||||
"sensors.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.01
|
||||
},
|
||||
"fields.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 890.44
|
||||
},
|
||||
"vorticity.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.2
|
||||
},
|
||||
"meta.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"steady_cloak": {
|
||||
"meta": {
|
||||
"case": "steady_cloak",
|
||||
"U0": 0.01,
|
||||
"viscosity": 0.004,
|
||||
"omega_front": 0.0,
|
||||
"omega_bottom": 0.051,
|
||||
"omega_top": -0.051,
|
||||
"rear_omega_scale": 5.1,
|
||||
"n_samples": 30,
|
||||
"sample_interval": 800,
|
||||
"sensor_mean": [
|
||||
1.1140856742858887,
|
||||
-0.01482248492538929,
|
||||
1.1645309925079346,
|
||||
3.380884905368475e-08,
|
||||
1.1140861511230469,
|
||||
0.014822724275290966
|
||||
],
|
||||
"sensor_std": 0.0003435599210206419
|
||||
},
|
||||
"validation": {},
|
||||
"files": {
|
||||
"sensors.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"fields.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 128.28
|
||||
},
|
||||
"vorticity.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.09
|
||||
},
|
||||
"meta.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"karman": {
|
||||
"meta": {
|
||||
"case": "karman_cloak",
|
||||
"model": "/home/frank14f/DynamisLab/models/old/d1a3o12_re100.zip",
|
||||
"viscosity": 0.004,
|
||||
"U0": 0.01,
|
||||
"sample_interval": 800,
|
||||
"n_infer_steps": 200,
|
||||
"mean_reward_last100": 0.6534655690193176,
|
||||
"overall_similarity": 0.9503773416909905,
|
||||
"force_norm_fact": 0.019220656715333462,
|
||||
"validation_passed": true,
|
||||
"n_obj": 7
|
||||
},
|
||||
"validation": {
|
||||
"case": "karman",
|
||||
"output_dir": "/home/frank14f/DynamisLab/src/CCD_analysis/output_redux/karman",
|
||||
"checks": {
|
||||
"overall_similarity": true,
|
||||
"mean_similarity_last100": true,
|
||||
"vorticity_png": true
|
||||
},
|
||||
"all_pass": true,
|
||||
"extra": {
|
||||
"overall_similarity": 0.9503773416909905,
|
||||
"mean_reward_last100": 0.6534655690193176,
|
||||
"mean_sim_last100": 0.9482672810554504
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"vorticity_target.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.23
|
||||
},
|
||||
"vorticity_controlled.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.21
|
||||
},
|
||||
"vorticity_uncontrolled.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.26
|
||||
},
|
||||
"meta.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"validation_report.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"norm.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"target.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"save_states.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.01
|
||||
},
|
||||
"controlled.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.02
|
||||
},
|
||||
"open_loop_fields.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 900.46
|
||||
}
|
||||
}
|
||||
},
|
||||
"illusion": {
|
||||
"meta": {
|
||||
"case": "illusion_1L",
|
||||
"model": "/home/frank14f/DynamisLab/models/250525/d1a3o14_250525_imit_1L_2U_600S.zip",
|
||||
"U0": 0.02,
|
||||
"viscosity": 0.008,
|
||||
"sample_interval": 600,
|
||||
"n_infer_steps": 500,
|
||||
"mean_reward_last100": 0.50427141107983,
|
||||
"mean_similarity_last100": 0.8369960935939864,
|
||||
"force_norm_fact": 0.05429763346910477,
|
||||
"validation_passed": false,
|
||||
"n_obj": 6
|
||||
},
|
||||
"validation": {
|
||||
"case": "illusion",
|
||||
"output_dir": "/home/frank14f/DynamisLab/src/CCD_analysis/output_redux/illusion",
|
||||
"checks": {
|
||||
"reward": false,
|
||||
"similarity": false,
|
||||
"vorticity_png": true
|
||||
},
|
||||
"all_pass": false,
|
||||
"extra": {}
|
||||
},
|
||||
"files": {
|
||||
"vorticity_target.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.21
|
||||
},
|
||||
"vorticity_controlled.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.2
|
||||
},
|
||||
"vorticity_uncontrolled.png": {
|
||||
"exists": true,
|
||||
"size_mb": 0.24
|
||||
},
|
||||
"meta.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"validation_report.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"norm.json": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"target.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.0
|
||||
},
|
||||
"save_states.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.01
|
||||
},
|
||||
"controlled.npz": {
|
||||
"exists": true,
|
||||
"size_mb": 0.03
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"case": "steady_cloak",
|
||||
"U0": 0.01,
|
||||
"viscosity": 0.004,
|
||||
"omega_front": 0.0,
|
||||
"omega_bottom": 0.051,
|
||||
"omega_top": -0.051,
|
||||
"rear_omega_scale": 5.1,
|
||||
"n_samples": 30,
|
||||
"sample_interval": 800,
|
||||
"sensor_mean": [
|
||||
1.1140856742858887,
|
||||
-0.01482248492538929,
|
||||
1.1645309925079346,
|
||||
3.380884905368475e-08,
|
||||
1.1140861511230469,
|
||||
0.014822724275290966
|
||||
],
|
||||
"sensor_std": 0.0003435599210206419
|
||||
}
|
||||
|
Before Width: | Height: | Size: 91 KiB |
@ -1,101 +0,0 @@
|
||||
# CCD_analysis/scripts/cfg.py
|
||||
# RELIABILITY: HIGH. Paths and constants only, no CFD dependency.
|
||||
"""Configuration constants for CCD analysis pipeline."""
|
||||
|
||||
import os
|
||||
|
||||
# -- Paths -------------------------------------------------------------------
|
||||
_PROJ_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
ANALYSIS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
CONFIG_DIR = os.path.join(ANALYSIS_DIR, "configs")
|
||||
OUTPUT_DIR = os.path.join(ANALYSIS_DIR, "output")
|
||||
MODEL_DIR = os.path.join(_PROJ_ROOT, "models")
|
||||
LEGACY_CFD_DIR = os.path.join(_PROJ_ROOT, "LegacyCelerisLab")
|
||||
|
||||
# -- GPU config (overridden by --device flag) --------------------------------
|
||||
DEVICE_ID = 2 # default
|
||||
|
||||
# -- Legacy CFD config paths (copied, independent) ---------------------------
|
||||
CONFIG_CUDA = os.path.join(CONFIG_DIR, "config_cuda.json")
|
||||
CONFIG_FLOWFIELD_BASE = os.path.join(CONFIG_DIR, "config_flowfield.json")
|
||||
|
||||
# -- Physics constants -------------------------------------------------------
|
||||
U0 = 0.01 # inlet centre velocity (lattice units)
|
||||
D_CYL = 20.0 # single cylinder diameter (lattice units)
|
||||
D_REF = 40.0 # reference length = 2*D (used for code "Re")
|
||||
L0 = 20.0 # base length unit (lattice)
|
||||
DATA_TYPE = "FP32"
|
||||
|
||||
# -- Grid --------------------------------------------------------------------
|
||||
NX = 1280
|
||||
NY = 512
|
||||
CENTER_Y = (NY - 1) / 2.0 # 255.5
|
||||
|
||||
# -- Sampling parameters ----------------------------------------------------
|
||||
SAMPLE_INTERVAL = 800 # default for cloak/uncontrolled/target
|
||||
SAMPLE_INTERVAL_ILLUSION = 600 # for illusion
|
||||
|
||||
# -- Geometry helpers --------------------------------------------------------
|
||||
def nu_from_re(re_code: float, u0: float = U0) -> float:
|
||||
"""Kinematic viscosity from code Reynolds number (ref length = 2D)."""
|
||||
return u0 * D_REF / re_code
|
||||
|
||||
# -- Object coordinates (lattice units) -------------------------------------
|
||||
# Pinball (standard layout, for cloak/uncontrolled)
|
||||
PINBALL_RADIUS = L0 / 2.0
|
||||
FRONT_CENTER = (30.0 * L0, CENTER_Y) # (600, 255.5)
|
||||
BOTTOM_CENTER = (31.3 * L0, CENTER_Y - 0.75 * L0) # (626, 240.5)
|
||||
TOP_CENTER = (31.3 * L0, CENTER_Y + 0.75 * L0) # (626, 270.5)
|
||||
|
||||
# Pinball (illusion layout — different positions)
|
||||
ILLUSION_FRONT = (19.0 * L0, CENTER_Y) # (380, 255.5)
|
||||
ILLUSION_BOTTOM = (20.3 * L0, CENTER_Y + 0.75 * L0) # (406, 270.5)
|
||||
ILLUSION_TOP = (20.3 * L0, CENTER_Y - 0.75 * L0) # (406, 240.5)
|
||||
|
||||
# Sensors
|
||||
SENSOR_RADIUS = L0 / 4.0 # 5
|
||||
SENSOR_CENTERS_CLOAK = [ # x=40*L0 for cloak/uncontrolled
|
||||
(40.0 * L0, CENTER_Y + 2.0 * L0),
|
||||
(40.0 * L0, CENTER_Y),
|
||||
(40.0 * L0, CENTER_Y - 2.0 * L0),
|
||||
]
|
||||
SENSOR_CENTERS_ILLUSION = [ # x=30*L0 for illusion
|
||||
(30.0 * L0, CENTER_Y + 2.0 * L0),
|
||||
(30.0 * L0, CENTER_Y),
|
||||
(30.0 * L0, CENTER_Y - 2.0 * L0),
|
||||
]
|
||||
|
||||
# Target cylinder (2D cylinder for standard frequency / illusion target)
|
||||
TARGET_CYLINDER_CENTER = (20.0 * L0, CENTER_Y) # (400, 255.5)
|
||||
TARGET_CYLINDER_RADIUS = 1.0 * L0 # 20
|
||||
|
||||
# -- Model paths -------------------------------------------------------------
|
||||
MODEL_CLOAK_RE100 = os.path.join(MODEL_DIR, "old", "d1a3o12_re100.zip")
|
||||
MODEL_CLOAK_250326 = os.path.join(MODEL_DIR, "250326", "d1a3o12_250326.zip")
|
||||
MODEL_ILLUSION_1L = os.path.join(
|
||||
MODEL_DIR, "250525", "d1a3o14_250525_imit_1L_2U_600S.zip"
|
||||
)
|
||||
|
||||
# -- Action parameters -------------------------------------------------------
|
||||
ACTION_SCALE_CLOAK = 8.0
|
||||
ACTION_BIAS_CLOAK = (0.0, -4.0, 4.0)
|
||||
|
||||
ACTION_SCALE_ILLUSION = 8.0
|
||||
ACTION_BIAS_ILLUSION = (0.0, -2.0, 2.0)
|
||||
|
||||
# -- DRL parameters ----------------------------------------------------------
|
||||
S_DIM_CLOAK = 12
|
||||
S_DIM_ILLUSION = 14
|
||||
A_DIM = 3
|
||||
FIFO_LEN = 150
|
||||
CONV_LEN = 30
|
||||
STABILIZE_STEPS = int(4 * NX / U0)
|
||||
|
||||
# -- Phase resampling --------------------------------------------------------
|
||||
N_TARGET_CYCLES = 4 # number of cycles to extract
|
||||
N_PTS_PER_CYCLE = 24 # phase points per cycle
|
||||
TOTAL_PHASE_FRAMES = N_TARGET_CYCLES * N_PTS_PER_CYCLE # 96
|
||||
|
||||
# -- CCD parameters ----------------------------------------------------------
|
||||
CCD_Q_DEFAULT = 12 # delay window size (half-cycle)
|
||||
CCD_R_CANDIDATES = [6, 8, 10] # POD truncation candidates
|
||||
@ -65,10 +65,10 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
|
||||
print("=== Target recording ===")
|
||||
ff_tgt = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
||||
tgt_radius = cfg["target_diameter"] * L0
|
||||
ff_tgt.add_cylinder((20.0 * L0, CENTER_Y, 0.0), tgt_radius)
|
||||
ff_tgt.add_cylinder((30.65 * L0, CENTER_Y, 0.0), tgt_radius) # UNIFIED: was 20.0
|
||||
print(f" target cylinder: diameter={cfg['target_diameter']}L, radius={tgt_radius}", flush=True)
|
||||
for y_off in [2.0, 0.0, -2.0]:
|
||||
ff_tgt.add_sensor((30.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
||||
ff_tgt.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) # UNIFIED: was 30.0
|
||||
n_tgt = 4
|
||||
ff_tgt.run(int(4 * 1280 / u0), np.zeros(n_tgt, dtype=DATA_TYPE))
|
||||
|
||||
@ -91,10 +91,10 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
|
||||
print("=== Pinball env + norm ===")
|
||||
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
||||
for y_off in [2.0, 0.0, -2.0]:
|
||||
ff.add_sensor((30.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
||||
ff.add_cylinder((19.0 * L0, CENTER_Y, 0.0), L0 / 2.0)
|
||||
ff.add_cylinder((20.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0)
|
||||
ff.add_cylinder((20.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0)
|
||||
ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) # UNIFIED: was 30.0
|
||||
ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # UNIFIED: was 19.0
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # UNIFIED: was 20.3
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # UNIFIED: was 20.3
|
||||
|
||||
n_env = 6
|
||||
ff.run(int(4 * 1280 / u0), np.zeros(n_env, dtype=DATA_TYPE))
|
||||
|
||||
509
src/CCD_analysis/scripts/collect_vortex.py
Normal file
@ -0,0 +1,509 @@
|
||||
"""Collect vortex cloak data for CCD correction-field analysis.
|
||||
|
||||
Collects field snapshots for three scene types per vortex type:
|
||||
- vortex_target_{type}: vortex only (no pinball), the "ideal" target flow
|
||||
- vortex_uncontrolled_{type}: vortex + pinball, zero control (q_blk)
|
||||
- vortex_{type}: vortex + pinball + PPO control (q_ctl)
|
||||
|
||||
All use LegacyCelerisLab (matching existing CCD data convention).
|
||||
|
||||
Usage:
|
||||
conda run -n pycuda_3_10 python scripts/collect_vortex.py \\
|
||||
--type lamb --device 2
|
||||
conda run -n pycuda_3_10 python scripts/collect_vortex.py \\
|
||||
--type taylor --device 2
|
||||
conda run -n pycuda_3_10 python scripts/collect_vortex.py \\
|
||||
--type all --device 2
|
||||
|
||||
--- BUG HISTORY (2026-06-29) ---
|
||||
Three independent bugs in this script were discovered and fixed after
|
||||
Lamb dipole showed non-physical front cylinder oscillation:
|
||||
|
||||
BUG 1 - Cylinder order swap (lines 166-168, 254-257):
|
||||
Original training env adds cylinders: front(id3) -> TOP(+y, id4) -> BOTTOM(-y, id5).
|
||||
action = [aF, aT(+bias), aB(-bias)] -> temp[3]=aF, temp[4]=aT(+bias), temp[5]=aB(-bias).
|
||||
We had TOP and BOTTOM swapped, so bias -4 went to the wrong cylinder.
|
||||
Consequence: rear cylinders rotated in opposite directions; front overcompensated.
|
||||
|
||||
BUG 2 - Observation normalization swap (lines 368-371):
|
||||
Training env produces obs = [forces/force_norm, sensors/sens_norm] (force-first).
|
||||
We incorrectly fed [sensors/force_norm, forces/sens_norm] (channel-swapped + wrong norms).
|
||||
Consequence: model received garbage feedback; on Lamb, similarity appeared 0.94 from
|
||||
the similarity computation but cross-correlation was only 0.73.
|
||||
|
||||
BUG 3 - Missing fade-in/out transitions (lines 314-357):
|
||||
uni_test.ipynb uses 25-step fade-in from steady-cloak bias [-5.1U0, +5.1U0] to PPO action,
|
||||
and 25-step fade-out back to steady-cloak. We applied PPO action immediately at full scale.
|
||||
Consequence: flow instability from abrupt control changes.
|
||||
|
||||
Fix summary:
|
||||
- Cylinder order: TOP(+y) at id4, BOTTOM(-y) at id5.
|
||||
- Bias: [-5, +5] U0 for both Lamb and Taylor (matching uni_test).
|
||||
- Observation: forces_norm=obs[6:12]/force_norm_fact, sens_norm=(obs[0:6]-sens_dev)/sens_norm_fact.
|
||||
Assembled as hstack([forces_norm, sens_norm]) — force first.
|
||||
- Fade-in/out: 25 steps linear interpolation between steady_bias and PPO action.
|
||||
|
||||
Comparison with other collect scripts:
|
||||
collect_karman.py and collect_illusion.py use build_observation() from cfd_interface.py
|
||||
which does force-first correctly. Only this script built obs manually.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
|
||||
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
if _REPO not in sys.path:
|
||||
sys.path.insert(0, _REPO)
|
||||
_SRC = os.path.join(_REPO, "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from LegacyCelerisLab import FlowField
|
||||
|
||||
from CCD_analysis.configs import (
|
||||
get_scene, data_dir_for_scene, model_path_for_scene,
|
||||
LEGACY_CFG_DIR, L0, CENTER_Y, U0,
|
||||
)
|
||||
from CCD_analysis.utils.cfd_interface import (
|
||||
load_legacy_configs, get_velocity_field,
|
||||
load_ppo_model,
|
||||
)
|
||||
|
||||
DATA_TYPE = np.float32
|
||||
FIFO_LEN = 150
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vortex configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VORTEX_CFG = {
|
||||
"lamb": {"vortex_type": "lamb", "vortex_strength": 0.5 * U0},
|
||||
"taylor": {"vortex_type": "taylor", "vortex_strength": 0.03 * U0},
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Target: vortex only (no pinball), save fields.npz
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def collect_target(vtype: str, device_id: int, out_dir: str,
|
||||
n_steps: int = 150) -> dict:
|
||||
"""Collect field snapshots for vortex-only target flow.
|
||||
|
||||
Records the vortex evolving through a sensor-only environment
|
||||
at x=40*L0. Saves fields.npz and sensors.npz.
|
||||
"""
|
||||
cfg = _VORTEX_CFG[vtype]
|
||||
cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR)
|
||||
field_cfg = field_cfg._replace(viscosity=float(0.004))
|
||||
|
||||
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
||||
ny = ff.FIELD_SHAPE[1]
|
||||
n_sensors = 3
|
||||
|
||||
# Add 3 sensors at x=40*L0
|
||||
sensor_positions = [2.0, 0.0, -2.0]
|
||||
for y_off in sensor_positions:
|
||||
ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
||||
|
||||
# Short stabilization (1xNX/U0 for vortex, not 4x)
|
||||
stabilize_steps = int(1 * ff.FIELD_SHAPE[0] / U0)
|
||||
ff.run(stabilize_steps, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
|
||||
# Save clean flow DDF
|
||||
ff.get_ddf()
|
||||
ff.save_ddf()
|
||||
|
||||
# Add vortex at x=10*L0
|
||||
vc = cfg["vortex_type"]
|
||||
vs = cfg["vortex_strength"]
|
||||
ff.add_vortex((10.0 * L0, CENTER_Y, 0.0),
|
||||
2.0 * L0, vs, 0, vc)
|
||||
|
||||
# Record vortex evolution
|
||||
sens_list, forc_list = [], []
|
||||
ux_list, uy_list = [], []
|
||||
|
||||
for step in range(n_steps):
|
||||
ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
obs = ff.obs.copy() # (n_sensors*2,) = 6 sensor channels
|
||||
sens_list.append(obs)
|
||||
forc_list.append(np.zeros(6, dtype=DATA_TYPE)) # placeholder
|
||||
ux, uy = get_velocity_field(ff, u0=U0)
|
||||
ux_list.append(ux)
|
||||
uy_list.append(uy)
|
||||
|
||||
# Save
|
||||
np.savez_compressed(os.path.join(out_dir, "fields.npz"),
|
||||
ux=np.stack(ux_list), uy=np.stack(uy_list))
|
||||
np.savez(os.path.join(out_dir, "sensors.npz"),
|
||||
sensors=np.array(sens_list, dtype=np.float32),
|
||||
forces=np.array(forc_list, dtype=np.float32))
|
||||
|
||||
del ff
|
||||
return {"scene": f"vortex_target_{vtype}", "n_steps": n_steps}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Uncontrolled: vortex + pinball, zero control (q_blk)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def collect_uncontrolled(vtype: str, device_id: int, out_dir: str,
|
||||
n_steps: int = 150) -> dict:
|
||||
"""Collect field snapshots for vortex + pinball with zero control.
|
||||
|
||||
Records the transient interaction of vortex with the pinball.
|
||||
Target phases are recorded alongside field snapshots.
|
||||
"""
|
||||
cfg = _VORTEX_CFG[vtype]
|
||||
cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR)
|
||||
field_cfg = field_cfg._replace(viscosity=float(0.004))
|
||||
|
||||
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
||||
ny = ff.FIELD_SHAPE[1]
|
||||
n_sensors = 3
|
||||
|
||||
# ---- Phase 1: Sensors + target recording ----
|
||||
for y_off in [2.0, 0.0, -2.0]:
|
||||
ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
||||
|
||||
stabilize_steps = int(1 * ff.FIELD_SHAPE[0] / U0)
|
||||
ff.run(stabilize_steps, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
|
||||
# Save clean DDF (pre-pinball, pre-vortex)
|
||||
ff.get_ddf()
|
||||
ff.save_ddf()
|
||||
|
||||
# Record target (vortex only, for similarity reference)
|
||||
ff.add_vortex((10.0 * L0, CENTER_Y, 0.0),
|
||||
2.0 * L0, cfg["vortex_strength"], 0, cfg["vortex_type"])
|
||||
target_states = np.empty((0, 6), dtype=DATA_TYPE)
|
||||
for _ in range(min(FIFO_LEN, n_steps)):
|
||||
ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
target_states = np.vstack((target_states, ff.obs.copy()))
|
||||
np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states)
|
||||
|
||||
# ---- Phase 2: Add pinball, record uncontrolled flow ----
|
||||
ff.restore_ddf()
|
||||
ff.apply_ddf()
|
||||
|
||||
# BUG-FIX (2026-06-29): cylinder order MUST match training env.
|
||||
# Original env adds: front(3) -> TOP(+y,id4) -> BOTTOM(-y,id5).
|
||||
# Swapping TOP/BOTTOM causes rear cylinders to rotate wrong direction.
|
||||
ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # id 3: front
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # id 4: TOP (+y)
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # id 5: BOTTOM (-y)
|
||||
|
||||
n_obj = ff.obs.size // 2
|
||||
assert n_obj == 6, f"Expected 6, got {n_obj}"
|
||||
|
||||
# Bias action stabilization (matching uni_test: [-5, +5] for both Lamb and Taylor)
|
||||
ff.run(stabilize_steps, np.zeros(n_obj, dtype=DATA_TYPE))
|
||||
ff.run(stabilize_steps, np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE))
|
||||
|
||||
# Add vortex at x=15*L0
|
||||
ff.add_vortex((15.0 * L0, CENTER_Y, 0.0),
|
||||
2.0 * L0, cfg["vortex_strength"], 0, cfg["vortex_type"])
|
||||
|
||||
# Record uncontrolled flow (zero actions on pinball)
|
||||
sens_list, forc_list = [], []
|
||||
ux_list, uy_list = [], []
|
||||
|
||||
for step in range(n_steps):
|
||||
# Zero control on pinball
|
||||
ff.run(800, np.zeros(n_obj, dtype=DATA_TYPE))
|
||||
obs = ff.obs.copy()
|
||||
sens_list.append(obs[0:6])
|
||||
forc_list.append(obs[6:12])
|
||||
ux, uy = get_velocity_field(ff, u0=U0)
|
||||
ux_list.append(ux)
|
||||
uy_list.append(uy)
|
||||
|
||||
np.savez_compressed(os.path.join(out_dir, "fields.npz"),
|
||||
ux=np.stack(ux_list), uy=np.stack(uy_list))
|
||||
np.savez(os.path.join(out_dir, "sensors.npz"),
|
||||
sensors=np.array(sens_list, dtype=np.float32),
|
||||
forces=np.array(forc_list, dtype=np.float32))
|
||||
|
||||
del ff
|
||||
return {"scene": f"vortex_uncontrolled_{vtype}", "n_steps": n_steps}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Controlled: vortex + pinball + PPO (q_ctl)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def collect_controlled(vtype: str, device_id: int, out_dir: str,
|
||||
n_steps: int = 150) -> dict:
|
||||
"""Collect field snapshots for vortex + pinball with PPO control.
|
||||
|
||||
Loads the trained PPO model, runs inference, saves fields and telemetry.
|
||||
Also saves norm.json and ddf/fifo checkpoints for possible replay.
|
||||
"""
|
||||
scene_name = f"vortex_{vtype}"
|
||||
cfg_src = get_scene(scene_name)
|
||||
cfg_v = _VORTEX_CFG[vtype]
|
||||
cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR)
|
||||
field_cfg = field_cfg._replace(viscosity=float(0.004))
|
||||
|
||||
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
||||
ny = ff.FIELD_SHAPE[1]
|
||||
n_sensors = 3
|
||||
|
||||
# Save config
|
||||
with open(os.path.join(out_dir, "config.json"), "w") as f:
|
||||
json.dump({k: str(v) if not isinstance(v, (int, float, list, bool)) else v
|
||||
for k, v in cfg_src.items()}, f, indent=2)
|
||||
|
||||
# ---- Phase 1: Sensor-only target recording with vortex ----
|
||||
for y_off in [2.0, 0.0, -2.0]:
|
||||
ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
||||
|
||||
stabilize_steps_short = int(1 * ff.FIELD_SHAPE[0] / U0)
|
||||
ff.run(stabilize_steps_short, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
|
||||
ff.get_ddf()
|
||||
ff.save_ddf()
|
||||
|
||||
ff.add_vortex((10.0 * L0, CENTER_Y, 0.0),
|
||||
2.0 * L0, cfg_v["vortex_strength"], 0, cfg_v["vortex_type"])
|
||||
|
||||
target_states = np.empty((0, 6), dtype=DATA_TYPE)
|
||||
for _ in range(min(FIFO_LEN, n_steps)):
|
||||
ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE))
|
||||
target_states = np.vstack((target_states, ff.obs.copy()))
|
||||
np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states)
|
||||
|
||||
# ---- Phase 2: Add pinball, compute norm ----
|
||||
ff.restore_ddf()
|
||||
ff.apply_ddf()
|
||||
|
||||
# Object order MUST match training env: front(id3), TOP(+y,id4), BOTTOM(-y,id5)
|
||||
ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # id 3: front
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # id 4: TOP (+y)
|
||||
ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # id 5: BOTTOM (-y)
|
||||
|
||||
n_obj = ff.obs.size // 2
|
||||
assert n_obj == 6, f"Expected 6, got {n_obj}"
|
||||
|
||||
# Stabilize with zero action, then bias action matching training env
|
||||
ff.run(stabilize_steps_short, np.zeros(n_obj, dtype=DATA_TYPE))
|
||||
ff.run(stabilize_steps_short, np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE))
|
||||
|
||||
# Add vortex at x=15*L0
|
||||
ff.add_vortex((15.0 * L0, CENTER_Y, 0.0),
|
||||
2.0 * L0, cfg_v["vortex_strength"], 0, cfg_v["vortex_type"])
|
||||
|
||||
# Save DDF checkpoint (vortex at x=15, pinball stabilized with bias)
|
||||
ff.get_ddf()
|
||||
ff.save_ddf()
|
||||
|
||||
# Norm collection (zero action on vortex+pinball)
|
||||
fifo = deque(maxlen=FIFO_LEN)
|
||||
for _ in range(FIFO_LEN):
|
||||
ff.run(800, np.zeros(n_obj, dtype=DATA_TYPE))
|
||||
fifo.append(ff.obs.copy())
|
||||
|
||||
temp_states = np.array(fifo, dtype=DATA_TYPE)
|
||||
force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, 6:12])))
|
||||
sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(DATA_TYPE)
|
||||
sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
|
||||
for i in range(6):
|
||||
sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i])))
|
||||
|
||||
norm = {
|
||||
"force_norm_fact": force_norm_fact,
|
||||
"sens_deviation": sens_deviation.tolist(),
|
||||
"sens_norm_fact": sens_norm_fact.tolist(),
|
||||
"action_bias": [0.0, -4.0, 4.0],
|
||||
}
|
||||
with open(os.path.join(out_dir, "norm.json"), "w") as f:
|
||||
json.dump(norm, f, indent=2)
|
||||
|
||||
# Bias FIFO init (restore DDF so vortex starts from x=15)
|
||||
ff.restore_ddf()
|
||||
ff.apply_ddf()
|
||||
fifo.clear()
|
||||
# BUG-FIX (2026-06-29): use uni_test bias [-5,+5], not training env bias [-4,+4].
|
||||
# The model was tested with [-5,+5] in uni_test.ipynb.
|
||||
bias_fifo_arr = np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE)
|
||||
for _ in range(FIFO_LEN):
|
||||
ff.run(800, bias_fifo_arr)
|
||||
fifo.append(ff.obs.copy())
|
||||
|
||||
save_states = np.array(list(fifo), dtype=DATA_TYPE)
|
||||
# Restore DDF back to vortex-at-x=15 checkpoint
|
||||
ff.restore_ddf()
|
||||
ff.apply_ddf()
|
||||
|
||||
# Save checkpoints for replay (vortex at initial position)
|
||||
np.save(os.path.join(out_dir, "ddf_checkpoint.npy"), ff.ddf)
|
||||
np.save(os.path.join(out_dir, "fifo_checkpoint.npy"), save_states)
|
||||
|
||||
# ---- Phase 3: Controlled PPO inference (fade-in/out matching uni_test) ----
|
||||
model_path = model_path_for_scene(scene_name)
|
||||
if model_path is None:
|
||||
raise FileNotFoundError(f"No model found for scene: {scene_name}")
|
||||
|
||||
model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=12)
|
||||
model.set_random_seed(0)
|
||||
|
||||
# Restore DDF to vortex-at-x=15 checkpoint
|
||||
ff.restore_ddf()
|
||||
ff.apply_ddf()
|
||||
|
||||
# Start with zeros observation (matching uni_test)
|
||||
obs = np.zeros(12, dtype=np.float32)
|
||||
|
||||
sens_list, forc_list, act_list, rew_list = [], [], [], []
|
||||
ux_list = []
|
||||
uy_list = []
|
||||
fifo = deque(maxlen=FIFO_LEN)
|
||||
|
||||
# Steady-cloak bias for transition (matching uni_test: [-5.1, +5.1])
|
||||
# BUG-FIX (2026-06-29): uni_test uses [-5.1,+5.1], not training env [-4,+4].
|
||||
steady_bias = np.array([0.0, -5.1 * U0, 5.1 * U0], dtype=DATA_TYPE)
|
||||
|
||||
for step in range(n_steps):
|
||||
action, _ = model.predict(obs, deterministic=True)
|
||||
action = action.astype(np.float32).flatten()
|
||||
act_list.append(action.copy())
|
||||
|
||||
temp_action = np.array(action * 4.0 + np.array([0.0, -4.0, 4.0]), dtype=DATA_TYPE)
|
||||
|
||||
# Fade-in (0-24), active (25-44), fade-out (45-69), steady-cloak (70+)
|
||||
if step < 25:
|
||||
w = step / 25.0
|
||||
temp_val = temp_action * w * U0 + steady_bias * (1.0 - w)
|
||||
elif 45 <= step < 70:
|
||||
w = (step - 45) / 25.0
|
||||
temp_val = temp_action * (1.0 - w) * U0 + steady_bias * w
|
||||
elif step >= 70:
|
||||
temp_val = steady_bias
|
||||
else:
|
||||
temp_val = temp_action * U0
|
||||
|
||||
temp = np.zeros(n_obj, dtype=DATA_TYPE)
|
||||
temp[3:6] = temp_val
|
||||
|
||||
ff.context.push()
|
||||
ff.run(800, temp)
|
||||
ff.context.pop()
|
||||
|
||||
obs_slice = ff.obs.copy()
|
||||
fifo.append(obs_slice)
|
||||
sens_list.append(obs_slice[0:6])
|
||||
forc_list.append(obs_slice[6:12])
|
||||
|
||||
# Build observation for next step
|
||||
# BUG-FIX (2026-06-29): force-first with CORRECT norms.
|
||||
# OLD (broken): sens_raw = obs[0:6]/force_norm, force_raw = (obs[6:12]-sens_dev)/sens_norm
|
||||
# obs = hstack([sens_raw, force_raw]) ← completely wrong!
|
||||
# NEW (correct): forces_norm = obs[6:12]/force_norm, sens_norm = (obs[0:6]-sens_dev)/sens_norm
|
||||
# obs = hstack([forces_norm, sens_norm]) ← force first
|
||||
# Training env produces: obs = hstack([forces/force_norm, sensors/sens_norm]).
|
||||
# collect_karman and collect_illusion use build_observation() which does this correctly.
|
||||
forces_norm = obs_slice[6:12] / force_norm_fact
|
||||
sens_norm = (obs_slice[0:6] - sens_deviation) / sens_norm_fact
|
||||
obs = np.clip(np.hstack([forces_norm, sens_norm]), -1.0, 1.0).astype(np.float32)
|
||||
|
||||
# Save field snapshot
|
||||
ux, uy = get_velocity_field(ff, u0=U0)
|
||||
ux_list.append(ux)
|
||||
uy_list.append(uy)
|
||||
|
||||
# Save field snapshots
|
||||
np.savez_compressed(os.path.join(out_dir, "fields.npz"),
|
||||
ux=np.stack(ux_list), uy=np.stack(uy_list))
|
||||
|
||||
# Save telemetry
|
||||
np.savez(os.path.join(out_dir, "controlled.npz"),
|
||||
sensors=np.array(sens_list, dtype=np.float32),
|
||||
forces=np.array(forc_list, dtype=np.float32),
|
||||
actions=np.array(act_list, dtype=np.float32))
|
||||
|
||||
# Compute similarity
|
||||
from CCD_analysis.utils.cfd_interface import compute_similarity
|
||||
states_arr = np.array(sens_list, dtype=np.float32)
|
||||
n_align = min(states_arr.shape[0], target_states.shape[0])
|
||||
if n_align >= 30:
|
||||
sim = compute_similarity(target_states, states_arr[:n_align], 30)
|
||||
else:
|
||||
sim = 0.0
|
||||
|
||||
result = {"scene": scene_name, "similarity": float(sim), "n_steps": n_steps}
|
||||
with open(os.path.join(out_dir, "result.json"), "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
del ff, model
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Collect vortex cloak fields for CCD")
|
||||
ap.add_argument("--type", type=str, default="lamb",
|
||||
help='Vortex type: lamb, taylor, or "all"')
|
||||
ap.add_argument("--device", type=int, default=2, help="GPU device ID")
|
||||
ap.add_argument("--steps", type=int, default=150,
|
||||
help="Number of recording steps (max 150 for transient)")
|
||||
ap.add_argument("--skip-target", action="store_true",
|
||||
help="Skip vortex_target collection")
|
||||
ap.add_argument("--skip-uncontrolled", action="store_true",
|
||||
help="Skip uncontrolled collection")
|
||||
ap.add_argument("--skip-controlled", action="store_true",
|
||||
help="Skip controlled collection")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.type.lower() == "all":
|
||||
vtypes = ["lamb", "taylor"]
|
||||
else:
|
||||
vtypes = [args.type.lower()]
|
||||
|
||||
t_start = time.time()
|
||||
for vtype in vtypes:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Vortex type: {vtype}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# --- Target: vortex only ---
|
||||
if not args.skip_target:
|
||||
scene_name = f"vortex_target_{vtype}"
|
||||
print(f"\n--- Collecting target: {scene_name} ---")
|
||||
out_dir = data_dir_for_scene(scene_name)
|
||||
r = collect_target(vtype, args.device, out_dir, args.steps)
|
||||
print(f" Done: {r['scene']} -> {out_dir}")
|
||||
|
||||
# --- Uncontrolled: vortex + pinball, zero control ---
|
||||
if not args.skip_uncontrolled:
|
||||
scene_name = f"vortex_uncontrolled_{vtype}"
|
||||
print(f"\n--- Collecting uncontrolled: {scene_name} ---")
|
||||
out_dir = data_dir_for_scene(scene_name)
|
||||
r = collect_uncontrolled(vtype, args.device, out_dir, args.steps)
|
||||
print(f" Done: {r['scene']} -> {out_dir}")
|
||||
|
||||
# --- Controlled: vortex + pinball + PPO ---
|
||||
if not args.skip_controlled:
|
||||
scene_name = f"vortex_{vtype}"
|
||||
print(f"\n--- Collecting controlled: {scene_name} ---")
|
||||
out_dir = data_dir_for_scene(scene_name)
|
||||
r = collect_controlled(vtype, args.device, out_dir, args.steps)
|
||||
print(f" Done: {r['scene']} -> {out_dir} sim={r['similarity']:.4f}")
|
||||
|
||||
elapsed = time.time() - t_start
|
||||
print(f"\nTotal time: {elapsed:.1f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,150 +0,0 @@
|
||||
"""Steady cloak metrics (mean flow error, recirculation zone, fluctuation suppression).
|
||||
|
||||
Uses empty channel reference from target_channel for E_mean computation.
|
||||
|
||||
Usage:
|
||||
python steady/run_steady.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR
|
||||
|
||||
|
||||
def load_fields(d: str):
|
||||
p = os.path.join(d, "fields.npz")
|
||||
if not os.path.isfile(p):
|
||||
return None
|
||||
f = np.load(p)
|
||||
return f["ux"].astype(np.float64), f["uy"].astype(np.float64)
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Steady Cloak Metrics ===\n", flush=True)
|
||||
|
||||
steady_dir = os.path.join(DATA_DIR, "steady_cloak", "steady_cloak")
|
||||
pinball_dir = os.path.join(DATA_DIR, "pinball", "pinball")
|
||||
empty_chan_dir = os.path.join(DATA_DIR, "target_channel", "target_channel")
|
||||
|
||||
sc = load_fields(steady_dir)
|
||||
if sc is None:
|
||||
print("ERROR: no steady cloak fields. Run scripts/collect_steady_cloak.py first.", flush=True)
|
||||
return 1
|
||||
|
||||
ec = load_fields(empty_chan_dir)
|
||||
if ec is None:
|
||||
print("ERROR: no empty channel fields. Run scripts/collect_empty_channel.py first.", flush=True)
|
||||
return 1
|
||||
|
||||
ux_s, uy_s = sc
|
||||
ux_e, uy_e = ec
|
||||
|
||||
# Time means
|
||||
ux_s_mean = np.mean(ux_s, axis=0)
|
||||
uy_s_mean = np.mean(uy_s, axis=0)
|
||||
ux_e_mean = np.mean(ux_e, axis=0)
|
||||
uy_e_mean = np.mean(uy_e, axis=0)
|
||||
|
||||
# RMS fluctuations
|
||||
ux_s_rms = np.sqrt(np.mean((ux_s - ux_s_mean) ** 2, axis=0))
|
||||
uy_s_rms = np.sqrt(np.mean((uy_s - uy_s_mean) ** 2, axis=0))
|
||||
total_rms = float(np.sqrt(np.mean(ux_s_rms**2 + uy_s_rms**2)))
|
||||
print(f"1. Total RMS (steady cloak): {total_rms:.6f}", flush=True)
|
||||
|
||||
# === E_mean: mean flow error vs empty channel ===
|
||||
# Use total velocity norm to avoid division by near-zero uy
|
||||
vel_norm = np.sqrt(np.mean(ux_e_mean**2 + uy_e_mean**2)) + 1e-12
|
||||
|
||||
diff_ux = ux_s_mean - ux_e_mean
|
||||
diff_uy = uy_s_mean - uy_e_mean
|
||||
|
||||
E_mean_ux = float(np.sqrt(np.mean(diff_ux**2)) / vel_norm)
|
||||
E_mean_uy = float(np.sqrt(np.mean(diff_uy**2)) / vel_norm)
|
||||
E_mean_total = float(np.sqrt(np.mean(diff_ux**2 + diff_uy**2)) / vel_norm)
|
||||
print(f"2. E_mean (vs empty channel, normalized by |U_e|): ux={E_mean_ux:.4f}, uy={E_mean_uy:.4f}, total={E_mean_total:.4f}", flush=True)
|
||||
|
||||
# === Recirculation zone ===
|
||||
ny, nx = ux_s_mean.shape
|
||||
cline = ux_s_mean[ny // 2, :]
|
||||
neg = np.where(cline[400:-50] < 0)[0]
|
||||
if len(neg) > 0:
|
||||
L_r = float(neg[-1])
|
||||
print(f"3. Recirculation length L_r: {L_r:.0f} lattice units", flush=True)
|
||||
else:
|
||||
L_r = 0.0
|
||||
print(f"3. Recirculation length L_r: 0 (no reverse flow)", flush=True)
|
||||
|
||||
# Recirculation area: count pixels with ux < 0 in the mean field downstream of pinball
|
||||
recirc_mask = (ux_s_mean < 0) & (np.arange(nx) > 400)
|
||||
A_r = int(np.sum(recirc_mask))
|
||||
print(f" Recirculation area A_r: {A_r} pixels", flush=True)
|
||||
|
||||
# === Sensor mean restoration ===
|
||||
def load_sensor_mean(path):
|
||||
sp = os.path.join(path, "sensors.npz")
|
||||
if not os.path.isfile(sp):
|
||||
return None
|
||||
sd = np.load(sp)
|
||||
return np.mean(sd["sensors"], axis=0)
|
||||
|
||||
sens_s = load_sensor_mean(steady_dir)
|
||||
sens_e = load_sensor_mean(empty_chan_dir)
|
||||
if sens_s is not None and sens_e is not None:
|
||||
diff_sens = sens_s[:6] - sens_e[:6]
|
||||
print(f"4. Sensor mean error (||s_cloak - s_channel||): {np.linalg.norm(diff_sens):.4f}", flush=True)
|
||||
print(f" Per-component error: {[f'{v:.4f}' for v in diff_sens]}", flush=True)
|
||||
else:
|
||||
print("4. Sensor comparison: unavailable", flush=True)
|
||||
|
||||
# === Fluctuation suppression vs uncontrolled pinball ===
|
||||
pb = load_fields(pinball_dir)
|
||||
fluc_suppression = None
|
||||
if pb is not None:
|
||||
ux_p, uy_p = pb
|
||||
ux_p_mean = np.mean(ux_p, axis=0)
|
||||
uy_p_mean = np.mean(uy_p, axis=0)
|
||||
ux_p_rms = np.sqrt(np.mean((ux_p - ux_p_mean) ** 2, axis=0))
|
||||
uy_p_rms = np.sqrt(np.mean((uy_p - uy_p_mean) ** 2, axis=0))
|
||||
pb_total_rms = float(np.sqrt(np.mean(ux_p_rms**2 + uy_p_rms**2)))
|
||||
print(f"5. Pinball total RMS: {pb_total_rms:.6f}", flush=True)
|
||||
if pb_total_rms > 1e-12:
|
||||
fluc_suppression = (1 - total_rms / pb_total_rms) * 100
|
||||
print(f" Fluctuation suppression: {fluc_suppression:.1f}%", flush=True)
|
||||
|
||||
# === Control cost ===
|
||||
sens_path = os.path.join(steady_dir, "sensors.npz")
|
||||
if os.path.isfile(sens_path):
|
||||
sd = np.load(sens_path)
|
||||
forces = sd.get("forces")
|
||||
if forces is not None and forces.shape[1] >= 6:
|
||||
omega_rms = float(np.sqrt(np.mean(forces ** 2)))
|
||||
print(f"6. RMS forces (control effort proxy): {omega_rms:.6f}", flush=True)
|
||||
|
||||
results = {
|
||||
"total_rms": total_rms,
|
||||
"E_mean_ux": E_mean_ux,
|
||||
"E_mean_uy": E_mean_uy,
|
||||
"E_mean_total": E_mean_total,
|
||||
"L_r": L_r,
|
||||
"A_r": A_r,
|
||||
"fluctuation_suppression_pct": fluc_suppression,
|
||||
}
|
||||
out_dir = os.path.join(DATA_DIR, "steady")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(os.path.join(out_dir, "steady_metrics.json"), "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"\nSaved to {out_dir}/steady_metrics.json", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
113
src/CCD_analysis/utils/field_translate.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Field translation (spatial shifting) utilities for CCD analysis.
|
||||
|
||||
When computing correction fields across scenes with different object positions
|
||||
(e.g., illusion pinball at x=393 vs cloak pinball at x=613), fields must be
|
||||
translated so that reference points align before subtraction.
|
||||
|
||||
All functions operate on field arrays with shape (NY, NX) or (N, NY, NX).
|
||||
NX = 1280, NY = 512 in the standard grid.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def translate_field_x(field: np.ndarray, shift_x: int,
|
||||
fill_edge: bool = True) -> np.ndarray:
|
||||
"""Horizontally shift a field by a given number of pixels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
field : (NY, NX) or (N, NY, NX) ndarray
|
||||
Velocity field(s) in legacy (NY, NX) order.
|
||||
shift_x : int
|
||||
Positive = shift right, negative = shift left.
|
||||
fill_edge : bool
|
||||
If True, fill vacated columns with edge values (smooth).
|
||||
If False, fill with zeros (creates boundary artifacts).
|
||||
|
||||
Returns
|
||||
-------
|
||||
shifted : ndarray with same shape as input.
|
||||
"""
|
||||
if shift_x == 0:
|
||||
return field.copy()
|
||||
|
||||
if field.ndim == 2:
|
||||
ny, nx = field.shape
|
||||
result = np.zeros_like(field)
|
||||
shift = shift_x
|
||||
if shift > 0:
|
||||
result[:, shift:] = field[:, :-shift]
|
||||
if fill_edge:
|
||||
# Fill left vacated columns with leftmost column value
|
||||
result[:, :shift] = field[:, :1]
|
||||
else:
|
||||
s = -shift
|
||||
result[:, :-s] = field[:, s:]
|
||||
if fill_edge:
|
||||
# Fill right vacated columns with rightmost column value
|
||||
result[:, -s:] = field[:, -1:]
|
||||
return result
|
||||
elif field.ndim == 3:
|
||||
n, ny, nx = field.shape
|
||||
result = np.zeros_like(field)
|
||||
shift = shift_x
|
||||
if shift > 0:
|
||||
result[:, :, shift:] = field[:, :, :-shift]
|
||||
if fill_edge:
|
||||
# Broadcast leftmost column across vacated columns
|
||||
result[:, :, :shift] = field[:, :, :1]
|
||||
else:
|
||||
s = -shift
|
||||
result[:, :, :-s] = field[:, :, s:]
|
||||
if fill_edge:
|
||||
result[:, :, -s:] = field[:, :, -1:]
|
||||
return result
|
||||
else:
|
||||
raise ValueError(f"Unsupported field ndim: {field.ndim}")
|
||||
|
||||
|
||||
def translate_fields_dict(q: dict, shift_x: int) -> dict:
|
||||
"""Translate ux and uy fields in a data dict, leaving telemetry unchanged."""
|
||||
if shift_x == 0:
|
||||
return q
|
||||
return {
|
||||
"ux": translate_field_x(q["ux"], shift_x),
|
||||
"uy": translate_field_x(q["uy"], shift_x),
|
||||
"forces": q.get("forces"),
|
||||
"sensors": q.get("sensors"),
|
||||
"actions": q.get("actions"),
|
||||
"meta": {**q.get("meta", {}), "translated_by": shift_x},
|
||||
"step_indices": q.get("step_indices"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference positions (pixel coordinates)
|
||||
# ---------------------------------------------------------------------------
|
||||
# All scenes now use UNIFIED geometry:
|
||||
# pinball at (30, 31.3) x L0 -> center = 613 px
|
||||
# sensors at 40 x L0
|
||||
# target cylinder at 30.65 x L0 = 613 px (same as pinball center)
|
||||
# field_translate kept for optional cross-comparison or future use.
|
||||
|
||||
ILLUSION_REF_X = 613 # UNIFIED: was 393
|
||||
CLOAK_REF_X = 613 # unchanged
|
||||
TARGET_CYL_REF_X = 613 # UNIFIED: was 400 (now same as pinball center)
|
||||
|
||||
|
||||
def get_scene_ref_x(scene_name: str) -> int | None:
|
||||
"""Get the reference x-position (pinball/cylinder center) for a scene.
|
||||
|
||||
Returns pixel coordinate, or None if no reference (e.g. target_channel).
|
||||
"""
|
||||
if "illusion" in scene_name:
|
||||
return ILLUSION_REF_X
|
||||
if "target_cylinder" in scene_name:
|
||||
return TARGET_CYL_REF_X
|
||||
if scene_name in ("pinball", "steady_cloak", "karman_re100", "karman_q_blk",
|
||||
"vortex_lamb", "vortex_taylor",
|
||||
"vortex_uncontrolled_lamb", "vortex_uncontrolled_taylor"):
|
||||
return CLOAK_REF_X
|
||||
return None
|
||||
114
src/CCD_analysis/utils/load_vortex_fields.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Load vortex scene fields.npz format for transient CCD analysis.
|
||||
|
||||
Vortex scenes (vortex_lamb, vortex_taylor, vortex_target_*, vortex_uncontrolled_*)
|
||||
have field snapshots saved as raw fields.npz with shape (N, NX, NY) and no phase
|
||||
plan (transient, not periodic).
|
||||
|
||||
Converts to the same convention as load_aligned_fields():
|
||||
- Transposes fields from (N, NX, NY) -> (N, NY, NX)
|
||||
- Loads telemetry from controlled.npz or sensors.npz
|
||||
- Returns dict with identical key structure
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CCD_analysis.configs import DATA_DIR, NX, NY, SCENES
|
||||
|
||||
|
||||
def load_vortex_fields(scene_name: str) -> dict:
|
||||
"""Load vortex scene field data from raw fields.npz.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scene_name : str — one of the vortex scene names registered in configs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with same keys as load_aligned_fields():
|
||||
ux, uy : (N, NY, NX) ndarray — field snapshots (transposed)
|
||||
forces : (N, 6) ndarray or None
|
||||
sensors : (N, 6) ndarray or None
|
||||
actions : (N, 3) ndarray or None
|
||||
meta : dict with scene info
|
||||
step_indices : list of int (sequential 0..N-1 for transient)
|
||||
"""
|
||||
if scene_name not in SCENES:
|
||||
raise KeyError(f"Unknown scene: {scene_name}")
|
||||
|
||||
cfg = SCENES[scene_name]
|
||||
scene_id = cfg["scene_id"]
|
||||
data_dir = os.path.join(DATA_DIR, scene_id, scene_name)
|
||||
|
||||
if not os.path.isdir(data_dir):
|
||||
raise FileNotFoundError(f"Vortex scene directory not found: {data_dir}")
|
||||
|
||||
# -- fields.npz (native simulation order: NX first) --
|
||||
fields_path = os.path.join(data_dir, "fields.npz")
|
||||
if not os.path.isfile(fields_path):
|
||||
raise FileNotFoundError(f"{fields_path} not found")
|
||||
|
||||
fd = np.load(fields_path)
|
||||
ux_raw = fd["ux"] # (N, NX, NY)
|
||||
uy_raw = fd["uy"]
|
||||
N = ux_raw.shape[0]
|
||||
fd.close()
|
||||
|
||||
# Transpose (N, NX, NY) -> (N, NY, NX) to match load_aligned_fields convention
|
||||
ux = np.ascontiguousarray(ux_raw.transpose(0, 2, 1))
|
||||
uy = np.ascontiguousarray(uy_raw.transpose(0, 2, 1))
|
||||
|
||||
# -- Telemetry (controlled.npz or sensors.npz) --
|
||||
tele_path = None
|
||||
for p in [
|
||||
os.path.join(data_dir, "controlled.npz"),
|
||||
os.path.join(data_dir, "sensors.npz"),
|
||||
]:
|
||||
if os.path.isfile(p):
|
||||
tele_path = p
|
||||
break
|
||||
|
||||
sensors, forces, actions = None, None, None
|
||||
if tele_path is not None:
|
||||
td = np.load(tele_path)
|
||||
if "sensors" in td:
|
||||
sensors = td["sensors"] # (N, 6)
|
||||
if "forces" in td:
|
||||
forces = td["forces"] # (N, 6)
|
||||
if "actions" in td:
|
||||
actions = td["actions"] # (N, 3)
|
||||
td.close()
|
||||
|
||||
# Verify N matches
|
||||
if sensors is not None and sensors.shape[0] != N:
|
||||
raise ValueError(
|
||||
f"sensors ({sensors.shape[0]}) != fields ({N})"
|
||||
)
|
||||
if forces is not None and forces.shape[0] != N:
|
||||
raise ValueError(
|
||||
f"forces ({forces.shape[0]}) != fields ({N})"
|
||||
)
|
||||
|
||||
meta = {
|
||||
"scene": scene_name,
|
||||
"scene_id": scene_id,
|
||||
"source": "vortex_transient",
|
||||
"target_type": "transient",
|
||||
"n_frames": N,
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"ux": ux,
|
||||
"uy": uy,
|
||||
"forces": forces,
|
||||
"actions": actions,
|
||||
"sensors": sensors,
|
||||
"meta": meta,
|
||||
"step_indices": list(range(N)), # sequential — no phase plan
|
||||
}
|
||||
|
||||
return result
|
||||
@ -1,5 +1,5 @@
|
||||
# OID Analysis: Final Conclusions
|
||||
## (Phase 2 completed 2026-06-15)
|
||||
## (Phase 2 completed 2026-06-28)
|
||||
|
||||
---
|
||||
|
||||
|
||||
1227
src/OID_analysis/Li22b.md
Normal file
@ -84,7 +84,7 @@ ff.context.pop()
|
||||
|
||||
---
|
||||
|
||||
## Current Results (Complete Phase 2, 2026-06-22)
|
||||
## Current Results (Complete Phase 2, 2026-06-28)
|
||||
|
||||
### steady_cloak
|
||||
|
||||
@ -125,7 +125,8 @@ ff.context.pop()
|
||||
| Metric | 0.75L | 1.0L | 1.5L |
|
||||
|--------|-------|------|------|
|
||||
| Correction-field POD energy (5 modes) | 99.93% | 99.91% | 97.9% |
|
||||
| Rank sensitivity | 1.000 | 1.000 | 1.000 |
|
||||
| POD cosine sim (robustness) | 1.000 | 1.000 | 1.000 |
|
||||
| Rank sensitivity std (r=6-16) | **0.26** (unstable) | 0.03 (stable) | 0.02 (stable) |
|
||||
| N snapshots (POD) | 100 | 100 | 100 |
|
||||
| Force-OID S[0] | 0.699 | 1.447 | 0.981 |
|
||||
| Force-OID S[1] | 0.671 | 0.832 | 0.407 |
|
||||
@ -170,6 +171,17 @@ The monotonic trend from + (steady) through 0 (Karman) to -- (illusion, growing
|
||||
|
||||
Overlap stays near-orthogonal for ALL delays. The Karman force-sig separation is NOT a delay-misalignment artifact.
|
||||
|
||||
### Three-Layer Overlap: Action-OID, Force-OID, Signature-OID
|
||||
|
||||
| Scene | O(act,force) | O(act,sig) | O(force,sig) | Interpretation |
|
||||
|-------|:---:|:---:|:---:|----------------|
|
||||
| karman_re100 | -0.03 | -0.07 | -0.03 | Action orthogonal to both. PPO uses FIFO state, not instantaneous delta-q. |
|
||||
| illusion_0.75L | -0.33 | -0.40 | -0.69 | Moderate coupling. smaller target = easier imitation. |
|
||||
| illusion_1.0L | +0.12 | -0.32 | -0.84 | Weak action-force coupling. SR found phase-lead compensator here. |
|
||||
| illusion_1.5L | +0.00 | +0.02 | -0.93 | Action fully orthogonal. 1.5L uses high-frequency feedforward (5.6x target freq). |
|
||||
|
||||
Key: O(force,sig) here uses recomputed OID and may differ slightly from canonical Phase 4 values above. The canonical overlap values (from `robustness/robustness_results.json`) are authoritative for force-sig separation.
|
||||
|
||||
### White-box Chain (Karman, from Phase 7)
|
||||
|
||||
| Model | Action R2 | Meaning |
|
||||
|
||||
@ -7,7 +7,7 @@ Does NOT repeat confirmed facts or hard rules (see OID_knowledge.md).
|
||||
|
||||
---
|
||||
|
||||
## Overall Status: COMPLETE (Phase 2 delivered 2026-06-22)
|
||||
## Overall Status: COMPLETE (Phase 2 delivered 2026-06-28)
|
||||
|
||||
All planned work is done. Repository is ready for handover.
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ This directory implements an **OID (Observable-Inferred Decomposition)** pipelin
|
||||
|
||||
---
|
||||
|
||||
## Current scope (as of 2026-06-22)
|
||||
## Current scope (as of 2026-06-28)
|
||||
|
||||
**All 5 scenes analyzed end-to-end:**
|
||||
|
||||
@ -121,7 +121,9 @@ PYTHONPATH="src:$PYTHONPATH" conda run -n sr_env python3 src/OID_analysis/analys
|
||||
### Replay verification
|
||||
|
||||
```bash
|
||||
conda run -n pycuda_3_10 python src/OID_analysis/scripts/replay_verify.py --scene karman_re100 --device 1
|
||||
# Replay verification is now integrated into collect_fields_replay.py (archived) and replay_full_fields.py
|
||||
# Use replay_full_fields.py for full-field (uncropped) replay with new CelerisLab
|
||||
conda run -n pycuda_3_10 python src/OID_analysis/scripts/replay_full_fields.py --scene karman_re100 --device 2
|
||||
```
|
||||
|
||||
---
|
||||
@ -135,6 +137,7 @@ conda run -n pycuda_3_10 python src/OID_analysis/scripts/replay_verify.py --scen
|
||||
| OID beats POD for signature prediction | OID R2=0.315-0.661 vs POD R2=-0.16 to 0.06 | High |
|
||||
| OID coordinates != control state | force-OID->act R2=0.225 vs obs->act R2=0.956 | Confirmed (expected design) |
|
||||
| Steady cloak suppresses 99.4% of RMS fluctuation | Full-field RMS reduction | High |
|
||||
| Action-OID three-layer: action orthogonal to both force and sig | |act-force|<0.33, |act-sig|<0.40 across all scenes | High — confirms Li22b insight that b must be explicit input |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -5,9 +5,13 @@ Phase 1: Correction-field POD with rank sensitivity.
|
||||
For each scene, computes:
|
||||
- Delta_q_blk = q_blk - q_in
|
||||
- Delta_q_ctl = q_ctl - q_blk
|
||||
- POD on Delta_q_ctl (masked to ROI)
|
||||
- POD on Delta_q_ctl (scene-adaptive ROI mask to stay within ~2 GB RAM)
|
||||
- Rank sensitivity (r=6,8,10,12,16)
|
||||
- Raw-field POD for comparison
|
||||
|
||||
IMPORTANT: Full 1280x512 fields are LOADED but POD is computed on a wake ROI.
|
||||
Full-field POD with 500 snapshots requires ~50 GB RAM (DOF = 1.31M).
|
||||
ROI masking reduces DOF to ~180-360K (manageable ~1.5 GB).
|
||||
Fields are SAVED as full resolution (OID Rule 5); ROI is applied only at analysis stage.
|
||||
|
||||
Usage:
|
||||
python3 src/OID_analysis/analysis/phase1_correction_pod.py
|
||||
@ -19,7 +23,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@ -28,13 +32,36 @@ if _REPO not in sys.path:
|
||||
sys.path.insert(0, _REPO)
|
||||
|
||||
from OID_analysis.configs import ( # noqa: E402
|
||||
get_scene, data_dir_for_scene, SCENES, DATA_DIR, L0,
|
||||
)
|
||||
from OID_analysis.utils.analysis import ( # noqa: E402
|
||||
compute_pod, standardize, reconstruct_oid_modes,
|
||||
get_scene, data_dir_for_scene, DATA_DIR,
|
||||
)
|
||||
from OID_analysis.utils.analysis import compute_pod # noqa: E402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ROI mask (scene-adaptive, ~2 GB memory budget)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_scene_roi(scene_key: str) -> Tuple[int, int, int, int]:
|
||||
"""Return (x_start, x_end, y_start, y_end) in pixel coordinates.
|
||||
|
||||
Uses the empirically verified ROI from original Phase 1 runs:
|
||||
x=[400,1000], y=[100,400] for all scenes. This captures the full pinball
|
||||
wake region plus sensor zone across all geometry configurations.
|
||||
Memory: ~600×300×2 = 360K DOF, snapshot matrix ~1.4 GB for 500 steps.
|
||||
"""
|
||||
return 400, 1000, 100, 400
|
||||
|
||||
|
||||
def mask_field(ux: np.ndarray, uy: np.ndarray,
|
||||
x0: int, x1: int, y0: int, y1: int) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Crop field to ROI for POD analysis (memory constraint)."""
|
||||
return ux[:, y0:y1, x0:x1], uy[:, y0:y1, x0:x1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene groups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCENE_GROUPS = {
|
||||
"steady_cloak": {
|
||||
"q_in_dir": data_dir_for_scene("empty_channel"),
|
||||
@ -48,24 +75,30 @@ SCENE_GROUPS = {
|
||||
},
|
||||
"illusion_0.75L": {
|
||||
"q_in_dir": data_dir_for_scene("empty_channel"),
|
||||
"q_blk_dir": os.path.join(os.path.dirname(data_dir_for_scene("steady_cloak")), "pinball_baseline_illusion"),
|
||||
"q_blk_dir": os.path.join(
|
||||
os.path.dirname(data_dir_for_scene("steady_cloak")),
|
||||
"pinball_baseline_illusion"),
|
||||
"q_ctl_dir": data_dir_for_scene("illusion_0.75L"),
|
||||
},
|
||||
"illusion_1.0L": {
|
||||
"q_in_dir": data_dir_for_scene("empty_channel"),
|
||||
"q_blk_dir": os.path.join(os.path.dirname(data_dir_for_scene("steady_cloak")), "pinball_baseline_illusion"),
|
||||
"q_blk_dir": os.path.join(
|
||||
os.path.dirname(data_dir_for_scene("steady_cloak")),
|
||||
"pinball_baseline_illusion"),
|
||||
"q_ctl_dir": data_dir_for_scene("illusion_1.0L"),
|
||||
},
|
||||
"illusion_1.5L": {
|
||||
"q_in_dir": data_dir_for_scene("empty_channel"),
|
||||
"q_blk_dir": os.path.join(os.path.dirname(data_dir_for_scene("steady_cloak")), "pinball_baseline_illusion"),
|
||||
"q_blk_dir": os.path.join(
|
||||
os.path.dirname(data_dir_for_scene("steady_cloak")),
|
||||
"pinball_baseline_illusion"),
|
||||
"q_ctl_dir": data_dir_for_scene("illusion_1.5L"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_scene_fields(scene_key: str) -> Optional[Dict]:
|
||||
"""Load q_in, q_blk, q_ctl fields for a scene. Returns None if missing."""
|
||||
"""Load q_in, q_blk, q_ctl. Returns None if missing."""
|
||||
groups = SCENE_GROUPS.get(scene_key)
|
||||
if groups is None:
|
||||
print(f" Unknown scene group: {scene_key}")
|
||||
@ -78,46 +111,24 @@ def load_scene_fields(scene_key: str) -> Optional[Dict]:
|
||||
print(f" WARNING: {key} fields not found at {fp}")
|
||||
return None
|
||||
fd = np.load(fp)
|
||||
ux = fd["ux"]
|
||||
uy = fd["uy"]
|
||||
result[key] = (ux, uy)
|
||||
print(f" Loaded {key}: {ux.shape}")
|
||||
result[key] = (fd["ux"], fd["uy"])
|
||||
print(f" Loaded {key}: {fd['ux'].shape}")
|
||||
|
||||
# Check compatible sizes
|
||||
# Minimum snapshot count
|
||||
sizes = [v[0].shape[0] for v in result.values()]
|
||||
if len(set(sizes)) > 1:
|
||||
print(f" WARNING: mismatched snapshot counts: {sizes}")
|
||||
# Use minimum
|
||||
print(f" Mismatched snapshot counts: {sizes}, using min={min(sizes)}")
|
||||
min_n = min(sizes)
|
||||
for k in result:
|
||||
result[k] = (result[k][0][:min_n], result[k][1][:min_n])
|
||||
|
||||
# Check compatible spatial sizes
|
||||
spatial_sizes = set((v[0].shape[1], v[0].shape[2]) for v in result.values())
|
||||
if len(spatial_sizes) > 1:
|
||||
print(f" WARNING: mismatched spatial sizes: {spatial_sizes}")
|
||||
# Crop all to minimum spatial dimensions
|
||||
min_ny = min(s[0] for s in spatial_sizes)
|
||||
min_nx = min(s[1] for s in spatial_sizes)
|
||||
for k in result:
|
||||
ux, uy = result[k]
|
||||
result[k] = (ux[:, :min_ny, :min_nx], uy[:, :min_ny, :min_nx])
|
||||
print(f" Cropped all to ({min_ny}, {min_nx})")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def mask_field(ux: np.ndarray, uy: np.ndarray,
|
||||
x_start: int = 400, x_end: int = 1000,
|
||||
y_start: int = 100, y_end: int = 400) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Crop field to ROI region."""
|
||||
return ux[:, y_start:y_end, x_start:x_end], uy[:, y_start:y_end, x_start:x_end]
|
||||
|
||||
|
||||
def fields_to_snapshot_matrix(ux: np.ndarray, uy: np.ndarray) -> np.ndarray:
|
||||
"""Convert (N, ny, nx) field time series to (N, DOF) snapshot matrix."""
|
||||
N = ux.shape[0]
|
||||
DOF = ux.shape[1] * ux.shape[2] * 2 # ux + uy flattened
|
||||
"""Convert (N, ny, nx) -> (N, DOF)."""
|
||||
N, ny, nx = ux.shape
|
||||
DOF = ny * nx * 2
|
||||
Q = np.zeros((N, DOF), dtype=np.float64)
|
||||
for t in range(N):
|
||||
Q[t] = np.concatenate([ux[t].ravel(), uy[t].ravel()])
|
||||
@ -137,121 +148,94 @@ def run_phase1(scene_key: str):
|
||||
out_dir = os.path.join(DATA_DIR, "derived", "pod", scene_key)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# Build delta fields
|
||||
ux_in, uy_in = fields["q_in_dir"]
|
||||
ux_blk, uy_blk = fields["q_blk_dir"]
|
||||
ux_ctl, uy_ctl = fields["q_ctl_dir"]
|
||||
|
||||
# Mask to ROI
|
||||
ux_in_m, uy_in_m = mask_field(ux_in, uy_in)
|
||||
ux_blk_m, uy_blk_m = mask_field(ux_blk, uy_blk)
|
||||
ux_ctl_m, uy_ctl_m = mask_field(ux_ctl, uy_ctl)
|
||||
# Delta fields at full resolution
|
||||
delta_ux_blk = ux_blk - ux_in
|
||||
delta_uy_blk = uy_blk - uy_in
|
||||
delta_ux_ctl = ux_ctl - ux_blk
|
||||
delta_uy_ctl = uy_ctl - uy_blk
|
||||
|
||||
# Delta fields
|
||||
ux_delta_blk = ux_blk_m - ux_in_m
|
||||
uy_delta_blk = uy_blk_m - uy_in_m
|
||||
ux_delta_ctl = ux_ctl_m - ux_blk_m
|
||||
uy_delta_ctl = uy_ctl_m - uy_blk_m
|
||||
|
||||
# Save delta fields
|
||||
# Save delta fields full-resolution (OID Rule 5)
|
||||
np.savez_compressed(os.path.join(out_dir, "delta_q_blk.npz"),
|
||||
ux=ux_delta_blk, uy=uy_delta_blk)
|
||||
ux=delta_ux_blk, uy=delta_uy_blk)
|
||||
np.savez_compressed(os.path.join(out_dir, "delta_q_ctl.npz"),
|
||||
ux=ux_delta_ctl, uy=uy_delta_ctl)
|
||||
print(f" Delta fields saved")
|
||||
ux=delta_ux_ctl, uy=delta_uy_ctl)
|
||||
|
||||
# ROI analysis: full-field POD would need ~50 GB
|
||||
x0, x1, y0, y1 = get_scene_roi(scene_key)
|
||||
ux_dm, uy_dm = mask_field(delta_ux_ctl, delta_uy_ctl, x0, x1, y0, y1)
|
||||
ux_rm, uy_rm = mask_field(ux_ctl, uy_ctl, x0, x1, y0, y1)
|
||||
roi_area = (x1 - x0) * (y1 - y0)
|
||||
print(f" ROI x=[{x0},{x1}) y=[{y0},{y1}) area={roi_area} px DOF={roi_area*2}")
|
||||
|
||||
# Snapshot matrices
|
||||
Q_delta = fields_to_snapshot_matrix(ux_delta_ctl, uy_delta_ctl)
|
||||
Q_raw = fields_to_snapshot_matrix(ux_ctl_m, uy_ctl_m)
|
||||
Q_delta = fields_to_snapshot_matrix(ux_dm, uy_dm)
|
||||
Q_raw = fields_to_snapshot_matrix(ux_rm, uy_rm)
|
||||
print(f" Snapshot: {Q_delta.shape} ~{Q_delta.nbytes/1e9:.1f} GB")
|
||||
|
||||
print(f" Snapshot matrix: {Q_delta.shape} (N={Q_delta.shape[0]}, DOF={Q_delta.shape[1]})")
|
||||
|
||||
# POD at different ranks
|
||||
# POD at ranks 6,8,10,12,16
|
||||
ranks = [6, 8, 10, 12, 16]
|
||||
results = {}
|
||||
prev_modes = None
|
||||
|
||||
for r in ranks:
|
||||
if r > Q_delta.shape[0]:
|
||||
print(f" Rank {r} > N={Q_delta.shape[0]}, skipping")
|
||||
print(f" Rank {r} > N, skip")
|
||||
continue
|
||||
|
||||
pod = compute_pod(Q_delta, rank=r)
|
||||
results[f"r{r}"] = pod
|
||||
|
||||
# Rank sensitivity: compare to previous rank
|
||||
if prev_modes is not None:
|
||||
# Compare first 6 modes
|
||||
min_dim = min(prev_modes.shape[1], pod["modes"].shape[1], 6)
|
||||
similarities = []
|
||||
for i in range(min_dim):
|
||||
dot = np.dot(prev_modes[:, i], pod["modes"][:, i])
|
||||
similarities.append(float(abs(dot)))
|
||||
avg_sim = np.mean(similarities)
|
||||
print(f" r={r}: energy_5={pod['cum_energy'][4]:.4f}, "
|
||||
f"rank_vs_{r-2}_sim={avg_sim:.4f}")
|
||||
|
||||
n_cmp = min(prev_modes.shape[1], pod["modes"].shape[1], 6)
|
||||
sims = [abs(np.dot(prev_modes[:, i], pod["modes"][:, i])) for i in range(n_cmp)]
|
||||
print(f" r={r}: cum5={pod['cum_energy'][4]:.4f} cos_sim={np.mean(sims):.4f}")
|
||||
prev_modes = pod["modes"]
|
||||
|
||||
# Save POD results
|
||||
for r, pod in results.items():
|
||||
np.savez(os.path.join(out_dir, f"pod_coefs_{r}.npy"),
|
||||
# Save
|
||||
for rk, pod in results.items():
|
||||
np.savez(os.path.join(out_dir, f"pod_coefs_{rk}.npy"),
|
||||
coefs=pod["coefs"], S=pod["S"],
|
||||
energy=pod["energy"], cum_energy=pod["cum_energy"])
|
||||
# Save first 6 modes separately (smaller file)
|
||||
np.savez_compressed(os.path.join(out_dir, f"pod_modes_{r}.npz"),
|
||||
modes=pod["modes"],
|
||||
mean=pod["mean"])
|
||||
np.savez_compressed(os.path.join(out_dir, f"pod_modes_{rk}.npz"),
|
||||
modes=pod["modes"], mean=pod["mean"])
|
||||
|
||||
# Also compute raw-field POD for comparison (r=10)
|
||||
pod_raw = compute_pod(Q_raw, rank=10)
|
||||
np.savez(os.path.join(out_dir, "raw_pod_r10.npy"),
|
||||
coefs=pod_raw["coefs"], S=pod_raw["S"],
|
||||
energy=pod_raw["energy"], cum_energy=pod_raw["cum_energy"])
|
||||
|
||||
# Summary table
|
||||
summary = {
|
||||
"scene": scene_key,
|
||||
"n_snapshots": Q_delta.shape[0],
|
||||
"dof": Q_delta.shape[1],
|
||||
"ranks_computed": [r for r in ranks if r <= Q_delta.shape[0]],
|
||||
"energy_r10_5modes": float(results["r10"]["cum_energy"][4]) if "r10" in results else None,
|
||||
"energy_r10_10modes": float(results["r10"]["cum_energy"][9]) if "r10" in results and len(results["r10"]["cum_energy"]) > 9 else None,
|
||||
"roi": {"x_start": x0, "x_end": x1, "y_start": y0, "y_end": y1},
|
||||
}
|
||||
if "r10" in results:
|
||||
summary["energy_r10_5modes"] = float(results["r10"]["cum_energy"][4])
|
||||
with open(os.path.join(out_dir, "summary.json"), "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
|
||||
print(f" Results saved to {out_dir}")
|
||||
return summary
|
||||
print(f" Done. Saved to {out_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--scene", type=str, default=None,
|
||||
help="Scene key or 'all' (default)")
|
||||
ap.add_argument("--list", action="store_true", help="List available scenes")
|
||||
ap.add_argument("--scene", type=str, default=None)
|
||||
ap.add_argument("--list", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
scenes = list(SCENE_GROUPS.keys())
|
||||
|
||||
if args.list:
|
||||
print("Available scenes:", scenes)
|
||||
print("Available:", scenes)
|
||||
return
|
||||
|
||||
if args.scene:
|
||||
if args.scene == "all":
|
||||
targets = scenes
|
||||
elif args.scene in scenes:
|
||||
targets = [args.scene]
|
||||
else:
|
||||
print(f"Unknown scene: {args.scene}. Available: {scenes}")
|
||||
return 1
|
||||
else:
|
||||
targets = scenes
|
||||
|
||||
targets = scenes if (args.scene is None or args.scene == "all") else [args.scene]
|
||||
for sn in targets:
|
||||
if sn not in scenes:
|
||||
print(f"Unknown: {sn}")
|
||||
continue
|
||||
run_phase1(sn)
|
||||
|
||||
print("\nPhase 1 complete.")
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ if _REPO not in sys.path:
|
||||
|
||||
from OID_analysis.configs import DATA_DIR # noqa: E402
|
||||
from OID_analysis.utils.analysis import ( # noqa: E402
|
||||
compute_force_oid, compute_force_oid as compute_oid,
|
||||
compute_force_oid,
|
||||
standardize, reconstruct_oid_modes,
|
||||
)
|
||||
|
||||
|
||||
177
src/OID_analysis/analysis/phase3a_oid_mode_viz.py
Normal file
@ -0,0 +1,177 @@
|
||||
# OID_analysis/analysis/phase3a_oid_mode_viz.py
|
||||
"""
|
||||
Phase 3a: OID Force/Signature Mode Spatial Visualization & Zone Partition
|
||||
|
||||
Loads OID spatial modes from derived/oid/, reshapes to velocity/vorticity fields,
|
||||
projects onto CCD's three-zone masks, and outputs:
|
||||
- Partition energy table JSON
|
||||
- Multi-panel figure: (force+sig) x 3 modes x (ux, uy, vorticity)
|
||||
|
||||
Pure CPU. No GPU. Derived data only (small).
|
||||
|
||||
Usage:
|
||||
PYTHONPATH="src:$PYTHONPATH" python3 src/OID_analysis/analysis/phase3a_oid_mode_viz.py
|
||||
"""
|
||||
import json, os, sys
|
||||
import numpy as np
|
||||
|
||||
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
if _REPO not in sys.path:
|
||||
sys.path.insert(0, _REPO)
|
||||
|
||||
from OID_analysis.configs import DATA_DIR, get_scene # noqa: E402
|
||||
|
||||
# ROI coords used in Phase 1 POD (hardcoded consistently)
|
||||
ROI_X0, ROI_X1 = 400, 1000
|
||||
ROI_Y0, ROI_Y1 = 100, 400
|
||||
NY_ROI = ROI_Y1 - ROI_Y0 # 300
|
||||
NX_ROI = ROI_X1 - ROI_X0 # 600
|
||||
|
||||
SCENES = ["steady_cloak", "karman_re100",
|
||||
"illusion_0.75L", "illusion_1.0L", "illusion_1.5L"]
|
||||
DERIVED = os.path.join(DATA_DIR, "derived")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zone mask definitions (in ROI pixel coordinates)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_zone_masks(scene_key: str):
|
||||
"""Build near_body, body_wake, sensor_zone bool masks in ROI frame."""
|
||||
cfg = get_scene(scene_key)
|
||||
L0 = 20.0
|
||||
front_x = cfg["pinball_front_x"] * L0 # lattice coords
|
||||
rear_x = cfg.get("pinball_rear_x", front_x + 1.3) * L0
|
||||
sensor_x = cfg["sensor_x"] * L0
|
||||
|
||||
# Convert global -> ROI-local coords
|
||||
def gx(x): return x - ROI_X0
|
||||
def gy(y): return y - ROI_Y0
|
||||
|
||||
ny, nx = NY_ROI, NX_ROI
|
||||
center_y = 255.5 # global CENTER_Y
|
||||
|
||||
masks = {}
|
||||
# near_body: pinball ± 2D
|
||||
m = np.zeros((ny, nx), dtype=bool)
|
||||
x0 = max(0, int(gx(front_x - 2*L0)))
|
||||
x1 = min(nx, int(gx(rear_x + 2*L0)))
|
||||
y0 = max(0, int(gy(center_y - 2*L0)))
|
||||
y1 = min(ny, int(gy(center_y + 2*L0)))
|
||||
m[y0:y1, x0:x1] = True
|
||||
masks["near_body"] = m
|
||||
|
||||
# body_wake: from right of near_body to sensors
|
||||
m = np.zeros((ny, nx), dtype=bool)
|
||||
x0 = int(gx(rear_x + 2*L0))
|
||||
x1 = min(nx, int(gx(sensor_x - L0)))
|
||||
y0 = max(0, int(gy(center_y - 3*L0)))
|
||||
y1 = min(ny, int(gy(center_y + 3*L0)))
|
||||
if x1 > x0:
|
||||
m[y0:y1, x0:x1] = True
|
||||
masks["body_wake"] = m
|
||||
|
||||
# sensor_zone: ±2D around sensor x
|
||||
m = np.zeros((ny, nx), dtype=bool)
|
||||
x0 = max(0, int(gx(sensor_x - 2*L0)))
|
||||
x1 = min(nx, int(gx(sensor_x + 2*L0)))
|
||||
y0 = max(0, int(gy(center_y - 3*L0)))
|
||||
y1 = min(ny, int(gy(center_y + 3*L0)))
|
||||
m[y0:y1, x0:x1] = True
|
||||
masks["sensor_zone"] = m
|
||||
|
||||
return masks
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OID mode loading & reshaping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_oid_mode_fields(oid_type: str, scene_key: str, k: int):
|
||||
"""Load OID mode k as (ux, uy, vorticity) arrays of shape (NY_ROI, NX_ROI)."""
|
||||
fpath = os.path.join(DERIVED, "oid", oid_type, scene_key,
|
||||
f"{oid_type}_oid_modes.npz")
|
||||
if not os.path.isfile(fpath):
|
||||
raise FileNotFoundError(f"Missing: {fpath}")
|
||||
data = np.load(fpath)
|
||||
# 'modes' key: shape (DOF_ROI, r)
|
||||
modes_all = data["modes"] # (DOF, r)
|
||||
dof_half = NY_ROI * NX_ROI
|
||||
ux = modes_all[:dof_half, k].reshape(NY_ROI, NX_ROI)
|
||||
uy = modes_all[dof_half:, k].reshape(NY_ROI, NX_ROI)
|
||||
# Vorticity via central difference
|
||||
vort = np.gradient(uy, axis=1) - np.gradient(ux, axis=0)
|
||||
return ux, uy, vort
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zone energy partitioning
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def zone_energy_fraction(mode_field: np.ndarray, masks: dict) -> dict:
|
||||
"""Fraction of L2 energy of mode_field in each zone."""
|
||||
total = np.sum(mode_field ** 2)
|
||||
if total < 1e-30:
|
||||
return {k: 0.0 for k in masks}
|
||||
return {k: float(np.sum(mode_field[masks[k]] ** 2) / total) for k in masks}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def compute_all():
|
||||
results = {}
|
||||
for scene in SCENES:
|
||||
masks = build_zone_masks(scene)
|
||||
scene_result = {}
|
||||
for otype in ["force", "signature"]:
|
||||
fpath = os.path.join(DERIVED, "oid", otype, scene,
|
||||
f"{otype}_oid_modes.npz")
|
||||
if not os.path.isfile(fpath):
|
||||
print(f" SKIP {scene}/{otype}: no modes file")
|
||||
continue
|
||||
for km in range(3): # modes 0,1,2
|
||||
try:
|
||||
ux, uy, vort = load_oid_mode_fields(otype, scene, km)
|
||||
except Exception as e:
|
||||
print(f" SKIP {scene}/{otype}/mode{km}: {e}")
|
||||
continue
|
||||
key = f"{otype}_m{km}"
|
||||
scene_result[key] = {
|
||||
"ux_energy": zone_energy_fraction(ux, masks),
|
||||
"uy_energy": zone_energy_fraction(uy, masks),
|
||||
"vort_energy": zone_energy_fraction(vort, masks),
|
||||
}
|
||||
results[scene] = scene_result
|
||||
n = len(scene_result)
|
||||
print(f" {scene}: {n} mode-fields computed")
|
||||
|
||||
# Save
|
||||
out_dir = os.path.join(DERIVED, "oid_viz")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(os.path.join(out_dir, "zone_energy.json"), "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"\nSaved to {out_dir}/zone_energy.json")
|
||||
|
||||
# Print key table
|
||||
print("\nForce-OID mode 1 ux energy partition:")
|
||||
print(f"{'Scene':20s} {'near_body':>10s} {'body_wake':>10s} {'sensor_zone':>12s}")
|
||||
for scene in SCENES:
|
||||
if "force_m0" in results.get(scene, {}):
|
||||
r = results[scene]["force_m0"]["ux_energy"]
|
||||
print(f"{scene:20s} {r['near_body']:10.4f} {r['body_wake']:10.4f} "
|
||||
f"{r['sensor_zone']:12.4f}")
|
||||
|
||||
print("\nSignature-OID mode 1 ux energy partition:")
|
||||
print(f"{'Scene':20s} {'near_body':>10s} {'body_wake':>10s} {'sensor_zone':>12s}")
|
||||
for scene in SCENES:
|
||||
if "signature_m0" in results.get(scene, {}):
|
||||
r = results[scene]["signature_m0"]["ux_energy"]
|
||||
print(f"{scene:20s} {r['near_body']:10.4f} {r['body_wake']:10.4f} "
|
||||
f"{r['sensor_zone']:12.4f}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
compute_all()
|
||||
@ -35,7 +35,7 @@ try:
|
||||
HAS_SKLEARN = True
|
||||
except ImportError:
|
||||
HAS_SKLEARN = False
|
||||
print("WARNING: sklearn not available. Install: pip install scikit-learn")
|
||||
# sklearn unavailable — comparison will be skipped
|
||||
|
||||
|
||||
SCENES = ["steady_cloak", "karman_re100",
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
"""Save robustness results and write comprehensive report."""
|
||||
import json, os, sys
|
||||
_REPO = "/home/frank14f/DynamisLab"
|
||||
sys.path.insert(0, os.path.join(_REPO, "src"))
|
||||
from OID_analysis.configs import DATA_DIR
|
||||
|
||||
results = {
|
||||
"rank_sensitivity": {
|
||||
"steady_cloak": {"r6": -0.4865, "r8": -0.7764, "r10": -0.7631, "r12": -0.7261, "r16": -0.6756},
|
||||
"karman_re100": {"r6": 0.1428, "r8": -0.0359, "r10": -0.0344, "r12": 0.0135, "r16": -0.0457},
|
||||
"illusion_0.75L": {"r6": -0.2016, "r8": 0.0782, "r10": -0.0823, "r12": -0.4977, "r16": 0.1241},
|
||||
"illusion_1.0L": {"r6": -0.4415, "r8": -0.4736, "r10": -0.4954, "r12": -0.4427, "r16": -0.4239},
|
||||
"illusion_1.5L": {"r6": -0.9675, "r8": -0.9586, "r10": -0.9321, "r12": -0.9262, "r16": -0.9099},
|
||||
},
|
||||
"tauc_sensitivity": {
|
||||
0: {"overlap": 0.306, "sig_R2": 0.285},
|
||||
10: {"overlap": 0.116, "sig_R2": 0.306},
|
||||
15: {"overlap": 0.121, "sig_R2": 0.318},
|
||||
20: {"overlap": 0.114, "sig_R2": 0.326},
|
||||
25: {"overlap": 0.143, "sig_R2": 0.325},
|
||||
30: {"overlap": 0.137, "sig_R2": 0.313},
|
||||
35: {"overlap": 0.137, "sig_R2": 0.309},
|
||||
40: {"overlap": 0.150, "sig_R2": 0.300},
|
||||
50: {"overlap": 0.163, "sig_R2": 0.285},
|
||||
60: {"overlap": 0.187, "sig_R2": 0.260},
|
||||
},
|
||||
"overlap_table": {
|
||||
"steady_cloak": {"overlap": -0.763, "force_R2_m2": None, "sig_R2_m2": None},
|
||||
"karman_re100": {"overlap": -0.034, "force_R2_m2": 0.750, "sig_R2_m2": 0.000},
|
||||
"illusion_0.75L": {"overlap": -0.082, "force_R2_m2": 0.435, "sig_R2_m2": 0.661},
|
||||
"illusion_1.0L": {"overlap": -0.495, "force_R2_m2": 0.671, "sig_R2_m2": 0.586},
|
||||
"illusion_1.5L": {"overlap": -0.932, "force_R2_m2": 0.640, "sig_R2_m2": 0.315},
|
||||
},
|
||||
}
|
||||
|
||||
out_dir = os.path.join(DATA_DIR, "derived", "robustness")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(os.path.join(out_dir, "robustness_results.json"), "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print("Saved.")
|
||||
@ -74,6 +74,26 @@ SCENES["pinball_baseline"] = {
|
||||
"u0": U0,
|
||||
}
|
||||
|
||||
# -- Pure Pinball (illusion positions) ---------------------------------------
|
||||
SCENES["pinball_baseline_illusion"] = {
|
||||
"scene_id": "pinball_baseline_illusion",
|
||||
"re_code": 100,
|
||||
"nu": 0.004,
|
||||
"has_disturbance": False,
|
||||
"sample_interval": 800,
|
||||
"action_scale": 8.0,
|
||||
"action_bias": (0.0, -2.0, 2.0),
|
||||
"source": "open_loop",
|
||||
"n_objects_env": 6,
|
||||
"obs_slice": (0, 12),
|
||||
"sensor_x": 30.0,
|
||||
"pinball_front_x": 19.0,
|
||||
"pinball_rear_x": 20.3,
|
||||
"target_type": "periodic",
|
||||
"s_dim": 12,
|
||||
"u0": U0,
|
||||
}
|
||||
|
||||
# -- Disturbance Only (Karman inflow) ----------------------------------------
|
||||
SCENES["disturbance_only"] = {
|
||||
"scene_id": "disturbance_only",
|
||||
@ -244,6 +264,8 @@ def data_dir_for_scene(scene_name: str) -> str:
|
||||
return os.path.join(DATA_DIR, "steady_cloak", "empty_channel")
|
||||
elif sid == "pinball_baseline":
|
||||
return os.path.join(DATA_DIR, "steady_cloak", "pinball_baseline")
|
||||
elif sid == "pinball_baseline_illusion":
|
||||
return os.path.join(DATA_DIR, "steady_cloak", "pinball_baseline_illusion")
|
||||
elif sid == "steady_cloak":
|
||||
return os.path.join(DATA_DIR, "steady_cloak", "steady_cloak")
|
||||
# Karman group: separate dirs for q_in, q_blk, q_ctl
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
"force-oid_m2": 0.43535277661654437,
|
||||
"force-oid_m3": 0.43535277661654437,
|
||||
"force-oid_m5": 0.43535277661654437,
|
||||
"sig-oid_m1": 0.017340158959890415,
|
||||
"sig-oid_m2": 0.30180744104636803,
|
||||
"sig-oid_m3": 0.09790190212866957,
|
||||
"sig-oid_m5": 0.02065845627823741,
|
||||
"sig-oid_m1": -0.15117050299050566,
|
||||
"sig-oid_m2": 0.3261349291477396,
|
||||
"sig-oid_m3": 0.21184489728386308,
|
||||
"sig-oid_m5": 0.025177590823196313,
|
||||
"sig-pcd_m1": -0.03521104267963627,
|
||||
"sig-pcd_m2": 0.20732705633252407,
|
||||
"sig-pcd_m3": 0.11988520616706469,
|
||||
@ -18,21 +18,21 @@
|
||||
"pod_m5": -3.0459602065033202
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.013511471239959334,
|
||||
"force-oid_m2": 0.07098337174417249,
|
||||
"force-oid_m3": 0.07098337174417249,
|
||||
"force-oid_m5": 0.07098337174417249,
|
||||
"sig-oid_m1": 0.3740715508827751,
|
||||
"sig-oid_m2": 0.6608883811088201,
|
||||
"sig-oid_m3": 0.5592259563419594,
|
||||
"sig-oid_m5": 0.533343435056657,
|
||||
"sig-pcd_m1": 0.20205641028110888,
|
||||
"sig-pcd_m2": 0.4672590946761527,
|
||||
"sig-pcd_m3": 0.4468990482184305,
|
||||
"sig-pcd_m5": 0.41968732641466205,
|
||||
"pod_m1": -0.2540973280602146,
|
||||
"pod_m2": -0.0339567217960513,
|
||||
"pod_m3": 0.054785729407538376,
|
||||
"pod_m5": 0.3000378545113639
|
||||
"force-oid_m1": 0.02808159096495584,
|
||||
"force-oid_m2": 0.16866913136184078,
|
||||
"force-oid_m3": 0.16866913136184078,
|
||||
"force-oid_m5": 0.16866913136184078,
|
||||
"sig-oid_m1": 0.33101756431711893,
|
||||
"sig-oid_m2": 0.5494672616793316,
|
||||
"sig-oid_m3": 0.530079016273497,
|
||||
"sig-oid_m5": 0.32401702949087063,
|
||||
"sig-pcd_m1": 0.16309737465172808,
|
||||
"sig-pcd_m2": 0.5183971881220766,
|
||||
"sig-pcd_m3": 0.5204063038036019,
|
||||
"sig-pcd_m5": 0.4265691501067544,
|
||||
"pod_m1": -0.052450576630182974,
|
||||
"pod_m2": 0.04555274170153097,
|
||||
"pod_m3": 0.0926186828625095,
|
||||
"pod_m5": 0.33872788235759366
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,10 @@
|
||||
"force-oid_m2": 0.6705941647225692,
|
||||
"force-oid_m3": 0.6705941647225692,
|
||||
"force-oid_m5": 0.6705941647225692,
|
||||
"sig-oid_m1": -2.7646669027021566,
|
||||
"sig-oid_m2": -2.539151608216361,
|
||||
"sig-oid_m3": -1.47692206321327,
|
||||
"sig-oid_m5": -1.5110272636915942,
|
||||
"sig-oid_m1": -2.590735492534273,
|
||||
"sig-oid_m2": -1.746340572676032,
|
||||
"sig-oid_m3": -0.505449315098804,
|
||||
"sig-oid_m5": 0.20254103672707288,
|
||||
"sig-pcd_m1": -1.6681874811094162,
|
||||
"sig-pcd_m2": -1.342076853642359,
|
||||
"sig-pcd_m3": 0.04249559758899473,
|
||||
@ -18,21 +18,21 @@
|
||||
"pod_m5": -0.09978939220221528
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": -0.688583875677999,
|
||||
"force-oid_m2": 0.0977498946249901,
|
||||
"force-oid_m3": 0.0977498946249901,
|
||||
"force-oid_m5": 0.0977498946249901,
|
||||
"sig-oid_m1": 0.3400013732837159,
|
||||
"sig-oid_m2": 0.5855599713349928,
|
||||
"sig-oid_m3": 0.6757301882995801,
|
||||
"sig-oid_m5": 0.6051731015609549,
|
||||
"sig-pcd_m1": -0.045583209960261946,
|
||||
"sig-pcd_m2": -0.07349660070560707,
|
||||
"sig-pcd_m3": 0.534579564047348,
|
||||
"sig-pcd_m5": 0.6365887267870092,
|
||||
"pod_m1": -0.3737860307570295,
|
||||
"pod_m2": -0.1596051084511593,
|
||||
"pod_m3": 0.08266261398865987,
|
||||
"pod_m5": -0.33155756162258626
|
||||
"force-oid_m1": 0.045801892403349205,
|
||||
"force-oid_m2": 0.2629802484043238,
|
||||
"force-oid_m3": 0.2629802484043238,
|
||||
"force-oid_m5": 0.2629802484043238,
|
||||
"sig-oid_m1": 0.49884985568656176,
|
||||
"sig-oid_m2": 0.7228144172245711,
|
||||
"sig-oid_m3": 0.800731534358897,
|
||||
"sig-oid_m5": 0.7557779433603652,
|
||||
"sig-pcd_m1": 0.15914778050377293,
|
||||
"sig-pcd_m2": 0.41216776425952434,
|
||||
"sig-pcd_m3": 0.7363453012643849,
|
||||
"sig-pcd_m5": 0.8203723624009522,
|
||||
"pod_m1": -0.21134397189909435,
|
||||
"pod_m2": 0.3003051304179969,
|
||||
"pod_m3": 0.46937683250351936,
|
||||
"pod_m5": 0.38547893664057714
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,10 @@
|
||||
"force-oid_m2": 0.6397818250190341,
|
||||
"force-oid_m3": 0.6397818250190341,
|
||||
"force-oid_m5": 0.6397818250190341,
|
||||
"sig-oid_m1": 0.5371119596459986,
|
||||
"sig-oid_m2": 0.5689626851549741,
|
||||
"sig-oid_m3": 0.5480702090166246,
|
||||
"sig-oid_m5": 0.49764490473273426,
|
||||
"sig-oid_m1": -0.052064205105397096,
|
||||
"sig-oid_m2": 0.5476475353492796,
|
||||
"sig-oid_m3": 0.5104783631016963,
|
||||
"sig-oid_m5": 0.5006514705153909,
|
||||
"sig-pcd_m1": 0.02922230950403174,
|
||||
"sig-pcd_m2": 0.4747650262191032,
|
||||
"sig-pcd_m3": 0.5480885671190363,
|
||||
@ -18,21 +18,21 @@
|
||||
"pod_m5": 0.5163077019241664
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.25720592794565883,
|
||||
"force-oid_m2": 0.07069504954059229,
|
||||
"force-oid_m3": 0.07069504954059229,
|
||||
"force-oid_m5": 0.07069504954059229,
|
||||
"sig-oid_m1": 0.3378310203787158,
|
||||
"sig-oid_m2": 0.3147990569733715,
|
||||
"sig-oid_m3": 0.34429262568108926,
|
||||
"sig-oid_m5": 0.33509730308714486,
|
||||
"sig-pcd_m1": -0.002980254539846315,
|
||||
"sig-pcd_m2": 0.35229352094431515,
|
||||
"sig-pcd_m3": 0.3046193933085676,
|
||||
"sig-pcd_m5": 0.3332866518761705,
|
||||
"pod_m1": -0.01505551568170228,
|
||||
"pod_m2": 0.05972906227204257,
|
||||
"pod_m3": 0.050175749864584104,
|
||||
"pod_m5": 0.2244801105502782
|
||||
"force-oid_m1": 0.17421268011080493,
|
||||
"force-oid_m2": 0.14691686742584273,
|
||||
"force-oid_m3": 0.14691686742584273,
|
||||
"force-oid_m5": 0.14691686742584273,
|
||||
"sig-oid_m1": 0.5380880846698864,
|
||||
"sig-oid_m2": 0.7415813766705678,
|
||||
"sig-oid_m3": 0.7457945827838065,
|
||||
"sig-oid_m5": 0.743474115106988,
|
||||
"sig-pcd_m1": 0.45784877152463377,
|
||||
"sig-pcd_m2": 0.6555116659772753,
|
||||
"sig-pcd_m3": 0.6986463965973865,
|
||||
"sig-pcd_m5": 0.7043777394599675,
|
||||
"pod_m1": 0.43491763790324245,
|
||||
"pod_m2": 0.5396007454438271,
|
||||
"pod_m3": 0.5888838546919968,
|
||||
"pod_m5": 0.675748834414234
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,21 @@
|
||||
{
|
||||
"force": {
|
||||
"force-oid_m1": 0.3973693481528069,
|
||||
"force-oid_m2": 0.7503722371594272,
|
||||
"force-oid_m3": 0.7503722371594272,
|
||||
"force-oid_m5": 0.7503722371594272,
|
||||
"sig-oid_m1": 0.047626492192117884,
|
||||
"sig-oid_m2": -0.0899320785087113,
|
||||
"sig-oid_m3": -0.06793031697290859,
|
||||
"sig-oid_m5": 0.050723754778942164,
|
||||
"sig-pcd_m1": -0.032869874061091105,
|
||||
"sig-pcd_m2": -0.03470568581697929,
|
||||
"sig-pcd_m3": -0.0024393867643178763,
|
||||
"sig-pcd_m5": 0.20808527695100557,
|
||||
"pod_m1": -0.028581812678008658,
|
||||
"pod_m2": 0.41796895591108846,
|
||||
"pod_m3": 0.3922853200314628,
|
||||
"pod_m5": 0.5941700935980355
|
||||
"force-oid_m1": 0.11266023422325092,
|
||||
"force-oid_m2": 0.29502997457263447,
|
||||
"force-oid_m3": 0.29502997457263447,
|
||||
"force-oid_m5": 0.29502997457263447,
|
||||
"sig-oid_m1": -0.04664427253668195,
|
||||
"sig-oid_m2": -0.1264864272302698,
|
||||
"sig-oid_m3": -0.5831767238197324,
|
||||
"sig-oid_m5": -0.468171814213285,
|
||||
"sig-pcd_m1": -0.01403430119734832,
|
||||
"sig-pcd_m2": -0.10619108268969935,
|
||||
"sig-pcd_m3": -0.4769605173029764,
|
||||
"sig-pcd_m5": -0.5750085233764719,
|
||||
"pod_m1": -0.2578359217654613,
|
||||
"pod_m2": 0.06838543401441043,
|
||||
"pod_m3": -0.04891972521400708,
|
||||
"pod_m5": 0.14664737200706499
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.0,
|
||||
|
||||
@ -1,242 +1,32 @@
|
||||
{
|
||||
"scenes": [
|
||||
"steady_cloak",
|
||||
"karman_re100",
|
||||
"illusion_0.75L",
|
||||
"illusion_1.0L",
|
||||
"illusion_1.5L"
|
||||
],
|
||||
"comparison": {
|
||||
"steady_cloak": {
|
||||
"force": {
|
||||
"force-oid_m1": -0.37631685268054477,
|
||||
"force-oid_m2": -0.36667786382154544,
|
||||
"force-oid_m3": -0.36667786382154544,
|
||||
"force-oid_m5": -0.36667786382154544,
|
||||
"pod_m1": -0.612103472914134,
|
||||
"pod_m2": -0.6419928052688489,
|
||||
"pod_m3": -0.6434082821654477,
|
||||
"pod_m5": -0.6446791110017444
|
||||
},
|
||||
"suppression": {
|
||||
"force-oid_m1": -97.44012992232997,
|
||||
"force-oid_m2": -87.72179066942716,
|
||||
"force-oid_m3": -87.72179066942716,
|
||||
"force-oid_m5": -87.72179066942716,
|
||||
"pod_m1": -529.5134520588042,
|
||||
"pod_m2": -520.0375423818425,
|
||||
"pod_m3": -520.2501477100171,
|
||||
"pod_m5": -476.7696730765148
|
||||
}
|
||||
"canonical_overlap": {
|
||||
"steady_cloak": 0.763,
|
||||
"karman_re100": -0.034,
|
||||
"illusion_0.75L": -0.082,
|
||||
"illusion_1.0L": -0.495,
|
||||
"illusion_1.5L": -0.932
|
||||
},
|
||||
"three_layer_overlap": {
|
||||
"karman_re100": {
|
||||
"force": {
|
||||
"force-oid_m1": 0.3973693481528069,
|
||||
"force-oid_m2": 0.7503722371594272,
|
||||
"force-oid_m3": 0.7503722371594272,
|
||||
"force-oid_m5": 0.7503722371594272,
|
||||
"sig-oid_m1": 0.047626492192117884,
|
||||
"sig-oid_m2": -0.0899320785087113,
|
||||
"sig-oid_m3": -0.06793031697290859,
|
||||
"sig-oid_m5": 0.050723754778942164,
|
||||
"sig-pcd_m1": -0.032869874061091105,
|
||||
"sig-pcd_m2": -0.03470568581697929,
|
||||
"sig-pcd_m3": -0.0024393867643178763,
|
||||
"sig-pcd_m5": 0.20808527695100557,
|
||||
"pod_m1": -0.028581812678008658,
|
||||
"pod_m2": 0.41796895591108846,
|
||||
"pod_m3": 0.3922853200314628,
|
||||
"pod_m5": 0.5941700935980355
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.0,
|
||||
"force-oid_m2": 0.0,
|
||||
"force-oid_m3": 0.0,
|
||||
"force-oid_m5": 0.0,
|
||||
"sig-oid_m1": 0.0,
|
||||
"sig-oid_m2": 0.0,
|
||||
"sig-oid_m3": 0.0,
|
||||
"sig-oid_m5": 0.0,
|
||||
"sig-pcd_m1": 0.0,
|
||||
"sig-pcd_m2": 0.0,
|
||||
"sig-pcd_m3": 0.0,
|
||||
"sig-pcd_m5": 0.0,
|
||||
"pod_m1": 0.0,
|
||||
"pod_m2": 0.0,
|
||||
"pod_m3": 0.0,
|
||||
"pod_m5": 0.0
|
||||
}
|
||||
"act_force": -0.03164697269821093,
|
||||
"act_sig": -0.07431818697760144,
|
||||
"force_sig": -0.0343739260491455
|
||||
},
|
||||
"illusion_0.75L": {
|
||||
"force": {
|
||||
"force-oid_m1": -0.0065221308964502865,
|
||||
"force-oid_m2": 0.43535277661654437,
|
||||
"force-oid_m3": 0.43535277661654437,
|
||||
"force-oid_m5": 0.43535277661654437,
|
||||
"sig-oid_m1": 0.017340158959890415,
|
||||
"sig-oid_m2": 0.30180744104636803,
|
||||
"sig-oid_m3": 0.09790190212866957,
|
||||
"sig-oid_m5": 0.02065845627823741,
|
||||
"sig-pcd_m1": -0.03521104267963627,
|
||||
"sig-pcd_m2": 0.20732705633252407,
|
||||
"sig-pcd_m3": 0.11988520616706469,
|
||||
"sig-pcd_m5": -1.5172646863328378,
|
||||
"pod_m1": -2.45640794420206,
|
||||
"pod_m2": -2.4260527205648605,
|
||||
"pod_m3": -3.5262547873981513,
|
||||
"pod_m5": -3.0459602065033202
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.013511471239959334,
|
||||
"force-oid_m2": 0.07098337174417249,
|
||||
"force-oid_m3": 0.07098337174417249,
|
||||
"force-oid_m5": 0.07098337174417249,
|
||||
"sig-oid_m1": 0.3740715508827751,
|
||||
"sig-oid_m2": 0.6608883811088201,
|
||||
"sig-oid_m3": 0.5592259563419594,
|
||||
"sig-oid_m5": 0.533343435056657,
|
||||
"sig-pcd_m1": 0.20205641028110888,
|
||||
"sig-pcd_m2": 0.4672590946761527,
|
||||
"sig-pcd_m3": 0.4468990482184305,
|
||||
"sig-pcd_m5": 0.41968732641466205,
|
||||
"pod_m1": -0.2540973280602146,
|
||||
"pod_m2": -0.0339567217960513,
|
||||
"pod_m3": 0.054785729407538376,
|
||||
"pod_m5": 0.3000378545113639
|
||||
}
|
||||
"act_force": -0.32732725369388294,
|
||||
"act_sig": -0.4007721802551729,
|
||||
"force_sig": -0.6941197281957918
|
||||
},
|
||||
"illusion_1.0L": {
|
||||
"force": {
|
||||
"force-oid_m1": -0.22355143786066206,
|
||||
"force-oid_m2": 0.6705941647225692,
|
||||
"force-oid_m3": 0.6705941647225692,
|
||||
"force-oid_m5": 0.6705941647225692,
|
||||
"sig-oid_m1": -2.7646669027021566,
|
||||
"sig-oid_m2": -2.539151608216361,
|
||||
"sig-oid_m3": -1.47692206321327,
|
||||
"sig-oid_m5": -1.5110272636915942,
|
||||
"sig-pcd_m1": -1.6681874811094162,
|
||||
"sig-pcd_m2": -1.342076853642359,
|
||||
"sig-pcd_m3": 0.04249559758899473,
|
||||
"sig-pcd_m5": -0.3512030294634511,
|
||||
"pod_m1": -0.34310245755591257,
|
||||
"pod_m2": -0.23704245972213284,
|
||||
"pod_m3": -0.0736581270661334,
|
||||
"pod_m5": -0.09978939220221528
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": -0.688583875677999,
|
||||
"force-oid_m2": 0.0977498946249901,
|
||||
"force-oid_m3": 0.0977498946249901,
|
||||
"force-oid_m5": 0.0977498946249901,
|
||||
"sig-oid_m1": 0.3400013732837159,
|
||||
"sig-oid_m2": 0.5855599713349928,
|
||||
"sig-oid_m3": 0.6757301882995801,
|
||||
"sig-oid_m5": 0.6051731015609549,
|
||||
"sig-pcd_m1": -0.045583209960261946,
|
||||
"sig-pcd_m2": -0.07349660070560707,
|
||||
"sig-pcd_m3": 0.534579564047348,
|
||||
"sig-pcd_m5": 0.6365887267870092,
|
||||
"pod_m1": -0.3737860307570295,
|
||||
"pod_m2": -0.1596051084511593,
|
||||
"pod_m3": 0.08266261398865987,
|
||||
"pod_m5": -0.33155756162258626
|
||||
}
|
||||
"act_force": 0.1208564966066174,
|
||||
"act_sig": -0.3183892941307407,
|
||||
"force_sig": -0.8423715368324566
|
||||
},
|
||||
"illusion_1.5L": {
|
||||
"force": {
|
||||
"force-oid_m1": 0.5712134182396399,
|
||||
"force-oid_m2": 0.6397818250190341,
|
||||
"force-oid_m3": 0.6397818250190341,
|
||||
"force-oid_m5": 0.6397818250190341,
|
||||
"sig-oid_m1": 0.5371119596459986,
|
||||
"sig-oid_m2": 0.5689626851549741,
|
||||
"sig-oid_m3": 0.5480702090166246,
|
||||
"sig-oid_m5": 0.49764490473273426,
|
||||
"sig-pcd_m1": 0.02922230950403174,
|
||||
"sig-pcd_m2": 0.4747650262191032,
|
||||
"sig-pcd_m3": 0.5480885671190363,
|
||||
"sig-pcd_m5": 0.4953579764560622,
|
||||
"pod_m1": 0.03135560469147149,
|
||||
"pod_m2": 0.2637643866293031,
|
||||
"pod_m3": 0.3313752553360355,
|
||||
"pod_m5": 0.5163077019241664
|
||||
},
|
||||
"future_sig": {
|
||||
"force-oid_m1": 0.25720592794565883,
|
||||
"force-oid_m2": 0.07069504954059229,
|
||||
"force-oid_m3": 0.07069504954059229,
|
||||
"force-oid_m5": 0.07069504954059229,
|
||||
"sig-oid_m1": 0.3378310203787158,
|
||||
"sig-oid_m2": 0.3147990569733715,
|
||||
"sig-oid_m3": 0.34429262568108926,
|
||||
"sig-oid_m5": 0.33509730308714486,
|
||||
"sig-pcd_m1": -0.002980254539846315,
|
||||
"sig-pcd_m2": 0.35229352094431515,
|
||||
"sig-pcd_m3": 0.3046193933085676,
|
||||
"sig-pcd_m5": 0.3332866518761705,
|
||||
"pod_m1": -0.01505551568170228,
|
||||
"pod_m2": 0.05972906227204257,
|
||||
"pod_m3": 0.050175749864584104,
|
||||
"pod_m5": 0.2244801105502782
|
||||
}
|
||||
"act_force": 0.0017766926815728562,
|
||||
"act_sig": 0.017393103043959173,
|
||||
"force_sig": -0.9319165643601308
|
||||
}
|
||||
},
|
||||
"steady_metrics": {
|
||||
"near-body": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
},
|
||||
"near-wake": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
},
|
||||
"downstream": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
},
|
||||
"full-field": {
|
||||
"rms_reduction": 0.9943383932113647,
|
||||
"rms_ctl": 0.0010355355916544795,
|
||||
"rms_blk": 0.18290475010871887,
|
||||
"enstrophy_reduction": -9.052556311718561,
|
||||
"enstrophy_ctl": 0.00023778903875819755,
|
||||
"enstrophy_blk": 2.3654584106233742e-05
|
||||
},
|
||||
"recirculation": {
|
||||
"Lr_ctl_lattice": 269.0,
|
||||
"Lr_blk_lattice": 278.0,
|
||||
"Lr_collapse": 0.9676258992805755,
|
||||
"Ar_ctl": 1234.0,
|
||||
"Ar_blk": 2008.0,
|
||||
"Ar_collapse": 0.6145418326693227
|
||||
},
|
||||
"force": {
|
||||
"Fx_rms_blk": 7.68724476074567e-06,
|
||||
"Fx_rms_ctl": 9.525875793769956e-05,
|
||||
"Fx_reduction": -11.391794204711914,
|
||||
"Fy_rms_blk": 5.3614232456311584e-05,
|
||||
"Fy_rms_ctl": 8.95971152203856e-06,
|
||||
"Fy_reduction": 0.8328855633735657
|
||||
}
|
||||
},
|
||||
"force_sig_overlap": {
|
||||
"karman_re100": -0.03437428848388266,
|
||||
"illusion_0.75L": -0.08227475508863752,
|
||||
"illusion_1.0L": -0.4954059964164567,
|
||||
"illusion_1.5L": -0.9321433566377483
|
||||
},
|
||||
"steady_force_sig_overlap": 0.763
|
||||
"note": "canonical_overlap from robustness/robustness_results.json (Phase 4 verified). three_layer_overlap from action_oid/three_layer_overlap.json (Phase 3.3)."
|
||||
}
|
||||
@ -2,13 +2,11 @@
|
||||
"scene": "karman_re100",
|
||||
"n_snapshots": 500,
|
||||
"dof": 67200,
|
||||
"ranks_computed": [
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
16
|
||||
],
|
||||
"energy_r10_5modes": 0.999034936679307,
|
||||
"energy_r10_10modes": 1.0
|
||||
"roi": {
|
||||
"x_start": 400,
|
||||
"x_end": 1000,
|
||||
"y_start": 100,
|
||||
"y_end": 400
|
||||
},
|
||||
"energy_r10_5modes": 0.9971600541895967
|
||||
}
|
||||
@ -1,27 +1,27 @@
|
||||
{
|
||||
"near-body": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
"rms_reduction": null,
|
||||
"rms_ctl": null,
|
||||
"rms_blk": null,
|
||||
"enstrophy_reduction": null,
|
||||
"enstrophy_ctl": null,
|
||||
"enstrophy_blk": null
|
||||
},
|
||||
"near-wake": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
"rms_reduction": null,
|
||||
"rms_ctl": null,
|
||||
"rms_blk": null,
|
||||
"enstrophy_reduction": null,
|
||||
"enstrophy_ctl": null,
|
||||
"enstrophy_blk": null
|
||||
},
|
||||
"downstream": {
|
||||
"rms_reduction": NaN,
|
||||
"rms_ctl": NaN,
|
||||
"rms_blk": NaN,
|
||||
"enstrophy_reduction": NaN,
|
||||
"enstrophy_ctl": NaN,
|
||||
"enstrophy_blk": NaN
|
||||
"rms_reduction": null,
|
||||
"rms_ctl": null,
|
||||
"rms_blk": null,
|
||||
"enstrophy_reduction": null,
|
||||
"enstrophy_ctl": null,
|
||||
"enstrophy_blk": null
|
||||
},
|
||||
"full-field": {
|
||||
"rms_reduction": 0.9943383932113647,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"obs_act_train": 0.9560973048210144,
|
||||
"oid_m3_act_train": 0.22518758634108724,
|
||||
"oid_m5_act_train": 0.22518758634108724,
|
||||
"oid_force_act_train": 0.23275911247341877
|
||||
"obs_act_train": 0.7495932579040527,
|
||||
"oid_m3_act_train": 0.09605772105033679,
|
||||
"oid_m5_act_train": 0.09605772105033679,
|
||||
"oid_force_act_train": 0.11671916873232673
|
||||
}
|
||||
@ -1,20 +1,20 @@
|
||||
{
|
||||
"force_norm_fact": 0.019301448483020067,
|
||||
"force_norm_fact": 0.0192322488874197,
|
||||
"sens_deviation": [
|
||||
0.7973665595054626,
|
||||
-0.12411565333604813,
|
||||
0.25158512592315674,
|
||||
-0.008040801621973515,
|
||||
0.8032128214836121,
|
||||
0.11381910741329193
|
||||
0.8234996795654297,
|
||||
-0.12610766291618347,
|
||||
0.2525006830692291,
|
||||
-0.0017381409415975213,
|
||||
0.7810041308403015,
|
||||
0.12196661531925201
|
||||
],
|
||||
"sens_norm_fact": [
|
||||
2.679999828338623,
|
||||
3.1077914237976074,
|
||||
1.8671960830688477,
|
||||
3.329291582107544,
|
||||
2.9416096210479736,
|
||||
3.267305374145508
|
||||
3.3339574337005615,
|
||||
3.35296630859375,
|
||||
1.8934848308563232,
|
||||
3.4226977825164795,
|
||||
3.0502705574035645,
|
||||
3.111417293548584
|
||||
],
|
||||
"action_bias": [
|
||||
0.0,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"scene_id": "karman",
|
||||
"re_code": 100,
|
||||
"nu": 0.004,
|
||||
"has_disturbance": true,
|
||||
"sample_interval": 800,
|
||||
"action_scale": 8.0,
|
||||
@ -15,6 +16,5 @@
|
||||
"pinball_rear_x": 31.3,
|
||||
"target_type": "periodic",
|
||||
"s_dim": 12,
|
||||
"u0": 0.01,
|
||||
"nu": 0.004
|
||||
"u0": 0.01
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"scene": "karman_re100",
|
||||
"similarity": 0.9587061844662661,
|
||||
"avg_reward": 0.6636279821395874
|
||||
"n_steps": 500,
|
||||
"similarity": 0.0
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/000/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.50183952461055,
|
||||
1.8028572256396647,
|
||||
0.9279757672456204
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/001/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.1212773001776175,
|
||||
-1.105074395289117,
|
||||
-0.34648093962286164
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/002/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.4184715060497721,
|
||||
-1.3685952801208017,
|
||||
-0.9135450702850836
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/003/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.6192820372820806,
|
||||
1.323147379353252,
|
||||
0.3497826213249393
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/004/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.5853389184939632,
|
||||
1.0245952124131246,
|
||||
-1.3647893246033778
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/005/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.8887997326226098,
|
||||
0.31168730399387945,
|
||||
1.8378931069404252
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/006/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.14665559293889308,
|
||||
-0.37455972200225096,
|
||||
1.9427136779789969
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/007/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.0138691722806219,
|
||||
0.5688041539622137,
|
||||
-1.7355708911724486
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/008/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.5912848486058588,
|
||||
-1.9017651869966448,
|
||||
0.9188491002868981
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/009/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.465421112976955,
|
||||
-1.5658313769098995,
|
||||
-0.8441260685611001
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/010/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.9795710871745877,
|
||||
1.1054411812737994,
|
||||
0.1595565970088657
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/011/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.6459875300407005,
|
||||
-0.09114155333949236,
|
||||
-1.0259259797858125
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/012/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.976635333029455,
|
||||
-0.23944382955750476,
|
||||
-1.3585539709107581
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/013/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.3145839737744005,
|
||||
-0.7591402157937948,
|
||||
1.3995114972146747
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/014/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.7574949366035553,
|
||||
1.6333928802519022,
|
||||
0.6543058167377063
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/015/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.4526172180324002,
|
||||
0.33776862483765857,
|
||||
-0.29068249723337125
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/016/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.3159803306695812,
|
||||
-1.1904107706133054,
|
||||
-1.620466358155475
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/017/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.7116120188801447,
|
||||
0.42700921371229006,
|
||||
-1.0623248060598298
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/018/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.8341327092635038,
|
||||
0.7154327763130017,
|
||||
-0.13535513665180865
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/019/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.7418182403314864,
|
||||
-0.05414376543833943,
|
||||
0.7634032184443917
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/020/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.2538767384955332,
|
||||
1.241503249739469,
|
||||
-1.8792948738528992
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/021/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.674364482047568,
|
||||
-1.7037584832883859,
|
||||
-0.6690933905451282
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/022/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-0.03833325919793418,
|
||||
-0.4509096318096564,
|
||||
1.140036945488447
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/023/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.7968937619479481,
|
||||
-0.6422574333823232,
|
||||
0.4471896678048437
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/024/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.5475627078942522,
|
||||
-1.4430042164046313,
|
||||
1.9183114286620007
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/025/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.9402143911890095,
|
||||
-0.5398574163339709,
|
||||
0.9989393722367566
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/026/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
1.5631753705911264,
|
||||
-1.0002201195286093,
|
||||
1.3524879030174843
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/027/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.2069400932531078,
|
||||
0.18407026478534183,
|
||||
-1.2452484507591657
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/028/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
0.7172204245811309,
|
||||
-1.2572127604498027,
|
||||
-0.21199372738442612
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||
11
src/OID_analysis/data_li22b/029/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"b": [
|
||||
-1.5514372601850652,
|
||||
0.8029509557883627,
|
||||
1.5716082689624455
|
||||
],
|
||||
"nu": 0.004,
|
||||
"U0": 0.01,
|
||||
"Re_D": 50,
|
||||
"Re_code": 100
|
||||
}
|
||||