Files
DynamisLab/src/drl_pinball/legacy_test/uni_test.ipynb
T
2026-06-09 18:46:59 +08:00

1.1 MiB
Raw Blame History

In [1]:
from typing import Tuple, Union
from collections import deque
import matplotlib.pyplot as plt
import numpy as np
from stable_baselines3 import PPO
import pycuda.driver as cuda
import pandas as pd
import pickle
import sys
import os
from gym_dummy import CustomEnv as DummyEnv

current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
sys.path.append(parent_dir)

from CelerisLab import FlowField
from CelerisLab import utils

env_12 = DummyEnv(s_dim=12)
env_14 = DummyEnv(s_dim=14)
model_cloak_re100 = PPO.load(os.path.join(parent_dir, "models", "old", "d1a3o12_re100.zip"), env=env_12, device="cuda:0")
model_illusion = PPO.load(os.path.join(parent_dir, "models", "250525", "d1a3o14_250525_imit_1L_2U_600S.zip"), env=env_14, device="cuda:0")
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")
model_illusion_15L = PPO.load(os.path.join(parent_dir, "models", "250525", "d1a3o14_250525_imit_15L_2U.zip"), env=env_14, device="cuda:0")
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")
model_cloak_lamb = PPO.load(os.path.join(parent_dir, "models", "old", "vortex_lamb.zip"), env=env_12, device="cuda:0")
model_cloak_taylor = PPO.load(os.path.join(parent_dir, "models", "old", "vortex_taylor.zip"), env=env_12, device="cuda:0")

model_cloak_re100.set_random_seed(0)
model_illusion.set_random_seed(19)
model_illusion_075L.set_random_seed(19)
model_illusion_15L.set_random_seed(19)
model_erase.set_random_seed(19)
model_cloak_lamb.set_random_seed(0)
model_cloak_taylor.set_random_seed(0)

cuda.init()
context = cuda.Device(0).make_context()
config_cuda = utils.load_cuda_config(os.path.join(parent_dir, "configs", "config_cuda.json"))
config_field = utils.load_flow_field_config(os.path.join(parent_dir, "configs", "config_flowfield.json"))

L0 = 20
U0 = config_field.velocity
DATA_TYPE = np.float32
CONV_LEN = 36

context.push()
flow_field = FlowField(config_field, config_cuda, device_id=0)
NX = flow_field.FIELD_SHAPE[0]
NY = flow_field.FIELD_SHAPE[1]
In [2]:
def save_field(flow_field, filename):
    NX = flow_field.FIELD_SHAPE[0]
    NY = flow_field.FIELD_SHAPE[1]
    flow_field.get_ddf()
    ddf_plot = flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
    flag_plot = flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)
    ux = (ddf_plot[:, :, 1] + ddf_plot[:, :, 5] + ddf_plot[:, :, 8] - ddf_plot[:, :, 3] - ddf_plot[:, :, 6] - ddf_plot[:, :, 7]) / U0
    uy = (ddf_plot[:, :, 2] + ddf_plot[:, :, 5] + ddf_plot[:, :, 6] - ddf_plot[:, :, 4] - ddf_plot[:, :, 7] - ddf_plot[:, :, 8]) / U0
    with open(os.path.join(parent_dir, "output", filename), "w") as f:
        f.write("Title= \"LBM 2D\"\r\n")
        f.write("VARIABLES= \"X\",\"Y\",\"flag\",\"U\",\"V\",\r\n")
        f.write(f"ZONE T= \"BOX\",I= {NX},J= {NY},F=POINT\r\n")
        for j in range(NY):
            for i in range(NX):
                f.write(f"{i},{j},{flag_plot[i, j]},{ux[i, j]},{uy[i, j]}\r\n")

class SimpleMeta:
    pass

def analyze_harmonics(states, n_harmonics):
    N, D = states.shape
    result = []
    for d in range(D):
        y = states[:, d]
        fft_coef = np.fft.rfft(y)
        freqs = np.fft.rfftfreq(N, d=1)
        amps = 2 * np.abs(fft_coef) / N
        phases = np.angle(fft_coef)
        idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1
        harmonics = {
            'dc': np.real(fft_coef[0]) / N,
            'amps': amps[idx],
            'freqs': freqs[idx],
            'phases': phases[idx]
        }
        result.append(harmonics)
    return result
In [3]:
target_states = np.empty((0, 6), dtype=DATA_TYPE)
meta_cloak_steady = SimpleMeta()
meta_cloak_dipole = SimpleMeta()
meta_cloak_monopole = SimpleMeta()
meta_illusion = SimpleMeta()
meta_cloak_karman = SimpleMeta()

center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(2*NX/U0), np.zeros(3, dtype=DATA_TYPE))

for i in range(150):
    flow_field.run(600, np.zeros(3, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:6]
    target_states = np.vstack((target_states, new_state))

meta_cloak_steady.target_states = np.mean(target_states, axis=0)

# save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", "target_steady.dat"))

target_states = np.empty((0, 6), dtype=DATA_TYPE)
flow_field.get_ddf()
flow_field.save_ddf()

center_vor: Tuple[float, float, float] = (15 * L0, (NY - 1) / 2, 0)
flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, "lamb")

