{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "381b36b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Tuple, Union\n",
    "from collections import deque\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from stable_baselines3 import PPO\n",
    "import pycuda.driver as cuda\n",
    "import pandas as pd\n",
    "import pickle\n",
    "import sys\n",
    "import os\n",
    "from gym_dummy import CustomEnv as DummyEnv\n",
    "\n",
    "current_dir = os.path.dirname(os.path.abspath(\"__file__\"))\n",
    "parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))\n",
    "sys.path.append(parent_dir)\n",
    "\n",
    "from CelerisLab import FlowField\n",
    "from CelerisLab import utils\n",
    "\n",
    "env_12 = DummyEnv(s_dim=12)\n",
    "env_14 = DummyEnv(s_dim=14)\n",
    "model_cloak_re100 = PPO.load(os.path.join(parent_dir, \"models\", \"old\", \"d1a3o12_re100.zip\"), env=env_12, device=\"cuda:0\")\n",
    "model_illusion = PPO.load(os.path.join(parent_dir, \"models\", \"250525\", \"d1a3o14_250525_imit_1L_2U_600S.zip\"), env=env_14, device=\"cuda:0\")\n",
    "model_illusion_075L = PPO.load(os.path.join(parent_dir, \"models\", \"250525\", \"d1a3o14_250525_imit_075L_2U_400S.zip\"), env=env_14, device=\"cuda:0\")\n",
    "model_illusion_15L = PPO.load(os.path.join(parent_dir, \"models\", \"250525\", \"d1a3o14_250525_imit_15L_2U.zip\"), env=env_14, device=\"cuda:0\")\n",
    "model_erase = PPO.load(os.path.join(parent_dir, \"models\", \"250729\", \"d1a3o12_250729_250326_erase_250804_20D_retrain2.zip\"), env=env_12, device=\"cuda:0\")\n",
    "model_cloak_lamb = PPO.load(os.path.join(parent_dir, \"models\", \"old\", \"vortex_lamb.zip\"), env=env_12, device=\"cuda:0\")\n",
    "model_cloak_taylor = PPO.load(os.path.join(parent_dir, \"models\", \"old\", \"vortex_taylor.zip\"), env=env_12, device=\"cuda:0\")\n",
    "\n",
    "model_cloak_re100.set_random_seed(0)\n",
    "model_illusion.set_random_seed(19)\n",
    "model_illusion_075L.set_random_seed(19)\n",
    "model_illusion_15L.set_random_seed(19)\n",
    "model_erase.set_random_seed(19)\n",
    "model_cloak_lamb.set_random_seed(0)\n",
    "model_cloak_taylor.set_random_seed(0)\n",
    "\n",
    "cuda.init()\n",
    "context = cuda.Device(0).make_context()\n",
    "config_cuda = utils.load_cuda_config(os.path.join(parent_dir, \"configs\", \"config_cuda.json\"))\n",
    "config_field = utils.load_flow_field_config(os.path.join(parent_dir, \"configs\", \"config_flowfield.json\"))\n",
    "\n",
    "L0 = 20\n",
    "U0 = config_field.velocity\n",
    "DATA_TYPE = np.float32\n",
    "CONV_LEN = 36\n",
    "\n",
    "context.push()\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "NX = flow_field.FIELD_SHAPE[0]\n",
    "NY = flow_field.FIELD_SHAPE[1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a276c1b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def save_field(flow_field, filename):\n",
    "    NX = flow_field.FIELD_SHAPE[0]\n",
    "    NY = flow_field.FIELD_SHAPE[1]\n",
    "    flow_field.get_ddf()\n",
    "    ddf_plot = flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)\n",
    "    flag_plot = flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)\n",
    "    ux = (ddf_plot[:, :, 1] + ddf_plot[:, :, 5] + ddf_plot[:, :, 8] - ddf_plot[:, :, 3] - ddf_plot[:, :, 6] - ddf_plot[:, :, 7]) / U0\n",
    "    uy = (ddf_plot[:, :, 2] + ddf_plot[:, :, 5] + ddf_plot[:, :, 6] - ddf_plot[:, :, 4] - ddf_plot[:, :, 7] - ddf_plot[:, :, 8]) / U0\n",
    "    with open(os.path.join(parent_dir, \"output\", filename), \"w\") as f:\n",
    "        f.write(\"Title= \\\"LBM 2D\\\"\\r\\n\")\n",
    "        f.write(\"VARIABLES= \\\"X\\\",\\\"Y\\\",\\\"flag\\\",\\\"U\\\",\\\"V\\\",\\r\\n\")\n",
    "        f.write(f\"ZONE T= \\\"BOX\\\",I= {NX},J= {NY},F=POINT\\r\\n\")\n",
    "        for j in range(NY):\n",
    "            for i in range(NX):\n",
    "                f.write(f\"{i},{j},{flag_plot[i, j]},{ux[i, j]},{uy[i, j]}\\r\\n\")\n",
    "\n",
    "class SimpleMeta:\n",
    "    pass\n",
    "\n",
    "def analyze_harmonics(states, n_harmonics):\n",
    "    N, D = states.shape\n",
    "    result = []\n",
    "    for d in range(D):\n",
    "        y = states[:, d]\n",
    "        fft_coef = np.fft.rfft(y)\n",
    "        freqs = np.fft.rfftfreq(N, d=1)\n",
    "        amps = 2 * np.abs(fft_coef) / N\n",
    "        phases = np.angle(fft_coef)\n",
    "        idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1\n",
    "        harmonics = {\n",
    "            'dc': np.real(fft_coef[0]) / N,\n",
    "            'amps': amps[idx],\n",
    "            'freqs': freqs[idx],\n",
    "            'phases': phases[idx]\n",
    "        }\n",
    "        result.append(harmonics)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "751ba334",
   "metadata": {},
   "outputs": [],
   "source": [
    "target_states = np.empty((0, 6), dtype=DATA_TYPE)\n",
    "meta_cloak_steady = SimpleMeta()\n",
    "meta_cloak_dipole = SimpleMeta()\n",
    "meta_cloak_monopole = SimpleMeta()\n",
    "meta_illusion = SimpleMeta()\n",
    "meta_cloak_karman = SimpleMeta()\n",
    "\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(2*NX/U0), np.zeros(3, dtype=DATA_TYPE))\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(600, np.zeros(3, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:6]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_cloak_steady.target_states = np.mean(target_states, axis=0)\n",
    "\n",
    "# save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", \"target_steady.dat\"))\n",
    "\n",
    "target_states = np.empty((0, 6), dtype=DATA_TYPE)\n",
    "flow_field.get_ddf()\n",
    "flow_field.save_ddf()\n",
    "\n",
    "center_vor: Tuple[float, float, float] = (15 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, \"lamb\")\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(3, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:6]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_cloak_dipole.target_states = np.mean(target_states, axis=0)\n",
    "# flow_field.restore_ddf()\n",
    "# flow_field.apply_ddf()\n",
    "# flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, \"lamb\")\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(3, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_lamb.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "\n",
    "target_states = np.empty((0, 6), dtype=DATA_TYPE)\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, \"taylor\")\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(3, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:6]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_cloak_monopole.target_states = np.mean(target_states, axis=0)\n",
    "# flow_field.restore_ddf()\n",
    "# flow_field.apply_ddf()\n",
    "# flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, \"taylor\")\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(3, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_taylor.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5a23560c",
   "metadata": {},
   "outputs": [],
   "source": [
    "target_states = np.empty((0, 6), dtype=DATA_TYPE)\n",
    "fifo_states = deque(maxlen=150)\n",
    "\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(6, dtype=DATA_TYPE))\n",
    "flow_field.get_ddf()\n",
    "flow_field.save_ddf()\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(600, np.zeros(6, dtype=DATA_TYPE))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])\n",
    "\n",
    "temp_states = np.array(fifo_states)\n",
    "meta_illusion.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))\n",
    "\n",
    "meta_illusion.sens_deviation = np.zeros(6, dtype=DATA_TYPE)\n",
    "meta_illusion.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)\n",
    "for i in range(6):\n",
    "    meta_illusion.sens_deviation[i] = np.mean(temp_states[:, i])\n",
    "    meta_illusion.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_illusion.sens_deviation[i]))\n",
    "\n",
    "fifo_states = deque(maxlen=150)\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "flow_field.run(int(2*NX/U0), np.array([0.0, 0.0, 0.0, 0.0, -5*U0, 5*U0], dtype=DATA_TYPE))\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, \"lamb\")\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(6, dtype=DATA_TYPE))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])\n",
    "\n",
    "temp_states = np.array(fifo_states)\n",
    "meta_cloak_dipole.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))\n",
    "\n",
    "meta_cloak_dipole.sens_deviation = np.zeros(6, dtype=DATA_TYPE)\n",
    "meta_cloak_dipole.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)\n",
    "for i in range(6):\n",
    "    meta_cloak_dipole.sens_deviation[i] = np.mean(temp_states[:, i])\n",
    "    meta_cloak_dipole.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_dipole.sens_deviation[i]))\n",
    "\n",
    "fifo_states = deque(maxlen=150)\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "flow_field.run(int(2*NX/U0), np.array([0.0, 0.0, 0.0, 0.0, -5*U0, 5*U0], dtype=DATA_TYPE))\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, \"taylor\")\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(6, dtype=DATA_TYPE))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])\n",
    "\n",
    "temp_states = np.array(fifo_states)\n",
    "meta_cloak_monopole.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))\n",
    "\n",
    "meta_cloak_monopole.sens_deviation = np.zeros(6, dtype=DATA_TYPE)\n",
    "meta_cloak_monopole.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)\n",
    "for i in range(6):\n",
    "    meta_cloak_monopole.sens_deviation[i] = np.mean(temp_states[:, i])\n",
    "    meta_cloak_monopole.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_monopole.sens_deviation[i]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "fc50665e",
   "metadata": {},
   "outputs": [],
   "source": [
    "fifo_states = deque(maxlen=150)\n",
    "\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, 1*L0)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(7, dtype=DATA_TYPE))\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(7, dtype=DATA_TYPE))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])\n",
    "\n",
    "temp_states = np.array(fifo_states)\n",
    "meta_cloak_karman.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))\n",
    "\n",
    "meta_cloak_karman.sens_deviation = np.zeros(6, dtype=DATA_TYPE)\n",
    "meta_cloak_karman.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)\n",
    "for i in range(6):\n",
    "    meta_cloak_karman.sens_deviation[i] = np.mean(temp_states[:, i])\n",
    "    meta_cloak_karman.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_karman.sens_deviation[i]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "a5eee254",
   "metadata": {},
   "outputs": [],
   "source": [
    "del flow_field\n",
    "\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, 1*L0)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))\n",
    "\n",
    "target_states = np.empty((0, 6), dtype=DATA_TYPE)\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[2:8]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_cloak_karman.target_states = target_states\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_karman.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "feb7c904",
   "metadata": {},
   "outputs": [],
   "source": [
    "del flow_field\n",
    "\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, 1*L0)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))\n",
    "\n",
    "target_states = np.empty((0, 8), dtype=DATA_TYPE)\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:8]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_illusion.target_states_1L = target_states\n",
    "meta_illusion.target_harmonics_1L = analyze_harmonics(target_states, n_harmonics=5)\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_1L.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "573cda50",
   "metadata": {},
   "outputs": [],
   "source": [
    "del flow_field\n",
    "\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, 0.75*L0)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))\n",
    "\n",
    "target_states = np.empty((0, 8), dtype=DATA_TYPE)\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(400, np.zeros(4, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:8]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_illusion.target_states_075L = target_states\n",
    "meta_illusion.target_harmonics_075L = analyze_harmonics(target_states, n_harmonics=5)\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_075L.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "56f4be7d",
   "metadata": {},
   "outputs": [],
   "source": [
    "del flow_field\n",
    "\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, 1.5*L0)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))\n",
    "\n",
    "target_states = np.empty((0, 8), dtype=DATA_TYPE)\n",
    "\n",
    "for i in range(150):\n",
    "    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))\n",
    "    new_state = flow_field.obs.copy()[0:8]\n",
    "    target_states = np.vstack((target_states, new_state))\n",
    "\n",
    "meta_illusion.target_states_15L = target_states\n",
    "meta_illusion.target_harmonics_15L = analyze_harmonics(target_states, n_harmonics=5)\n",
    "\n",
    "# for i in range(100):\n",
    "#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))\n",
    "#     file_name = f\"target_15L.{i:03d}\"\n",
    "#     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d30ec201",
   "metadata": {},
   "outputs": [],
   "source": [
    "del flow_field\n",
    "\n",
    "flow_field = FlowField(config_field, config_cuda, device_id=0)\n",
    "center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)\n",
    "flow_field.add_cylinder(center, L0 / 2)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)\n",
    "flow_field.add_sensor(center, L0 / 4)\n",
    "flow_field.run(int(4*NX/U0), np.zeros(6, dtype=DATA_TYPE))\n",
    "\n",
    "flow_field.get_ddf()\n",
    "flow_field.save_ddf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "75309ab9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# flow_field.restore_ddf()\n",
    "# flow_field.apply_ddf()\n",
    "fifo_states = deque(maxlen=150)\n",
    "for i in range(100):\n",
    "    flow_field.run(1000, np.zeros(6, dtype=DATA_TYPE))\n",
    "    file_name = f\"act_nc.{i:03d}\"\n",
    "    # save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "608f0eec",
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(75):\n",
    "    flow_field.run(1000, np.array([0.0, -5.1*U0, 5.1*U0, 0.0, 0.0, 0.0], dtype=DATA_TYPE))\n",
    "    file_name = f\"act_cloak_steady.{i:03d}\"\n",
    "    # save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "2999f0ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "flow_field.get_ddf()\n",
    "flow_field.save_ddf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "9c4b02f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, \"lamb\")\n",
    "\n",
    "obs = np.zeros(12, dtype=np.float32)\n",
    "for i in range(125):\n",
    "    action, _states = model_cloak_lamb.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(6, dtype=DATA_TYPE)\n",
    "    if i < 25:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/25) + temp_transition * (1 - i/25)\n",
    "    elif 45 <= i < 70:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (1-(i-45)/25) + temp_transition * ((i-45)/25)\n",
    "    elif i >= 70:\n",
    "        temp[0:3] = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "    else:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(800, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_cloak_dipole.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_cloak_dipole.sens_deviation) / meta_cloak_dipole.sens_norm_fact\n",
    "    obs = np.hstack([forces, sens])\n",
    "    file_name = f\"act_cloak_dipole.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "546e86c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, \"taylor\")\n",
    "\n",
    "obs = np.zeros(12, dtype=np.float32)\n",
    "for i in range(125):\n",
    "    action, _states = model_cloak_taylor.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(6, dtype=DATA_TYPE)\n",
    "    if i < 20:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/20) + temp_transition * (1 - i/20)\n",
    "    elif 45 <= i < 70:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (1-(i-45)/25) + temp_transition * ((i-45)/25)\n",
    "    elif i >= 70:\n",
    "        temp[0:3] = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "    else:\n",
    "        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(800, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_cloak_monopole.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_cloak_monopole.sens_deviation) / meta_cloak_monopole.sens_norm_fact\n",
    "    obs = np.hstack([forces, sens])\n",
    "    file_name = f\"act_cloak_monopole.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "    fifo_states.append(flow_field.obs.copy()[0:12])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "1f57113b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gen_target_states_at(t, harmonics):\n",
    "    t = np.asarray(t)\n",
    "    D = len(harmonics)\n",
    "    result = np.zeros((t.size, D), dtype=np.float32)\n",
    "    for d, h in enumerate(harmonics):\n",
    "        val = np.full(t.shape, h['dc'], dtype=np.float32)\n",
    "        for amp, freq, phase in zip(h['amps'], h['freqs'], h['phases']):\n",
    "            val += amp * np.cos(2 * np.pi * freq * t + phase)\n",
    "        result[:, d] = val\n",
    "    if result.shape[0] == 1:\n",
    "        return result[0]\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "a7999510",
   "metadata": {},
   "outputs": [],
   "source": [
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "\n",
    "obs = np.zeros(14, dtype=np.float32)\n",
    "for i in range(200):\n",
    "    action, _states = model_illusion.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(6, dtype=DATA_TYPE)\n",
    "    if i < 10:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)\n",
    "    else:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(800, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_illusion.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact\n",
    "    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_1L)\n",
    "    target_cd = target_states[0] / meta_illusion.force_norm_fact\n",
    "    target_cl = target_states[1] / meta_illusion.force_norm_fact\n",
    "    obs = np.hstack([forces, sens, target_cd, target_cl])\n",
    "    file_name = f\"act_illusion_1L.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "    # if i % 2 == 0:\n",
    "    #     index = i // 2\n",
    "    #     file_name = f\"act_illusion_1L.{index:03d}\"\n",
    "    #     save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "65b31ee4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# flow_field.apply_ddf()\n",
    "\n",
    "obs = np.zeros(14, dtype=np.float32)\n",
    "for i in range(400):\n",
    "    action, _states = model_illusion_075L.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(6, dtype=DATA_TYPE)\n",
    "    if i < 20:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)\n",
    "    else:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(400, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_illusion.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact\n",
    "    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_075L)\n",
    "    target_cd = target_states[0] / meta_illusion.force_norm_fact\n",
    "    target_cl = target_states[1] / meta_illusion.force_norm_fact\n",
    "    obs = np.hstack([forces, sens, target_cd, target_cl])\n",
    "    if i % 2 == 0:\n",
    "        index = i // 2\n",
    "        file_name = f\"act_illusion_075L.{index:03d}\"\n",
    "        save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "af362132",
   "metadata": {},
   "outputs": [],
   "source": [
    "# flow_field.apply_ddf()\n",
    "\n",
    "obs = np.zeros(14, dtype=np.float32)\n",
    "for i in range(200):\n",
    "    action, _states = model_illusion_15L.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(6, dtype=DATA_TYPE)\n",
    "    if i < 10:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)\n",
    "    else:\n",
    "        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(800, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_illusion.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact\n",
    "    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_15L)\n",
    "    target_cd = target_states[0] / meta_illusion.force_norm_fact\n",
    "    target_cl = target_states[1] / meta_illusion.force_norm_fact\n",
    "    obs = np.hstack([forces, sens, target_cd, target_cl])\n",
    "    file_name = f\"act_illusion_15L.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "c1eed77f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)\n",
    "# flow_field.add_cylinder(center, 1*L0)\n",
    "flow_field.restore_ddf()\n",
    "flow_field.apply_ddf()\n",
    "\n",
    "obs = np.zeros(12, dtype=np.float32)\n",
    "for i in range(200):\n",
    "    action, _states = model_cloak_re100.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(7, dtype=DATA_TYPE)\n",
    "    if i < 10:\n",
    "        temp_action = np.array([0, 0, 0], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)\n",
    "    else:\n",
    "        temp_action = np.array([0, 0, 0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(1000, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_cloak_karman.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_cloak_karman.sens_deviation) / meta_cloak_karman.sens_norm_fact\n",
    "    obs = np.hstack([forces, sens])\n",
    "    file_name = f\"act_karman_nc.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))\n",
    "\n",
    "for i in range(200):\n",
    "    action, _states = model_cloak_re100.predict(observation=obs, deterministic=True)\n",
    "    temp = np.zeros(7, dtype=DATA_TYPE)\n",
    "    if i < 10:\n",
    "        temp_action = np.array(action*8 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp_transition = np.array([0, 0, 0], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)\n",
    "    else:\n",
    "        temp_action = np.array(action*8 + [0, -4, 4], dtype=DATA_TYPE)\n",
    "        temp[0:3] = temp_action * U0\n",
    "    flow_field.run(800, temp)\n",
    "    states = np.array(flow_field.obs.copy()[0:12])\n",
    "    forces = states[0:6] / meta_cloak_karman.force_norm_fact\n",
    "    cd = (forces[0] + forces[2] + forces[4]) / 3\n",
    "    cl = (forces[1] + forces[3] + forces[5]) / 3\n",
    "    sens = (states[6:12] - meta_cloak_karman.sens_deviation) / meta_cloak_karman.sens_norm_fact\n",
    "    obs = np.hstack([forces, sens])\n",
    "    file_name = f\"act_karman_cloak.{i:03d}\"\n",
    "    save_field(flow_field, os.path.join(parent_dir, \"output\", \"250823\", \"data\", file_name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c8cb1e8",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "946a1212",
   "metadata": {},
   "source": [
    "## POD 与 Z₂ 对称性分析（新增）\n",
    "\n",
    "这一部分用于你现有输出快照（`output/250823/data`）的离线分析，不需要重新跑 CFD。\n",
    "\n",
    "### 1) POD（Proper Orthogonal Decomposition）\n",
    "对快照矩阵做均值去除后 SVD：\n",
    "\n",
    "$$\n",
    "\\mathbf{X}' = \\mathbf{U}\\,\\mathbf{\\Sigma}\\,\\mathbf{V}^T\n",
    "$$\n",
    "\n",
    "其中 POD 能量占比：\n",
    "\n",
    "$$\n",
    "\\lambda_k = \\frac{\\sigma_k^2}{\\sum_i \\sigma_i^2}\n",
    "$$\n",
    "\n",
    "累计能量：\n",
    "\n",
    "$$\n",
    "C_K = \\sum_{k=1}^{K} \\lambda_k\n",
    "$$\n",
    "\n",
    "### 2) Z₂ 对称性分析（关于中线 $y\\to -y$）\n",
    "对速度场 $(u,v)$ 的反射算子定义为：\n",
    "\n",
    "$$\n",
    "\\mathcal{R}(u,v) = (u(x,-y), -v(x,-y))\n",
    "$$\n",
    "\n",
    "对称/反对称分解：\n",
    "\n",
    "$$\n",
    "\\mathbf{u}_s = \\frac{\\mathbf{u}+\\mathcal{R}\\mathbf{u}}{2}, \\qquad\n",
    "\\mathbf{u}_a = \\frac{\\mathbf{u}-\\mathcal{R}\\mathbf{u}}{2}\n",
    "$$\n",
    "\n",
    "定义对称性破缺指数：\n",
    "\n",
    "$$\n",
    "\\eta_{Z_2} = \\frac{\\|\\mathbf{u}_a\\|^2}{\\|\\mathbf{u}_s\\|^2 + \\|\\mathbf{u}_a\\|^2}\n",
    "$$\n",
    "\n",
    "- $\\eta_{Z_2}\\approx 0$：近似镜像对称\n",
    "- $\\eta_{Z_2}$ 越大：对称性破缺越明显"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c251cc40",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "POD/Z2 helper functions ready.\n",
      "Data dir: /home/frank14f/Frank_LBM/output/250823/data\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import re\n",
    "import glob\n",
    "from pathlib import Path\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "\n",
    "def _parse_zone_ij(file_path: str) -> tuple[int, int]:\n",
    "    with open(file_path, \"r\") as f:\n",
    "        _ = f.readline()\n",
    "        _ = f.readline()\n",
    "        zone_line = f.readline()\n",
    "    m_i = re.search(r\"I=\\s*(\\d+)\", zone_line)\n",
    "    m_j = re.search(r\"J=\\s*(\\d+)\", zone_line)\n",
    "    if m_i is None or m_j is None:\n",
    "        raise ValueError(f\"Cannot parse I/J in: {file_path}\")\n",
    "    return int(m_i.group(1)), int(m_j.group(1))\n",
    "\n",
    "\n",
    "def load_tecplot_snapshot(file_path: str):\n",
    "    \"\"\"Load one snapshot saved by save_field().\n",
    "\n",
    "    Returns\n",
    "    -------\n",
    "    flag, u, v: ndarray with shape (NX, NY)\n",
    "    \"\"\"\n",
    "    NX, NY = _parse_zone_ij(file_path)\n",
    "    df = pd.read_csv(\n",
    "        file_path,\n",
    "        skiprows=3,\n",
    "        header=None,\n",
    "        names=[\"i\", \"j\", \"flag\", \"u\", \"v\"],\n",
    "    )\n",
    "    arr = df[[\"flag\", \"u\", \"v\"]].to_numpy().reshape(NY, NX, 3)\n",
    "    flag = arr[:, :, 0].T\n",
    "    u = arr[:, :, 1].T\n",
    "    v = arr[:, :, 2].T\n",
    "    return flag, u, v\n",
    "\n",
    "\n",
    "def collect_case_files(data_dir: str, prefix: str, n_snapshots: int | None = None):\n",
    "    files = sorted(glob.glob(str(Path(data_dir) / f\"{prefix}.*\")))\n",
    "    if n_snapshots is not None:\n",
    "        files = files[:n_snapshots]\n",
    "    if len(files) == 0:\n",
    "        raise FileNotFoundError(f\"No files found for prefix={prefix} in {data_dir}\")\n",
    "    return files\n",
    "\n",
    "\n",
    "def build_snapshot_matrix(file_list: list[str]):\n",
    "    \"\"\"Build snapshot matrix X for POD from velocity fields.\n",
    "\n",
    "    X shape: (n_features, n_snapshots)\n",
    "    n_features = 2 * n_fluid_points (u + v).\n",
    "    \"\"\"\n",
    "    flags = []\n",
    "    uv_fields = []\n",
    "\n",
    "    for fp in file_list:\n",
    "        flag, u, v = load_tecplot_snapshot(fp)\n",
    "        flags.append(flag)\n",
    "        uv_fields.append((u, v))\n",
    "\n",
    "    # Heuristic: the most frequent flag value is considered fluid.\n",
    "    first_flag = flags[0]\n",
    "    vals, cnts = np.unique(first_flag, return_counts=True)\n",
    "    fluid_flag = vals[np.argmax(cnts)]\n",
    "    fluid_mask = first_flag == fluid_flag\n",
    "\n",
    "    cols = []\n",
    "    for u, v in uv_fields:\n",
    "        cols.append(np.hstack([u[fluid_mask], v[fluid_mask]]))\n",
    "\n",
    "    X = np.stack(cols, axis=1)\n",
    "    return X, fluid_mask, uv_fields, fluid_flag\n",
    "\n",
    "\n",
    "def pod_from_snapshots(X: np.ndarray):\n",
    "    \"\"\"Compute POD by SVD after mean subtraction.\"\"\"\n",
    "    Xc = X - X.mean(axis=1, keepdims=True)\n",
    "    U, S, Vt = np.linalg.svd(Xc, full_matrices=False)\n",
    "    eig = S**2\n",
    "    ratio = eig / np.sum(eig)\n",
    "    cumsum = np.cumsum(ratio)\n",
    "    return {\n",
    "        \"U\": U,\n",
    "        \"S\": S,\n",
    "        \"Vt\": Vt,\n",
    "        \"ratio\": ratio,\n",
    "        \"cumsum\": cumsum,\n",
    "    }\n",
    "\n",
    "\n",
    "def z2_metrics_for_field(u: np.ndarray, v: np.ndarray, fluid_mask: np.ndarray):\n",
    "    \"\"\"Compute Z2 symmetry metrics for one snapshot.\n",
    "\n",
    "    Reflection operator: R(u, v) = (u(x,-y), -v(x,-y)).\n",
    "    \"\"\"\n",
    "    u_ref = u[:, ::-1]\n",
    "    v_ref = -v[:, ::-1]\n",
    "\n",
    "    u_sym = 0.5 * (u + u_ref)\n",
    "    v_sym = 0.5 * (v + v_ref)\n",
    "    u_anti = 0.5 * (u - u_ref)\n",
    "    v_anti = 0.5 * (v - v_ref)\n",
    "\n",
    "    E_sym = np.sum(u_sym[fluid_mask] ** 2 + v_sym[fluid_mask] ** 2)\n",
    "    E_anti = np.sum(u_anti[fluid_mask] ** 2 + v_anti[fluid_mask] ** 2)\n",
    "\n",
    "    eta_z2 = E_anti / (E_sym + E_anti + 1e-12)\n",
    "\n",
    "    # Signed order parameter: mean cross-stream velocity (normalized by kinetic norm)\n",
    "    signed_m = np.sum(v[fluid_mask]) / np.sqrt(np.sum(u[fluid_mask] ** 2 + v[fluid_mask] ** 2) + 1e-12)\n",
    "\n",
    "    return {\n",
    "        \"eta_z2\": float(eta_z2),\n",
    "        \"E_sym\": float(E_sym),\n",
    "        \"E_anti\": float(E_anti),\n",
    "        \"signed_m\": float(signed_m),\n",
    "    }\n",
    "\n",
    "\n",
    "def analyze_case(data_dir: str, prefix: str, n_snapshots: int = 40):\n",
    "    files = collect_case_files(data_dir, prefix, n_snapshots=n_snapshots)\n",
    "    X, fluid_mask, uv_fields, fluid_flag = build_snapshot_matrix(files)\n",
    "\n",
    "    pod = pod_from_snapshots(X)\n",
    "    z2 = [z2_metrics_for_field(u, v, fluid_mask) for (u, v) in uv_fields]\n",
    "\n",
    "    eta = np.array([z[\"eta_z2\"] for z in z2])\n",
    "    signed_m = np.array([z[\"signed_m\"] for z in z2])\n",
    "\n",
    "    return {\n",
    "        \"prefix\": prefix,\n",
    "        \"n_snapshots\": len(files),\n",
    "        \"fluid_flag\": float(fluid_flag),\n",
    "        \"pod_ratio\": pod[\"ratio\"],\n",
    "        \"pod_cumsum\": pod[\"cumsum\"],\n",
    "        \"eta_z2_series\": eta,\n",
    "        \"signed_m_series\": signed_m,\n",
    "    }\n",
    "\n",
    "\n",
    "def summarize_case(result: dict):\n",
    "    r = result[\"pod_ratio\"]\n",
    "    c = result[\"pod_cumsum\"]\n",
    "    eta = result[\"eta_z2_series\"]\n",
    "    m = result[\"signed_m_series\"]\n",
    "\n",
    "    return {\n",
    "        \"case\": result[\"prefix\"],\n",
    "        \"n\": result[\"n_snapshots\"],\n",
    "        \"POD_r1\": float(r[0]),\n",
    "        \"POD_r2\": float(r[1]) if len(r) > 1 else np.nan,\n",
    "        \"POD_r3\": float(r[2]) if len(r) > 2 else np.nan,\n",
    "        \"POD_c3\": float(c[2]) if len(c) > 2 else np.nan,\n",
    "        \"POD_c5\": float(c[4]) if len(c) > 4 else np.nan,\n",
    "        \"etaZ2_mean\": float(np.mean(eta)),\n",
    "        \"etaZ2_std\": float(np.std(eta)),\n",
    "        \"signed_m_mean\": float(np.mean(m)),\n",
    "        \"signed_m_std\": float(np.std(m)),\n",
    "    }\n",
    "\n",
    "\n",
    "resolved_parent_dir = globals().get(\"parent_dir\")\n",
    "if resolved_parent_dir is None:\n",
    "    # Fallback: notebook is under scripts/, so parent is workspace root.\n",
    "    resolved_parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))\n",
    "\n",
    "DATA_DIR = os.path.join(resolved_parent_dir, \"output\", \"250823\", \"data\")\n",
    "print(\"POD/Z2 helper functions ready.\")\n",
    "print(\"Data dir:\", DATA_DIR)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f723b634",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "case",
         "rawType": "object",
         "type": "string"
        },
        {
         "name": "n",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "POD_r1",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_r2",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_r3",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_c3",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_c5",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mean",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_std",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mean",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_std",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "369f7cd9-34a1-48ac-aeb8-f0e2fb81ccb3",
       "rows": [
        [
         "0",
         "act_nc",
         "20",
         "0.5453104678544648",
         "0.4184484628978619",
         "0.01011473983280817",
         "0.9738736705851349",
         "0.9911362641227843",
         "0.02712569493861817",
         "0.0006030596241233723",
         "0.010536330795236654",
         "0.12530499612443965"
        ],
        [
         "1",
         "act_cloak_steady",
         "20",
         "0.4846147427998569",
         "0.39204425512169633",
         "0.07097285712676774",
         "0.9476318550483209",
         "0.9775350644795309",
         "0.024850363332746362",
         "0.0021645245169620085",
         "0.004568624042934281",
         "0.11349791354083755"
        ],
        [
         "2",
         "act_karman_nc",
         "20",
         "0.43071185973764003",
         "0.4005729581129849",
         "0.086936743836616",
         "0.9182215616872409",
         "0.9707082281716332",
         "0.039163345627187277",
         "0.005226781281080615",
         "0.03873218521259392",
         "0.22464298994564075"
        ],
        [
         "3",
         "act_karman_cloak",
         "20",
         "0.47436163138736653",
         "0.4175490471265246",
         "0.07043821442543226",
         "0.9623488929393234",
         "0.986613473430638",
         "0.043867790491814294",
         "0.001102249057068186",
         "-0.02289334335091659",
         "0.1039502152755866"
        ],
        [
         "4",
         "act_illusion_1L",
         "20",
         "0.7894457334235928",
         "0.15231860942150405",
         "0.04002898617863977",
         "0.9817933290237366",
         "0.996634746254787",
         "0.00018755543194344526",
         "0.0001171814014465266",
         "-0.0013336041773132632",
         "0.0047226859733438915"
        ]
       ],
       "shape": {
        "columns": 11,
        "rows": 5
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>case</th>\n",
       "      <th>n</th>\n",
       "      <th>POD_r1</th>\n",
       "      <th>POD_r2</th>\n",
       "      <th>POD_r3</th>\n",
       "      <th>POD_c3</th>\n",
       "      <th>POD_c5</th>\n",
       "      <th>etaZ2_mean</th>\n",
       "      <th>etaZ2_std</th>\n",
       "      <th>signed_m_mean</th>\n",
       "      <th>signed_m_std</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>act_nc</td>\n",
       "      <td>20</td>\n",
       "      <td>0.545310</td>\n",
       "      <td>0.418448</td>\n",
       "      <td>0.010115</td>\n",
       "      <td>0.973874</td>\n",
       "      <td>0.991136</td>\n",
       "      <td>0.027126</td>\n",
       "      <td>0.000603</td>\n",
       "      <td>0.010536</td>\n",
       "      <td>0.125305</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>act_cloak_steady</td>\n",
       "      <td>20</td>\n",
       "      <td>0.484615</td>\n",
       "      <td>0.392044</td>\n",
       "      <td>0.070973</td>\n",
       "      <td>0.947632</td>\n",
       "      <td>0.977535</td>\n",
       "      <td>0.024850</td>\n",
       "      <td>0.002165</td>\n",
       "      <td>0.004569</td>\n",
       "      <td>0.113498</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>act_karman_nc</td>\n",
       "      <td>20</td>\n",
       "      <td>0.430712</td>\n",
       "      <td>0.400573</td>\n",
       "      <td>0.086937</td>\n",
       "      <td>0.918222</td>\n",
       "      <td>0.970708</td>\n",
       "      <td>0.039163</td>\n",
       "      <td>0.005227</td>\n",
       "      <td>0.038732</td>\n",
       "      <td>0.224643</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>act_karman_cloak</td>\n",
       "      <td>20</td>\n",
       "      <td>0.474362</td>\n",
       "      <td>0.417549</td>\n",
       "      <td>0.070438</td>\n",
       "      <td>0.962349</td>\n",
       "      <td>0.986613</td>\n",
       "      <td>0.043868</td>\n",
       "      <td>0.001102</td>\n",
       "      <td>-0.022893</td>\n",
       "      <td>0.103950</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>act_illusion_1L</td>\n",
       "      <td>20</td>\n",
       "      <td>0.789446</td>\n",
       "      <td>0.152319</td>\n",
       "      <td>0.040029</td>\n",
       "      <td>0.981793</td>\n",
       "      <td>0.996635</td>\n",
       "      <td>0.000188</td>\n",
       "      <td>0.000117</td>\n",
       "      <td>-0.001334</td>\n",
       "      <td>0.004723</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "               case   n    POD_r1    POD_r2    POD_r3    POD_c3    POD_c5  \\\n",
       "0            act_nc  20  0.545310  0.418448  0.010115  0.973874  0.991136   \n",
       "1  act_cloak_steady  20  0.484615  0.392044  0.070973  0.947632  0.977535   \n",
       "2     act_karman_nc  20  0.430712  0.400573  0.086937  0.918222  0.970708   \n",
       "3  act_karman_cloak  20  0.474362  0.417549  0.070438  0.962349  0.986613   \n",
       "4   act_illusion_1L  20  0.789446  0.152319  0.040029  0.981793  0.996635   \n",
       "\n",
       "   etaZ2_mean  etaZ2_std  signed_m_mean  signed_m_std  \n",
       "0    0.027126   0.000603       0.010536      0.125305  \n",
       "1    0.024850   0.002165       0.004569      0.113498  \n",
       "2    0.039163   0.005227       0.038732      0.224643  \n",
       "3    0.043868   0.001102      -0.022893      0.103950  \n",
       "4    0.000188   0.000117      -0.001334      0.004723  "
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 你可以按需增减 case。这里先给一个与你当前结果最相关的集合。\n",
    "cases = [\n",
    "    \"act_nc\",            # 无控制基线（steady）\n",
    "    \"act_cloak_steady\",  # steady cloaking\n",
    "    \"act_karman_nc\",     # karman 来流无控制\n",
    "    \"act_karman_cloak\",  # karman 来流 cloaking\n",
    "    \"act_illusion_1L\",   # illusion (1.0L target)\n",
    "]\n",
    "\n",
    "results = []\n",
    "for c in cases:\n",
    "    res = analyze_case(DATA_DIR, c, n_snapshots=20)\n",
    "    results.append(res)\n",
    "\n",
    "summary_df = pd.DataFrame([summarize_case(r) for r in results])\n",
    "summary_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f9b07ac0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 53604 characters]",
      "text/plain": [
       "<Figure size 1200x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[act_nc] POD c3=0.974, c5=0.991; etaZ2=0.0271±0.0006\n",
      "[act_cloak_steady] POD c3=0.948, c5=0.978; etaZ2=0.0249±0.0022\n",
      "[act_karman_nc] POD c3=0.918, c5=0.971; etaZ2=0.0392±0.0052\n",
      "[act_karman_cloak] POD c3=0.962, c5=0.987; etaZ2=0.0439±0.0011\n",
      "[act_illusion_1L] POD c3=0.982, c5=0.997; etaZ2=0.0002±0.0001\n"
     ]
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "# 可视化：POD 前三模态能量占比 + Z2 指数分布\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "\n",
    "x = np.arange(len(summary_df))\n",
    "barw = 0.25\n",
    "axes[0].bar(x - barw, summary_df[\"POD_r1\"], width=barw, label=\"r1\")\n",
    "axes[0].bar(x, summary_df[\"POD_r2\"], width=barw, label=\"r2\")\n",
    "axes[0].bar(x + barw, summary_df[\"POD_r3\"], width=barw, label=\"r3\")\n",
    "axes[0].set_xticks(x)\n",
    "axes[0].set_xticklabels(summary_df[\"case\"], rotation=30, ha=\"right\")\n",
    "axes[0].set_ylabel(\"Energy ratio\")\n",
    "axes[0].set_title(\"POD modal energy (top 3)\")\n",
    "axes[0].legend()\n",
    "\n",
    "axes[1].errorbar(\n",
    "    x,\n",
    "    summary_df[\"etaZ2_mean\"],\n",
    "    yerr=summary_df[\"etaZ2_std\"],\n",
    "    fmt=\"o\",\n",
    "    capsize=4,\n",
    ")\n",
    "axes[1].set_xticks(x)\n",
    "axes[1].set_xticklabels(summary_df[\"case\"], rotation=30, ha=\"right\")\n",
    "axes[1].set_ylabel(\"$\\\\eta_{Z_2}$\")\n",
    "axes[1].set_title(\"Z2 symmetry-breaking index\")\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 打印一段自动化文字结论（初步）\n",
    "for _, row in summary_df.iterrows():\n",
    "    print(\n",
    "        f\"[{row['case']}] POD c3={row['POD_c3']:.3f}, c5={row['POD_c5']:.3f}; \"\n",
    "        f\"etaZ2={row['etaZ2_mean']:.4f}±{row['etaZ2_std']:.4f}\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6131addd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyDMD import OK\n",
      "PyDMD path: /home/frank14f/anaconda3/envs/pycuda_3_10/lib/python3.10/site-packages/pydmd/__init__.py\n",
      "Core classes: ['DMD', 'BOPDMD', 'HODMD', 'MrDMD', 'DMDc']\n",
      "Note: POD 本身不依赖 PyDMD，已由 SVD 完成；PyDMD 主要可用于后续 DMD 频率/模态分析。\n"
     ]
    }
   ],
   "source": [
    "# PyDMD 可用性检查（可选）\n",
    "try:\n",
    "    import pydmd\n",
    "    print(\"PyDMD import OK\")\n",
    "    print(\"PyDMD path:\", getattr(pydmd, \"__file__\", \"N/A\"))\n",
    "    available = [\"DMD\", \"BOPDMD\", \"HODMD\", \"MrDMD\", \"DMDc\"]\n",
    "    print(\"Core classes:\", [name for name in available if hasattr(pydmd, name)])\n",
    "except Exception as e:\n",
    "    print(\"PyDMD not available in current env:\", repr(e))\n",
    "    print(\"Tip 1: pip install pydmd\")\n",
    "    print(\"Tip 2: or install local repo /home/frank14f/Frank_LBM/JFM_WYQ/PyDMD with dependencies\")\n",
    "\n",
    "print(\"Note: POD 本身不依赖 PyDMD，已由 SVD 完成；PyDMD 主要可用于后续 DMD 频率/模态分析。\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "159dcf0e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== act_nc: POD mode Z2 (top 6) ===\n"
     ]
    },
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "mode",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "energy_ratio",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mode",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mode",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "7486a29c-6938-44c9-8d9c-af76e68fbd34",
       "rows": [
        [
         "0",
         "1",
         "0.5453104678544648",
         "0.9994080119919929",
         "0.45275043997789943"
        ],
        [
         "1",
         "2",
         "0.4184484628978619",
         "0.9999419968042454",
         "-1.0526882909856006"
        ],
        [
         "2",
         "3",
         "0.01011473983280817",
         "0.8521120820192422",
         "-0.19003334589718332"
        ],
        [
         "3",
         "4",
         "0.009767173069931025",
         "0.7344566942976254",
         "-0.556252781636925"
        ],
        [
         "4",
         "5",
         "0.007495420467718459",
         "0.2657819245402694",
         "-0.31450018031016985"
        ],
        [
         "5",
         "6",
         "0.007301407311240587",
         "0.14655055988416574",
         "-0.11972677252940012"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 6
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>mode</th>\n",
       "      <th>energy_ratio</th>\n",
       "      <th>etaZ2_mode</th>\n",
       "      <th>signed_m_mode</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.545310</td>\n",
       "      <td>0.999408</td>\n",
       "      <td>0.452750</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.418448</td>\n",
       "      <td>0.999942</td>\n",
       "      <td>-1.052688</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.010115</td>\n",
       "      <td>0.852112</td>\n",
       "      <td>-0.190033</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.009767</td>\n",
       "      <td>0.734457</td>\n",
       "      <td>-0.556253</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.007495</td>\n",
       "      <td>0.265782</td>\n",
       "      <td>-0.314500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.007301</td>\n",
       "      <td>0.146551</td>\n",
       "      <td>-0.119727</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   mode  energy_ratio  etaZ2_mode  signed_m_mode\n",
       "0     1      0.545310    0.999408       0.452750\n",
       "1     2      0.418448    0.999942      -1.052688\n",
       "2     3      0.010115    0.852112      -0.190033\n",
       "3     4      0.009767    0.734457      -0.556253\n",
       "4     5      0.007495    0.265782      -0.314500\n",
       "5     6      0.007301    0.146551      -0.119727"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== act_cloak_steady: POD mode Z2 (top 6) ===\n"
     ]
    },
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "mode",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "energy_ratio",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mode",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mode",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "4c0bc756-70bc-492c-bbba-7895f29d1451",
       "rows": [
        [
         "0",
         "1",
         "0.4846147427998569",
         "0.9380281704643914",
         "-0.6496960949814631"
        ],
        [
         "1",
         "2",
         "0.39204425512169633",
         "0.9138584975862195",
         "-0.6406265951571893"
        ],
        [
         "2",
         "3",
         "0.07097285712676774",
         "0.27480590846948716",
         "-0.9278430938622736"
        ],
        [
         "3",
         "4",
         "0.018648443037237623",
         "0.5777029619135854",
         "-1.039376204036435"
        ],
        [
         "4",
         "5",
         "0.01125476639397228",
         "0.4170610888043789",
         "0.9383708863979906"
        ],
        [
         "5",
         "6",
         "0.009137738195040303",
         "0.8664358753167714",
         "0.21471060434184322"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 6
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>mode</th>\n",
       "      <th>energy_ratio</th>\n",
       "      <th>etaZ2_mode</th>\n",
       "      <th>signed_m_mode</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.484615</td>\n",
       "      <td>0.938028</td>\n",
       "      <td>-0.649696</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.392044</td>\n",
       "      <td>0.913858</td>\n",
       "      <td>-0.640627</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.070973</td>\n",
       "      <td>0.274806</td>\n",
       "      <td>-0.927843</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.018648</td>\n",
       "      <td>0.577703</td>\n",
       "      <td>-1.039376</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.011255</td>\n",
       "      <td>0.417061</td>\n",
       "      <td>0.938371</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.009138</td>\n",
       "      <td>0.866436</td>\n",
       "      <td>0.214711</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   mode  energy_ratio  etaZ2_mode  signed_m_mode\n",
       "0     1      0.484615    0.938028      -0.649696\n",
       "1     2      0.392044    0.913858      -0.640627\n",
       "2     3      0.070973    0.274806      -0.927843\n",
       "3     4      0.018648    0.577703      -1.039376\n",
       "4     5      0.011255    0.417061       0.938371\n",
       "5     6      0.009138    0.866436       0.214711"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== act_karman_nc: POD mode Z2 (top 6) ===\n"
     ]
    },
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "mode",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "energy_ratio",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mode",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mode",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "a729ed30-bc26-4cc0-93c2-21b64db8582a",
       "rows": [
        [
         "0",
         "1",
         "0.43071185973764003",
         "0.8050578423957492",
         "0.9292417993136286"
        ],
        [
         "1",
         "2",
         "0.4005729581129849",
         "0.8711046658788251",
         "0.05690189633267477"
        ],
        [
         "2",
         "3",
         "0.086936743836616",
         "0.5854347934835601",
         "0.31627914865929146"
        ],
        [
         "3",
         "4",
         "0.03822169148275825",
         "0.713888226187294",
         "0.541525500257247"
        ],
        [
         "4",
         "5",
         "0.014264975001634072",
         "0.5716665773277405",
         "-0.5381156468530371"
        ],
        [
         "5",
         "6",
         "0.009711805880947477",
         "0.768920786314115",
         "0.424069185564871"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 6
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>mode</th>\n",
       "      <th>energy_ratio</th>\n",
       "      <th>etaZ2_mode</th>\n",
       "      <th>signed_m_mode</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.430712</td>\n",
       "      <td>0.805058</td>\n",
       "      <td>0.929242</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.400573</td>\n",
       "      <td>0.871105</td>\n",
       "      <td>0.056902</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.086937</td>\n",
       "      <td>0.585435</td>\n",
       "      <td>0.316279</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.038222</td>\n",
       "      <td>0.713888</td>\n",
       "      <td>0.541526</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.014265</td>\n",
       "      <td>0.571667</td>\n",
       "      <td>-0.538116</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.009712</td>\n",
       "      <td>0.768921</td>\n",
       "      <td>0.424069</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   mode  energy_ratio  etaZ2_mode  signed_m_mode\n",
       "0     1      0.430712    0.805058       0.929242\n",
       "1     2      0.400573    0.871105       0.056902\n",
       "2     3      0.086937    0.585435       0.316279\n",
       "3     4      0.038222    0.713888       0.541526\n",
       "4     5      0.014265    0.571667      -0.538116\n",
       "5     6      0.009712    0.768921       0.424069"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== act_karman_cloak: POD mode Z2 (top 6) ===\n"
     ]
    },
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "mode",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "energy_ratio",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mode",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mode",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "2cdca3d1-438c-49f1-ab37-396b0a90ef8d",
       "rows": [
        [
         "0",
         "1",
         "0.47436163138736653",
         "0.8751319885061303",
         "0.6769883516132917"
        ],
        [
         "1",
         "2",
         "0.4175490471265246",
         "0.9580735144087666",
         "-0.044610252133867045"
        ],
        [
         "2",
         "3",
         "0.07043821442543226",
         "0.7824188986670307",
         "-0.36616531749688763"
        ],
        [
         "3",
         "4",
         "0.016490145442195788",
         "0.5980339994937104",
         "-0.30644658307936956"
        ],
        [
         "4",
         "5",
         "0.007774435049118909",
         "0.3988135834851191",
         "0.3518113555876005"
        ],
        [
         "5",
         "6",
         "0.005982100144970479",
         "0.8042291893964388",
         "-0.18963533442943759"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 6
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>mode</th>\n",
       "      <th>energy_ratio</th>\n",
       "      <th>etaZ2_mode</th>\n",
       "      <th>signed_m_mode</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.474362</td>\n",
       "      <td>0.875132</td>\n",
       "      <td>0.676988</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.417549</td>\n",
       "      <td>0.958074</td>\n",
       "      <td>-0.044610</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.070438</td>\n",
       "      <td>0.782419</td>\n",
       "      <td>-0.366165</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.016490</td>\n",
       "      <td>0.598034</td>\n",
       "      <td>-0.306447</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.007774</td>\n",
       "      <td>0.398814</td>\n",
       "      <td>0.351811</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.005982</td>\n",
       "      <td>0.804229</td>\n",
       "      <td>-0.189635</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   mode  energy_ratio  etaZ2_mode  signed_m_mode\n",
       "0     1      0.474362    0.875132       0.676988\n",
       "1     2      0.417549    0.958074      -0.044610\n",
       "2     3      0.070438    0.782419      -0.366165\n",
       "3     4      0.016490    0.598034      -0.306447\n",
       "4     5      0.007774    0.398814       0.351811\n",
       "5     6      0.005982    0.804229      -0.189635"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== act_illusion_1L: POD mode Z2 (top 6) ===\n"
     ]
    },
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "mode",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "energy_ratio",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mode",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mode",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "b8d70cac-9266-4045-9cc1-f2ab1c7ed086",
       "rows": [
        [
         "0",
         "1",
         "0.7894457334235928",
         "0.0040886828114025",
         "-0.017435264313956228"
        ],
        [
         "1",
         "2",
         "0.15231860942150405",
         "0.08274130721502927",
         "-0.11572931922071367"
        ],
        [
         "2",
         "3",
         "0.04002898617863977",
         "0.27349974145089123",
         "-0.0210728683252721"
        ],
        [
         "3",
         "4",
         "0.011281681951248682",
         "0.40987687838885856",
         "-0.23023901547165737"
        ],
        [
         "4",
         "5",
         "0.0035597352798016823",
         "0.4608974294665576",
         "-0.46398520728112275"
        ],
        [
         "5",
         "6",
         "0.0017309079961941116",
         "0.36849945701350023",
         "-0.7231875306582263"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 6
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>mode</th>\n",
       "      <th>energy_ratio</th>\n",
       "      <th>etaZ2_mode</th>\n",
       "      <th>signed_m_mode</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.789446</td>\n",
       "      <td>0.004089</td>\n",
       "      <td>-0.017435</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.152319</td>\n",
       "      <td>0.082741</td>\n",
       "      <td>-0.115729</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.040029</td>\n",
       "      <td>0.273500</td>\n",
       "      <td>-0.021073</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.011282</td>\n",
       "      <td>0.409877</td>\n",
       "      <td>-0.230239</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.003560</td>\n",
       "      <td>0.460897</td>\n",
       "      <td>-0.463985</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.001731</td>\n",
       "      <td>0.368499</td>\n",
       "      <td>-0.723188</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   mode  energy_ratio  etaZ2_mode  signed_m_mode\n",
       "0     1      0.789446    0.004089      -0.017435\n",
       "1     2      0.152319    0.082741      -0.115729\n",
       "2     3      0.040029    0.273500      -0.021073\n",
       "3     4      0.011282    0.409877      -0.230239\n",
       "4     5      0.003560    0.460897      -0.463985\n",
       "5     6      0.001731    0.368499      -0.723188"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 进阶：POD 模态级别的 Z2 对称性（前6模态）\n",
    "\n",
    "def pod_mode_z2_table(data_dir: str, prefix: str, n_snapshots: int = 40, n_modes: int = 6):\n",
    "    files = collect_case_files(data_dir, prefix, n_snapshots=n_snapshots)\n",
    "    X, fluid_mask, uv_fields, _ = build_snapshot_matrix(files)\n",
    "    pod = pod_from_snapshots(X)\n",
    "\n",
    "    n_fluid = int(np.sum(fluid_mask))\n",
    "    Umat = pod[\"U\"]\n",
    "    out = []\n",
    "\n",
    "    for k in range(min(n_modes, Umat.shape[1])):\n",
    "        mode_vec = Umat[:, k]\n",
    "        u = np.zeros_like(uv_fields[0][0])\n",
    "        v = np.zeros_like(uv_fields[0][1])\n",
    "        u[fluid_mask] = mode_vec[:n_fluid]\n",
    "        v[fluid_mask] = mode_vec[n_fluid:]\n",
    "        z2 = z2_metrics_for_field(u, v, fluid_mask)\n",
    "        out.append(\n",
    "            {\n",
    "                \"mode\": k + 1,\n",
    "                \"energy_ratio\": float(pod[\"ratio\"][k]),\n",
    "                \"etaZ2_mode\": z2[\"eta_z2\"],\n",
    "                \"signed_m_mode\": z2[\"signed_m\"],\n",
    "            }\n",
    "        )\n",
    "\n",
    "    return pd.DataFrame(out)\n",
    "\n",
    "\n",
    "for c in [\"act_nc\", \"act_cloak_steady\", \"act_karman_nc\", \"act_karman_cloak\", \"act_illusion_1L\"]:\n",
    "    print(f\"\\n=== {c}: POD mode Z2 (top 6) ===\")\n",
    "    display(pod_mode_z2_table(DATA_DIR, c, n_snapshots=20, n_modes=6))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a66314ce",
   "metadata": {},
   "source": [
    "### tail-30 版本（推荐汇报口径）\n",
    "\n",
    "你提到 250823 是连续衔接测试流，这里采用每个 case 的最后 30 帧进行 POD 与 Z2 分析，减少过渡段影响。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "ed7c80d5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "case",
         "rawType": "object",
         "type": "string"
        },
        {
         "name": "n_tail",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "POD_r1",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_r2",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_r3",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_c3",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "POD_c5",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_mean",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "etaZ2_std",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_mean",
         "rawType": "float64",
         "type": "float"
        },
        {
         "name": "signed_m_std",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "9ee7e798-b3f2-4a24-a667-778f0b913d1a",
       "rows": [
        [
         "0",
         "act_nc",
         "30",
         "0.5265958599850638",
         "0.4370990934814651",
         "0.01033728938373843",
         "0.9740322428502673",
         "0.9913319351209482",
         "0.02700101939788688",
         "0.0006078344180394181",
         "-0.019123779320809126",
         "0.12851059100756945"
        ],
        [
         "1",
         "act_cloak_steady",
         "30",
         "0.7402694361793121",
         "0.18870959559890788",
         "0.050305109447756666",
         "0.9792841412259766",
         "0.998620444604609",
         "0.0002530544349978394",
         "0.0007131590242358435",
         "0.0010666717685869265",
         "0.014347794059403923"
        ],
        [
         "2",
         "act_karman_nc",
         "30",
         "0.4310014707680547",
         "0.3977404106377873",
         "0.06779031660271409",
         "0.8965321980085561",
         "0.9636615894587849",
         "0.047404791862959275",
         "0.0022445429038198557",
         "0.000857481224825774",
         "0.11008100445563687"
        ],
        [
         "3",
         "act_karman_cloak",
         "30",
         "0.545639719200303",
         "0.4228124895508167",
         "0.010409110156730333",
         "0.9788613189078499",
         "0.9931341240624365",
         "0.04105746265066539",
         "0.000857878145676811",
         "0.01755965733390702",
         "0.1310440942793705"
        ],
        [
         "4",
         "act_illusion_1L",
         "30",
         "0.5394370944564028",
         "0.422263454521596",
         "0.010111493849711528",
         "0.9718120428277104",
         "0.9885182903636507",
         "0.02912351245151389",
         "0.0007798087813011604",
         "-0.03246098517688319",
         "0.12469165927172388"
        ]
       ],
       "shape": {
        "columns": 11,
        "rows": 5
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>case</th>\n",
       "      <th>n_tail</th>\n",
       "      <th>POD_r1</th>\n",
       "      <th>POD_r2</th>\n",
       "      <th>POD_r3</th>\n",
       "      <th>POD_c3</th>\n",
       "      <th>POD_c5</th>\n",
       "      <th>etaZ2_mean</th>\n",
       "      <th>etaZ2_std</th>\n",
       "      <th>signed_m_mean</th>\n",
       "      <th>signed_m_std</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>act_nc</td>\n",
       "      <td>30</td>\n",
       "      <td>0.526596</td>\n",
       "      <td>0.437099</td>\n",
       "      <td>0.010337</td>\n",
       "      <td>0.974032</td>\n",
       "      <td>0.991332</td>\n",
       "      <td>0.027001</td>\n",
       "      <td>0.000608</td>\n",
       "      <td>-0.019124</td>\n",
       "      <td>0.128511</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>act_cloak_steady</td>\n",
       "      <td>30</td>\n",
       "      <td>0.740269</td>\n",
       "      <td>0.188710</td>\n",
       "      <td>0.050305</td>\n",
       "      <td>0.979284</td>\n",
       "      <td>0.998620</td>\n",
       "      <td>0.000253</td>\n",
       "      <td>0.000713</td>\n",
       "      <td>0.001067</td>\n",
       "      <td>0.014348</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>act_karman_nc</td>\n",
       "      <td>30</td>\n",
       "      <td>0.431001</td>\n",
       "      <td>0.397740</td>\n",
       "      <td>0.067790</td>\n",
       "      <td>0.896532</td>\n",
       "      <td>0.963662</td>\n",
       "      <td>0.047405</td>\n",
       "      <td>0.002245</td>\n",
       "      <td>0.000857</td>\n",
       "      <td>0.110081</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>act_karman_cloak</td>\n",
       "      <td>30</td>\n",
       "      <td>0.545640</td>\n",
       "      <td>0.422812</td>\n",
       "      <td>0.010409</td>\n",
       "      <td>0.978861</td>\n",
       "      <td>0.993134</td>\n",
       "      <td>0.041057</td>\n",
       "      <td>0.000858</td>\n",
       "      <td>0.017560</td>\n",
       "      <td>0.131044</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>act_illusion_1L</td>\n",
       "      <td>30</td>\n",
       "      <td>0.539437</td>\n",
       "      <td>0.422263</td>\n",
       "      <td>0.010111</td>\n",
       "      <td>0.971812</td>\n",
       "      <td>0.988518</td>\n",
       "      <td>0.029124</td>\n",
       "      <td>0.000780</td>\n",
       "      <td>-0.032461</td>\n",
       "      <td>0.124692</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "               case  n_tail    POD_r1    POD_r2    POD_r3    POD_c3    POD_c5  \\\n",
       "0            act_nc      30  0.526596  0.437099  0.010337  0.974032  0.991332   \n",
       "1  act_cloak_steady      30  0.740269  0.188710  0.050305  0.979284  0.998620   \n",
       "2     act_karman_nc      30  0.431001  0.397740  0.067790  0.896532  0.963662   \n",
       "3  act_karman_cloak      30  0.545640  0.422812  0.010409  0.978861  0.993134   \n",
       "4   act_illusion_1L      30  0.539437  0.422263  0.010111  0.971812  0.988518   \n",
       "\n",
       "   etaZ2_mean  etaZ2_std  signed_m_mean  signed_m_std  \n",
       "0    0.027001   0.000608      -0.019124      0.128511  \n",
       "1    0.000253   0.000713       0.001067      0.014348  \n",
       "2    0.047405   0.002245       0.000857      0.110081  \n",
       "3    0.041057   0.000858       0.017560      0.131044  \n",
       "4    0.029124   0.000780      -0.032461      0.124692  "
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def analyze_case_tail(data_dir: str, prefix: str, n_tail: int = 30):\n",
    "    files = sorted(glob.glob(str(Path(data_dir) / f\"{prefix}.*\")))[-n_tail:]\n",
    "    if len(files) == 0:\n",
    "        raise FileNotFoundError(f\"No files found for {prefix}\")\n",
    "\n",
    "    X, fluid_mask, uv_fields, fluid_flag = build_snapshot_matrix(files)\n",
    "    pod = pod_from_snapshots(X)\n",
    "    z2 = [z2_metrics_for_field(u, v, fluid_mask) for (u, v) in uv_fields]\n",
    "\n",
    "    eta = np.array([z[\"eta_z2\"] for z in z2])\n",
    "    signed_m = np.array([z[\"signed_m\"] for z in z2])\n",
    "\n",
    "    return {\n",
    "        \"prefix\": prefix,\n",
    "        \"n_tail\": len(files),\n",
    "        \"fluid_flag\": float(fluid_flag),\n",
    "        \"pod_ratio\": pod[\"ratio\"],\n",
    "        \"pod_cumsum\": pod[\"cumsum\"],\n",
    "        \"pod_U\": pod[\"U\"],\n",
    "        \"eta_z2_series\": eta,\n",
    "        \"signed_m_series\": signed_m,\n",
    "        \"fluid_mask\": fluid_mask,\n",
    "    }\n",
    "\n",
    "\n",
    "def summarize_case_tail(result: dict):\n",
    "    r = result[\"pod_ratio\"]\n",
    "    c = result[\"pod_cumsum\"]\n",
    "    eta = result[\"eta_z2_series\"]\n",
    "    m = result[\"signed_m_series\"]\n",
    "\n",
    "    return {\n",
    "        \"case\": result[\"prefix\"],\n",
    "        \"n_tail\": result[\"n_tail\"],\n",
    "        \"POD_r1\": float(r[0]),\n",
    "        \"POD_r2\": float(r[1]) if len(r) > 1 else np.nan,\n",
    "        \"POD_r3\": float(r[2]) if len(r) > 2 else np.nan,\n",
    "        \"POD_c3\": float(c[2]) if len(c) > 2 else np.nan,\n",
    "        \"POD_c5\": float(c[4]) if len(c) > 4 else np.nan,\n",
    "        \"etaZ2_mean\": float(np.mean(eta)),\n",
    "        \"etaZ2_std\": float(np.std(eta)),\n",
    "        \"signed_m_mean\": float(np.mean(m)),\n",
    "        \"signed_m_std\": float(np.std(m)),\n",
    "    }\n",
    "\n",
    "\n",
    "cases_tail = [\"act_nc\", \"act_cloak_steady\", \"act_karman_nc\", \"act_karman_cloak\", \"act_illusion_1L\"]\n",
    "results_tail = [analyze_case_tail(DATA_DIR, c, n_tail=30) for c in cases_tail]\n",
    "summary_tail_df = pd.DataFrame([summarize_case_tail(r) for r in results_tail])\n",
    "summary_tail_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "efdb2548",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 55972 characters]",
      "text/plain": [
       "<Figure size 1200x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# tail-30 汇总可视化\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "\n",
    "x = np.arange(len(summary_tail_df))\n",
    "barw = 0.25\n",
    "axes[0].bar(x - barw, summary_tail_df[\"POD_r1\"], width=barw, label=\"r1\")\n",
    "axes[0].bar(x, summary_tail_df[\"POD_r2\"], width=barw, label=\"r2\")\n",
    "axes[0].bar(x + barw, summary_tail_df[\"POD_r3\"], width=barw, label=\"r3\")\n",
    "axes[0].set_xticks(x)\n",
    "axes[0].set_xticklabels(summary_tail_df[\"case\"], rotation=30, ha=\"right\")\n",
    "axes[0].set_ylabel(\"Energy ratio\")\n",
    "axes[0].set_title(\"POD modal energy (tail-30)\")\n",
    "axes[0].legend()\n",
    "\n",
    "axes[1].errorbar(\n",
    "    x,\n",
    "    summary_tail_df[\"etaZ2_mean\"],\n",
    "    yerr=summary_tail_df[\"etaZ2_std\"],\n",
    "    fmt=\"o\",\n",
    "    capsize=4,\n",
    ")\n",
    "axes[1].set_xticks(x)\n",
    "axes[1].set_xticklabels(summary_tail_df[\"case\"], rotation=30, ha=\"right\")\n",
    "axes[1].set_ylabel(\"eta_Z2\")\n",
    "axes[1].set_title(\"Z2 symmetry-breaking index (tail-30)\")\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "71b140db",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 183092 characters]",
      "text/plain": [
       "<Figure size 1250x650 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 126060 characters]",
      "text/plain": [
       "<Figure size 1250x650 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 242756 characters]",
      "text/plain": [
       "<Figure size 1250x650 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 177440 characters]",
      "text/plain": [
       "<Figure size 1250x650 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 179192 characters]",
      "text/plain": [
       "<Figure size 1250x650 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# tail-30 的 POD 模态图（每个 case 前3模态，u/v 分量）\n",
    "\n",
    "def plot_top3_modes_from_result(res: dict):\n",
    "    U = res[\"pod_U\"]\n",
    "    mask = res[\"fluid_mask\"]\n",
    "    n_fluid = int(np.sum(mask))\n",
    "\n",
    "    fig, axes = plt.subplots(2, 3, figsize=(12.5, 6.5))\n",
    "    for k in range(3):\n",
    "        mode = U[:, k]\n",
    "        um = np.zeros(mask.shape, dtype=np.float32)\n",
    "        vm = np.zeros(mask.shape, dtype=np.float32)\n",
    "        um[mask] = mode[:n_fluid]\n",
    "        vm[mask] = mode[n_fluid:]\n",
    "\n",
    "        lim_u = np.max(np.abs(um)) + 1e-12\n",
    "        lim_v = np.max(np.abs(vm)) + 1e-12\n",
    "\n",
    "        im_u = axes[0, k].imshow(um.T, origin=\"lower\", cmap=\"RdBu_r\", vmin=-lim_u, vmax=lim_u)\n",
    "        axes[0, k].set_title(f\"Mode {k+1} - u\")\n",
    "        axes[0, k].set_xticks([])\n",
    "        axes[0, k].set_yticks([])\n",
    "        fig.colorbar(im_u, ax=axes[0, k], fraction=0.046, pad=0.04)\n",
    "\n",
    "        im_v = axes[1, k].imshow(vm.T, origin=\"lower\", cmap=\"RdBu_r\", vmin=-lim_v, vmax=lim_v)\n",
    "        axes[1, k].set_title(f\"Mode {k+1} - v\")\n",
    "        axes[1, k].set_xticks([])\n",
    "        axes[1, k].set_yticks([])\n",
    "        fig.colorbar(im_v, ax=axes[1, k], fraction=0.046, pad=0.04)\n",
    "\n",
    "    fig.suptitle(f\"POD spatial modes (tail-30): {res['prefix']}\", y=1.02)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "for rr in results_tail:\n",
    "    plot_top3_modes_from_result(rr)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pycuda_3_10",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
