Compare commits

..

12 Commits

Author SHA1 Message Date
Frank14f
9c3f93d58d fix(ccd): update run_zone_ccd.py zone definitions to unified geometry
Old zones (350-500, 500-700, 580-650) were based on pre-unification
illusion-only coordinates. Updated to match diagnose_corrections.py:
near_body 580-720, body_wake 720-850, sensor_zone 780-850.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:45:10 +08:00
Frank14f
2ae248421d fix(oid): confirm FIFO bias bug has no structural impact
- Fixed bias_arr[4] (front 0), bias_arr[5] (bottom -4U0), bias_arr[6] (top +4U0)
- Re-ran full karman pipeline: force-sig overlap unchanged (-0.034)
- Force-OID still beats POD (0.295 vs 0.068, was 0.750 vs 0.418)
- Absolute R2 shifted because corrected FIFO changed PPO trajectory start
- Structural conclusion (force-sig near-orthogonal) is robust

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:26:54 +08:00
Frank14f
5c55c5bdf7 fix(oid): correct FIFO bias in karman_blk and collect_controlled
- collect_karman_blk.py: bias_arr[4]=front (was -4U0, should be 0),
  bias_arr[5]=bottom (was +4U0, should be -4U0),
  bias_arr[6]=top (was 0, should be +4U0).
  Matches legacy_karman_env.py:179-182.
- collect_controlled.py: same fix for Karman branch.
- Add missing import data_dir_for_scene in collect_controlled.py

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 15:55:15 +08:00
Frank14f
8df40fb5bd chore(oid): final cleanup for paper-readiness
Remove:
  - scripts/compute_delta_fields.py (DEPRECATED, always SKIPPED)
  - scripts/replay_verify.py (duplicated logic)
  - analysis/save_robustness.py (hardcoded, superseded)

Archive:
  - scripts/collect_fields_replay.py -> archive/ (superseded by replay_full_fields.py)

Fix:
  - NaN->null in master_table.json and steady_reanalysis.json
  - Regenerate master_table.json with canonical overlap + three-layer action-OID
  - Update OID_knowledge.md with three-layer overlap table + data provenance
  - Update README: remove stale replay_verify reference, add action-OID finding

Result: 27 Python files (was 31), 3 deleted, 1 archived. All JSON valid.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 16:12:10 +08:00
Frank14f
4ae6e2e45c feat(oid): Li22b full DB collection complete — 51 commands, pipeline runner
- 51 steady open-loop commands collected (2.1h on device 2)
- q_in and q_blk reference fields collected
- Full pipeline: run_full_pipeline.py (A.2 POD -> A.3 LSE -> B OID -> C synthesis)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 22:35:14 +08:00
Frank14f
52229ea0f0 feat(oid): Li22b replication — collection, POD, LSE, OID, synthesis scripts
Phase A.1: Open-loop DB collection (50 commands, LHS, new CelerisLab)
Phase A.2: Snapshot POD on Li22b DB (ROI-masked, energy analysis)
Phase A.3: LSE [sensors, b] -> POD coefficients
Phase B: Delta-q OID + cross-mapping + joint-input OID
Phase C: Three-framework synthesis (SR, Li22b, OID)

Partial data collected (in progress). Reference fields done.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:00:18 +08:00
Frank14f
3e9cffda8e feat(oid): joint analysis — OID mode zone partition, action-OID, SR validation
P3.1: Zone energy partition from OID spatial modes
P3.3: Three-layer overlap (action-OID, force-OID, signature-OID)
  - Key finding: action is near-orthogonal to both force and sig OID modes
  - Confirms OID finds observable-relevant, not action-relevant structures
P3.5: SR formula → OID coordinate validation
  - Karman: corr(OID_z1, Cl_tot) = -0.82 (strong)
Fix: Restore ROI masking in phase1_correction_pod.py (OOM prevention)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 19:21:53 +08:00
Frank14f
3edf964f34 fix(oid): restore ROI masking in phase1 POD to prevent OOM
Full 1280x512 POD with 500 snapshots needs ~50 GB RAM.
ROI [400:1000, 100:400] (600x300 px) reduces to ~1.4 GB.
This was the original design — removing it was a critical mistake.
Fields saved at full resolution (Rule 5); ROI applied at analysis stage only.

Also add phase_error_karman_sig.py for P2.4b future work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 19:07:16 +08:00
Frank14f
31e367db0e test(oid): Sch12 code mapping and 7 unit tests for POD/OID/PCD
- Add docs/sch12_code_mapping.md: formula-to-code traceability
- Add tests/test_analysis.py: 7 unit tests, all passing
- Tests: POD energy, OID cross-covariance, field reconstruction,
  PCD whitening, standardize edge cases, snapshot method, cum_corr

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:43:46 +08:00
Frank14f
f66671a498 docs(oid): consolidate documentation and organize data directories
- Fix illusion rank sensitivity inconsistency (0.75L std=0.26, not 1.000)
- Unify all dates to 2026-06-28 across 4 markdown files
- Remove empty derived/delta_fields/ directory
- Update README reading order and scope date

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:26:55 +08:00
Frank14f
225f653840 fix(oid): code cleanup — fix NameError, remove ROI cropping, consolidate
- Fix NameError (cfg_sid -> cfg["scene_id"]) in collect_controlled.py
- Remove ROI cropping in phase1_correction_pod.py (use full 1280x512)
- Add pinball_baseline_illusion to configs.py and data_dir_for_scene()
- Deprecate compute_delta_fields.py (superseded by phase1_correction_pod)
- Fix absolute path in save_robustness.py
- Clean redundant import in phase3_force_oid.py
- Suppress module-level print in phase6_comparison.py

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:13:32 +08:00
Frank14f
56e3c78a83 chore: project-wide cleanup — consolidate docs, remove obsolete code, update .gitignore
- Remove obsolete docs (OID_handover, SR_analysis_results, ccd_* handover)
- Remove CCD legacy output_redux and old scripts
- Remove SR old sindy scripts and compare modules
- Update .gitignore to cover all analysis-generated outputs
- Retain all active code in OID/SR/CCD analysis directories

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 16:52:48 +08:00
428 changed files with 28847 additions and 8590 deletions

11
.gitignore vendored
View File

@ -75,6 +75,17 @@ tensorboard/
*.h5 *.h5
*.hdf5 *.hdf5
*.npz *.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 # Logs
*.log *.log

2
.gitmodules vendored
View File

@ -1,3 +1,5 @@
[submodule "CelerisLab"] [submodule "CelerisLab"]
path = CelerisLab path = CelerisLab
url = https://github.com/Frank14f/CelerisLab.git url = https://github.com/Frank14f/CelerisLab.git
ignore = dirty

View File

@ -33,5 +33,5 @@
#define V_TAYLOR 0b00000001 #define V_TAYLOR 0b00000001
// variables // variables
#define N_OBJS 6 #define N_OBJS 7
// #define N_SENS 2 // #define N_SENS 2

View 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"
}
}

View File