for i in range(150):
    flow_field.run(800, np.zeros(3, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:6]
    target_states = np.vstack((target_states, new_state))

meta_cloak_dipole.target_states = np.mean(target_states, axis=0)
# flow_field.restore_ddf()
# flow_field.apply_ddf()
# flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, "lamb")

# for i in range(100):
#     flow_field.run(1000, np.zeros(3, dtype=DATA_TYPE))
#     file_name = f"target_lamb.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))

target_states = np.empty((0, 6), dtype=DATA_TYPE)
flow_field.restore_ddf()
flow_field.apply_ddf()
flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, "taylor")

for i in range(150):
    flow_field.run(800, np.zeros(3, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:6]
    target_states = np.vstack((target_states, new_state))

meta_cloak_monopole.target_states = np.mean(target_states, axis=0)
# flow_field.restore_ddf()
# flow_field.apply_ddf()
# flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, "taylor")

# for i in range(100):
#     flow_field.run(1000, np.zeros(3, dtype=DATA_TYPE))
#     file_name = f"target_taylor.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [4]:
target_states = np.empty((0, 6), dtype=DATA_TYPE)
fifo_states = deque(maxlen=150)

flow_field.restore_ddf()
flow_field.apply_ddf()
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
flow_field.add_cylinder(center, L0 / 2)
flow_field.run(int(4*NX/U0), np.zeros(6, dtype=DATA_TYPE))
flow_field.get_ddf()
flow_field.save_ddf()

for i in range(150):
    flow_field.run(600, np.zeros(6, dtype=DATA_TYPE))
    fifo_states.append(flow_field.obs.copy()[0:12])

temp_states = np.array(fifo_states)
meta_illusion.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))

meta_illusion.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
meta_illusion.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
for i in range(6):
    meta_illusion.sens_deviation[i] = np.mean(temp_states[:, i])
    meta_illusion.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_illusion.sens_deviation[i]))

fifo_states = deque(maxlen=150)
flow_field.restore_ddf()
flow_field.apply_ddf()
flow_field.run(int(2*NX/U0), np.array([0.0, 0.0, 0.0, 0.0, -5*U0, 5*U0], dtype=DATA_TYPE))
flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, "lamb")

for i in range(150):
    flow_field.run(800, np.zeros(6, dtype=DATA_TYPE))
    fifo_states.append(flow_field.obs.copy()[0:12])

temp_states = np.array(fifo_states)
meta_cloak_dipole.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))

meta_cloak_dipole.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
meta_cloak_dipole.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
for i in range(6):
    meta_cloak_dipole.sens_deviation[i] = np.mean(temp_states[:, i])
    meta_cloak_dipole.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_dipole.sens_deviation[i]))

fifo_states = deque(maxlen=150)
flow_field.restore_ddf()
flow_field.apply_ddf()
flow_field.run(int(2*NX/U0), np.array([0.0, 0.0, 0.0, 0.0, -5*U0, 5*U0], dtype=DATA_TYPE))
flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, "taylor")

for i in range(150):
    flow_field.run(800, np.zeros(6, dtype=DATA_TYPE))
    fifo_states.append(flow_field.obs.copy()[0:12])

temp_states = np.array(fifo_states)
meta_cloak_monopole.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))

meta_cloak_monopole.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
meta_cloak_monopole.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
for i in range(6):
    meta_cloak_monopole.sens_deviation[i] = np.mean(temp_states[:, i])
    meta_cloak_monopole.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_monopole.sens_deviation[i]))
In [5]:
fifo_states = deque(maxlen=150)

flow_field.restore_ddf()
flow_field.apply_ddf()
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, 1*L0)
flow_field.run(int(4*NX/U0), np.zeros(7, dtype=DATA_TYPE))

for i in range(150):
    flow_field.run(800, np.zeros(7, dtype=DATA_TYPE))
    fifo_states.append(flow_field.obs.copy()[0:12])

temp_states = np.array(fifo_states)
meta_cloak_karman.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))

meta_cloak_karman.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
meta_cloak_karman.sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
for i in range(6):
    meta_cloak_karman.sens_deviation[i] = np.mean(temp_states[:, i])
    meta_cloak_karman.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - meta_cloak_karman.sens_deviation[i]))
In [6]:
del flow_field

flow_field = FlowField(config_field, config_cuda, device_id=0)
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, 1*L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))

target_states = np.empty((0, 6), dtype=DATA_TYPE)

for i in range(150):
    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[2:8]
    target_states = np.vstack((target_states, new_state))

meta_cloak_karman.target_states = target_states

# for i in range(100):
#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))
#     file_name = f"target_karman.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [7]:
del flow_field

flow_field = FlowField(config_field, config_cuda, device_id=0)
center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, 1*L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))

target_states = np.empty((0, 8), dtype=DATA_TYPE)

for i in range(150):
    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:8]
    target_states = np.vstack((target_states, new_state))

meta_illusion.target_states_1L = target_states
meta_illusion.target_harmonics_1L = analyze_harmonics(target_states, n_harmonics=5)

# for i in range(100):
#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))
#     file_name = f"target_1L.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [8]:
del flow_field