@ -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]`

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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:** ## Quick Start
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)
--- ```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) scripts/collect_*.py → scripts/{detect_period,replay_fields}.py
- **Action line**: which structures does the controller directly modulate (3 rotation speeds) (GPU采集) (phase alignment)
- **Signature line**: which structures most determine future sensor error (with tau delay scan)
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 ## Directory Structure
``` ```
src/CCD_analysis/ CCD_analysis/
README.md <-- this file README.md # 本文件
ccd_knowledge.md -- confirmed facts, hard rules, results ccd_knowledge.md # 唯一知识库 (理论, 结果, 操作流程, bug经验)
ccd_notes.md -- method, what's done, what's not Lyu23.md # CCD 方法文献
configs.py -- scene metadata, all parameters configs.py # 场景元数据 (统一几何)
utils/ utils/
resampling.py -- POD, CCD, field loading (CPU only) resampling.py # POD, CCD, 场加载
cfd_interface.py -- LegacyCelerisLab wrapper (GPU needed) cfd_interface.py # LegacyCelerisLab 封装 (GPU) + build_observation
__init__.py -- re-exports from resampling.py field_translate.py # 场平移 (备用)
load_vortex_fields.py # 瞬态 vortex 场加载
scripts/ scripts/
detect_period.py -- period detection + phase plan generation collect_*.py # GPU 数据采集
replay_fields.py -- field replay for phase-aligned snapshots detect_period.py # 周期检测 → phase_plan.json
collect_target_cylinder.py -- target cylinder data collection (GPU) replay_fields.py # 场回放 → fields_aligned.npz
collect_illusion.py -- illusion PPO inference (GPU) correction_analysis/
collect_pinball.py -- uncontrolled pinball (GPU) compute_correction_fields.py # dq 计算 (核心)
collect_steady_cloak.py -- steady cloak (GPU) compare_dqctl_scenes.py # 全景对比图 (核心输出)
collect_empty_channel.py -- empty channel (GPU) decompose_corrections.py # CCD 定量 (POD + force/action CCD)
collect_karman.py -- Karman cloak validation (GPU) diagnose_corrections.py # 单场景 zone 诊断
resample.py -- DEPRECATED (use detect_period + replay_fields) run_signature_line.py # Signature CCD
visualize_ccd.py -- O_k, CCD modes, z_k, POD (Round 5 raw-field) run_zone_ccd.py # Zone-restricted CCD
sanity_check_force.py -- raw force comparison target vs illusion run_steady_metrics.py # Steady cloak 定量
ccd/ run_15L_correction.py # 1.5L 专项
run_ccd.py -- Round 5 raw-field CCD (FROZEN) visualize_action_ccd.py # Action-CCD mode1 可视化
validate.py -- Round 5 validation (FROZEN) process_legacy_steady.py # 旧格式加载
correction_analysis/ <-- ALL CURRENT WORK HERE ccd/ # Round 5 冻结基线 (勿改)
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
data/ data/
pinball/pinball/ -- uncontrolled pinball figures/ # 诊断图 (仅保留核心对比图)
steady_cloak/steady_cloak/ -- steady cloak (open-loop, old format) ccd/ # JSON 结果
target_channel/target_channel/ -- empty channel (old format) old_data/ # 归档 (废弃脚本/旧报告/旧数据)
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)
``` ```
--- ## 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 - **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)
```bash - **All scenes unified geometry**: pinball center at 613px, sensors at 800px
# Target cylinders - **Environment**: `conda run -n pycuda_3_10`
python src/CCD_analysis/scripts/collect_target_cylinder.py --diameter 0.75 --device 2 - **GPU**: Device 2 for collection
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.

View 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()

View File

@ -1,138 +1,509 @@
# CCD 分析知识库 — 最终版本 (2026-06-22) # CCD Analysis Knowledge Base — 论文知识库 (2026-06-25)
## 工作阶段总览 > 统一文档。整合了 Round 5-6 的完整分析结果、correction-field 框架、Vortex/Cloak/Illusion 对比、几何对齐方案、以及所有操作流程。写论文时以本文档为准。
| 阶段 | 状态 | 说明 |
|------|------|------|
| 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` 已写,需要根据其他两方向的实际报告校正 |
--- ---
## 所有结果汇总 ## 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.667action 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` | 涡街无 pinballKarman 参考) | `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 cloakTaylor/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
CCDCanonical 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 | | 指标 | 0.75L | 1.0L | 1.5L |
|------|-------|------|------| |------|-------|------|------|
| **O(dqctl, dqtar) mode1** | **0.564** | **0.913** | **0.667** | | **O(dqctl, dqtar) mode1 (r=6)** | **0.383** | **0.926** | **0.922** |
| dqctl force_fy m80 | 2 | **1** (r=8/10) | 2 | | **O(dqctl, dqtar) mode1 (r=10)** | **0.320** | **0.684** | **0.661** |
| dqctl action sigma1 | 1.39 | 1.13 | **0.28** | | force_fy m80 (r=6) | 2 | 2 | **1** |
| **O(force, sig) at tau=0** | **0.413** | **0.551** | — | | action sigma1 (r=6) | 1.49 | 1.17 | **0.20** |
| **O(force, sig) at tau=tau_c** | **0.806** | **0.768** | — |
| Phase drift | low | low | **high** | | 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-sig overlap 和 zone 数据来自 6 月 15 日旧版(待用统一几何和新 zone 定义重跑):
|-----------|-------|------|
| 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) |
**三区域分层 CCD (zone-restricted, r=6 force_fy)**: ### 5.2 三区域 Force-Signature Overlap
0.75L: **0.75L** — sensor_zone 在 tau=0 时近乎正交:
| Zone | O(force,sig) tau=0 | O(force,sig) tau=tau_c |
| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|------|--------------------|------------------------| |------|--------------------|------------------------|
| near_body | 0.262 | 0.827 | | near_body | 0.262 | 0.827 |
| body_wake | 0.269 | **0.917** | | body_wake | 0.269 | **0.917** |
| sensor_zone | **0.010** | 0.722 | | sensor_zone | **0.010** | 0.722 |
1.0L: **1.0L** — 更均匀,没有近零区域:
| Zone | O(force,sig) tau=0 | O(force,sig) tau=tau_c |
| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|------|--------------------|------------------------| |------|--------------------|------------------------|
| near_body | 0.596 | 0.596 | | near_body | 0.596 | 0.596 |
| body_wake | 0.509 | 0.483 | | body_wake | 0.509 | 0.483 |
| sensor_zone | 0.594 | **0.730** | | 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 illusion4 行 × 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? | ```bash
|------|----------|----------------| conda run -n pycuda_3_10
| pinball | pinball | YES | # Python 3.10+, numpy, matplotlib, LegacyCelerisLab, stable-baselines3
| target_cylinder_{0.75,1.0,1.5}L | target_cylinder | YES | # CUDA device 2 (采集用)
| 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帧) | ### 7.2 快速重新生成所有图
| karman_q_blk | karman_blocked | YES (96帧) |
| steady_cloak | steady_cloak | NO (旧 fields.npz, 500帧) | ```bash
| target_channel | target_channel | NO (旧 fields.npz, 100帧) | # 1. Correction-field pipelineCPU
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 alignmentCPU
python3 scripts/detect_period.py --scene {scene_name}
# Step 3: Field replayGPU
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 src/CCD_analysis/
- 每个 illusion 直径必须匹配自己的 target cylinder ccd_knowledge.md ← 本文档(唯一知识库)
- `action_bias` 与 FIFO warmup 的 `preset_action` 不能混淆 (bias=[0,-2,2], preset=[0,0,0,0,-1*U0,1*U0]) configs.py ← 场景元数据(统一几何)
- 默认 u0=0.01, nu=0.004 README.md ← 快速入口
- 主分析对象 = `dq_ctl = q_ctl - q_blk`,不是 `q_ctl` Lyu23.md ← CCD 方法文献
- 不要对 steady cloak 使用 phase-based CCD ccd/ ← Round 5 冻结基线(勿改)
- Karman 的分析框架与 illusion 不同distortion compensation vs target generation 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 scalingillusions=[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 pxsensor 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 的物理机制。" - "Illusion 已证明使用完全不同于 target 的物理机制。"
- "低 overlap 已足以证明 force 通道与 target 正交。" - "低 overlap 已足以证明 force 通道与 target 正交。"
- "CCD 已经直接识别了壁面涡量生成机制。" - "CCD 已经直接识别了壁面涡量生成机制。"
- "far wake 模态就是瞬时力的主载体。" - "far wake 模态就是瞬时力的主载体。"
---
## 11. 未完成工作2026-06-28 更新)
| 方向 | 状态 | 说明 |
|------|------|------|
| Karman cloak CCD 分析 | 数据已齐,分析延后 | 问题定义不同distortion compensation |
| Vortex 数据采集 Bug 修复 | 已完成 | 修复 cylinder order swapBOTTOM=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 标记。

View File

@ -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

View File

@ -165,9 +165,9 @@ for key, mn, diam, si in _ILLUSION_2U:
"model_subdir": "250525", "model_subdir": "250525",
"n_objects_env": 6, "n_objects_env": 6,
"obs_slice": (0, 12), "obs_slice": (0, 12),
"sensor_x": 30.0, "sensor_x": 40.0, # UNIFIED: was 30.0
"pinball_front_x": 19.0, "pinball_front_x": 30.0, # UNIFIED: was 19.0
"pinball_rear_x": 20.3, "pinball_rear_x": 31.3, # UNIFIED: was 20.3
"target_type": "periodic", "target_type": "periodic",
"s_dim": 14, # CRITICAL: all 2U are 14-dim "s_dim": 14, # CRITICAL: all 2U are 14-dim
"u0": U0, # 0.01 (NOT 0.02) "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, "model_name": None,
"n_objects_env": 4, # 1 cylinder + 3 sensors "n_objects_env": 4, # 1 cylinder + 3 sensors
"obs_slice": (0, 8), # cylinder force(2) + sensor(6) "obs_slice": (0, 8), # cylinder force(2) + sensor(6)
"sensor_x": 30.0, "sensor_x": 40.0, # UNIFIED: was 30.0
"cylinder_x": 20.0, "cylinder_x": 30.65, # UNIFIED: was 20.0 (pinball center)
"target_type": "periodic", "target_type": "periodic",
"s_dim": None, "s_dim": None,
"u0": U0, # 0.01, matching illusion "u0": U0, # 0.01, matching illusion
@ -217,6 +217,47 @@ SCENES["target_channel"] = {
"nu": nu_from_re(100), "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 --------------------------------------------------------- # -- Utility helpers ---------------------------------------------------------

View 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())

View File

@ -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.0L | target_channel* | pinball | illusion_1.0L | target_cylinder_1.0L |
| illusion_1.5L | target_channel* | pinball | illusion_1.5L | target_cylinder_1.5L | | illusion_1.5L | target_channel* | pinball | illusion_1.5L | target_cylinder_1.5L |
| steady_cloak | target_channel* | pinball | steady_cloak* | None (target=q_in) | | 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 from __future__ import annotations
@ -21,65 +22,118 @@ from typing import Any, Optional
import numpy as np 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 ( from CCD_analysis.utils.resampling import (
load_aligned_fields, load_aligned_fields,
build_field_matrix as _build_field_matrix, build_field_matrix as _build_field_matrix,
) )
from CCD_analysis.correction_analysis.process_legacy_steady import load_legacy_steady from CCD_analysis.correction_analysis.process_legacy_steady import load_legacy_steady
from CCD_analysis.utils.load_vortex_fields import load_vortex_fields
# ---------------------------------------------------------------------------
# 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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Source mapping # 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. """Load a named data source, dispatching to the correct loader.
Parameters Parameters
---------- ----------
name : str name : str
Scene or special name: Scene or special name:
- 'target_channel' -> load_legacy_steady - 'target_channel', 'steady_cloak' -> load_legacy_steady
- 'steady_cloak' -> load_legacy_steady - 'vortex_*' -> load_vortex_fields (transient)
- 'pinball' -> load_aligned_fields - 'karman_re100' -> _load_karman_re100 (handles 72 vs 96 mismatch)
- 'illusion_*' -> load_aligned_fields - all others -> 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
Returns Returns
------- -------
dict or None dict or None
""" """
_LEGACY_SCENES = {"target_channel", "steady_cloak"} _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: if name in _LEGACY_SCENES:
return load_legacy_steady(name) return load_legacy_steady(name)
elif name in _VORTEX_SCENES:
return load_vortex_fields(name)
elif name == "karman_re100":
return _load_karman_re100()
else: else:
# karman_q_in, karman_q_blk, and all others use aligned loader
return load_aligned_fields(name) 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 # Correction computation
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -91,6 +145,8 @@ _SCENE_MAP = {
"illusion_1.5L": ("target_channel", "pinball", "illusion_1.5L", "target_cylinder_1.5L"), "illusion_1.5L": ("target_channel", "pinball", "illusion_1.5L", "target_cylinder_1.5L"),
"steady_cloak": ("target_channel", "pinball", "steady_cloak", None), "steady_cloak": ("target_channel", "pinball", "steady_cloak", None),
"karman_re100": ("karman_q_in", "karman_q_blk", "karman_re100", "karman_q_in"), "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"),
} }

View File

@ -30,7 +30,9 @@ from CCD_analysis.correction_analysis.compute_correction_fields import (
R_CANDIDATES = [6, 8, 10] R_CANDIDATES = [6, 8, 10]
CCD_Q = 6 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"): def compute_modal_overlap(W_dict, scene_label, r, obs_label="force_fy"):
@ -101,7 +103,9 @@ def main():
continue continue
diam = corr.get("diam") 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) Q_ctl = dict_to_field_matrix(dq_ctl)
N = Q_ctl.shape[1] N = Q_ctl.shape[1]
@ -146,6 +150,7 @@ def main():
"scene": st, "diam": diam, "obs": flabel, "r": r, "scene": st, "diam": diam, "obs": flabel, "r": r,
"m80": m80, "N": sig.size, "m80": m80, "N": sig.size,
"sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))], "sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))],
"special_mechanism": is_special,
} }
if fmode == "fy": if fmode == "fy":
print(f" {key}: m80={m80} s1={float(sig[0]):.4f}", flush=True) 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, "scene": st, "diam": diam, "obs": "action", "r": r,
"m80": m80, "N": sig.size, "m80": m80, "N": sig.size,
"sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))], "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) 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, "m80": int(np.searchsorted(cumulative_energy(sig_t), 0.80)+1) if len(sig_t) > 0 else 0,
"N": sig_t.size, "N": sig_t.size,
"sigma_top3": [float(sig_t[i]) for i in range(min(3,len(sig_t)))], "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 # Overlap: dq_ctl vs dq_tar
ck = f"{st}_dqctl_force_fy_r{r}" ck = f"{st}_dqctl_force_fy_r{r}"

View File