flow_field = FlowField(config_field, config_cuda, device_id=0)
center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, 0.75*L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))

target_states = np.empty((0, 8), dtype=DATA_TYPE)

for i in range(150):
    flow_field.run(400, np.zeros(4, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:8]
    target_states = np.vstack((target_states, new_state))

meta_illusion.target_states_075L = target_states
meta_illusion.target_harmonics_075L = analyze_harmonics(target_states, n_harmonics=5)

# for i in range(100):
#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))
#     file_name = f"target_075L.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [9]:
del flow_field

flow_field = FlowField(config_field, config_cuda, device_id=0)
center: Tuple[float, float, float] = (31 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, 1.5*L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))

target_states = np.empty((0, 8), dtype=DATA_TYPE)

for i in range(150):
    flow_field.run(800, np.zeros(4, dtype=DATA_TYPE))
    new_state = flow_field.obs.copy()[0:8]
    target_states = np.vstack((target_states, new_state))

meta_illusion.target_states_15L = target_states
meta_illusion.target_harmonics_15L = analyze_harmonics(target_states, n_harmonics=5)

# for i in range(100):
#     flow_field.run(1000, np.zeros(4, dtype=DATA_TYPE))
#     file_name = f"target_15L.{i:03d}"
#     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [10]:
del flow_field

flow_field = FlowField(config_field, config_cuda, device_id=0)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
flow_field.add_sensor(center, L0 / 4)
flow_field.run(int(4*NX/U0), np.zeros(6, dtype=DATA_TYPE))

flow_field.get_ddf()
flow_field.save_ddf()
In [11]:
# flow_field.restore_ddf()
# flow_field.apply_ddf()
fifo_states = deque(maxlen=150)
for i in range(100):
    flow_field.run(1000, np.zeros(6, dtype=DATA_TYPE))
    file_name = f"act_nc.{i:03d}"
    # save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
    fifo_states.append(flow_field.obs.copy()[0:12])
In [12]:
for i in range(75):
    flow_field.run(1000, np.array([0.0, -5.1*U0, 5.1*U0, 0.0, 0.0, 0.0], dtype=DATA_TYPE))
    file_name = f"act_cloak_steady.{i:03d}"
    # save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
    fifo_states.append(flow_field.obs.copy()[0:12])
In [29]:
flow_field.get_ddf()
flow_field.save_ddf()
In [16]:
flow_field.restore_ddf()
flow_field.apply_ddf()
flow_field.add_vortex(center_vor, L0 * 2, 0.5*U0, 0, "lamb")

obs = np.zeros(12, dtype=np.float32)
for i in range(125):
    action, _states = model_cloak_lamb.predict(observation=obs, deterministic=True)
    temp = np.zeros(6, dtype=DATA_TYPE)
    if i < 25:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/25) + temp_transition * (1 - i/25)
    elif 45 <= i < 70:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (1-(i-45)/25) + temp_transition * ((i-45)/25)
    elif i >= 70:
        temp[0:3] = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
    else:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(800, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_cloak_dipole.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_cloak_dipole.sens_deviation) / meta_cloak_dipole.sens_norm_fact
    obs = np.hstack([forces, sens])
    file_name = f"act_cloak_dipole.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
    fifo_states.append(flow_field.obs.copy()[0:12])
In [20]:
flow_field.restore_ddf()
flow_field.apply_ddf()
flow_field.add_vortex(center_vor, L0 * 2, 0.03*U0, 0, "taylor")

obs = np.zeros(12, dtype=np.float32)
for i in range(125):
    action, _states = model_cloak_taylor.predict(observation=obs, deterministic=True)
    temp = np.zeros(6, dtype=DATA_TYPE)
    if i < 20:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/20) + temp_transition * (1 - i/20)
    elif 45 <= i < 70:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (1-(i-45)/25) + temp_transition * ((i-45)/25)
    elif i >= 70:
        temp[0:3] = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
    else:
        temp_action = np.array(action*4 + [0, -4, 4], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(800, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_cloak_monopole.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_cloak_monopole.sens_deviation) / meta_cloak_monopole.sens_norm_fact
    obs = np.hstack([forces, sens])
    file_name = f"act_cloak_monopole.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
    fifo_states.append(flow_field.obs.copy()[0:12])
In [22]:
def gen_target_states_at(t, harmonics):
    t = np.asarray(t)
    D = len(harmonics)
    result = np.zeros((t.size, D), dtype=np.float32)
    for d, h in enumerate(harmonics):
        val = np.full(t.shape, h['dc'], dtype=np.float32)
        for amp, freq, phase in zip(h['amps'], h['freqs'], h['phases']):
            val += amp * np.cos(2 * np.pi * freq * t + phase)
        result[:, d] = val
    if result.shape[0] == 1:
        return result[0]
    return result
In [24]:
flow_field.restore_ddf()
flow_field.apply_ddf()

obs = np.zeros(14, dtype=np.float32)
for i in range(200):
    action, _states = model_illusion.predict(observation=obs, deterministic=True)
    temp = np.zeros(6, dtype=DATA_TYPE)
    if i < 10:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)
    else:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(800, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_illusion.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact
    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_1L)
    target_cd = target_states[0] / meta_illusion.force_norm_fact
    target_cl = target_states[1] / meta_illusion.force_norm_fact
    obs = np.hstack([forces, sens, target_cd, target_cl])
    file_name = f"act_illusion_1L.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
    # if i % 2 == 0:
    #     index = i // 2
    #     file_name = f"act_illusion_1L.{index:03d}"
    #     save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [25]:
# flow_field.apply_ddf()

obs = np.zeros(14, dtype=np.float32)
for i in range(400):
    action, _states = model_illusion_075L.predict(observation=obs, deterministic=True)
    temp = np.zeros(6, dtype=DATA_TYPE)
    if i < 20:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)
    else:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(400, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_illusion.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact
    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_075L)
    target_cd = target_states[0] / meta_illusion.force_norm_fact
    target_cl = target_states[1] / meta_illusion.force_norm_fact
    obs = np.hstack([forces, sens, target_cd, target_cl])
    if i % 2 == 0:
        index = i // 2
        file_name = f"act_illusion_075L.{index:03d}"
        save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [26]:
# flow_field.apply_ddf()

obs = np.zeros(14, dtype=np.float32)
for i in range(200):
    action, _states = model_illusion_15L.predict(observation=obs, deterministic=True)
    temp = np.zeros(6, dtype=DATA_TYPE)
    if i < 10:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)
    else:
        temp_action = np.array(action*8 + [0, -2, 2], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(800, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_illusion.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_illusion.sens_deviation) / meta_illusion.sens_norm_fact
    target_states = gen_target_states_at(i, meta_illusion.target_harmonics_15L)
    target_cd = target_states[0] / meta_illusion.force_norm_fact
    target_cl = target_states[1] / meta_illusion.force_norm_fact
    obs = np.hstack([forces, sens, target_cd, target_cl])
    file_name = f"act_illusion_15L.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [31]:
# center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
# flow_field.add_cylinder(center, 1*L0)
flow_field.restore_ddf()
flow_field.apply_ddf()

obs = np.zeros(12, dtype=np.float32)
for i in range(200):
    action, _states = model_cloak_re100.predict(observation=obs, deterministic=True)
    temp = np.zeros(7, dtype=DATA_TYPE)
    if i < 10:
        temp_action = np.array([0, 0, 0], dtype=DATA_TYPE)
        temp_transition = np.array([0.0, -5.1*U0, 5.1*U0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)
    else:
        temp_action = np.array([0, 0, 0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(1000, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_cloak_karman.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_cloak_karman.sens_deviation) / meta_cloak_karman.sens_norm_fact
    obs = np.hstack([forces, sens])
    file_name = f"act_karman_nc.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))

for i in range(200):
    action, _states = model_cloak_re100.predict(observation=obs, deterministic=True)
    temp = np.zeros(7, dtype=DATA_TYPE)
    if i < 10:
        temp_action = np.array(action*8 + [0, -4, 4], dtype=DATA_TYPE)
        temp_transition = np.array([0, 0, 0], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0 * (i/10) + temp_transition * (1 - i/10)
    else:
        temp_action = np.array(action*8 + [0, -4, 4], dtype=DATA_TYPE)
        temp[0:3] = temp_action * U0
    flow_field.run(800, temp)
    states = np.array(flow_field.obs.copy()[0:12])
    forces = states[0:6] / meta_cloak_karman.force_norm_fact
    cd = (forces[0] + forces[2] + forces[4]) / 3
    cl = (forces[1] + forces[3] + forces[5]) / 3
    sens = (states[6:12] - meta_cloak_karman.sens_deviation) / meta_cloak_karman.sens_norm_fact
    obs = np.hstack([forces, sens])
    file_name = f"act_karman_cloak.{i:03d}"
    save_field(flow_field, os.path.join(parent_dir, "output", "250823", "data", file_name))
In [ ]:

POD 与 Z₂ 对称性分析(新增)

这一部分用于你现有输出快照(output/250823/data)的离线分析,不需要重新跑 CFD。

1) PODProper Orthogonal Decomposition

对快照矩阵做均值去除后 SVD


\mathbf{X}' = \mathbf{U}\,\mathbf{\Sigma}\,\mathbf{V}^T

其中 POD 能量占比:


\lambda_k = \frac{\sigma_k^2}{\sum_i \sigma_i^2}

累计能量:


C_K = \sum_{k=1}^{K} \lambda_k

2) Z₂ 对称性分析(关于中线 $y\to -y$)

对速度场 (u,v) 的反射算子定义为:


\mathcal{R}(u,v) = (u(x,-y), -v(x,-y))

对称/反对称分解:


\mathbf{u}_s = \frac{\mathbf{u}+\mathcal{R}\mathbf{u}}{2}, \qquad
\mathbf{u}_a = \frac{\mathbf{u}-\mathcal{R}\mathbf{u}}{2}

定义对称性破缺指数:


\eta_{Z_2} = \frac{\|\mathbf{u}_a\|^2}{\|\mathbf{u}_s\|^2 + \|\mathbf{u}_a\|^2}
  • $\eta_{Z_2}\approx 0$:近似镜像对称
  • \eta_{Z_2} 越大:对称性破缺越明显
In [1]:
import os
import re
import glob
from pathlib import Path

import numpy as np
import pandas as pd


def _parse_zone_ij(file_path: str) -> tuple[int, int]:
    with open(file_path, "r") as f:
        _ = f.readline()
        _ = f.readline()
        zone_line = f.readline()
    m_i = re.search(r"I=\s*(\d+)", zone_line)
    m_j = re.search(r"J=\s*(\d+)", zone_line)
    if m_i is None or m_j is None:
        raise ValueError(f"Cannot parse I/J in: {file_path}")
    return int(m_i.group(1)), int(m_j.group(1))


def load_tecplot_snapshot(file_path: str):
    """Load one snapshot saved by save_field().

    Returns
    -------
    flag, u, v: ndarray with shape (NX, NY)
    """
    NX, NY = _parse_zone_ij(file_path)
    df = pd.read_csv(
        file_path,
        skiprows=3,
        header=None,
        names=["i", "j", "flag", "u", "v"],
    )
    arr = df[["flag", "u", "v"]].to_numpy().reshape(NY, NX, 3)
    flag = arr[:, :, 0].T
    u = arr[:, :, 1].T
    v = arr[:, :, 2].T
    return flag, u, v


def collect_case_files(data_dir: str, prefix: str, n_snapshots: int | None = None):
    files = sorted(glob.glob(str(Path(data_dir) / f"{prefix}.*")))
    if n_snapshots is not None:
        files = files[:n_snapshots]
    if len(files) == 0:
        raise FileNotFoundError(f"No files found for prefix={prefix} in {data_dir}")
    return files


def build_snapshot_matrix(file_list: list[str]):
    """Build snapshot matrix X for POD from velocity fields.

    X shape: (n_features, n_snapshots)
    n_features = 2 * n_fluid_points (u + v).
    """
    flags = []
    uv_fields = []

    for fp in file_list:
        flag, u, v = load_tecplot_snapshot(fp)
        flags.append(flag)
        uv_fields.append((u, v))

    # Heuristic: the most frequent flag value is considered fluid.
    first_flag = flags[0]
    vals, cnts = np.unique(first_flag, return_counts=True)
    fluid_flag = vals[np.argmax(cnts)]
    fluid_mask = first_flag == fluid_flag

    cols = []
    for u, v in uv_fields:
        cols.append(np.hstack([u[fluid_mask], v[fluid_mask]]))

    X = np.stack(cols, axis=1)
    return X, fluid_mask, uv_fields, fluid_flag


def pod_from_snapshots(X: np.ndarray):
    """Compute POD by SVD after mean subtraction."""
    Xc = X - X.mean(axis=1, keepdims=True)
    U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
    eig = S**2
    ratio = eig / np.sum(eig)
    cumsum = np.cumsum(ratio)
    return {
        "U": U,
        "S": S,
        "Vt": Vt,
        "ratio": ratio,
        "cumsum": cumsum,
    }


def z2_metrics_for_field(u: np.ndarray, v: np.ndarray, fluid_mask: np.ndarray):
    """Compute Z2 symmetry metrics for one snapshot.

    Reflection operator: R(u, v) = (u(x,-y), -v(x,-y)).
    """
    u_ref = u[:, ::-1]
    v_ref = -v[:, ::-1]

    u_sym = 0.5 * (u + u_ref)
    v_sym = 0.5 * (v + v_ref)
    u_anti = 0.5 * (u - u_ref)
    v_anti = 0.5 * (v - v_ref)

    E_sym = np.sum(u_sym[fluid_mask] ** 2 + v_sym[fluid_mask] ** 2)
    E_anti = np.sum(u_anti[fluid_mask] ** 2 + v_anti[fluid_mask] ** 2)

    eta_z2 = E_anti / (E_sym + E_anti + 1e-12)

    # Signed order parameter: mean cross-stream velocity (normalized by kinetic norm)
    signed_m = np.sum(v[fluid_mask]) / np.sqrt(np.sum(u[fluid_mask] ** 2 + v[fluid_mask] ** 2) + 1e-12)

    return {
        "eta_z2": float(eta_z2),
        "E_sym": float(E_sym),
        "E_anti": float(E_anti),
        "signed_m": float(signed_m),
    }


def analyze_case(data_dir: str, prefix: str, n_snapshots: int = 40):
    files = collect_case_files(data_dir, prefix, n_snapshots=n_snapshots)
    X, fluid_mask, uv_fields, fluid_flag = build_snapshot_matrix(files)

    pod = pod_from_snapshots(X)
    z2 = [z2_metrics_for_field(u, v, fluid_mask) for (u, v) in uv_fields]

    eta = np.array([z["eta_z2"] for z in z2])
    signed_m = np.array([z["signed_m"] for z in z2])

    return {
        "prefix": prefix,
        "n_snapshots": len(files),
        "fluid_flag": float(fluid_flag),
        "pod_ratio": pod["ratio"],
        "pod_cumsum": pod["cumsum"],
        "eta_z2_series": eta,
        "signed_m_series": signed_m,
    }


def summarize_case(result: dict):
    r = result["pod_ratio"]
    c = result["pod_cumsum"]
    eta = result["eta_z2_series"]
    m = result["signed_m_series"]

    return {
        "case": result["prefix"],
        "n": result["n_snapshots"],
        "POD_r1": float(r[0]),
        "POD_r2": float(r[1]) if len(r) > 1 else np.nan,
        "POD_r3": float(r[2]) if len(r) > 2 else np.nan,
        "POD_c3": float(c[2]) if len(c) > 2 else np.nan,
        "POD_c5": float(c[4]) if len(c) > 4 else np.nan,
        "etaZ2_mean": float(np.mean(eta)),
        "etaZ2_std": float(np.std(eta)),
        "signed_m_mean": float(np.mean(m)),
        "signed_m_std": float(np.std(m)),
    }


resolved_parent_dir = globals().get("parent_dir")
if resolved_parent_dir is None:
    # Fallback: notebook is under scripts/, so parent is workspace root.
    resolved_parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))

DATA_DIR = os.path.join(resolved_parent_dir, "output", "250823", "data")
print("POD/Z2 helper functions ready.")
print("Data dir:", DATA_DIR)
POD/Z2 helper functions ready.
Data dir: /home/frank14f/Frank_LBM/output/250823/data
In [2]:
# 你可以按需增减 case。这里先给一个与你当前结果最相关的集合。
cases = [
    "act_nc",            # 无控制基线(steady
    "act_cloak_steady",  # steady cloaking
    "act_karman_nc",     # karman 来流无控制
    "act_karman_cloak",  # karman 来流 cloaking
    "act_illusion_1L",   # illusion (1.0L target)
]

results = []
for c in cases:
    res = analyze_case(DATA_DIR, c, n_snapshots=20)
    results.append(res)

summary_df = pd.DataFrame([summarize_case(r) for r in results])
summary_df
Out [2]:
case n POD_r1 POD_r2 POD_r3 POD_c3 POD_c5 etaZ2_mean etaZ2_std signed_m_mean signed_m_std
0 act_nc 20 0.545310 0.418448 0.010115 0.973874 0.991136 0.027126 0.000603 0.010536 0.125305
1 act_cloak_steady 20 0.484615 0.392044 0.070973 0.947632 0.977535 0.024850 0.002165 0.004569 0.113498
2 act_karman_nc 20 0.430712 0.400573 0.086937 0.918222 0.970708 0.039163 0.005227 0.038732 0.224643
3 act_karman_cloak 20 0.474362 0.417549 0.070438 0.962349 0.986613 0.043868 0.001102 -0.022893 0.103950
4 act_illusion_1L 20 0.789446 0.152319 0.040029 0.981793 0.996635 0.000188 0.000117 -0.001334 0.004723
In [3]:
import matplotlib.pyplot as plt
import numpy as np

# 可视化:POD 前三模态能量占比 + Z2 指数分布
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

x = np.arange(len(summary_df))
barw = 0.25
axes[0].bar(x - barw, summary_df["POD_r1"], width=barw, label="r1")
axes[0].bar(x, summary_df["POD_r2"], width=barw, label="r2")
axes[0].bar(x + barw, summary_df["POD_r3"], width=barw, label="r3")
axes[0].set_xticks(x)
axes[0].set_xticklabels(summary_df["case"], rotation=30, ha="right")
axes[0].set_ylabel("Energy ratio")
axes[0].set_title("POD modal energy (top 3)")
axes[0].legend()

axes[1].errorbar(
    x,
    summary_df["etaZ2_mean"],
    yerr=summary_df["etaZ2_std"],
    fmt="o",
    capsize=4,
)
axes[1].set_xticks(x)
axes[1].set_xticklabels(summary_df["case"], rotation=30, ha="right")
axes[1].set_ylabel("$\\eta_{Z_2}$")
axes[1].set_title("Z2 symmetry-breaking index")

plt.tight_layout()
plt.show()

# 打印一段自动化文字结论(初步)
for _, row in summary_df.iterrows():
    print(
        f"[{row['case']}] POD c3={row['POD_c3']:.3f}, c5={row['POD_c5']:.3f}; "
        f"etaZ2={row['etaZ2_mean']:.4f}±{row['etaZ2_std']:.4f}"
    )
[act_nc] POD c3=0.974, c5=0.991; etaZ2=0.0271±0.0006
[act_cloak_steady] POD c3=0.948, c5=0.978; etaZ2=0.0249±0.0022
[act_karman_nc] POD c3=0.918, c5=0.971; etaZ2=0.0392±0.0052
[act_karman_cloak] POD c3=0.962, c5=0.987; etaZ2=0.0439±0.0011
[act_illusion_1L] POD c3=0.982, c5=0.997; etaZ2=0.0002±0.0001
In [5]:
# PyDMD 可用性检查(可选)
try:
    import pydmd
    print("PyDMD import OK")
    print("PyDMD path:", getattr(pydmd, "__file__", "N/A"))
    available = ["DMD", "BOPDMD", "HODMD", "MrDMD", "DMDc"]
    print("Core classes:", [name for name in available if hasattr(pydmd, name)])
except Exception as e:
    print("PyDMD not available in current env:", repr(e))
    print("Tip 1: pip install pydmd")
    print("Tip 2: or install local repo /home/frank14f/Frank_LBM/JFM_WYQ/PyDMD with dependencies")

print("Note: POD 本身不依赖 PyDMD,已由 SVD 完成;PyDMD 主要可用于后续 DMD 频率/模态分析。")
PyDMD import OK
PyDMD path: /home/frank14f/anaconda3/envs/pycuda_3_10/lib/python3.10/site-packages/pydmd/__init__.py
Core classes: ['DMD', 'BOPDMD', 'HODMD', 'MrDMD', 'DMDc']
Note: POD 本身不依赖 PyDMD,已由 SVD 完成;PyDMD 主要可用于后续 DMD 频率/模态分析。
In [6]:
# 进阶:POD 模态级别的 Z2 对称性(前6模态)

def pod_mode_z2_table(data_dir: str, prefix: str, n_snapshots: int = 40, n_modes: int = 6):
    files = collect_case_files(data_dir, prefix, n_snapshots=n_snapshots)
    X, fluid_mask, uv_fields, _ = build_snapshot_matrix(files)
    pod = pod_from_snapshots(X)

    n_fluid = int(np.sum(fluid_mask))
    Umat = pod["U"]
    out = []

    for k in range(min(n_modes, Umat.shape[1])):
        mode_vec = Umat[:, k]
        u = np.zeros_like(uv_fields[0][0])
        v = np.zeros_like(uv_fields[0][1])
        u[fluid_mask] = mode_vec[:n_fluid]
        v[fluid_mask] = mode_vec[n_fluid:]
        z2 = z2_metrics_for_field(u, v, fluid_mask)
        out.append(
            {
                "mode": k + 1,
                "energy_ratio": float(pod["ratio"][k]),
                "etaZ2_mode": z2["eta_z2"],
                "signed_m_mode": z2["signed_m"],
            }
        )

    return pd.DataFrame(out)


for c in ["act_nc", "act_cloak_steady", "act_karman_nc", "act_karman_cloak", "act_illusion_1L"]:
    print(f"\n=== {c}: POD mode Z2 (top 6) ===")
    display(pod_mode_z2_table(DATA_DIR, c, n_snapshots=20, n_modes=6))
=== act_nc: POD mode Z2 (top 6) ===
mode energy_ratio etaZ2_mode signed_m_mode
0 1 0.545310 0.999408 0.452750
1 2 0.418448 0.999942 -1.052688
2 3 0.010115 0.852112 -0.190033
3 4 0.009767 0.734457 -0.556253
4 5 0.007495 0.265782 -0.314500
5 6 0.007301 0.146551 -0.119727
=== act_cloak_steady: POD mode Z2 (top 6) ===
mode energy_ratio etaZ2_mode signed_m_mode
0 1 0.484615 0.938028 -0.649696
1 2 0.392044 0.913858 -0.640627
2 3 0.070973 0.274806 -0.927843
3 4 0.018648 0.577703 -1.039376
4 5 0.011255 0.417061 0.938371
5 6 0.009138 0.866436 0.214711
=== act_karman_nc: POD mode Z2 (top 6) ===
mode energy_ratio etaZ2_mode signed_m_mode
0 1 0.430712 0.805058 0.929242
1 2 0.400573 0.871105 0.056902
2 3 0.086937 0.585435 0.316279
3 4 0.038222 0.713888 0.541526
4 5 0.014265 0.571667 -0.538116
5 6 0.009712 0.768921 0.424069
=== act_karman_cloak: POD mode Z2 (top 6) ===
mode energy_ratio etaZ2_mode signed_m_mode
0 1 0.474362 0.875132 0.676988
1 2 0.417549 0.958074 -0.044610
2 3 0.070438 0.782419 -0.366165
3 4 0.016490 0.598034 -0.306447
4 5 0.007774 0.398814 0.351811
5 6 0.005982 0.804229 -0.189635
=== act_illusion_1L: POD mode Z2 (top 6) ===
mode energy_ratio etaZ2_mode signed_m_mode
0 1 0.789446 0.004089 -0.017435
1 2 0.152319 0.082741 -0.115729
2 3 0.040029 0.273500 -0.021073
3 4 0.011282 0.409877 -0.230239
4 5 0.003560 0.460897 -0.463985
5 6 0.001731 0.368499 -0.723188

tail-30 版本(推荐汇报口径)

你提到 250823 是连续衔接测试流,这里采用每个 case 的最后 30 帧进行 POD 与 Z2 分析,减少过渡段影响。

In [7]:
def analyze_case_tail(data_dir: str, prefix: str, n_tail: int = 30):
    files = sorted(glob.glob(str(Path(data_dir) / f"{prefix}.*")))[-n_tail:]
    if len(files) == 0:
        raise FileNotFoundError(f"No files found for {prefix}")

    X, fluid_mask, uv_fields, fluid_flag = build_snapshot_matrix(files)
    pod = pod_from_snapshots(X)
    z2 = [z2_metrics_for_field(u, v, fluid_mask) for (u, v) in uv_fields]

    eta = np.array([z["eta_z2"] for z in z2])
    signed_m = np.array([z["signed_m"] for z in z2])

    return {
        "prefix": prefix,
        "n_tail": len(files),
        "fluid_flag": float(fluid_flag),
        "pod_ratio": pod["ratio"],
        "pod_cumsum": pod["cumsum"],
        "pod_U": pod["U"],
        "eta_z2_series": eta,
        "signed_m_series": signed_m,
        "fluid_mask": fluid_mask,
    }


def summarize_case_tail(result: dict):
    r = result["pod_ratio"]
    c = result["pod_cumsum"]
    eta = result["eta_z2_series"]
    m = result["signed_m_series"]

    return {
        "case": result["prefix"],
        "n_tail": result["n_tail"],
        "POD_r1": float(r[0]),
        "POD_r2": float(r[1]) if len(r) > 1 else np.nan,
        "POD_r3": float(r[2]) if len(r) > 2 else np.nan,
        "POD_c3": float(c[2]) if len(c) > 2 else np.nan,
        "POD_c5": float(c[4]) if len(c) > 4 else np.nan,
        "etaZ2_mean": float(np.mean(eta)),
        "etaZ2_std": float(np.std(eta)),
        "signed_m_mean": float(np.mean(m)),
        "signed_m_std": float(np.std(m)),
    }


cases_tail = ["act_nc", "act_cloak_steady", "act_karman_nc", "act_karman_cloak", "act_illusion_1L"]
results_tail = [analyze_case_tail(DATA_DIR, c, n_tail=30) for c in cases_tail]
summary_tail_df = pd.DataFrame([summarize_case_tail(r) for r in results_tail])
summary_tail_df
Out [7]:
case n_tail POD_r1 POD_r2 POD_r3 POD_c3 POD_c5 etaZ2_mean etaZ2_std signed_m_mean signed_m_std
0 act_nc 30 0.526596 0.437099 0.010337 0.974032 0.991332 0.027001 0.000608 -0.019124 0.128511
1 act_cloak_steady 30 0.740269 0.188710 0.050305 0.979284 0.998620 0.000253 0.000713 0.001067 0.014348
2 act_karman_nc 30 0.431001 0.397740 0.067790 0.896532 0.963662 0.047405 0.002245 0.000857 0.110081
3 act_karman_cloak 30 0.545640 0.422812 0.010409 0.978861 0.993134 0.041057 0.000858 0.017560 0.131044
4 act_illusion_1L 30 0.539437 0.422263 0.010111 0.971812 0.988518 0.029124 0.000780 -0.032461 0.124692
In [8]:
# tail-30 汇总可视化
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

x = np.arange(len(summary_tail_df))
barw = 0.25
axes[0].bar(x - barw, summary_tail_df["POD_r1"], width=barw, label="r1")
axes[0].bar(x, summary_tail_df["POD_r2"], width=barw, label="r2")
axes[0].bar(x + barw, summary_tail_df["POD_r3"], width=barw, label="r3")
axes[0].set_xticks(x)
axes[0].set_xticklabels(summary_tail_df["case"], rotation=30, ha="right")
axes[0].set_ylabel("Energy ratio")
axes[0].set_title("POD modal energy (tail-30)")
axes[0].legend()

axes[1].errorbar(
    x,
    summary_tail_df["etaZ2_mean"],
    yerr=summary_tail_df["etaZ2_std"],
    fmt="o",
    capsize=4,
)
axes[1].set_xticks(x)
axes[1].set_xticklabels(summary_tail_df["case"], rotation=30, ha="right")
axes[1].set_ylabel("eta_Z2")
axes[1].set_title("Z2 symmetry-breaking index (tail-30)")

plt.tight_layout()
plt.show()
In [9]:
# tail-30 的 POD 模态图(每个 case 前3模态,u/v 分量)

def plot_top3_modes_from_result(res: dict):
    U = res["pod_U"]
    mask = res["fluid_mask"]
    n_fluid = int(np.sum(mask))

    fig, axes = plt.subplots(2, 3, figsize=(12.5, 6.5))
    for k in range(3):
        mode = U[:, k]
        um = np.zeros(mask.shape, dtype=np.float32)
        vm = np.zeros(mask.shape, dtype=np.float32)
        um[mask] = mode[:n_fluid]
        vm[mask] = mode[n_fluid:]

        lim_u = np.max(np.abs(um)) + 1e-12
        lim_v = np.max(np.abs(vm)) + 1e-12

        im_u = axes[0, k].imshow(um.T, origin="lower", cmap="RdBu_r", vmin=-lim_u, vmax=lim_u)
        axes[0, k].set_title(f"Mode {k+1} - u")
        axes[0, k].set_xticks([])
        axes[0, k].set_yticks([])
        fig.colorbar(im_u, ax=axes[0, k], fraction=0.046, pad=0.04)

        im_v = axes[1, k].imshow(vm.T, origin="lower", cmap="RdBu_r", vmin=-lim_v, vmax=lim_v)
        axes[1, k].set_title(f"Mode {k+1} - v")
        axes[1, k].set_xticks([])
        axes[1, k].set_yticks([])
        fig.colorbar(im_v, ax=axes[1, k], fraction=0.046, pad=0.04)

    fig.suptitle(f"POD spatial modes (tail-30): {res['prefix']}", y=1.02)
    plt.tight_layout()
    plt.show()


for rr in results_tail:
    plot_top3_modes_from_result(rr)