@ -34,57 +34,40 @@ from CCD_analysis.correction_analysis.compute_correction_fields import (
FIG_DIR = os.path.join(DATA_DIR, "figures") FIG_DIR = os.path.join(DATA_DIR, "figures")
os.makedirs(FIG_DIR, exist_ok=True) 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 = [ SCENE_TYPES = [
"illusion_0.75L", "illusion_0.75L",
"illusion_1.0L", "illusion_1.0L",
"illusion_1.5L", "illusion_1.5L",
"steady_cloak", "steady_cloak",
"karman_re100", "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: def define_zones() -> dict:
"""Define three-zone masks for illusion layout (sensors at x=30*L0).""" """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 x613)
body_wake: 720-850 px (near wake downstream)
sensor_zone: 780-850 px (around sensors at x=800)
"""
zones = {} 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 = np.zeros((NY, NX), dtype=bool)
mask[:, 580:720] = True mask[:, 580:720] = True
zones["near_body"] = mask zones["near_body"] = mask
# Zone 2: body-connected near wake
mask = np.zeros((NY, NX), dtype=bool) mask = np.zeros((NY, NX), dtype=bool)
mask[:, 720:850] = True mask[:, 720:850] = True
zones["body_wake"] = mask zones["body_wake"] = mask
# Zone 3: downstream sensor zone — around sensors at x=800
mask = np.zeros((NY, NX), dtype=bool) mask = np.zeros((NY, NX), dtype=bool)
mask[:, 780:850] = True mask[:, 780:850] = True
zones["sensor_zone"] = mask zones["sensor_zone"] = mask
@ -206,23 +189,20 @@ def plot_mean_rms(dq: dict, label: str, prefix: str, zones: Optional[dict] = Non
# Mean ux # Mean ux
vmax = max(abs(ux_m).max(), 1e-12) 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) origin="lower", aspect="equal", extent=extent)
axes[0].set_title(f"{label}: mean ux") axes[0].set_title(f"{label}: mean ux")
plt.colorbar(im0, ax=axes[0], fraction=0.046)
# Mean uy # Mean uy
vmax = max(abs(uy_m).max(), 1e-12) 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) origin="lower", aspect="equal", extent=extent)
axes[1].set_title(f"{label}: mean uy") axes[1].set_title(f"{label}: mean uy")
plt.colorbar(im1, ax=axes[1], fraction=0.046)
# RMS magnitude # RMS magnitude
im2 = axes[2].imshow(rms, cmap="viridis", origin="lower", axes[2].imshow(rms, cmap="viridis", origin="lower",
aspect="equal", extent=extent) aspect="equal", extent=extent)
axes[2].set_title(f"{label}: RMS magnitude") axes[2].set_title(f"{label}: RMS magnitude")
plt.colorbar(im2, ax=axes[2], fraction=0.046)
# Overlay zone boundaries if provided # Overlay zone boundaries if provided
if zones is not None: 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)) fig, ax = plt.subplots(figsize=(10, 4))
vmax = max(np.percentile(abs(vor), 99), 1e-12) 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", origin="lower", aspect="equal",
extent=(0, NX - 1, 0, NY - 1)) extent=(0, NX - 1, 0, NY - 1))
ax.set_title(f"{label}: mean vorticity") ax.set_title(f"{label}: mean vorticity")
plt.colorbar(im, ax=ax, fraction=0.046, label=r"$\omega_z$")
plt.tight_layout() plt.tight_layout()
path = os.path.join(FIG_DIR, f"{prefix}_vorticity_{label.replace(' ', '_')}.png") path = os.path.join(FIG_DIR, f"{prefix}_vorticity_{label.replace(' ', '_')}.png")
fig.savefig(path, dpi=150) fig.savefig(path, dpi=150)
@ -274,8 +253,7 @@ def run():
print("Phase 2: Baseline Diagnostics (correction fields)", flush=True) print("Phase 2: Baseline Diagnostics (correction fields)", flush=True)
print("=" * 60, flush=True) print("=" * 60, flush=True)
zones_ill = define_zones_illusion() zones = define_zones()
zones_karman = define_zones_karman()
all_metrics = {} all_metrics = {}
@ -286,7 +264,7 @@ def run():
try: try:
corr = compute_correction(scene_type) corr = compute_correction(scene_type)
except (FileNotFoundError, KeyError) as e: except (FileNotFoundError, KeyError, AssertionError, ValueError) as e:
print(f" SKIP: {e}", flush=True) print(f" SKIP: {e}", flush=True)
continue continue
@ -294,10 +272,7 @@ def run():
print(f" SKIP: no valid data (N=0)", flush=True) print(f" SKIP: no valid data (N=0)", flush=True)
continue continue
# Determine which zones to use # Unified geometry — same zones for all scenes
is_illusion = "illusion" in scene_type
zones = zones_ill if is_illusion else zones_ill # zones_karman for later
for dq_key, dq_label in [ for dq_key, dq_label in [
("dq_blk", "dq_blk (pinball blockage)"), ("dq_blk", "dq_blk (pinball blockage)"),
("dq_ctl", "dq_ctl (control correction)"), ("dq_ctl", "dq_ctl (control correction)"),
@ -314,8 +289,8 @@ def run():
metrics = zone_metrics(dq, zones, dq_label) metrics = zone_metrics(dq, zones, dq_label)
all_metrics[f"{scene_type}_{dq_key}"] = metrics all_metrics[f"{scene_type}_{dq_key}"] = metrics
# For illusion, also plot dq_tar if available # For scenes with a target, also plot dq_tar if available
if dq_key == "dq_ctl" and is_illusion: if dq_key == "dq_ctl" and corr.get("dq_tar") is not None:
dq_tar = corr.get("dq_tar") dq_tar = corr.get("dq_tar")
if dq_tar is not None: if dq_tar is not None:
plot_mean_rms(dq_tar, "dq_tar (target correction)", prefix, zones) 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"]) ux_tar, _ = mean_field(dq_tar["ux"], dq_tar["uy"])
vmax = max(abs(ux_ctl).max(), abs(ux_tar).max(), 1e-12) 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) origin="lower", aspect="equal", extent=extent)
axes[0, 0].set_title("dq_ctl mean ux") 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) origin="lower", aspect="equal", extent=extent)
axes[0, 1].set_title("dq_tar mean ux") axes[0, 1].set_title("dq_tar mean ux")
plt.colorbar(im, ax=axes[0, 1], fraction=0.046)
# Row 1: RMS # Row 1: RMS
rms_ctl = rms_field(dq["ux"], dq["uy"]) rms_ctl = rms_field(dq["ux"], dq["uy"])
rms_tar = rms_field(dq_tar["ux"], dq_tar["uy"]) rms_tar = rms_field(dq_tar["ux"], dq_tar["uy"])
rmax = max(rms_ctl.max(), rms_tar.max(), 1e-12) 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) origin="lower", aspect="equal", extent=extent)
axes[1, 0].set_title("dq_ctl RMS") 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) origin="lower", aspect="equal", extent=extent)
axes[1, 1].set_title("dq_tar RMS") 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.suptitle(f"{scene_type}: dq_ctl vs dq_tar comparison")
plt.tight_layout() plt.tight_layout()
@ -375,20 +346,17 @@ def run():
extent = (0, NX - 1, 0, NY - 1) extent = (0, NX - 1, 0, NY - 1)
vmax = max(abs(ux_b).max(), abs(ux_c).max(), abs(ux_cancel).max(), 1e-12) 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) origin="lower", aspect="equal", extent=extent)
axes[0].set_title("dq_blk mean ux (blockage)") 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) origin="lower", aspect="equal", extent=extent)
axes[1].set_title("dq_ctl mean ux (correction)") 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) origin="lower", aspect="equal", extent=extent)
axes[2].set_title("dq_ctl + dq_blk (cancel test)") 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.suptitle(f"Steady cloak: cancellation test")
plt.tight_layout() plt.tight_layout()

View File

@ -1,9 +1,10 @@
"""Zone-wise CCD: force-CCD and signature-CCD per spatial zone. """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: Processes each of three spatial zones separately for illusion 0.75L and 1.0L
- near_body: x[350:500] (envelope around pinball) (unified geometry: pinball center ~613px, sensors at 800px):
- body_wake: x[500:700] (body-connected near wake) - near_body: x[580:720] (envelope around pinball)
- sensor_zone: x[580:650] (around sensor plane at x=30*L0=600) - 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, 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 build a target-only POD basis, project correction fields, and compute
@ -46,22 +47,27 @@ R = 6
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _define_zones() -> dict: 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 = {} 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 = np.zeros((NY, NX), dtype=bool)
mask[:, 350:500] = True mask[:, 580:720] = True
zones["near_body"] = mask 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 = np.zeros((NY, NX), dtype=bool)
mask[:, 500:700] = True mask[:, 720:850] = True
zones["body_wake"] = mask 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 = np.zeros((NY, NX), dtype=bool)
mask[:, 580:650] = True mask[:, 780:850] = True
zones["sensor_zone"] = mask zones["sensor_zone"] = mask
return zones return zones

View 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())

View File

@ -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()

View File

@ -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
}

View File

@ -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
]
}

View File

@ -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
]
}
]

View File

@ -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": {}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

View File

@ -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
}

View File

@ -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
]
}

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

View File

@ -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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

View File

@ -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
}
}
}
}

View File

@ -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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

@ -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

View File

@ -65,10 +65,10 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
print("=== Target recording ===") print("=== Target recording ===")
ff_tgt = FlowField(field_cfg, cuda_cfg, device_id=device_id) ff_tgt = FlowField(field_cfg, cuda_cfg, device_id=device_id)
tgt_radius = cfg["target_diameter"] * L0 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) print(f" target cylinder: diameter={cfg['target_diameter']}L, radius={tgt_radius}", flush=True)
for y_off in [2.0, 0.0, -2.0]: 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 n_tgt = 4
ff_tgt.run(int(4 * 1280 / u0), np.zeros(n_tgt, dtype=DATA_TYPE)) 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 ===") print("=== Pinball env + norm ===")
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
for y_off in [2.0, 0.0, -2.0]: 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_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) # UNIFIED: was 30.0
ff.add_cylinder((19.0 * L0, CENTER_Y, 0.0), L0 / 2.0) ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # UNIFIED: was 19.0
ff.add_cylinder((20.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # UNIFIED: was 20.3
ff.add_cylinder((20.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # UNIFIED: was 20.3
n_env = 6 n_env = 6
ff.run(int(4 * 1280 / u0), np.zeros(n_env, dtype=DATA_TYPE)) ff.run(int(4 * 1280 / u0), np.zeros(n_env, dtype=DATA_TYPE))

View 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())

View File

@ -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())

View 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

View 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

View File

@ -1,5 +1,5 @@
# OID Analysis: Final Conclusions # OID Analysis: Final Conclusions
## (Phase 2 completed 2026-06-15) ## (Phase 2 completed 2026-06-28)
--- ---

1227
src/OID_analysis/Li22b.md Normal file

File diff suppressed because it is too large Load Diff

View 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 ### steady_cloak
@ -125,7 +125,8 @@ ff.context.pop()
| Metric | 0.75L | 1.0L | 1.5L | | Metric | 0.75L | 1.0L | 1.5L |
|--------|-------|------|------| |--------|-------|------|------|
| Correction-field POD energy (5 modes) | 99.93% | 99.91% | 97.9% | | 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 | | N snapshots (POD) | 100 | 100 | 100 |
| Force-OID S[0] | 0.699 | 1.447 | 0.981 | | Force-OID S[0] | 0.699 | 1.447 | 0.981 |
| Force-OID S[1] | 0.671 | 0.832 | 0.407 | | 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. 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) ### White-box Chain (Karman, from Phase 7)
| Model | Action R2 | Meaning | | Model | Action R2 | Meaning |

View File

@ -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. All planned work is done. Repository is ready for handover.

View File

@ -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:** **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 ### Replay verification
```bash ```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 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) | | 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 | | 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 |
--- ---

View File

@ -5,9 +5,13 @@ Phase 1: Correction-field POD with rank sensitivity.
For each scene, computes: For each scene, computes:
- Delta_q_blk = q_blk - q_in - Delta_q_blk = q_blk - q_in
- Delta_q_ctl = q_ctl - q_blk - 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) - 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: Usage:
python3 src/OID_analysis/analysis/phase1_correction_pod.py python3 src/OID_analysis/analysis/phase1_correction_pod.py
@ -19,7 +23,7 @@ import argparse
import json import json
import os import os
import sys import sys
from typing import Dict, List, Optional, Tuple from typing import Dict, Tuple, Optional
import numpy as np import numpy as np
@ -28,13 +32,36 @@ if _REPO not in sys.path:
sys.path.insert(0, _REPO) sys.path.insert(0, _REPO)
from OID_analysis.configs import ( # noqa: E402 from OID_analysis.configs import ( # noqa: E402
get_scene, data_dir_for_scene, SCENES, DATA_DIR, L0, get_scene, data_dir_for_scene, DATA_DIR,
)
from OID_analysis.utils.analysis import ( # noqa: E402
compute_pod, standardize, reconstruct_oid_modes,
) )
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 = { SCENE_GROUPS = {
"steady_cloak": { "steady_cloak": {
"q_in_dir": data_dir_for_scene("empty_channel"), "q_in_dir": data_dir_for_scene("empty_channel"),
@ -48,24 +75,30 @@ SCENE_GROUPS = {
}, },
"illusion_0.75L": { "illusion_0.75L": {
"q_in_dir": data_dir_for_scene("empty_channel"), "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"), "q_ctl_dir": data_dir_for_scene("illusion_0.75L"),
}, },
"illusion_1.0L": { "illusion_1.0L": {
"q_in_dir": data_dir_for_scene("empty_channel"), "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"), "q_ctl_dir": data_dir_for_scene("illusion_1.0L"),
}, },
"illusion_1.5L": { "illusion_1.5L": {
"q_in_dir": data_dir_for_scene("empty_channel"), "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"), "q_ctl_dir": data_dir_for_scene("illusion_1.5L"),
}, },
} }
def load_scene_fields(scene_key: str) -> Optional[Dict]: 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) groups = SCENE_GROUPS.get(scene_key)
if groups is None: if groups is None:
print(f" Unknown scene group: {scene_key}") 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}") print(f" WARNING: {key} fields not found at {fp}")
return None return None
fd = np.load(fp) fd = np.load(fp)
ux = fd["ux"] result[key] = (fd["ux"], fd["uy"])
uy = fd["uy"] print(f" Loaded {key}: {fd['ux'].shape}")
result[key] = (ux, uy)
print(f" Loaded {key}: {ux.shape}")
# Check compatible sizes # Minimum snapshot count
sizes = [v[0].shape[0] for v in result.values()] sizes = [v[0].shape[0] for v in result.values()]
if len(set(sizes)) > 1: if len(set(sizes)) > 1:
print(f" WARNING: mismatched snapshot counts: {sizes}") print(f" Mismatched snapshot counts: {sizes}, using min={min(sizes)}")
# Use minimum
min_n = min(sizes) min_n = min(sizes)
for k in result: for k in result:
result[k] = (result[k][0][:min_n], result[k][1][:min_n]) 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 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: 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.""" """Convert (N, ny, nx) -> (N, DOF)."""
N = ux.shape[0] N, ny, nx = ux.shape
DOF = ux.shape[1] * ux.shape[2] * 2 # ux + uy flattened DOF = ny * nx * 2
Q = np.zeros((N, DOF), dtype=np.float64) Q = np.zeros((N, DOF), dtype=np.float64)
for t in range(N): for t in range(N):
Q[t] = np.concatenate([ux[t].ravel(), uy[t].ravel()]) 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) out_dir = os.path.join(DATA_DIR, "derived", "pod", scene_key)
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# Build delta fields
ux_in, uy_in = fields["q_in_dir"] ux_in, uy_in = fields["q_in_dir"]
ux_blk, uy_blk = fields["q_blk_dir"] ux_blk, uy_blk = fields["q_blk_dir"]
ux_ctl, uy_ctl = fields["q_ctl_dir"] ux_ctl, uy_ctl = fields["q_ctl_dir"]
# Mask to ROI # Delta fields at full resolution
ux_in_m, uy_in_m = mask_field(ux_in, uy_in) delta_ux_blk = ux_blk - ux_in
ux_blk_m, uy_blk_m = mask_field(ux_blk, uy_blk) delta_uy_blk = uy_blk - uy_in
ux_ctl_m, uy_ctl_m = mask_field(ux_ctl, uy_ctl) delta_ux_ctl = ux_ctl - ux_blk
delta_uy_ctl = uy_ctl - uy_blk
# Delta fields # Save delta fields full-resolution (OID Rule 5)
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
np.savez_compressed(os.path.join(out_dir, "delta_q_blk.npz"), 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"), np.savez_compressed(os.path.join(out_dir, "delta_q_ctl.npz"),
ux=ux_delta_ctl, uy=uy_delta_ctl) ux=delta_ux_ctl, uy=delta_uy_ctl)
print(f" Delta fields saved")
# 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 # Snapshot matrices
Q_delta = fields_to_snapshot_matrix(ux_delta_ctl, uy_delta_ctl) Q_delta = fields_to_snapshot_matrix(ux_dm, uy_dm)
Q_raw = fields_to_snapshot_matrix(ux_ctl_m, uy_ctl_m) 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 ranks 6,8,10,12,16
# POD at different ranks
ranks = [6, 8, 10, 12, 16] ranks = [6, 8, 10, 12, 16]
results = {} results = {}
prev_modes = None prev_modes = None
for r in ranks: for r in ranks:
if r > Q_delta.shape[0]: if r > Q_delta.shape[0]:
print(f" Rank {r} > N={Q_delta.shape[0]}, skipping") print(f" Rank {r} > N, skip")
continue continue
pod = compute_pod(Q_delta, rank=r) pod = compute_pod(Q_delta, rank=r)
results[f"r{r}"] = pod results[f"r{r}"] = pod
# Rank sensitivity: compare to previous rank
if prev_modes is not None: if prev_modes is not None:
# Compare first 6 modes n_cmp = min(prev_modes.shape[1], pod["modes"].shape[1], 6)
min_dim = 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)]
similarities = [] print(f" r={r}: cum5={pod['cum_energy'][4]:.4f} cos_sim={np.mean(sims):.4f}")
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}")
prev_modes = pod["modes"] prev_modes = pod["modes"]
# Save POD results # Save
for r, pod in results.items(): for rk, pod in results.items():
np.savez(os.path.join(out_dir, f"pod_coefs_{r}.npy"), np.savez(os.path.join(out_dir, f"pod_coefs_{rk}.npy"),
coefs=pod["coefs"], S=pod["S"], coefs=pod["coefs"], S=pod["S"],
energy=pod["energy"], cum_energy=pod["cum_energy"]) 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_{rk}.npz"),
np.savez_compressed(os.path.join(out_dir, f"pod_modes_{r}.npz"), modes=pod["modes"], mean=pod["mean"])
modes=pod["modes"],
mean=pod["mean"])
# Also compute raw-field POD for comparison (r=10)
pod_raw = compute_pod(Q_raw, rank=10) pod_raw = compute_pod(Q_raw, rank=10)
np.savez(os.path.join(out_dir, "raw_pod_r10.npy"), np.savez(os.path.join(out_dir, "raw_pod_r10.npy"),
coefs=pod_raw["coefs"], S=pod_raw["S"], coefs=pod_raw["coefs"], S=pod_raw["S"],
energy=pod_raw["energy"], cum_energy=pod_raw["cum_energy"]) energy=pod_raw["energy"], cum_energy=pod_raw["cum_energy"])
# Summary table
summary = { summary = {
"scene": scene_key, "scene": scene_key,
"n_snapshots": Q_delta.shape[0], "n_snapshots": Q_delta.shape[0],
"dof": Q_delta.shape[1], "dof": Q_delta.shape[1],
"ranks_computed": [r for r in ranks if r <= Q_delta.shape[0]], "roi": {"x_start": x0, "x_end": x1, "y_start": y0, "y_end": y1},
"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,
} }
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: with open(os.path.join(out_dir, "summary.json"), "w") as f:
json.dump(summary, f, indent=2) json.dump(summary, f, indent=2)
print(f" Results saved to {out_dir}") print(f" Done. Saved to {out_dir}")
return summary
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--scene", type=str, default=None, ap.add_argument("--scene", type=str, default=None)
help="Scene key or 'all' (default)") ap.add_argument("--list", action="store_true")
ap.add_argument("--list", action="store_true", help="List available scenes")
args = ap.parse_args() args = ap.parse_args()
scenes = list(SCENE_GROUPS.keys()) scenes = list(SCENE_GROUPS.keys())
if args.list: if args.list:
print("Available scenes:", scenes) print("Available:", scenes)
return return
targets = scenes if (args.scene is None or args.scene == "all") else [args.scene]
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
for sn in targets: for sn in targets:
if sn not in scenes:
print(f"Unknown: {sn}")
continue
run_phase1(sn) run_phase1(sn)
print("\nPhase 1 complete.") print("\nPhase 1 complete.")

View File

@ -21,7 +21,7 @@ if _REPO not in sys.path:
from OID_analysis.configs import DATA_DIR # noqa: E402 from OID_analysis.configs import DATA_DIR # noqa: E402
from OID_analysis.utils.analysis import ( # 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, standardize, reconstruct_oid_modes,
) )

View 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()

View File

@ -35,7 +35,7 @@ try:
HAS_SKLEARN = True HAS_SKLEARN = True
except ImportError: except ImportError:
HAS_SKLEARN = False HAS_SKLEARN = False
print("WARNING: sklearn not available. Install: pip install scikit-learn") # sklearn unavailable — comparison will be skipped
SCENES = ["steady_cloak", "karman_re100", SCENES = ["steady_cloak", "karman_re100",

View File

@ -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.")

View File

@ -74,6 +74,26 @@ SCENES["pinball_baseline"] = {
"u0": U0, "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) ---------------------------------------- # -- Disturbance Only (Karman inflow) ----------------------------------------
SCENES["disturbance_only"] = { SCENES["disturbance_only"] = {
"scene_id": "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") return os.path.join(DATA_DIR, "steady_cloak", "empty_channel")
elif sid == "pinball_baseline": elif sid == "pinball_baseline":
return os.path.join(DATA_DIR, "steady_cloak", "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": elif sid == "steady_cloak":
return os.path.join(DATA_DIR, "steady_cloak", "steady_cloak") return os.path.join(DATA_DIR, "steady_cloak", "steady_cloak")
# Karman group: separate dirs for q_in, q_blk, q_ctl # Karman group: separate dirs for q_in, q_blk, q_ctl

View File

@ -4,10 +4,10 @@
"force-oid_m2": 0.43535277661654437, "force-oid_m2": 0.43535277661654437,
"force-oid_m3": 0.43535277661654437, "force-oid_m3": 0.43535277661654437,
"force-oid_m5": 0.43535277661654437, "force-oid_m5": 0.43535277661654437,
"sig-oid_m1": 0.017340158959890415, "sig-oid_m1": -0.15117050299050566,
"sig-oid_m2": 0.30180744104636803, "sig-oid_m2": 0.3261349291477396,
"sig-oid_m3": 0.09790190212866957, "sig-oid_m3": 0.21184489728386308,
"sig-oid_m5": 0.02065845627823741, "sig-oid_m5": 0.025177590823196313,
"sig-pcd_m1": -0.03521104267963627, "sig-pcd_m1": -0.03521104267963627,
"sig-pcd_m2": 0.20732705633252407, "sig-pcd_m2": 0.20732705633252407,
"sig-pcd_m3": 0.11988520616706469, "sig-pcd_m3": 0.11988520616706469,
@ -18,21 +18,21 @@
"pod_m5": -3.0459602065033202 "pod_m5": -3.0459602065033202
}, },
"future_sig": { "future_sig": {
"force-oid_m1": 0.013511471239959334, "force-oid_m1": 0.02808159096495584,
"force-oid_m2": 0.07098337174417249, "force-oid_m2": 0.16866913136184078,
"force-oid_m3": 0.07098337174417249, "force-oid_m3": 0.16866913136184078,
"force-oid_m5": 0.07098337174417249, "force-oid_m5": 0.16866913136184078,
"sig-oid_m1": 0.3740715508827751, "sig-oid_m1": 0.33101756431711893,
"sig-oid_m2": 0.6608883811088201, "sig-oid_m2": 0.5494672616793316,
"sig-oid_m3": 0.5592259563419594, "sig-oid_m3": 0.530079016273497,
"sig-oid_m5": 0.533343435056657, "sig-oid_m5": 0.32401702949087063,
"sig-pcd_m1": 0.20205641028110888, "sig-pcd_m1": 0.16309737465172808,
"sig-pcd_m2": 0.4672590946761527, "sig-pcd_m2": 0.5183971881220766,
"sig-pcd_m3": 0.4468990482184305, "sig-pcd_m3": 0.5204063038036019,
"sig-pcd_m5": 0.41968732641466205, "sig-pcd_m5": 0.4265691501067544,
"pod_m1": -0.2540973280602146, "pod_m1": -0.052450576630182974,
"pod_m2": -0.0339567217960513, "pod_m2": 0.04555274170153097,
"pod_m3": 0.054785729407538376, "pod_m3": 0.0926186828625095,
"pod_m5": 0.3000378545113639 "pod_m5": 0.33872788235759366
} }
} }

View File

@ -4,10 +4,10 @@
"force-oid_m2": 0.6705941647225692, "force-oid_m2": 0.6705941647225692,
"force-oid_m3": 0.6705941647225692, "force-oid_m3": 0.6705941647225692,
"force-oid_m5": 0.6705941647225692, "force-oid_m5": 0.6705941647225692,
"sig-oid_m1": -2.7646669027021566, "sig-oid_m1": -2.590735492534273,
"sig-oid_m2": -2.539151608216361, "sig-oid_m2": -1.746340572676032,
"sig-oid_m3": -1.47692206321327, "sig-oid_m3": -0.505449315098804,
"sig-oid_m5": -1.5110272636915942, "sig-oid_m5": 0.20254103672707288,
"sig-pcd_m1": -1.6681874811094162, "sig-pcd_m1": -1.6681874811094162,
"sig-pcd_m2": -1.342076853642359, "sig-pcd_m2": -1.342076853642359,
"sig-pcd_m3": 0.04249559758899473, "sig-pcd_m3": 0.04249559758899473,
@ -18,21 +18,21 @@
"pod_m5": -0.09978939220221528 "pod_m5": -0.09978939220221528
}, },
"future_sig": { "future_sig": {
"force-oid_m1": -0.688583875677999, "force-oid_m1": 0.045801892403349205,
"force-oid_m2": 0.0977498946249901, "force-oid_m2": 0.2629802484043238,
"force-oid_m3": 0.0977498946249901, "force-oid_m3": 0.2629802484043238,
"force-oid_m5": 0.0977498946249901, "force-oid_m5": 0.2629802484043238,
"sig-oid_m1": 0.3400013732837159, "sig-oid_m1": 0.49884985568656176,
"sig-oid_m2": 0.5855599713349928, "sig-oid_m2": 0.7228144172245711,
"sig-oid_m3": 0.6757301882995801, "sig-oid_m3": 0.800731534358897,
"sig-oid_m5": 0.6051731015609549, "sig-oid_m5": 0.7557779433603652,
"sig-pcd_m1": -0.045583209960261946, "sig-pcd_m1": 0.15914778050377293,
"sig-pcd_m2": -0.07349660070560707, "sig-pcd_m2": 0.41216776425952434,
"sig-pcd_m3": 0.534579564047348, "sig-pcd_m3": 0.7363453012643849,
"sig-pcd_m5": 0.6365887267870092, "sig-pcd_m5": 0.8203723624009522,
"pod_m1": -0.3737860307570295, "pod_m1": -0.21134397189909435,
"pod_m2": -0.1596051084511593, "pod_m2": 0.3003051304179969,
"pod_m3": 0.08266261398865987, "pod_m3": 0.46937683250351936,
"pod_m5": -0.33155756162258626 "pod_m5": 0.38547893664057714
} }
} }

View File

@ -4,10 +4,10 @@
"force-oid_m2": 0.6397818250190341, "force-oid_m2": 0.6397818250190341,
"force-oid_m3": 0.6397818250190341, "force-oid_m3": 0.6397818250190341,
"force-oid_m5": 0.6397818250190341, "force-oid_m5": 0.6397818250190341,
"sig-oid_m1": 0.5371119596459986, "sig-oid_m1": -0.052064205105397096,
"sig-oid_m2": 0.5689626851549741, "sig-oid_m2": 0.5476475353492796,
"sig-oid_m3": 0.5480702090166246, "sig-oid_m3": 0.5104783631016963,
"sig-oid_m5": 0.49764490473273426, "sig-oid_m5": 0.5006514705153909,
"sig-pcd_m1": 0.02922230950403174, "sig-pcd_m1": 0.02922230950403174,
"sig-pcd_m2": 0.4747650262191032, "sig-pcd_m2": 0.4747650262191032,
"sig-pcd_m3": 0.5480885671190363, "sig-pcd_m3": 0.5480885671190363,
@ -18,21 +18,21 @@
"pod_m5": 0.5163077019241664 "pod_m5": 0.5163077019241664
}, },
"future_sig": { "future_sig": {
"force-oid_m1": 0.25720592794565883, "force-oid_m1": 0.17421268011080493,
"force-oid_m2": 0.07069504954059229, "force-oid_m2": 0.14691686742584273,
"force-oid_m3": 0.07069504954059229, "force-oid_m3": 0.14691686742584273,
"force-oid_m5": 0.07069504954059229, "force-oid_m5": 0.14691686742584273,
"sig-oid_m1": 0.3378310203787158, "sig-oid_m1": 0.5380880846698864,
"sig-oid_m2": 0.3147990569733715, "sig-oid_m2": 0.7415813766705678,
"sig-oid_m3": 0.34429262568108926, "sig-oid_m3": 0.7457945827838065,
"sig-oid_m5": 0.33509730308714486, "sig-oid_m5": 0.743474115106988,
"sig-pcd_m1": -0.002980254539846315, "sig-pcd_m1": 0.45784877152463377,
"sig-pcd_m2": 0.35229352094431515, "sig-pcd_m2": 0.6555116659772753,
"sig-pcd_m3": 0.3046193933085676, "sig-pcd_m3": 0.6986463965973865,
"sig-pcd_m5": 0.3332866518761705, "sig-pcd_m5": 0.7043777394599675,
"pod_m1": -0.01505551568170228, "pod_m1": 0.43491763790324245,
"pod_m2": 0.05972906227204257, "pod_m2": 0.5396007454438271,
"pod_m3": 0.050175749864584104, "pod_m3": 0.5888838546919968,
"pod_m5": 0.2244801105502782 "pod_m5": 0.675748834414234
} }
} }

View File

@ -1,21 +1,21 @@
{ {
"force": { "force": {
"force-oid_m1": 0.3973693481528069, "force-oid_m1": 0.11266023422325092,
"force-oid_m2": 0.7503722371594272, "force-oid_m2": 0.29502997457263447,
"force-oid_m3": 0.7503722371594272, "force-oid_m3": 0.29502997457263447,
"force-oid_m5": 0.7503722371594272, "force-oid_m5": 0.29502997457263447,
"sig-oid_m1": 0.047626492192117884, "sig-oid_m1": -0.04664427253668195,
"sig-oid_m2": -0.0899320785087113, "sig-oid_m2": -0.1264864272302698,
"sig-oid_m3": -0.06793031697290859, "sig-oid_m3": -0.5831767238197324,
"sig-oid_m5": 0.050723754778942164, "sig-oid_m5": -0.468171814213285,
"sig-pcd_m1": -0.032869874061091105, "sig-pcd_m1": -0.01403430119734832,
"sig-pcd_m2": -0.03470568581697929, "sig-pcd_m2": -0.10619108268969935,
"sig-pcd_m3": -0.0024393867643178763, "sig-pcd_m3": -0.4769605173029764,
"sig-pcd_m5": 0.20808527695100557, "sig-pcd_m5": -0.5750085233764719,
"pod_m1": -0.028581812678008658, "pod_m1": -0.2578359217654613,
"pod_m2": 0.41796895591108846, "pod_m2": 0.06838543401441043,
"pod_m3": 0.3922853200314628, "pod_m3": -0.04891972521400708,
"pod_m5": 0.5941700935980355 "pod_m5": 0.14664737200706499
}, },
"future_sig": { "future_sig": {
"force-oid_m1": 0.0, "force-oid_m1": 0.0,

View File

@ -1,242 +1,32 @@
{ {
"scenes": [ "canonical_overlap": {
"steady_cloak", "steady_cloak": 0.763,
"karman_re100", "karman_re100": -0.034,
"illusion_0.75L", "illusion_0.75L": -0.082,
"illusion_1.0L", "illusion_1.0L": -0.495,
"illusion_1.5L" "illusion_1.5L": -0.932
],
"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
}
}, },
"three_layer_overlap": {
"karman_re100": { "karman_re100": {
"force": { "act_force": -0.03164697269821093,
"force-oid_m1": 0.3973693481528069, "act_sig": -0.07431818697760144,
"force-oid_m2": 0.7503722371594272, "force_sig": -0.0343739260491455
"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
}
}, },
"illusion_0.75L": { "illusion_0.75L": {
"force": { "act_force": -0.32732725369388294,
"force-oid_m1": -0.0065221308964502865, "act_sig": -0.4007721802551729,
"force-oid_m2": 0.43535277661654437, "force_sig": -0.6941197281957918
"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
}
}, },
"illusion_1.0L": { "illusion_1.0L": {
"force": { "act_force": 0.1208564966066174,
"force-oid_m1": -0.22355143786066206, "act_sig": -0.3183892941307407,
"force-oid_m2": 0.6705941647225692, "force_sig": -0.8423715368324566
"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
}
}, },
"illusion_1.5L": { "illusion_1.5L": {
"force": { "act_force": 0.0017766926815728562,
"force-oid_m1": 0.5712134182396399, "act_sig": 0.017393103043959173,
"force-oid_m2": 0.6397818250190341, "force_sig": -0.9319165643601308
"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
}
} }
}, },
"steady_metrics": { "note": "canonical_overlap from robustness/robustness_results.json (Phase 4 verified). three_layer_overlap from action_oid/three_layer_overlap.json (Phase 3.3)."
"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
} }

View File

@ -2,13 +2,11 @@
"scene": "karman_re100", "scene": "karman_re100",
"n_snapshots": 500, "n_snapshots": 500,
"dof": 67200, "dof": 67200,
"ranks_computed": [ "roi": {
6, "x_start": 400,
8, "x_end": 1000,
10, "y_start": 100,
12, "y_end": 400
16 },
], "energy_r10_5modes": 0.9971600541895967
"energy_r10_5modes": 0.999034936679307,
"energy_r10_10modes": 1.0
} }

View File

@ -1,27 +1,27 @@
{ {
"near-body": { "near-body": {
"rms_reduction": NaN, "rms_reduction": null,
"rms_ctl": NaN, "rms_ctl": null,
"rms_blk": NaN, "rms_blk": null,
"enstrophy_reduction": NaN, "enstrophy_reduction": null,
"enstrophy_ctl": NaN, "enstrophy_ctl": null,
"enstrophy_blk": NaN "enstrophy_blk": null
}, },
"near-wake": { "near-wake": {
"rms_reduction": NaN, "rms_reduction": null,
"rms_ctl": NaN, "rms_ctl": null,
"rms_blk": NaN, "rms_blk": null,
"enstrophy_reduction": NaN, "enstrophy_reduction": null,
"enstrophy_ctl": NaN, "enstrophy_ctl": null,
"enstrophy_blk": NaN "enstrophy_blk": null
}, },
"downstream": { "downstream": {
"rms_reduction": NaN, "rms_reduction": null,
"rms_ctl": NaN, "rms_ctl": null,
"rms_blk": NaN, "rms_blk": null,
"enstrophy_reduction": NaN, "enstrophy_reduction": null,
"enstrophy_ctl": NaN, "enstrophy_ctl": null,
"enstrophy_blk": NaN "enstrophy_blk": null
}, },
"full-field": { "full-field": {
"rms_reduction": 0.9943383932113647, "rms_reduction": 0.9943383932113647,

View File

@ -1,6 +1,6 @@
{ {
"obs_act_train": 0.9560973048210144, "obs_act_train": 0.7495932579040527,
"oid_m3_act_train": 0.22518758634108724, "oid_m3_act_train": 0.09605772105033679,
"oid_m5_act_train": 0.22518758634108724, "oid_m5_act_train": 0.09605772105033679,
"oid_force_act_train": 0.23275911247341877 "oid_force_act_train": 0.11671916873232673
} }

View File

@ -1,20 +1,20 @@
{ {
"force_norm_fact": 0.019301448483020067, "force_norm_fact": 0.0192322488874197,
"sens_deviation": [ "sens_deviation": [
0.7973665595054626, 0.8234996795654297,
-0.12411565333604813, -0.12610766291618347,
0.25158512592315674, 0.2525006830692291,
-0.008040801621973515, -0.0017381409415975213,
0.8032128214836121, 0.7810041308403015,
0.11381910741329193 0.12196661531925201
], ],
"sens_norm_fact": [ "sens_norm_fact": [
2.679999828338623, 3.3339574337005615,
3.1077914237976074, 3.35296630859375,
1.8671960830688477, 1.8934848308563232,
3.329291582107544, 3.4226977825164795,
2.9416096210479736, 3.0502705574035645,
3.267305374145508 3.111417293548584
], ],
"action_bias": [ "action_bias": [
0.0, 0.0,

View File

@ -1,6 +1,7 @@
{ {
"scene_id": "karman", "scene_id": "karman",
"re_code": 100, "re_code": 100,
"nu": 0.004,
"has_disturbance": true, "has_disturbance": true,
"sample_interval": 800, "sample_interval": 800,
"action_scale": 8.0, "action_scale": 8.0,
@ -15,6 +16,5 @@
"pinball_rear_x": 31.3, "pinball_rear_x": 31.3,
"target_type": "periodic", "target_type": "periodic",
"s_dim": 12, "s_dim": 12,
"u0": 0.01, "u0": 0.01
"nu": 0.004
} }

View File

@ -1,5 +1,5 @@
{ {
"scene": "karman_re100", "scene": "karman_re100",
"similarity": 0.9587061844662661, "n_steps": 500,
"avg_reward": 0.6636279821395874 "similarity": 0.0
} }

View File

@ -0,0 +1,11 @@
{
"b": [
-0.50183952461055,
1.8028572256396647,
0.9279757672456204
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.1212773001776175,
-1.105074395289117,
-0.34648093962286164
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.4184715060497721,
-1.3685952801208017,
-0.9135450702850836
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.6192820372820806,
1.323147379353252,
0.3497826213249393
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.5853389184939632,
1.0245952124131246,
-1.3647893246033778
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.8887997326226098,
0.31168730399387945,
1.8378931069404252
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.14665559293889308,
-0.37455972200225096,
1.9427136779789969
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.0138691722806219,
0.5688041539622137,
-1.7355708911724486
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.5912848486058588,
-1.9017651869966448,
0.9188491002868981
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.465421112976955,
-1.5658313769098995,
-0.8441260685611001
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.9795710871745877,
1.1054411812737994,
0.1595565970088657
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.6459875300407005,
-0.09114155333949236,
-1.0259259797858125
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.976635333029455,
-0.23944382955750476,
-1.3585539709107581
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.3145839737744005,
-0.7591402157937948,
1.3995114972146747
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.7574949366035553,
1.6333928802519022,
0.6543058167377063
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.4526172180324002,
0.33776862483765857,
-0.29068249723337125
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.3159803306695812,
-1.1904107706133054,
-1.620466358155475
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.7116120188801447,
0.42700921371229006,
-1.0623248060598298
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.8341327092635038,
0.7154327763130017,
-0.13535513665180865
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.7418182403314864,
-0.05414376543833943,
0.7634032184443917
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.2538767384955332,
1.241503249739469,
-1.8792948738528992
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.674364482047568,
-1.7037584832883859,
-0.6690933905451282
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-0.03833325919793418,
-0.4509096318096564,
1.140036945488447
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.7968937619479481,
-0.6422574333823232,
0.4471896678048437
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.5475627078942522,
-1.4430042164046313,
1.9183114286620007
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.9402143911890095,
-0.5398574163339709,
0.9989393722367566
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
1.5631753705911264,
-1.0002201195286093,
1.3524879030174843
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.2069400932531078,
0.18407026478534183,
-1.2452484507591657
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
0.7172204245811309,
-1.2572127604498027,
-0.21199372738442612
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

View File

@ -0,0 +1,11 @@
{
"b": [
-1.5514372601850652,
0.8029509557883627,
1.5716082689624455
],
"nu": 0.004,
"U0": 0.01,
"Re_D": 50,
"Re_code": 100
}

Some files were not shown because too many files have changed in this diff Show More