第一轮分析工作暂存

This commit is contained in:
Frank14f
2026-06-09 18:46:59 +08:00
parent 4612928398
commit d1b9922c6b
111 changed files with 24287 additions and 1581 deletions
+1
View File
@@ -0,0 +1 @@
+890
View File
@@ -0,0 +1,890 @@
# DynamisLab Comprehensive Knowledge Document
> **Project**: Active hydrodynamic cloaking and illusion using Deep Reinforcement Learning (DRL) on a fluidic pinball.
> **Solver**: GPU-accelerated Lattice Boltzmann Method (LBM, D2Q9, MRT)
> **DRL**: PPO with Sin activation, Stable-Baselines3
---
## 1. Grid Configuration
### 1.1 Legacy Grid
Two config files are multiplied to produce the actual lattice:
| Config | Base (1U) | Multiplier (field_dim_in_U) | Result |
|--------|-----------|----------------------------|--------|
| `config_cuda.json` | X_1U=128, Y_1U=32, Z_1U=1 | — | — |
| `config_flowfield.json` | — | field_dim_in_U=[10, 16, 1] | — |
| **Actual grid** | — | — | **nx=1280, ny=512, nz=1** |
`L0=20` is the base length unit. The "U" grid units (128, 32) are chosen to align with CUDA SM count on the GPU.
### 1.2 New Grid (CelerisLab)
| Config File | nx | ny | nz |
|------------|----|----|-----|
| `config_lbm_pinball.json` | 1280 | 512 | 1 |
The new grid is **identical** to the legacy grid. `L0=20` remains the base length unit.
### 1.3 Config File Locations
- Legacy configs: `configs/legacy_configs/config_cuda.json`, `config_flowfield.json`
- New config: `configs/config_lbm_pinball.json`
- Body config: `configs/config_body.json`
- Reference: `configs/CONFIG.md` (full config schema documentation)
---
## 2. Reynolds Number Definition
**Critical**: The project uses two different Re definitions.
| Symbol | Reference Length | Formula | Default Value |
|--------|-----------------|---------|---------------|
| Re_D (report/paper) | Single cylinder diameter D=20 | U0·D/ν | 0.01×20/0.004 = **50** |
| Re (code) | 2×D = 40 | U0·(2D)/ν | 0.01×40/0.004 = **100** |
**Key mappings**:
- Confirmation report `Re_D = 50` ↔ code `Re=100`
- Code `re100` models → physical `Re_D=50`
- Code `re50` models → physical `Re_D=25`
- Upstream disturbance cylinder (diameter = L0×1 = 20) → Re=U0×20/ν=50 (single diameter definition)
### 2.1 Re via Viscosity
| Code Name | Viscosity ν | Re (code, 2D ref) | Re_D (report) |
|-----------|-------------|-------------------|---------------|
| re50 | 0.008 | 50 | 25 |
| re100 | 0.004 | 100 | 50 |
| re200 | 0.002 | 200 | 100 |
| re400 | 0.001 | 400 | 200 |
Formula: `Re = U0 * ref_length / ν` where ref_length = 2D = 40 for code Re.
---
## 3. Boundary Conditions
Both legacy and new simulations use the same boundary configuration:
| Boundary | Condition | Details |
|----------|-----------|---------|
| Inlet (x=0) | **Parabolic profile**, Zou-He local scheme | u_x parabolic, u_y=0 |
| Outlet (x=64D) | Convective / NEQ extrapolation | `neq_extrap` mode, backflow clamp enabled |
| Top/Bottom walls (y=±12.8D) | **Bounce-back** (no-slip) | `y_wall_bc: "bounce_back"` |
| Cylinder surfaces | Ghost-node interpolation with prescribed rotational velocity | u_wall = a·(-sinθ, cosθ)^T |
**Note**: The `config_lbm_pinball.json` explicitly uses `y_wall_bc: "bounce_back"`, which is equivalent to the legacy no-slip walls. The validation config `run_kan99b` uses `free_slip`, so be careful to use the correct config.
**Fixed parameters**:
- U0 = 0.01 (inlet center velocity, lattice units)
- ν = 0.004 (default for Re=100 code)
- ρ = 1.0
- Collision: MRT
- Streaming: esopull
- Data type: FP32
---
## 4. Complete Old-to-New API Conversion Table
| Feature | Old API (FlowField, LegacyCelerisLab) | New API (Simulation, CelerisLab) |
|---------|--------------------------------------|----------------------------------|
| **Force reading** | `obs[i]` after `run()` = per-step average (internal ÷N) | `read_force(id)` = N-step cumulative sum; **must divide by N** |
| **Sensor reading** | `obs[i]` after `run()` = per-step average (internal ÷N) | `read_sensor(id, normalize=True)` = raw sum ÷ cell_count; **must still divide by N** |
| **Action setting** | `run(N, action_array)` — objects indexed by order in single array | `set_body(id, omega=value)` — each cylinder set separately |
| **Action smoothing** | Built-in exponential smoothing (weight=0.1) | None — implement manual `ActionSmoother` if needed |
| **Checkpoint/save** | `save_ddf()` / `restore_ddf()` / `apply_ddf()` (host memory) | `snapshot()` / `restore()` (memory) or `save_checkpoint(path)` / `load_checkpoint(path)` (HDF5) |
| **Initialization** | Constructor `FlowField(config_field, config_cuda, device_id)` auto-initializes | `Simulation(config)` then call `initialize()` separately |
| **Field output** | `save_field()` writes Tecplot `.dat` | `get_macroscopic()` returns numpy arrays |
| **Object addition** | `add_cylinder()`, `add_sensor()` on FlowField | Objects defined in `config_body.json` or added before `initialize()` |
| **Vortex addition** | `add_vortex(center, radius, strength, ...)` | Unknown — check API |
| **Numeric error check** | `flow_field.has_numeric_error()`, `flow_field.last_error_flag` | Manual implementation needed |
| **Context management** | `flow_field.context.push()` / `.pop()` | Stream management via API |
### 4.1 Conversion Formulas
```python
# Old API (per-step average):
flow_field.run(SAMPLE_INTERVAL, action_array)
obs = flow_field.obs # already per-step average
# New API (must divide by N):
sim.bodies.zero_force_segment_async(stream)
sim.bodies.zero_sensor_segment_async(stream)
sim.run(SAMPLE_INTERVAL)
fx_per_step = sim.read_force(body_id)[0] / SAMPLE_INTERVAL
fy_per_step = sim.read_force(body_id)[1] / SAMPLE_INTERVAL
ux_per_step = sim.read_sensor(sensor_id)[0] / SAMPLE_INTERVAL
uy_per_step = sim.read_sensor(sensor_id)[1] / SAMPLE_INTERVAL
```
---
## 5. Per-Scene Geometry Map
All coordinates in `L0=20` lattice units. Multiply by `L0` to get lattice coordinates unless otherwise noted.
- `CENTER_Y = (NY-1)/2 = 255.5` (lattice units)
- `NY = 512`, `NX = 1280`
### 5.1 Karman Cloak / Erase / ReducedObs (Standard Pinball Layout)
| Object | Position (L0 units) | Position (lattice) | Radius (L0) | Radius (lattice) |
|--------|---------------------|--------------------|-------------|------------------|
| Disturbance cylinder (upstream) | (10, CENTER_Y/L0, 0) | (200, 255.5, 0) | 1.0×L0 | 20 |
| Sensors (3x) | x=40, y=CENTER_Y/L0 + [2, 0, -2] | x=800 | L0/4 | 5 |
| Pinball front | (30, CENTER_Y/L0, 0) | (600, 255.5, 0) | L0/2 | 10 |
| Pinball bottom | (31.3, CENTER_Y/L0 0.75, 0) | (626, 240.5, 0) | L0/2 | 10 |
| Pinball top | (31.3, CENTER_Y/L0 + 0.75, 0) | (626, 270.5, 0) | L0/2 | 10 |
**Object order in legacy API** (for cloak/erase/reduce_obs):
1. sensor0 (top, y=CENTER_Y+2*L0)
2. sensor1 (center, y=CENTER_Y)
3. sensor2 (bottom, y=CENTER_Y-2*L0)
4. dist_cylinder (upstream disturbance)
5. pinball_front
6. pinball_bottom
7. pinball_top
### 5.2 Illusion (Imit) Layout
**Target cylinder (recorded separately)**:
| Object | Position (L0 units) | Radius |
|--------|---------------------|--------|
| Target cylinder | (20, CENTER_Y/L0, 0) | [0.75, 1.0, 1.5]×L0 (varies) |
| Sensors (3x) | x=30, y=CENTER_Y/L0 + [2, 0, -2] | L0/4 |
**Pinball + sensors** (trained env):
| Object | Position (L0 units) | Radius |
|--------|---------------------|--------|
| Sensors (3x) | x=30, y=CENTER_Y/L0 + [2, 0, -2] | L0/4 |
| Pinball front | (19, CENTER_Y/L0, 0) | L0/2 |
| Pinball bottom | (20.3, CENTER_Y/L0 + 0.75, 0) | L0/2 |
| Pinball top | (20.3, CENTER_Y/L0 0.75, 0) | L0/2 |
**Object order** (illusion, 6 objects — no disturbance cylinder):
1. sensor0 (top, y=CENTER_Y+2*L0)
2. sensor1 (center, y=CENTER_Y)
3. sensor2 (bottom, y=CENTER_Y-2*L0)
4. pinball_front
5. pinball_bottom
6. pinball_top
Action array: `temp[3:6] = (action*8 + [0, -2, 2]) * U0`
### 5.3 Vortex Layout
**Target phase** (sensors + vortex, no pinball):
| Object | Position (L0 units) | Radius |
|--------|---------------------|--------|
| Sensors (3x) | x=40, y=CENTER_Y/L0 + [2, 0, -2] | L0/4 |
| Vortex | (10, CENTER_Y/L0, 0) | 2×L0 |
**Pinball phase** (sensors + pinball + vortex):
| Object | Position (L0 units) | Radius |
|--------|---------------------|--------|
| Sensors (3x) | x=40 | L0/4 |
| Pinball front | (30, CENTER_Y/L0, 0) | L0/2 |
| Pinball bottom | (31.3, CENTER_Y/L0 + 0.75, 0) | L0/2 |
| Pinball top | (31.3, CENTER_Y/L0 0.75, 0) | L0/2 |
| Vortex | (15, CENTER_Y/L0, 0) | 2×L0 |
**Vortex types**:
- **Lamb dipole**: strength=0.5×U0, type="lamb"
- **Taylor monopole**: strength=0.03×U0, type="taylor"
**MAX_STEPS = 150** (transient event — not infinite like other scenes)
**Object order** (vortex, 6 objects — no disturbance cylinder):
1. sensor0 (top)
2. sensor1 (center)
3. sensor2 (bottom)
4. pinball_front
5. pinball_bottom
6. pinball_top
Action array: `temp[3:6] = (action*4 + [0, -4, 4]) * U0`
---
## 6. Per-Scene Action Scaling
Each scene maps the normalized DRL action (range [-1, 1]) to physical angular velocity ω (U0 multiples):
| Scene | Formula (ω/U0) | Scale | Bias | Physical range [front, bottom, top] |
|-------|---------------|-------|------|-------------------------------------|
| **Cloak (Karman)** | `action×8 + [0, -4, 4]` | 8 | [0, -4, 4] | front: [-8,8], bottom: [-12,4], top: [-4,12] |
| **Erase** | `action×8 + [0, -8, 8]` | 8 | [0, -8, 8] | front: [-8,8], bottom: [-16,0], top: [0,16] |
| **Illusion (Imit)** | `action×8 + [0, -2, 2]` | 8 | [0, -2, 2] | front: [-8,8], bottom: [-10,6], top: [-6,10] |
| **Vortex** | `action×4 + [0, -4, 4]` | 4 | [0, -4, 4] | front: [-4,4], bottom: [-8,0], top: [0,8] |
**Final omega in lattice units**: Multiply the result by `U0=0.01`.
**Example** (Cloak, action=[1, 1, 1]):
```
ω_front = (1*8 + 0) * 0.01 = 0.08
ω_bottom = (1*8 + (-4)) * 0.01 = 0.04
ω_top = (1*8 + 4) * 0.01 = 0.12
```
**Example** (Cloak, action=[0, 0, 0] — the bias actions):
```
ω_front = 0
ω_bottom = -4 * 0.01 = -0.04
ω_top = 4 * 0.01 = 0.04
```
### 6.1 Action Smoothing (Legacy Only)
Legacy `FlowField.run()` has **built-in exponential smoothing**:
```python
action_pinned = (1 - weight) * action_pinned + weight * action_target
# weight = 0.1
```
This means the actual applied ω smoothly transitions toward the target. The new API has **no built-in smoothing** — implement `ActionSmoother` manually if needed for numerical stability.
---
## 7. Norm Semantics
The normalization values are computed during environment initialization and **must be identical during inference**. They are model-specific and cannot be reused across different scenarios.
### 7.1 Norm Collection Procedure (Standard Pattern)
```python
# Phase 1: Zero-action rollout
for i in range(FIFO_LEN):
flow_field.run(SAMPLE_INTERVAL, zero_action) # 4 or 7 objects depending on phase
fifo_states.append(flow_field.obs[sensor_select]) # e.g. [2:14] skips dist_cyl
# Phase 2: Compute normalization factors
temp_states = np.array(fifo_states) # shape: (FIFO_LEN, N_sensors+N_forces)
force_norm_fact = 6 * max(|forces|) # forces = temp_states[:, 6:12]
for i in range(6):
sens_deviation[i] = mean(sensor_i) # sensor_i = temp_states[:, i]
sens_norm_fact[i] = 5 * max(|sensor_i - mean|)
# Phase 3: Bias-action rollout (for FIFO initialization)
flow_field.apply_ddf() # restore checkpoint
for i in range(FIFO_LEN):
flow_field.run(SAMPLE_INTERVAL, bias_action)
fifo_states.append(...)
save_states = fifo_states.copy()
```
### 7.2 Norm Format
```python
norm = {
"force_norm_fact": float, # scalar = 6 * max(|forces|)
"sens_deviation": [6 floats], # mean per sensor channel
"sens_norm_fact": [6 floats], # 5 * max(|sensor - deviation|) per channel
"save_states": ndarray, # FIFO_LEN × N_obs array after bias rollout
"action_bias": [b_front, b_bottom, b_top], # e.g. [0.0, -4.0, 4.0]
"n_obj_total": int # total objects in flow field
}
```
### 7.3 Scene-Specific Norm Variations
| Scene | force_norm_fact formula | sens_norm_fact factor | Obs slice (from fifo) |
|-------|------------------------|----------------------|----------------------|
| Cloak (standard) | `6 * max(\|forces\|)` | 5 | `obs[2:14]` (skip 2 dist_cyl sensor channels) |
| Erase | `100 * max(\|forces\|)` | 10 | `obs[0:14]` (full 14, incl. dist force) |
| Illusion | `6 * max(\|forces\|)` | 5 | `obs[0:12]` (full 12) |
| Vortex | `6 * max(\|forces\|)` | 5 | `obs[0:12]` (full 12) |
| ReducedObs | `10 * max(\|forces\|)` | 5 | `obs[2:14]` |
### 7.4 Observation Normalization (per step)
```python
# cloak/standard:
forces = obs_slice[6:12] / force_norm_fact
sens = (obs_slice[0:6] - sens_deviation) / sens_norm_fact
observation = clip(hstack([forces, sens]), -1, 1)
# erase:
forces = obs_slice[6:14] / force_norm_fact # Note: 8 force values (includes dist_cylinder)
# But only forces[2:8] (pinball forces) are used in observation
sens = (obs_slice[0:6] - sens_deviation) / sens_norm_fact
```
### 7.5 L0 Units vs Lattice Coordinates
Multiple sources define positions in L0 units; multiply by L0=20 to get lattice (pixel) coordinates.
| Description | L0 units | Lattice (pixels) |
|-------------|----------|-------------------|
| Grid size | — | 1280 × 512 |
| Center Y (CENTER_Y) | — | (512-1)/2 = 255.5 |
| Disturbance cylinder x | 10×L0 → 10 | 200 |
| Sensor x (cloak/erase/vortex/reduce) | 40×L0 → 40 | 800 |
| Sensor x (illusion) | 30×L0 → 30 | 600 |
| Pinball front x (cloak/erase/vortex/reduce) | 30×L0 | 600 |
| Pinball front x (illusion) | 19×L0 | 380 |
| Pinball bottom/top x (cloak/erase/vortex/reduce) | 31.3×L0 | 626 |
| Pinball bottom/top x (illusion) | 20.3×L0 | 406 |
| Pinball radius (all) | L0/2 | 10 |
| Sensor radius (all) | L0/4 | 5 |
| Disturbance cylinder radius (cloak/erase) | L0 | 20 |
| Vortex radius | 2×L0 | 40 |
| Target cylinder radius (illusion) | [0.75, 1.0, 1.5]×L0 | [15, 20, 30] |
---
## 8. Per-Scene Running Parameters
| Parameter | Cloak | Erase | Illusion | Vortex | ReducedObs |
|-----------|-------|-------|----------|--------|------------|
| S_DIM | 12 | 12 | 14 | 12 | varies (3→2) |
| A_DIM | 3 | 3 | 3 | 3 | 3 |
| SAMPLE_INTERVAL | 800 | 600 | varies | 800 | 800 |
| FIFO_LEN | 150 | 150 | 150 | 150 | 150 |
| CONV_LEN | 30 | 36 | 36 | 30 | 36 |
| MAX_STEPS | 500 | 500 | 500 | **150** | 500 |
| T0 | 1000 | 1000 | 1000 | 1000 | 1000 |
| Objects in env | 7 | 7 | 6 | 6 | 7 |
| Has disturbance cyl | Yes | Yes | No | No | Yes |
| DRL training initial model | — (scratch) | `d1a3o12_250326_erase` | — (scratch) | `d1a3o12_re100` | — (scratch) |
### 8.1 Reward Functions
**Cloak (Karman)**:
```python
reward_cd = exp(-|cd * 20|) # cd = (Σforces_fx) / 3
reward_cl = exp(-|cl * 80|) # cl = (Σforces_fy) / 3
reward_sim = exp(-10 * |sim - 1|) # sim = DTW-based similarity
reward = min(0.3*reward_cd + 0.4*reward_cl + 0.3*reward_sim, 1.0)
```
**Erase**:
```python
# Target = clean inflow mean (steady), not the noisy vortex street
reward_u = exp(-|diff_u * 40|) # diff of current vs target sensor u
reward_v = 0.7*exp(-|amp_v*20|) + 0.3*exp(-|diff_v*20|)
reward_sim = similarities # raw DTW similarity (not exponentiated)
reward = min(0.4*reward_u + 0.4*reward_v + 0.2*reward_sim, 1.0)
```
**Illusion**:
```python
# Target forces from harmonics reconstruction of target cylinder
reward_cd = exp(-|(cd - cd_target) * 10|)
reward_cl = exp(-|(cl - cl_target) * 10|)
reward_sim = exp(-10 * |sim - 1|)
reward = min(0.3*reward_cd + 0.3*reward_cl + 0.4*reward_sim, 1.0)
```
**Vortex**:
```python
reward_cd = exp(-|cd * 20|)
reward_cl = exp(-|cl * 80|)
reward_sim = exp(-10 * |sim - 1|)
reward = min(0.2*reward_cd + 0.3*reward_cl + 0.5*reward_sim, 1.0)
```
---
## 9. Old API Obs Layout
### 9.1 Cloak (Karman) / Standard Env — 7 Objects
Object addition order:
1. Disturbance cylinder (id=0)
2. Sensor0 / top (id=1)
3. Sensor1 / center (id=2)
4. Sensor2 / bottom (id=3)
5. Pinball front (id=4)
6. Pinball bottom (id=5)
7. Pinball top (id=6)
**`flow_field.obs` array** (14 values = 7 objects × 2):
```
obs[0:2] = dist_cylinder force (fx, fy) — ignored in training
obs[2:4] = sensor0 velocity (ux, uy)
obs[4:6] = sensor1 velocity (ux, uy)
obs[6:8] = sensor2 velocity (ux, uy)
obs[8:10] = front_pinball force (fx, fy)
obs[10:12] = bottom_pinball force (fx, fy)
obs[12:14] = top_pinball force (fx, fy)
```
**Normalized observation** (after `obs[2:14]` slice):
```
obs_norm[0:6] = sensor0_ux, sensor0_uy, sensor1_ux, sensor1_uy, sensor2_ux, sensor2_uy
obs_norm[6:12] = front_fx, front_fy, bottom_fx, bottom_fy, top_fx, top_fy
```
**Action array** (7 entries, n_objects=7):
```
temp[0:4] = 0 (sensors + dist_cylinder — ignored)
temp[4] = front omega
temp[5] = bottom omega
temp[6] = top omega
```
### 9.2 Erase Env — 7 Objects
Same addition order as Cloak (disturbance cylinder radius=0.75*L0 instead of 1.0*L0).
**`flow_field.obs` array** (14 values):
```
obs[0:2] = dist_cylinder force (fx, fy)
obs[2:4] = sensor0 velocity (ux, uy)
obs[4:6] = sensor1 velocity (ux, uy)
obs[6:8] = sensor2 velocity (ux, uy)
obs[8:10] = front_pinball force (fx, fy)
obs[10:12] = bottom_pinball force (fx, fy)
obs[12:14] = top_pinball force (fx, fy)
```
**Normalized observation** (full `obs[0:14]` slice — include dist_cylinder forces):
```
forces = obs[6:14] / force_norm_fact # 8 force values
# But only forces[2:8] (pinball) used in step()
sens = (obs[0:6] - sens_deviation) / sens_norm_fact
```
**Target recording** uses `obs[0:6]` (sensor only, no dist cylinder force), since erase has **no disturbance cylinder during target phase**.
### 9.3 Illusion (Imit) Env — 6 Objects
Object addition order (sensors first, then pinball):
1. Sensor0 / top (id=0)
2. Sensor1 / center (id=1)
3. Sensor2 / bottom (id=2)
4. Pinball front (id=3)
5. Pinball bottom (id=4)
6. Pinball top (id=5)
**`flow_field.obs` array** (12 values):
```
obs[0:2] = sensor0 velocity (ux, uy)
obs[2:4] = sensor1 velocity (ux, uy)
obs[4:6] = sensor2 velocity (ux, uy)
obs[6:8] = front_pinball force (fx, fy)
obs[8:10] = bottom_pinball force (fx, fy)
obs[10:12] = top_pinball force (fx, fy)
```
**Normalized observation** (full `obs[0:12]`):
```
forces = obs[6:12] / force_norm_fact
sens = (obs[0:6] - sens_deviation) / sens_norm_fact
obs_norm = hstack([forces, sens]) # 12 values
# Plus 2 additional: target_cd, target_cl → total 14 (S_DIM=14)
```
**Action array** (6 entries):
```
temp[0:3] = 0 (sensors — ignored)
temp[3] = front omega
temp[4] = bottom omega
temp[5] = top omega
```
**Target recording** uses `obs[0:8]` (3 sensor × 2 + 1 cylinder × 2 = 8 values from target cylinder + 3 sensors).
### 9.4 Vortex Env — 6 Objects
Same object order as Illusion (sensors + pinball, no disturbance cylinder).
Same obs layout as Illusion (12 values, obs[0:12] used as-is).
**Target recording**: In the target phase, `obs` has 3 sensors only (6 values, `obs[0:6]`).
### 9.5 ReducedObs Env — 7 Objects
Same geometry and object order as Karman Cloak (7 objects: dist_cyl + 3 sensors + 3 pinball).
**Obs slice**: `obs[2:14]` (same as Cloak, skipping dist_cylinder forces).
**Additional torque observation**: Some reduced-obs models also compute torque:
```python
obs_torque = (-obs[1] - obs[2]*3/2 + obs[3]/2 + obs[4]*3/2 + obs[5]/2) / torque_norm_fact
```
The observation layout varies by model name suffix:
| Model name | S_DIM | Observation components |
|-----------|-------|----------------------|
| `forces02` | 3 | `[obs_torque, dist_fx, dist_fy]` (?) |
| `total_force` | 3 | Total force components |
| `torque+forces02` | 5 | torque + dist forces |
| `torque+forces02+sens24` | 9 | torque + dist forces + sensors 2,4 |
| `torque+forces04+sens04` | 5 | torque + forces + sensors |
See `legacy_env_reduce_obs.py` line 191 for exact obs composition.
---
## 10. Complete Model Inventory
All model `.zip` files are PPO policies with Sin activation and 64×64 hidden layers.
### 10.1 `models/old/` — Original Training (Cloak, Various Re)
| File | Env | S_DIM | A_DIM | Action Scale/Bias | Sample Interval | Re (code) | Description |
|------|-----|-------|-------|-------------------|-----------------|-----------|-------------|
| `d1a3o12_re50.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | 800 | 50 | Cloak at lower Re; base model |
| `d1a3o12_re100.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | 800 | 100 | Cloak at Re=100 (ν=0.004); **most used base model** |
| `d1a3o12_re200.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | 800 | 200 | Cloak at higher Re |
| `d1a3o12_re400.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | 800 | 400 | Cloak at highest Re |
| `vortex_lamb.zip` | Vortex | 12 | 3 | 4 / [0,-4,4] | 800 | 100 | Cloak of Lamb dipole vortex; **transfer** from re100 |
| `vortex_taylor.zip` | Vortex | 12 | 3 | 4 / [0,-4,4] | 800 | 100 | Cloak of Taylor monopole vortex; **transfer** from re100 |
**Naming convention** `d1a3o12`:
- `d1` = 1 disturbance cylinder
- `a3` = 3 actuators (pinball cylinders)
- `o12` = 12 observations
- `o14` = 14 observations (used for illusion, includes target forces)
### 10.2 `models/250326/` — Re-trained Cloak
| File | Env | S_DIM | A_DIM | Scale/Bias | Desc |
|------|-----|-------|-------|------------|------|
| `d1a3o12_250326.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | Re-trained from scratch; equivalent to re100 |
### 10.3 `models/250329/` — No-offset Cloak
| File | Env | S_DIM | A_DIM | Scale/Bias | Desc |
|------|-----|-------|-------|------------|------|
| `d0a3o12_250329_nooffset.zip` | Cloak | 12 | 3 | 8 / [0,0,0] | Disturbance-cylinder-free (d0), zero bias actions |
### 10.4 `models/250421/` — Reduced Observation
| File | Env | S_DIM | A_DIM | Scale/Bias | Desc |
|------|-----|-------|-------|------------|------|
| `d1a3o12_250421_forces02.zip` | ReducedObs | 3 | 3 | 8 / [0,-4,4] | Obs reduced to 3 values |
| `d1a3o12_250421_torque+forces02.zip` | ReducedObs | 5 | 3 | 8 / [0,-4,4] | Torque + 2 force values |
| `d1a3o12_250421_torque+forces02+sens24.zip` | ReducedObs | 9 | 3 | 8 / [0,-4,4] | Torque + forces + sensors |
| `d1a3o12_250421_torque+forces04+sens04.zip` | ReducedObs | 5 | 3 | 8 / [0,-4,4] | Modified obs composition |
| `d1a3o12_250421_torque+total_force.zip` | ReducedObs | 5 | 3 | 8 / [0,-4,4] | Torque + total force |
| `d1a3o12_250421_total_force.zip` | ReducedObs | 3 | 3 | 8 / [0,-4,4] | Only total force |
All trained from scratch (no transfer). Obs reduction experiments for experimental hardware simplification.
### 10.5 `models/250525/` — Illusion (Imit)
| File | Env | S_DIM | A_DIM | Scale/Bias | Target | Sample Interval |
|------|-----|-------|-------|------------|--------|-----------------|
| `d1a3o12_250525_imit_075L_1U.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 0.75L cylinder, U0=0.01 | 600 |
| `d1a3o12_250525_imit_1L_1U.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder, U0=0.01 | 600 |
| `d1a3o12_250525_imit_1L_1U_trans.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder, U0=0.01 | 600 (transfer) |
| `d1a3o14_250525_imit_075L_2U.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 0.75L cylinder, 2×U0 | 600 |
| `d1a3o14_250525_imit_075L_2U_1.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 0.75L cylinder | ~600 |
| `d1a3o14_250525_imit_075L_2U_400S.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 0.75L cylinder | 400 |
| `d1a3o14_250525_imit_15L_2U.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.5L cylinder, 2×U0 | 600 |
| `d1a3o14_250525_imit_1L_2U.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder, 2×U0 | 600 |
| `d1a3o14_250525_imit_1L_2U_1.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder | ~600 |
| `d1a3o14_250525_imit_1L_2U_400S_02Vis.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder | 400 |
| `d1a3o14_250525_imit_1L_2U_600S.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder | 600 |
| `d1a3o14_250525_imit_1L_2U_800S_08Vis.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder | 800 |
| `d1a3o14_250525_imit_1L_2U_1000S_08Vis.zip` | Illusion | 14 | 3 | 8 / [0,-2,2] | 1.0L cylinder | 1000 |
**Naming conventions**:
- `075L` = target cylinder diameter = 0.75×L0
- `1L` = target cylinder diameter = 1.0×L0
- `15L` = target cylinder diameter = 1.5×L0
- `1U` = U0=0.01 (standard), `2U` = 2×U0=0.02
- `400S` etc. = SAMPLE_INTERVAL
- `02Vis` etc. = ν (viscosity) multiplier (e.g. 0.08×ν)
- `trans` = transfer learning model
- `_1` suffix = variant
### 10.6 `models/250729/` — Erase & Re-cloak
| File | Env | S_DIM | A_DIM | Scale/Bias | Base Model | Desc |
|------|-----|-------|-------|------------|------------|------|
| `d1a3o12_250729_250326_cloak_800S_02Vis.zip` | Cloak | 12 | 3 | 8 / [0,-4,4] | `d1a3o12_250326` | Re-cloak, 02×ν |
| `d1a3o12_250729_250326_erase.zip` | Erase | 12 | 3 | 8 / [0,-8,8] | `d1a3o12_250326` | Erase, transfer from cloak |
| `d1a3o12_250729_250326_erase_250804_20D_retrain2.zip` | Erase | 12 | 3 | 8 / [0,-8,8] | `erase` | Erase retrain, 20D delay |
| `d1a3o12_250729_250326_erase_250804_20D_retrain3.zip` | Erase | 12 | 3 | 8 / [0,-8,8] | `erase` | Erase retrain v3 |
Erase models: SAMPLE_INTERVAL=600, CONV_LEN=36 (vs 800/30 for cloak).
---
## 11. DRL Hyperparameters
| Parameter | Value |
|-----------|-------|
| Algorithm | PPO (Stable-Baselines3 `PPO`) |
| Policy network | `MlpPolicy` |
| Hidden layers | 64 × 64 (both actor and critic) |
| Activation function | **Sin** (custom `torch.nn.Module`) |
| Optimizer | Adam |
| Learning rate (actor) | 3×10⁻⁴ |
| Learning rate (critic) | 4×10⁻⁴ |
| Episode length | 600 T₀ (T₀ = D/U₀ = 2000 LBM steps) |
| Action interval | 0.8 T₀ (one action per 0.8 flow-through times) |
| Training timesteps/iteration | 360-400 (varies by scene) |
| Total episodes | ~500 (varies; vortex uses ~100) |
| Device | CUDA GPU (device_id varies) |
| Deterministic inference | Yes (`use_deterministic=True`) |
### 11.1 Custom Sin Activation
```python
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
```
Used in place of Tanh/ReLU because trigonometric functions better preserve spectral fidelity for vortex-dominated flows. Networks are:
```
Input(s_t) → Linear(64) → Sin → Linear(64) → Sin → Linear(n_actions) → Output(a_t)
```
### 11.2 Time Scales
| Quantity | LBM steps | Description |
|----------|-----------|-------------|
| T₀ = D/U₀ | 20/0.01 = 2000 | One flow-through time (single cylinder diameter) |
| SAMPLE_INTERVAL | 600-800 | Steps between DRL actions |
| Action interval | 0.8 T₀ | SAMPLE_INTERVAL / T₀ = 800/2000 = 0.4 ... actually 800/2000=0.4 |
| Episode length | 600 T₀ = 1.2M steps | Total episode duration |
**Correction**: The paper says "actuations per T₀ = 1.25" and "action interval = 0.8 T₀". This means SAMPLE_INTERVAL = 0.8×2000 = 1600 LBM steps? But the code says SAMPLE_INTERVAL=800. This is a discrepancy — note that the paper uses T₀ = D/U₀ and SAMPLE_INTERVAL=800, which gives 800/2000 = 0.4 T₀ = 2.5 actuations per T₀. The paper may use a different T₀ definition, or the hyperparameter table may be aspirational vs actual.
---
## 12. Target Signal & Illusion Harmonics
### 12.1 Karman Cloak Target
Recorded from: 1 disturbance cylinder + 3 sensors (no pinball).
- 150 steps × SAMPLE_INTERVAL = 800 steps
- `obs[2:8]` → 6 sensor channels stored
- Stored in `target_states` (150, 6)
### 12.2 Erase Target
Recorded from: 3 sensors only (no disturbance cylinder).
- 150 steps × SAMPLE_INTERVAL = 600 steps
- `obs[0:6]` → 6 sensor channels stored
- No periodic signal → target is mean only (clean inflow)
### 12.3 Illusion Target + Harmonics
Recorded from: 1 target cylinder + 3 sensors (no pinball).
- 150 steps × SAMPLE_INTERVAL (varies)
- `obs[0:8]` → 3 sensor (6) + 1 cylinder force (2) = 8 channels stored
- **Harmonics analysis**: FFT over target_states extracts DC + top 5 frequency harmonics per channel
- Stored as `target_harmonics`: list of dicts with `{dc, amps, freqs, phases}` per channel (8 channels total)
- During training, target forces are reconstructed via `gen_target_states_at(step, harmonics)`
### 12.4 Vortex Target
Recorded from: 3 sensors + Lamb dipole or Taylor monopole (no pinball).
- 150 steps × SAMPLE_INTERVAL = 800 steps
- `obs[0:6]` → 6 sensor channels stored
- Vortex is transient: target_states contains the evolving vortex signal
---
## 13. Training Strategies
### 13.1 Training Loop Pattern (All Scenes)
```python
for i in range(total_episodes): # 100-500
model.learn(total_timesteps=K) # K = 360-1500
test_env = model.get_env()
test_obs = test_env.reset()
for step in range(eval_steps): # 150-360
test_action, _ = model.predict(test_obs)
test_obs, reward, done, info = test_env.step(test_action)
list_reward.append(reward)
avg_reward = mean(list_reward[-tail:]) # last 100-180 steps
# Save if best
if avg_reward > max_reward:
model.save(...)
```
### 13.2 Transfer Learning
| Source Model | Target Model | Method |
|-------------|-------------|--------|
| `d1a3o12_re100` (Cloak) | `vortex_lamb`, `vortex_taylor` | Transfer: loaded as base, retrained on vortex env |
| `d1a3o12_250326` (Cloak) | Erase models | Transfer: loaded as base, retrained on erase env |
| Erase base | Erase retrain models | Transfer: loaded from previously trained erase |
| `d1a3o12_250525_imit_1L_2U_600S` | `..._1000S_08Vis` etc. | Transfer: varied SAMPLE_INTERVAL/viscosity |
Base models are loaded via `PPO.load(path, env=new_env, device=...)` and then fine-tuned.
### 13.3 Checkpoint & Reset Mechanism
```python
# During __init__:
flow_field.run(...) # stabilize
flow_field.get_ddf() # host ← GPU
flow_field.save_ddf() # save to host memory
# During reset/restore:
flow_field.restore_ddf() # restore from host memory
flow_field.apply_ddf() # host → GPU
```
The restored state includes the stable pinball wake (with vortex for vortex scenes). The FIFO is re-initialized from `save_states`.
---
## 14. File Structure Reference
```
DynamisLab/
├── configs/
│ ├── config_lbm_pinball.json # NEW LBM config (nx=1280, ny=512)
│ ├── config_body.json # Body/object definitions
│ ├── CONFIG.md # Config schema documentation
│ └── legacy_configs/
│ ├── config_cuda.json # Legacy CUDA config (X_1U=128, etc.)
│ ├── config_flowfield.json # Legacy flow field config
│ └── config_gym.json # Legacy gym config
├── models/
│ ├── old/ # Original re100/re200/re400/re50 + vortex
│ ├── 250326/ # Re-trained cloak
│ ├── 250329/ # No-offset cloak
│ ├── 250421/ # Reduced observation
│ ├── 250525/ # Illusion (imit)
│ └── 250729/ # Erase + re-cloak
├── src/
│ ├── drl_pinball/
│ │ ├── knowledge.md ← THIS FILE
│ │ ├── legacy_env/ # Old-API environment classes
│ │ │ ├── legacy_env_karman_cloak_standard.py
│ │ │ ├── legacy_env_erase.py
│ │ │ ├── legacy_env_imit.py
│ │ │ ├── legacy_env_imit_target.py
│ │ │ ├── legacy_env_vortex.py
│ │ │ ├── legacy_env_reduce_obs.py
│ │ │ └── legacy_karman_env.py # Reference implementation (re100)
│ │ ├── legacy_train/ # Training scripts
│ │ │ ├── karman_cloak.py
│ │ │ ├── erase.py
│ │ │ ├── imit.py
│ │ │ ├── vortex.py
│ │ │ └── reduce_obs.py
│ │ └── legacy_test/ # Test/evaluation scripts
│ ├── analysis_crossre/ # Cross-Re analysis (SINDy, etc.)
│ └── CelerisLab/ or LegacyCelerisLab/ # Solver libraries
├── docs/
│ ├── understanding_notes.md # Earlier knowledge consolidation
│ └── My_Confirmation/Chapters/ # LaTeX thesis chapters
├── LegacyCelerisLab/ # Old solver library
└── output/ # Field output, .pkl files
```
---
## 15. Key Numerical Values Quick Reference
| Quantity | Value | Notes |
|----------|-------|-------|
| NX | 1280 | Grid x-dimension |
| NY | 512 | Grid y-dimension |
| L0 | 20 | Base length unit (lattice) |
| U0 | 0.01 | Centerline inlet velocity (lattice) |
| ν (default) | 0.004 | Kinematic viscosity → Re=100 (code) |
| T₀ = D/U₀ | 2000 | Flow-through time (single cylinder diameter) |
| Reynolds (code) | Re = U0·(2D)/ν | Uses 2D reference |
| Reynolds (report) | Re_D = U0·D/ν | Uses 1D reference |
| SAMPLE_INTERVAL | 600-800 | Steps between DRL actions |
| FIFO_LEN | 150 | History buffer length |
| Action scale | 4 or 8 | Scene-dependent |
| Action bias | varies | Scene-dependent |
| Omega guard | [0.01, 1.99] | From config_lbm_pinball.json |
### 15.1 Physics-to-Lattice Unit Relations
```
D (cylinder diameter) = 20 lattice units = 1.0 in L0 units
Single cylinder Re: Re_D = U0 × D / ν = 0.01 × 20 / 0.004 = 50
Code Re: Re_code = U0 × 2D / ν = 0.01 × 40 / 0.004 = 100
```
---
## 16. Important Implementation Notes
### 16.1 Obs Slice Differences
The sensor indices in `obs` are **not** consistent across envs:
- **Cloak/standard**: uses `obs[2:14]` — skips first 2 values (disturbance cylinder forces)
- **Erase**: uses `obs[0:14]` — includes all values (disturbance cylinder forces are part of observation)
- **Illusion/Vortex**: uses `obs[0:12]` — sensors(6) + pinball forces(6)
### 16.2 Center Y Computation
```python
CENTER_Y = (NY - 1) / 2.0 # = 255.5 for NY=512
```
This is because NY is even (512), so center is between two lattice rows.
### 16.3 Force Norm Factors
These are scene-specific constants that must NOT be shared between scenes:
| Scene | force_norm_fact | sens_norm_fact factor |
|-------|----------------|----------------------|
| Cloak/standard | `6 * max(|forces|)` | 5 |
| Erase | `100 * max(|forces|)` | 10 |
| Illusion | `6 * max(|forces|)` | 5 |
| Vortex | `6 * max(|forces|)` | 5 |
| ReducedObs | `10 * max(|forces|)` | 5 |
### 16.4 Vortex Scene Termination
The vortex env is the only scene with **bounded episodes**:
- `MAX_STEPS = 150` (vs 500 for all other scenes)
- `done = self.current_step >= MAX_STEPS`
- This is because the vortex is a transient event that passes through the domain
### 16.5 Experimental Setup (from thesis)
- Water tunnel with towing platform (25cm width)
- Custom force sensor integrating air bearing + 2D force sensor (semiconductor strain gauges)
- Custom low-noise, high-precision data acquisition system
- Planar PIV for flow field measurement
- Current challenges: sensor noise from over-constraint/welding; rail manufacturing precision
---
## 17. Key Differences Between Legacy and New Solvers
| Aspect | Legacy | New |
|--------|--------|-----|
| Solver name | `LegacyCelerisLab` / `CelerisLab` | `CelerisLab` (new) |
| Main class | `FlowField(config_field, config_cuda, device_id)` | `Simulation(config)` |
| Object setup | `add_cylinder()`, `add_sensor()` at runtime | Pre-defined in JSON or added before `initialize()` |
| Checkpoint | `get_ddf()` / `save_ddf()` / `restore_ddf()` / `apply_ddf()` | `snapshot()` / `restore()` or save_checkpoint |
| Force reading | Part of unified `obs` array | `read_force(body_id)` per body |
| Sensor reading | Part of unified `obs` array | `read_sensor(body_id)` per body |
| Torque reading | Not used in legacy (only in ReduceObs) | `read_torque(body_id)` |
| Action smoothing | Built-in (weight=0.1) | None |
| Action application | Single `run(N, action_array)` | `set_body(id, omega=val)` per body then `run(N)` |
| Context management | `context.push()` / `.pop()` for CUDA isolation | Stream-based |
### 17.1 Force/Sensor Value Correction
The fundamental difference in how forces and sensors are accumulated:
**Legacy**: `flow_field.run(N, action)` → internal loop averages over N steps → `obs` contains per-step averages.
**New**: `sim.run(N)` → accelerators accumulate raw sums → `read_force(id)` gives N-step sum → **must divide by N**.
---
*End of comprehensive knowledge document. Last updated: 2026-06-05.*
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,304 @@
# 这个是erase的env,用于训练和评估d1a3o12_250729_250326_erase系列模型
# 上游扰流圆柱,场景与Karman_cloak_standard一致,
# 但是目标是希望pinball后流场跟入口一致,即抹除扰流圆柱尾迹
# 模型名中D代表信号延迟
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 12, 3
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 600
FIFO_LEN = 150
CONV_LEN = 36
MAX_STEPS = 500
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 6), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_u = 0.0
self.reward_v = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
# center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
# self.flow_field.add_cylinder(center, L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(2*NX/U0), np.zeros(3, dtype=DATA_TYPE))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(3, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()[0:6]
self.target_states = np.vstack((self.target_states, new_state))
self.target_sensors = np.mean(self.target_states, axis=0)
# self.flow_field.apply_ddf()
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, 0.75*L0)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
self.flow_field.run(int(4*NX/U0), np.zeros(7, dtype=DATA_TYPE))
self.flow_field.get_ddf()
self.flow_field.save_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(7, dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[0:14])
temp_states = np.array(self.fifo_states)
self.force_norm_fact = 100 * np.max(np.abs(temp_states[:, 8:14]))
for i in range(6):
self.sens_deviation[i] = np.mean(temp_states[:, i])
self.sens_norm_fact[i] = 10 * np.max(np.abs(temp_states[:, i] - self.sens_deviation[i]))
self.flow_field.apply_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.array([0.0, 0.0, 0.0, 0.0, 0.0, -8*U0, 8*U0], dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[0:14])
self.save_states = self.fifo_states.copy()
self.flow_field.apply_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(7, dtype=DATA_TYPE)
temp[4:7] = np.array((action*8+[0,-8,8])*U0, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
self.fifo_states.append(self.flow_field.obs.copy()[0:14])
def proc_data():
states = np.array(self.fifo_states)
forces = states[-1, 6:14] / self.force_norm_fact
cd = (forces[2] + forces[4] + forces[6]) / 3
cl = (forces[3] + forces[5] + forces[7]) / 3
sens = (states[-1, 0:6] - self.sens_deviation) / self.sens_norm_fact
target_sens = (self.target_sensors - self.sens_deviation) / self.sens_norm_fact
similarities = 0.0
def calc_lag(target, state):
target_mean = np.mean(target)
state_mean = np.mean(state)
correlation = np.correlate(target - target_mean, state - state_mean, "full")
lags = np.arange(-len(target) + 1, len(target))
max_lag = lags[np.argmax(correlation)]
return max_lag
def calc_sim(target, state):
# 计算幅值差异权重
target_std = np.std(target) if np.std(target) > 1e-8 else 1e-8
state_std = np.std(state) if np.std(state) > 1e-8 else 1e-8
amplitude_ratio = min(target_std, state_std) / max(target_std, state_std)
# 计算均值差异
mean_diff = abs(np.mean(target) - np.mean(state))
max_scale = max(abs(np.mean(target)), abs(np.mean(state)), 1e-8)
mean_similarity = 1 / (1 + mean_diff / max_scale * 10)
# DTW计算
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(target[i - 1] - state[j - 1])
last_min = min(dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1])
dtw_matrix[i, j] = cost + last_min
# 改进的归一化方法
max_possible_cost = max(np.max(np.abs(target)), np.max(np.abs(state)), 1e-8)
dtw_distance = dtw_matrix[n, m] / (len(target) * max_possible_cost)
DTW_similarity = max(0, 1 - dtw_distance)
# 综合相似度:形状相似度 * 幅值相似度 * 均值相似度
total_similarity = 0.8 * DTW_similarity + 0.1 * amplitude_ratio + 0.1 * mean_similarity
return total_similarity
# id_sens = 1
# target_seq = self.target_states[CONV_LEN:2*CONV_LEN, id_sens]
target_seq = -states[CONV_LEN:2*CONV_LEN, 7]
# state_seq = states[-CONV_LEN:, id_sens]
state_seq = states[-CONV_LEN:, 9]
lag = calc_lag(target_seq, state_seq)
for i in range(0, 2):
target_seq = -np.roll(states[:, i+6], -lag)[CONV_LEN:2*CONV_LEN]
state_seq = states[-CONV_LEN:, i+8] + states[-CONV_LEN:, i+10] + states[-CONV_LEN:, i+12]
similarities += calc_sim(target_seq, state_seq) / 2
diff_u = (np.abs(sens[0] - target_sens[0]) + np.abs(sens[2] - target_sens[2]) + np.abs(sens[4] - target_sens[4]))/3
diff_v = (np.abs(sens[1] - target_sens[1]) + np.abs(sens[3] - target_sens[3]) + np.abs(sens[5] - target_sens[5]))/3
mean_u = np.mean(np.abs(states[:, 0] - self.target_sensors[0]) \
+ np.abs(states[:, 2] - self.target_sensors[2]) \
+ np.abs(states[:, 4] - self.target_sensors[4])) / self.sens_norm_fact[2]
amp_v = np.std(states[:, 1] + states[:, 3] + states[:, 5]) / self.sens_norm_fact[3]
self.reward_u = np.exp(-np.abs(diff_u * 40))
self.reward_v = 0.7 * np.exp(-np.abs(amp_v * 20)) + 0.3 * np.exp(-np.abs(diff_v * 20))
# self.reward_sim = np.exp(-50*np.abs(similarities - 1)**2)
self.reward_sim = similarities
reward = np.minimum(0.4 * self.reward_u + 0.4 * self.reward_v + 0.2 * self.reward_sim, 1.0)
result_queue.put((np.hstack([forces[2:8], sens]), reward))
run_flow_field(action)
proc_data()
observation, reward = result_queue.get()
truncated = bool(np.any(observation > 1) or np.any(observation < -1))
observation = np.clip(observation, -1, 1)
self.current_step += 1
# done = self.current_step >= MAX_STEPS
done = False
return observation, float(reward), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.fifo_states = self.save_states.copy()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def average_field(self, mode=["add", "save", "clear"], filename="average_field.dat"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_new = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
if "add" in mode:
self.ddf_ave = self.ddf_ave + ddf_new
self.ddf_ave_cont += 1
if "save" in mode:
if self.ddf_ave_cont == 0:
raise ValueError("No data to save. Please run 'add' mode first.")
ux = (self.ddf_ave[:, :, 1] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 8] - self.ddf_ave[:, :, 3] - self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 7]) / U0 / self.ddf_ave_cont
uy = (self.ddf_ave[:, :, 2] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 4] - self.ddf_ave[:, :, 7] - self.ddf_ave[:, :, 8]) / U0 / self.ddf_ave_cont
flag_plot = self.flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)
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")
print(f"Average field amount: {self.ddf_ave_cont}")
if "clear" in mode:
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,318 @@
# 这个是imit圆柱的env,用于训练和评估d1a3o14_250525_imit系列模型
# 上游干净来流,目标是pinball后流场跟设定尺寸圆柱一致
# 模型名中L代表目标直径,S代表SAMPLE_INTERVAL
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 14, 3
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 600 # 这里会随着圆柱尺寸和粘性变动,在模型名中体现
FIFO_LEN = 150
CONV_LEN = 36
MAX_STEPS = 500
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 8), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_cd = 0.0
self.reward_cl = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
center: Tuple[float, float, float] = (20 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, 1.5*L0) # 这里会随着圆柱尺寸变动,在模型名中体现
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(4, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()[0:8]
self.target_states = np.vstack((self.target_states, new_state))
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
self.target_harmonics = analyze_harmonics(self.target_states, n_harmonics=5)
del self.flow_field
self.flow_field = FlowField(config_field, config_cuda, device_id)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (19 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (20.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (20.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
self.flow_field.run(int(4*NX/U0), np.zeros(6, dtype=DATA_TYPE))
self.flow_field.get_ddf()
self.flow_field.save_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(6, dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[0:12])
temp_states = np.array(self.fifo_states)
self.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))
for i in range(6):
self.sens_deviation[i] = np.mean(temp_states[:, i])
self.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - self.sens_deviation[i]))
self.flow_field.apply_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.array([0.0, 0.0, 0.0, 0.0, -1*U0, 1*U0], dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[0:12])
self.save_states = self.fifo_states.copy()
self.flow_field.get_ddf()
self.flow_field.save_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(6, dtype=DATA_TYPE)
temp[3:6] = np.array((action*8+[0,-2,2])*U0, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
self.fifo_states.append(self.flow_field.obs.copy()[0:12])
def proc_data():
states = np.array(self.fifo_states)
forces = states[-1, 6:12] / self.force_norm_fact
cd = forces[0] + forces[2] + forces[4]
cl = forces[1] + forces[3] + forces[5]
sens = (states[-1, 0:6] - self.sens_deviation) / self.sens_norm_fact
similarities = 0.0
def calc_lag(target, state):
target_mean = np.mean(target)
state_mean = np.mean(state)
correlation = np.correlate(target - target_mean, state - state_mean, "full")
lags = np.arange(-len(target) + 1, len(target))
max_lag = lags[np.argmax(correlation)]
return max_lag
def calc_sim(target, state):
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(target[i - 1] - state[j - 1])
last_min = min(dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1])
dtw_matrix[i, j] = cost + last_min
return 1 - (dtw_matrix[n, m] / len(target))
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
id_sens = 1
target_seq = self.target_states[CONV_LEN:2*CONV_LEN, id_sens+2]
state_seq = states[-CONV_LEN:, id_sens]
lag = calc_lag(target_seq, state_seq)
for i in range(0, 6):
target_seq = np.roll(self.target_states[:, i+2], -lag)[CONV_LEN:2*CONV_LEN]
state_seq = states[-CONV_LEN:, i]
similarities += calc_sim(target_seq, state_seq) / 6
target_states = gen_target_states_at(self.current_step, self.target_harmonics)
target_cd = target_states[0] / self.force_norm_fact
target_cl = target_states[1] / self.force_norm_fact
self.reward_cd = np.exp(-np.abs((cd-target_cd) * 10))
self.reward_cl = np.exp(-np.abs((cl-target_cl) * 10))
self.reward_sim = np.exp(-10*np.abs(similarities - 1))
reward = np.minimum(0.3 * self.reward_cd + 0.3 * self.reward_cl + 0.4 * self.reward_sim, 1.0)
result_queue.put((np.hstack([forces, sens, target_cd, target_cl]), reward))
run_flow_field(action)
proc_data()
observation, reward = result_queue.get()
truncated = bool(np.any(observation > 1) or np.any(observation < -1))
observation = np.clip(observation, -1, 1)
self.current_step += 1
# done = self.current_step >= MAX_STEPS
done = False
return observation, float(reward), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.fifo_states = self.save_states.copy()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def average_field(self, mode=["add", "save", "clear"], filename="average_field.dat"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_new = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
if "add" in mode:
self.ddf_ave = self.ddf_ave + ddf_new
self.ddf_ave_cont += 1
if "save" in mode:
if self.ddf_ave_cont == 0:
raise ValueError("No data to save. Please run 'add' mode first.")
ux = (self.ddf_ave[:, :, 1] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 8] - self.ddf_ave[:, :, 3] - self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 7]) / U0 / self.ddf_ave_cont
uy = (self.ddf_ave[:, :, 2] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 4] - self.ddf_ave[:, :, 7] - self.ddf_ave[:, :, 8]) / U0 / self.ddf_ave_cont
flag_plot = self.flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)
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")
print(f"Average field amount: {self.ddf_ave_cont}")
if "clear" in mode:
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,207 @@
# 这个是imit圆柱的env,用于产生d1a3o14_250525_imit系列模型的目标流场
# 跟随模型,要匹配圆柱直径和采样频率
# 模型名中L代表目标直径,S代表SAMPLE_INTERVAL
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 14, 3
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 600 # 这里会随着圆柱尺寸和粘性变动,在模型名中体现
FIFO_LEN = 150
CONV_LEN = 36
MAX_STEPS = 500
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 8), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_cd = 0.0
self.reward_cl = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
center: Tuple[float, float, float] = (20 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, 1*L0) # 这里会随着圆柱尺寸变动,在模型名中体现
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(4, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()[0:8]
self.target_states = np.vstack((self.target_states, new_state))
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
self.target_harmonics = analyze_harmonics(self.target_states, n_harmonics=5)
self.flow_field.get_ddf()
self.flow_field.save_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(4, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
run_flow_field(action)
truncated = False
observation = np.zeros(14, dtype=DATA_TYPE)
self.current_step += 1
# done = self.current_step >= MAX_STEPS
done = False
return observation, float(0), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def average_field(self, mode=["add", "save", "clear"], filename="average_field.dat"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_new = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
if "add" in mode:
self.ddf_ave = self.ddf_ave + ddf_new
self.ddf_ave_cont += 1
if "save" in mode:
if self.ddf_ave_cont == 0:
raise ValueError("No data to save. Please run 'add' mode first.")
ux = (self.ddf_ave[:, :, 1] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 8] - self.ddf_ave[:, :, 3] - self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 7]) / U0 / self.ddf_ave_cont
uy = (self.ddf_ave[:, :, 2] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 4] - self.ddf_ave[:, :, 7] - self.ddf_ave[:, :, 8]) / U0 / self.ddf_ave_cont
flag_plot = self.flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)
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")
print(f"Average field amount: {self.ddf_ave_cont}")
if "clear" in mode:
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,270 @@
# 这个是Karman_cloak_standard的env,用于训练和评估d1a3o12_re系列模型和250326模型
# 上游一个2D扰流圆柱,目标是pinball后流场跟无pinball一致
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 12, 3
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 800
FIFO_LEN = 150
CONV_LEN = 30
MAX_STEPS = 500
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 6), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_cd = 0.0
self.reward_cl = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(4, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()[2:8]
self.target_states = np.vstack((self.target_states, new_state))
# self.flow_field.apply_ddf()
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
self.flow_field.run(int(4*NX/U0), np.zeros(7, dtype=DATA_TYPE))
self.flow_field.get_ddf()
self.flow_field.save_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(7, dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
temp_states = np.array(self.fifo_states)
self.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))
for i in range(6):
self.sens_deviation[i] = np.mean(temp_states[:, i])
self.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - self.sens_deviation[i]))
self.flow_field.apply_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.array([0.0, 0.0, 0.0, 0.0, 0.0, -4*U0, 4*U0], dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
self.save_states = self.fifo_states.copy()
self.flow_field.apply_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(7, dtype=DATA_TYPE)
temp[4:7] = np.array((action*8+[0,-4,4])*U0, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
def proc_data():
states = np.array(self.fifo_states)
forces = states[-1, 6:12] / self.force_norm_fact
cd = (forces[0] + forces[2] + forces[4]) / 3
cl = (forces[1] + forces[3] + forces[5]) / 3
sens = (states[-1, 0:6] - self.sens_deviation) / self.sens_norm_fact
similarities = 0.0
def calc_lag(target, state):
target_mean = np.mean(target)
state_mean = np.mean(state)
correlation = np.correlate(target - target_mean, state - state_mean, "full")
lags = np.arange(-len(target) + 1, len(target))
max_lag = lags[np.argmax(correlation)]
return max_lag
def calc_sim(target, state):
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(target[i - 1] - state[j - 1])
last_min = min(dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1])
dtw_matrix[i, j] = cost + last_min
return 1 - (dtw_matrix[n, m] / len(target))
id_sens = 1
target_seq = self.target_states[CONV_LEN:2*CONV_LEN, id_sens]
state_seq = states[-CONV_LEN:, id_sens]
lag = calc_lag(target_seq, state_seq)
for i in range(0, 6):
target_seq = np.roll(self.target_states[:, i], -lag)[CONV_LEN:2*CONV_LEN]
state_seq = states[-CONV_LEN:, i]
similarities += calc_sim(target_seq, state_seq) / 6
self.reward_cd = np.exp(-np.abs(cd * 20))
self.reward_cl = np.exp(-np.abs(cl * 80))
self.reward_sim = np.exp(-10*np.abs(similarities - 1))
reward = np.minimum(0.3 * self.reward_cd + 0.4 * self.reward_cl + 0.3 * self.reward_sim, 1.0)
result_queue.put((np.hstack([forces, sens]), reward))
run_flow_field(action)
proc_data()
observation, reward = result_queue.get()
truncated = bool(np.any(observation > 1) or np.any(observation < -1))
observation = np.clip(observation, -1, 1)
self.current_step += 1
# done = self.current_step >= MAX_STEPS
done = False
return observation, float(reward), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.fifo_states = self.save_states.copy()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def average_field(self, mode=["add", "save", "clear"], filename="average_field.dat"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_new = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
if "add" in mode:
self.ddf_ave = self.ddf_ave + ddf_new
self.ddf_ave_cont += 1
if "save" in mode:
if self.ddf_ave_cont == 0:
raise ValueError("No data to save. Please run 'add' mode first.")
ux = (self.ddf_ave[:, :, 1] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 8] - self.ddf_ave[:, :, 3] - self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 7]) / U0 / self.ddf_ave_cont
uy = (self.ddf_ave[:, :, 2] + self.ddf_ave[:, :, 5] + self.ddf_ave[:, :, 6] - self.ddf_ave[:, :, 4] - self.ddf_ave[:, :, 7] - self.ddf_ave[:, :, 8]) / U0 / self.ddf_ave_cont
flag_plot = self.flow_field.flag.copy().reshape((NY, NX)).transpose(1, 0)
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")
print(f"Average field amount: {self.ddf_ave_cont}")
if "clear" in mode:
self.ddf_ave = np.zeros((NX, NY, 9), dtype=DATA_TYPE)
self.ddf_ave_cont = 0
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,245 @@
# 这个是reduce_obs的env,用于训练和评估d1a3o12_250421系列模型
# 上游扰流圆柱,场景与Karman_cloak_standard一致
# obs从12逐渐减少至2,观察模型是否能够适应,具体观察量在模型名中体现
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 3, 3 # 这里会随着obs数量变动,在模型名中体现
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 800
FIFO_LEN = 150
CONV_LEN = 36
MAX_STEPS = 500
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 6), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.torque_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_cd = 0.0
self.reward_cl = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(4*NX/U0), np.zeros(4, dtype=DATA_TYPE))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(4, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()[2:8]
self.target_states = np.vstack((self.target_states, new_state))
# self.flow_field.apply_ddf()
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
self.flow_field.run(int(4*NX/U0), np.zeros(7, dtype=DATA_TYPE))
self.flow_field.get_ddf()
self.flow_field.save_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(7, dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
temp_states = np.array(self.fifo_states)
self.force_norm_fact = 10 * np.max(np.abs(temp_states[:, 6:12]))
temp_torque = -temp_states[:, 1] - temp_states[:, 2]*np.sqrt(3)/2 + temp_states[:, 3]/2 + temp_states[:, 4]*np.sqrt(3)/2 + temp_states[:, 5]/2
self.torque_norm_fact = 10 * np.max(np.abs(temp_torque))
for i in range(6):
self.sens_deviation[i] = np.mean(temp_states[:, i])
self.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - self.sens_deviation[i]))
self.flow_field.apply_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.array([0.0, 0.0, 0.0, 0.0, 0.0, -4*U0, 4*U0], dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
self.save_states = self.fifo_states.copy()
self.flow_field.apply_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(7, dtype=DATA_TYPE)
temp[4:7] = np.array((action*8+[0,-4,4])*U0, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
self.fifo_states.append(self.flow_field.obs.copy()[2:14])
def proc_data():
states = np.array(self.fifo_states)
forces = states[-1, 6:12] / self.force_norm_fact
obs_torque = (-states[-1, 1] - states[-1, 2]*np.sqrt(3)/2 + states[-1, 3]/2 + states[-1, 4]*np.sqrt(3)/2 + states[-1, 5]/2) / self.torque_norm_fact
obs_drag = (forces[0] + forces[2] + forces[4]) / 3
obs_lift = (forces[1] + forces[3] + forces[5]) / 3
sens = (states[-1, 0:6] - self.sens_deviation) / self.sens_norm_fact
similarities = 0.0
def calc_lag(target, state):
target_mean = np.mean(target)
state_mean = np.mean(state)
correlation = np.correlate(target - target_mean, state - state_mean, "full")
lags = np.arange(-len(target) + 1, len(target))
max_lag = lags[np.argmax(correlation)]
return max_lag
def calc_sim(target, state):
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(target[i - 1] - state[j - 1])
last_min = min(dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1])
dtw_matrix[i, j] = cost + last_min
return 1 - (dtw_matrix[n, m] / len(target))
id_sens = 1
target_seq = self.target_states[CONV_LEN:2*CONV_LEN, id_sens]
state_seq = states[-CONV_LEN:, id_sens]
lag = calc_lag(target_seq, state_seq)
for i in range(0, 6):
target_seq = np.roll(self.target_states[:, i], -lag)[CONV_LEN:2*CONV_LEN]
state_seq = states[-CONV_LEN:, i]
similarities += calc_sim(target_seq, state_seq) / 6
self.reward_cd = np.exp(-np.abs(obs_drag * 20))
self.reward_cl = np.exp(-np.abs(obs_lift * 80))
self.reward_sim = np.exp(-10*np.abs(similarities - 1))
reward = np.minimum(0.3 * self.reward_cd + 0.3 * self.reward_cl + 0.4 * self.reward_sim, 1.0)
result_queue.put((np.hstack([obs_torque, forces[0:2]]), reward)) # 这里会随着obs数量变动,在模型名中体现
run_flow_field(action)
proc_data()
observation, reward = result_queue.get()
truncated = bool(np.any(observation > 1) or np.any(observation < -1))
observation = np.clip(observation, -1, 1)
self.current_step += 1
# done = self.current_step >= MAX_STEPS
done = False
return observation, float(reward), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.fifo_states = self.save_states.copy()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,237 @@
# 这个是vortex的env,用于训练和评估vortex_taylor和vortex_lamb模型
# 上游干净来流,目标是vortex流过的时序信号于无pinball情况一致
# vortes系列模型都基于d1a3o12_re100模型迁移训练
import gymnasium as gym
import numpy as np
from gymnasium import spaces
import ctypes
from collections import deque
from typing import Tuple
import sys
import os
import matplotlib.pyplot as plt
import queue
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
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
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")
)
S_DIM, A_DIM = 12, 3
U0 = config_field.velocity
T0 = 1000
SAMPLE_INTERVAL = 800
FIFO_LEN = 150
CONV_LEN = 30
MAX_STEPS = 150
if config_field.data_type == "FP32":
DATA_TYPE = np.float32
else:
raise ValueError(f"Unsupported data type {config_field.data_type}.")
class CustomEnv(gym.Env):
"""Custom Environment that follows gym interface."""
metadata = {"render_modes": ["human"], "render_fps": T0 / SAMPLE_INTERVAL}
def __init__(self, device_id=0):
super().__init__()
self.action_space = spaces.Box(low=-1, high=1, shape=(A_DIM,), dtype=DATA_TYPE)
self.observation_space = spaces.Box(
low=-1, high=1, shape=(S_DIM,), dtype=DATA_TYPE
)
self.fifo_states = deque(maxlen=FIFO_LEN)
self.target_states = np.empty((0, 6), dtype=DATA_TYPE)
self.force_norm_fact = 1.0
self.sens_norm_fact = np.ones(6, dtype=DATA_TYPE)
self.sens_deviation = np.zeros(6, dtype=DATA_TYPE)
self.reward_cd = 0.0
self.reward_cl = 0.0
self.reward_sim = 0.0
self.current_step = 0
self.flow_field = FlowField(config_field, config_cuda, device_id)
L0 = 20
U0 = config_field.velocity
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 + 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2, 0)
self.flow_field.add_sensor(center, L0 / 4)
center: Tuple[float, float, float] = (40 * L0, (NY - 1) / 2 - 2 * L0, 0)
self.flow_field.add_sensor(center, L0 / 4)
self.flow_field.run(int(1*NX/U0), np.zeros(3, dtype=DATA_TYPE))
self.flow_field.get_ddf()
self.flow_field.save_ddf()
center: Tuple[float, float, float] = (10 * L0, (NY - 1) / 2, 0)
# self.flow_field.add_vortex(center, L0 * 2, 0.5*U0, 0, "lamb")
self.flow_field.add_vortex(center, L0 * 2, 0.03*U0, 0, "taylor") # 这里会更改vortex类型,在模型名中体现
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(3, dtype=DATA_TYPE))
new_state = self.flow_field.obs.copy()
self.target_states = np.vstack((self.target_states, new_state))
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
center: Tuple[float, float, float] = (30 * L0, (NY - 1) / 2, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 + 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
center: Tuple[float, float, float] = (31.3 * L0, (NY - 1) / 2 - 0.75 * L0, 0)
self.flow_field.add_cylinder(center, L0 / 2)
self.flow_field.run(int(1*NX/U0), np.zeros(6, dtype=DATA_TYPE))
self.flow_field.run(int(1*NX/U0), np.array([0.0, 0.0, 0.0, 0.0, -4*U0, 4*U0], dtype=DATA_TYPE))
# self.flow_field.get_ddf()
# self.flow_field.save_ddf()
center: Tuple[float, float, float] = (15 * L0, (NY - 1) / 2, 0)
# self.flow_field.add_vortex(center, L0 * 2, 0.5*U0, 0, "lamb")
self.flow_field.add_vortex(center, L0 * 2, 0.03*U0, 0, "taylor")
self.flow_field.save_ddf()
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.zeros(6, dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy())
# self.save_states = self.fifo_states.copy()
self.flow_field.apply_ddf()
temp_states = np.array(self.fifo_states)
self.force_norm_fact = 6 * np.max(np.abs(temp_states[:, 6:12]))
for i in range(6):
self.sens_deviation[i] = np.mean(temp_states[:, i])
self.sens_norm_fact[i] = 5 * np.max(np.abs(temp_states[:, i] - self.sens_deviation[i]))
for i in range(FIFO_LEN):
self.flow_field.run(SAMPLE_INTERVAL, np.array([0.0, 0.0, 0.0, 0.0, -4*U0, 4*U0], dtype=DATA_TYPE))
self.fifo_states.append(self.flow_field.obs.copy())
self.save_states = self.fifo_states.copy()
self.flow_field.apply_ddf()
def step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid" % (
action,
type(action),
)
# barrier = threading.Barrier(2)
result_queue = queue.Queue()
def run_flow_field(action):
self.flow_field.context.push()
U0 = config_field.velocity
try:
temp = np.zeros(6, dtype=DATA_TYPE)
temp[3:6] = np.array((action*4+[0,-4,4])*U0, dtype=DATA_TYPE)
self.flow_field.run(SAMPLE_INTERVAL, temp)
finally:
self.flow_field.context.pop()
# barrier.wait()
self.fifo_states.append(self.flow_field.obs.copy())
def proc_data():
states = np.array(self.fifo_states)
forces = states[-1, 6:12] / self.force_norm_fact
cd = (forces[0] + forces[2] + forces[4]) / 3
cl = (forces[1] + forces[3] + forces[5]) / 3
sens = (states[-1, 0:6] - self.sens_deviation) / self.sens_norm_fact
similarities = 0.0
def calc_sim(target, state):
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(target[i - 1] - state[j - 1])
last_min = min(dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1])
dtw_matrix[i, j] = cost + last_min
return 1 - (dtw_matrix[n, m] / len(target))
for i in range(0, 6):
target_seq = np.roll(self.target_states[-CONV_LEN:, i], -self.current_step-1)
state_seq = states[-CONV_LEN:, i]
similarities += calc_sim(target_seq, state_seq) / 6
self.reward_cd = np.exp(-np.abs(cd * 20))
self.reward_cl = np.exp(-np.abs(cl * 80))
self.reward_sim = np.exp(-10*np.abs(similarities - 1))
reward = np.minimum(0.2 * self.reward_cd + 0.3 * self.reward_cl + 0.5 * self.reward_sim, 1.0)
# barrier.wait()
result_queue.put((np.hstack([forces, sens]), reward))
run_flow_field(action)
proc_data()
observation, reward = result_queue.get()
truncated = bool(np.any(observation > 1) or np.any(observation < -1))
observation = np.clip(observation, -1, 1)
self.current_step += 1
done = self.current_step >= MAX_STEPS
return observation, float(reward), done, truncated, {}
def reset(self, seed=None):
self.flow_field.restore_ddf()
self.flow_field.apply_ddf()
self.fifo_states = self.save_states.copy()
self.current_step = 0
return np.zeros(S_DIM, dtype=np.float32), {}
def render(self, mode="human"):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 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
speed = np.sqrt(ux**2 + uy**2)
plt.figure(figsize=(10, 5))
plt.imshow(speed.T, origin='lower', cmap='viridis', extent=[0, NX, 0, NY])
plt.colorbar(label='Speed')
plt.title('Scalar Velocity Field')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()
def save_field(self, filename):
NX = self.flow_field.FIELD_SHAPE[0]
NY = self.flow_field.FIELD_SHAPE[1]
self.flow_field.get_ddf()
ddf_plot = self.flow_field.ddf.copy().reshape((9, NY, NX)).transpose(2, 1, 0)
flag_plot = self.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")
def close(self):
self.flow_field.__del__()
@@ -0,0 +1,518 @@
# drl_pinball/legacy_env/legacy_karman_env.py
"""
Standalone Karman cloak re100 environment using LegacyCelerisLab.
This module exactly reproduces env_karman_cloak_standard.py but exposes
ALL intermediate data for validation against the new CelerisLab API.
Usage::
from legacy_karman_env import legacy_build_re100, legacy_infer_re100
data = legacy_build_re100(device_id=0)
# data['target_states'], data['norm'], data['flow_field'], ...
results = legacy_infer_re100(data['flow_field'], model, data['target_states'], data['norm'], n_steps=50)
# results['sensors'], results['forces'], results['obs'], results['actions'], results['rewards']
"""
from __future__ import annotations
import os
import sys
from collections import deque
from typing import Any, Dict, Optional, Tuple
import numpy as np
# Add repo root for LegacyCelerisLab import
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
if _REPO not in sys.path:
sys.path.insert(0, _REPO)
from LegacyCelerisLab import FlowField # noqa: E402
from LegacyCelerisLab import utils as legacy_utils # noqa: E402
# ---------------------------------------------------------------------------
# Constants — matching env_karman_cloak_standard.py EXACTLY
# ---------------------------------------------------------------------------
CONFIG_DIR = os.path.join(_REPO, "configs", "legacy_configs")
S_DIM, A_DIM = 12, 3
U0 = 0.01
T0 = 1000
SAMPLE_INTERVAL = 800
FIFO_LEN = 150
CONV_LEN = 30
MAX_STEPS = 500
DATA_TYPE = np.float32
ACTION_SMOOTH_WEIGHT = 0.1 # legacy run() uses internal exponential smoothing
# Geometry constants (in lattice units)
L0 = 20.0
# Disturbance cylinder (id=0 in env order)
DIST_CENTER = (10.0 * L0, None, 0.0) # y will be set at runtime
DIST_RADIUS = 1.0 * L0
# Sensors (ids=1,2,3)
SENSOR_RADIUS = L0 / 4.0
# Pinball cylinders (ids=4,5,6)
PINBALL_RADIUS = L0 / 2.0
FRONT_CENTER = (30.0 * L0, None, 0.0) # y = CENTER_Y
BOTTOM_CENTER = (31.3 * L0, None, 0.0) # y = CENTER_Y - 0.75*L0
TOP_CENTER = (31.3 * L0, None, 0.0) # y = CENTER_Y + 0.75*L0
def _center_y(ff: FlowField) -> float:
"""Return the center y-coordinate of the flow field."""
return (ff.FIELD_SHAPE[1] - 1) / 2.0
def _fill_y(cfg: Tuple[float, Optional[float], float], cy: float) -> tuple:
"""Replace None y with actual center y."""
x, y, z = cfg
if y is None:
y = cy
return (x, y, z)
# ---------------------------------------------------------------------------
# Phase 1a: Build reference dataset (reproduces env.__init__)
# ---------------------------------------------------------------------------
def legacy_build_re100(
device_id: int = 0,
viscosity: Optional[float] = None,
) -> Dict[str, Any]:
"""Reproduce env_karman_cloak_standard.__init__() exactly.
Parameters
----------
device_id : int
GPU device ID.
viscosity : float, optional
Override viscosity (default: 0.004 for Re=100).
Returns
-------
dict with keys:
flow_field : FlowField — the CFD instance (state at end of init)
target_states : ndarray (FIFO_LEN, 6) — target sensor signals
norm : dict with force_norm_fact, sens_deviation, sens_norm_fact
config : dict with all runtime parameters
fifo_after_bias : deque — FIFO state after bias rollout
"""
# Default viscosity for Re=100 (code Re, using 2D reference)
if viscosity is None:
viscosity = 0.004
# Load legacy configs
cuda_cfg = legacy_utils.load_cuda_config(
os.path.join(CONFIG_DIR, "config_cuda.json")
)
field_cfg = legacy_utils.load_flow_field_config(
os.path.join(CONFIG_DIR, "config_flowfield.json")
)
# Override viscosity
field_cfg = field_cfg._replace(viscosity=float(viscosity))
# -- Step 0: Create FlowField ------------------------------------------
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
cy = _center_y(ff)
NX = ff.FIELD_SHAPE[0]
NY = ff.FIELD_SHAPE[1]
# -- Step 1: Add disturbance cylinder + sensors -------------------------
# Order matters for obs indexing: dist_cyl(0), sensor0(1), sensor1(2), sensor2(3)
ff.add_cylinder(_fill_y(DIST_CENTER, cy), DIST_RADIUS)
for y_off in [2.0, 0.0, -2.0]:
sc = (40.0 * L0, cy + y_off * L0, 0.0)
ff.add_sensor(sc, SENSOR_RADIUS)
n_obj_phase1 = ff.obs.size // 2 # 4 objects
assert n_obj_phase1 == 4, f"Expected 4 objects after sensors, got {n_obj_phase1}"
# -- Step 2: Stabilize --------------------------------------------------
stabilize_steps = int(4 * NX / U0)
ff.run(stabilize_steps, np.zeros(n_obj_phase1, dtype=DATA_TYPE))
# -- Step 3: Record target signals --------------------------------------
target_states = np.empty((0, 6), dtype=DATA_TYPE)
for _ in range(FIFO_LEN):
ff.run(SAMPLE_INTERVAL, np.zeros(n_obj_phase1, dtype=DATA_TYPE))
new_state = ff.obs.copy()[2:8] # sensors only skip dist_cyl
target_states = np.vstack((target_states, new_state))
# -- Step 4: Add pinball cylinders (ids=4,5,6) -------------------------
ff.add_cylinder(_fill_y(FRONT_CENTER, cy), PINBALL_RADIUS)
ff.add_cylinder(_fill_y(BOTTOM_CENTER, cy - 0.75 * L0), PINBALL_RADIUS)
ff.add_cylinder(_fill_y(TOP_CENTER, cy + 0.75 * L0), PINBALL_RADIUS)
n_obj_total = ff.obs.size // 2 # 7 objects
assert n_obj_total == 7, f"Expected 7 objects, got {n_obj_total}"
# -- Step 5: Stabilize with pinball -------------------------------------
ff.run(stabilize_steps, np.zeros(n_obj_total, dtype=DATA_TYPE))
# -- Step 6: Checkpoint DDF (steady pinball + disturbance state) --------
ff.get_ddf()
ff.save_ddf()
# -- Step 7: Zero-action norm collection --------------------------------
fifo = deque(maxlen=FIFO_LEN)
for _ in range(FIFO_LEN):
ff.run(SAMPLE_INTERVAL, np.zeros(n_obj_total, dtype=DATA_TYPE))
fifo.append(ff.obs.copy()[2:14]) # sensor[6] + force[6]
temp_states = np.array(fifo, dtype=DATA_TYPE)
force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, 6:12])))
sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(DATA_TYPE)
sens_norm_fact = np.zeros(6, dtype=DATA_TYPE)
for i in range(6):
sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i])))
# -- Step 8: Bias-action rollout (for FIFO init in controlled runs) -----
ff.apply_ddf() # restore pre-bias state
# Action bias: front=0, bottom=-4*U0, top=4*U0
bias_arr = np.zeros(n_obj_total, dtype=DATA_TYPE)
bias_arr[n_obj_total - 3] = float((0.0 * 8.0 + 0.0) * U0) # front = 0
bias_arr[n_obj_total - 2] = float((0.0 * 8.0 + (-4.0)) * U0) # bottom = -4*U0
bias_arr[n_obj_total - 1] = float((0.0 * 8.0 + 4.0) * U0) # top = 4*U0
fifo.clear()
for _ in range(FIFO_LEN):
ff.run(SAMPLE_INTERVAL, bias_arr)
fifo.append(ff.obs.copy()[2:14])
save_states = np.array(list(fifo), dtype=DATA_TYPE)
# -- Step 9: Restore to steady state (ready for reset) ------------------
ff.apply_ddf()
norm = {
"force_norm_fact": force_norm_fact,
"sens_deviation": sens_deviation.tolist(),
"sens_norm_fact": sens_norm_fact.tolist(),
"save_states": save_states,
"action_bias": [0.0, -4.0, 4.0],
"n_obj_total": n_obj_total,
}
config = {
"device_id": device_id,
"viscosity": viscosity,
"u0": U0,
"sample_interval": SAMPLE_INTERVAL,
"fifo_len": FIFO_LEN,
"conv_len": CONV_LEN,
"nx": NX,
"ny": NY,
"n_obj_total": n_obj_total,
"action_scale": 8.0,
"action_bias": [0.0, -4.0, 4.0],
}
return {
"flow_field": ff,
"target_states": target_states,
"norm": norm,
"config": config,
"fifo_after_bias": fifo,
}
# ---------------------------------------------------------------------------
# Phase 1b: Inference — reproduces env.step() exactly
# ---------------------------------------------------------------------------
def legacy_infer_re100(
flow_field: FlowField,
model: Any,
target_states: np.ndarray,
norm: Dict[str, Any],
n_steps: int = 50,
*,
use_deterministic: bool = True,
) -> Dict[str, np.ndarray]:
"""Run Karman cloak re100 controlled inference with legacy CFD.
This follows the exact pattern in env_karman_cloak_standard.step() and
analysis_crossre/scripts/phase1_infer.py.
Parameters
----------
flow_field : FlowField
Initialized flow field (must be in steady pinball state).
model : PPO
Trained PPO model (with Sin activation).
target_states : ndarray (FIFO_LEN, 6)
Target sensor signals.
norm : dict
Normalization factors from legacy_build_re100().
n_steps : int
Number of inference steps (each = SAMPLE_INTERVAL LBM steps).
use_deterministic : bool
Use deterministic action (True) or stochastic (False).
Returns
-------
dict with keys:
sensors : (n_steps, 6) raw sensor velocities
forces : (n_steps, 6) raw forces (all 6 force components)
obs : (n_steps, 12) normalised DRL observations
actions : (n_steps, 3) normalised DRL actions in [-1, 1]
rewards : (n_steps,) step rewards
reward_cd : (n_steps,)
reward_cl : (n_steps,)
reward_sim : (n_steps,)
similarities : (n_steps,) DTW similarity scores
"""
n_obj_total = norm.get("n_obj_total", 7)
action_scale = 8.0
action_bias = np.array(norm.get("action_bias", [0.0, -4.0, 4.0]), dtype=np.float32)
force_norm_fact = float(norm["force_norm_fact"])
sens_deviation = np.array(norm["sens_deviation"], dtype=np.float32)
sens_norm_fact = np.array(norm["sens_norm_fact"], dtype=np.float32)
# Restore steady state
flow_field.restore_ddf()
flow_field.apply_ddf()
# Bias-action FIFO init (reproduces env.__init__ bias rollout)
fifo = deque(maxlen=FIFO_LEN)
bias_arr = np.zeros(n_obj_total, dtype=DATA_TYPE)
bias_arr[n_obj_total - 3] = float(action_bias[0] * U0)
bias_arr[n_obj_total - 2] = float(action_bias[1] * U0)
bias_arr[n_obj_total - 1] = float(action_bias[2] * U0)
for _ in range(FIFO_LEN):
flow_field.run(SAMPLE_INTERVAL, bias_arr)
fifo.append(flow_field.obs.copy()[2:14])
# Inference loop
sens_list, forc_list, obs_list = [], [], []
action_list, reward_list = [], []
reward_cd_list, reward_cl_list, reward_sim_list = [], [], []
sim_list = []
obs = np.zeros(S_DIM, dtype=np.float32)
for step in range(n_steps):
# --- PPO action ---
action, _states = model.predict(obs, deterministic=use_deterministic)
action = action.astype(np.float32).flatten()
action_list.append(action.copy())
# --- Convert to legacy action array ---
action_arr = np.zeros(n_obj_total, dtype=DATA_TYPE)
omega = (action * action_scale + action_bias) * U0
action_arr[n_obj_total - 3:] = omega
# --- Run CFD ---
# Context management: push/pop to avoid PyTorch CUDA context conflicts
flow_field.context.push()
try:
flow_field.run(SAMPLE_INTERVAL, action_arr)
finally:
flow_field.context.pop()
# --- Read telemetry ---
obs_slice = flow_field.obs.copy()[2:14]
fifo.append(obs_slice)
sens_list.append(obs_slice[0:6].copy())
forc_list.append(obs_slice[6:12].copy())
# --- Build normalised observation ---
forces_norm = obs_slice[6:12] / force_norm_fact
sens_norm = (obs_slice[0:6] - sens_deviation) / sens_norm_fact
obs = np.clip(np.hstack([forces_norm, sens_norm]), -1.0, 1.0).astype(np.float32)
obs_list.append(obs)
# --- Compute reward (exactly matching env logic) ---
states_arr = np.array(fifo, dtype=np.float32)
if len(states_arr) >= CONV_LEN:
forces = states_arr[-1, 6:12] / force_norm_fact
cd = float((forces[0] + forces[2] + forces[4]) / 3.0)
cl = float((forces[1] + forces[3] + forces[5]) / 3.0)
# Similarity = lag-compensated DTW over all 6 sensor channels
# calc_lag on middle sensor (index 1 = sensor1_uy)
target_seq = target_states[CONV_LEN:2 * CONV_LEN, 1]
state_seq = states_arr[-CONV_LEN:, 1]
lag = _calc_lag(target_seq, state_seq)
sim_sum = 0.0
for i in range(6):
t_seq = np.roll(target_states[:, i], -lag)[CONV_LEN:2 * CONV_LEN]
s_seq = states_arr[-CONV_LEN:, i]
sim_sum += _calc_dtw_sim(t_seq, s_seq) / 6.0
similarities = float(sim_sum)
sim_list.append(similarities)
r_cd = float(np.exp(-abs(cd * 20.0)))
r_cl = float(np.exp(-abs(cl * 80.0)))
r_sim = float(np.exp(-10.0 * abs(similarities - 1.0)))
reward = float(min(0.3 * r_cd + 0.4 * r_cl + 0.3 * r_sim, 1.0))
else:
reward = 0.0
r_cd = 0.0
r_cl = 0.0
r_sim = 0.0
similarities = 0.0
reward_list.append(reward)
reward_cd_list.append(r_cd)
reward_cl_list.append(r_cl)
reward_sim_list.append(r_sim)
return {
"sensors": np.array(sens_list, dtype=np.float32),
"forces": np.array(forc_list, dtype=np.float32),
"obs": np.array(obs_list, dtype=np.float32),
"actions": np.array(action_list, dtype=np.float32),
"rewards": np.array(reward_list, dtype=np.float32),
"reward_cd": np.array(reward_cd_list, dtype=np.float32),
"reward_cl": np.array(reward_cl_list, dtype=np.float32),
"reward_sim": np.array(reward_sim_list, dtype=np.float32),
"similarities": np.array(sim_list, dtype=np.float32) if sim_list else np.zeros(n_steps, dtype=np.float32),
}
# ---------------------------------------------------------------------------
# Helper: uncontrolled rollout (zero action)
# ---------------------------------------------------------------------------
def legacy_uncontrolled_re100(
flow_field: FlowField,
n_steps: int = 50,
) -> Dict[str, np.ndarray]:
"""Run uncontrolled Karman cloak re100 inference.
Parameters
----------
flow_field : FlowField
Must be in saved DDF state (steady pinball + disturbance).
n_steps : int
Number of steps.
Returns
-------
dict with sensors, forces.
"""
n_obj_total = 7
flow_field.restore_ddf()
flow_field.apply_ddf()
sens_list, forc_list = [], []
for _ in range(n_steps):
flow_field.run(SAMPLE_INTERVAL, np.zeros(n_obj_total, dtype=DATA_TYPE))
obs_slice = flow_field.obs.copy()[2:14]
sens_list.append(obs_slice[0:6].copy())
forc_list.append(obs_slice[6:12].copy())
return {
"sensors": np.array(sens_list, dtype=np.float32),
"forces": np.array(forc_list, dtype=np.float32),
}
# ---------------------------------------------------------------------------
# DTW helpers (exact copies from env_karman_cloak_standard.py)
# ---------------------------------------------------------------------------
def _calc_lag(target: np.ndarray, state: np.ndarray) -> int:
target_mean = float(np.mean(target))
state_mean = float(np.mean(state))
correlation = np.correlate(
target - target_mean,
state - state_mean,
mode="full",
)
lags = np.arange(-len(target) + 1, len(target))
return int(lags[np.argmax(correlation)])
def _calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float:
n = len(target)
m = len(state)
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0.0
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = abs(float(target[i - 1]) - float(state[j - 1]))
last_min = min(
dtw_matrix[i - 1, j],
dtw_matrix[i, j - 1],
dtw_matrix[i - 1, j - 1],
)
dtw_matrix[i, j] = cost + last_min
return float(1.0 - dtw_matrix[n, m] / n)
# ---------------------------------------------------------------------------
# CLI entry point for reference dataset generation
# ---------------------------------------------------------------------------
def main():
"""Generate reference dataset for Karman re100 validation."""
import argparse
import json
ap = argparse.ArgumentParser(description="Legacy Karman re100 reference dataset")
ap.add_argument("--device", type=int, default=0, help="GPU device ID")
ap.add_argument("--out", type=str, default="output/validate_re100", help="Output directory")
args = ap.parse_args()
out_dir = os.path.abspath(args.out)
os.makedirs(out_dir, exist_ok=True)
print("=== Building legacy Karman re100 reference dataset ===")
print(f"Output: {out_dir}")
# Build env
data = legacy_build_re100(device_id=args.device)
ff = data["flow_field"]
# Save target and norm
np.savez(os.path.join(out_dir, "target.npz"),
target_states=data["target_states"])
norm_json = {
"force_norm_fact": float(data["norm"]["force_norm_fact"]),
"sens_deviation": list(float(x) for x in data["norm"]["sens_deviation"]),
"sens_norm_fact": list(float(x) for x in data["norm"]["sens_norm_fact"]),
"action_bias": data["norm"]["action_bias"],
}
with open(os.path.join(out_dir, "norm.json"), "w") as f:
json.dump(norm_json, f, indent=2)
np.savez(os.path.join(out_dir, "save_states.npz"),
save_states=data["norm"]["save_states"])
# Save config
with open(os.path.join(out_dir, "config.json"), "w") as f:
json.dump({k: str(v) if isinstance(v, (np.integer, np.floating)) else v
for k, v in data["config"].items()}, f, indent=2)
# Uncontrolled rollout
print(" uncontrolled rollout (50 steps)...")
unc = legacy_uncontrolled_re100(ff, n_steps=50)
np.savez(os.path.join(out_dir, "uncontrolled.npz"),
sensors=unc["sensors"], forces=unc["forces"])
print(" Reference dataset saved.")
print(f" force_norm_fact = {norm_json['force_norm_fact']:.6f}")
print(f" sens_deviation = {norm_json['sens_deviation']}")
print(f" sens_norm_fact = {norm_json['sens_norm_fact']}")
# Cleanup
del ff
print("Done.")
if __name__ == "__main__":
main()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+79
View File
@@ -0,0 +1,79 @@
# 训练d1a3o12_250729_250326_erase系列模型
# 上游扰流圆柱,场景与Karman_cloak_standard一致,
# 但是目标是希望pinball后流场跟入口一致,即抹除扰流圆柱尾迹
# 模型名中D代表信号延迟
# erase模型类似re100,其余模型基于erase模型迁移训练
import os
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "8"
os.environ["MKL_NUM_THREADS"] = "8"
import torch
import numpy as np
from torch.nn import Module
import gymnasium as gym
from legacy_env.legacy_env_erase import CustomEnv
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.vec_env import DummyVecEnv
from sb3_contrib import RecurrentPPO
from torch.utils.tensorboard import SummaryWriter
import pickle
current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
if __name__ == '__main__':
vec_env = CustomEnv(device_id=2)
name = "d1a3o12_250729_250326_erase_250804_20D_retrain2"
model = PPO.load(os.path.join(parent_dir, "models", "250729", "d1a3o12_250729_250326_erase_250804_20D.zip"), env=vec_env, device=torch.device("cuda:2"))
# model = PPO(
# "MlpPolicy",
# policy_kwargs=dict(activation_fn=Sin),
# env=vec_env,
# device=torch.device("cuda:1"),
# # n_steps=3000,
# # batch_size=300,
# verbose=0)
writer = SummaryWriter(log_dir=os.path.join(parent_dir, "tensorboard", name))
max_reward = 0
history_data = []
for i in range(500):
model.learn(total_timesteps=400)
test_env = model.get_env()
test_obs = test_env.reset()
list_reward = []
episolde_data = {'actions': [], 'observations': [], 'rewards': []}
for step in range(200):
test_action, _states = model.predict(observation=test_obs)
test_obs, test_rewards, test_dones, info = test_env.step(test_action)
list_reward.append(test_rewards)
episolde_data['actions'].append(test_action[0, :])
episolde_data['observations'].append(np.array(test_obs))
episolde_data['rewards'].append(test_rewards)
history_data.append(episolde_data)
avg_reward = np.mean(list_reward[-100:])
writer.add_scalar('Reward', np.mean(avg_reward), i)
if avg_reward > max_reward:
max_reward = avg_reward
model.save(os.path.join(parent_dir, "models", "250729", name + ".zip"))
# if i % 10 == 0:
# model.save(os.path.join(parent_dir, "models", "250329", name + f"_{i}.zip"))
# with open(os.path.join(parent_dir, "output", name + ".pkl"), 'wb') as f:
# pickle.dump(history_data, f)
+77
View File
@@ -0,0 +1,77 @@
# 训练d1a3o14_250525_imit系列模型
# 上游干净来流,目标是pinball后流场跟设定尺寸圆柱一致
# 模型名中L代表目标直径,S代表SAMPLE_INTERVAL
import os
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "8"
os.environ["MKL_NUM_THREADS"] = "8"
import torch
import numpy as np
from torch.nn import Module
import gymnasium as gym
from legacy_env.legacy_env_imit import CustomEnv
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.vec_env import DummyVecEnv
from sb3_contrib import RecurrentPPO
from torch.utils.tensorboard import SummaryWriter
import pickle
current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
if __name__ == '__main__':
vec_env = CustomEnv(device_id=1)
name = "d1a3o14_250525_imit_1L_2U_1000S_08Vis"
model = PPO.load(os.path.join(parent_dir, "models", "250525", "d1a3o14_250525_imit_1L_2U_600S"), env=vec_env, device=torch.device("cuda:1"))
# model = PPO(
# "MlpPolicy",
# policy_kwargs=dict(activation_fn=Sin),
# env=vec_env,
# device=torch.device("cuda:2"),
# # n_steps=3000,
# # batch_size=300,
# verbose=0)
writer = SummaryWriter(log_dir=os.path.join(parent_dir, "tensorboard", name))
max_reward = 0
history_data = []
for i in range(500):
model.learn(total_timesteps=400)
test_env = model.get_env()
test_obs = test_env.reset()
list_reward = []
episolde_data = {'actions': [], 'observations': [], 'rewards': []}
for step in range(300):
test_action, _states = model.predict(observation=test_obs)
test_obs, test_rewards, test_dones, info = test_env.step(test_action)
list_reward.append(test_rewards)
episolde_data['actions'].append(test_action[0, :])
episolde_data['observations'].append(np.array(test_obs))
episolde_data['rewards'].append(test_rewards)
history_data.append(episolde_data)
avg_reward = np.mean(list_reward[-100:])
writer.add_scalar('Reward', np.mean(avg_reward), i)
if avg_reward > max_reward:
max_reward = avg_reward
model.save(os.path.join(parent_dir, "models", "250525", name + ".zip"))
# if i % 10 == 0:
# model.save(os.path.join(parent_dir, "models", "250421", name + f"_{i}.zip"))
with open(os.path.join(parent_dir, "output", name + ".pkl"), 'wb') as f:
pickle.dump(history_data, f)
@@ -0,0 +1,77 @@
# 训练d1a3o12_250326模型,用于训练和评估d1a3o12_re系列模型和250326模型
# re100应等同于250326模型,不同雷诺数使用不同粘性实现
# re系列都基于re100模型迁移训练,250326模型直接训练
import os
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "8"
os.environ["MKL_NUM_THREADS"] = "8"
import torch
import numpy as np
from torch.nn import Module
import gymnasium as gym
from legacy_env.legacy_env_karman_cloak_standard import CustomEnv
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.vec_env import DummyVecEnv
from sb3_contrib import RecurrentPPO
from torch.utils.tensorboard import SummaryWriter
import pickle
current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
if __name__ == '__main__':
vec_env = CustomEnv(device_id=3)
name = "d1a3o12_250326"
# model = PPO.load(os.path.join(parent_dir, "models", "d1a3o12_c0"), env=vec_env, device=torch.device("cuda:1"))
model = PPO(
"MlpPolicy",
policy_kwargs=dict(activation_fn=Sin),
env=vec_env,
device=torch.device("cuda:3"),
# n_steps=3000,
# batch_size=300,
verbose=0)
writer = SummaryWriter(log_dir=os.path.join(parent_dir, "tensorboard", name))
max_reward = 0
history_data = []
for i in range(500):
model.learn(total_timesteps=360)
test_env = model.get_env()
test_obs = test_env.reset()
list_reward = []
episolde_data = {'actions': [], 'observations': [], 'rewards': []}
for step in range(360):
test_action, _states = model.predict(observation=test_obs)
test_obs, test_rewards, test_dones, info = test_env.step(test_action)
list_reward.append(test_rewards)
episolde_data['actions'].append(test_action[0, :])
episolde_data['observations'].append(np.array(test_obs))
episolde_data['rewards'].append(test_rewards)
history_data.append(episolde_data)
avg_reward = np.mean(list_reward[-180:])
writer.add_scalar('Reward', np.mean(avg_reward), i)
if avg_reward > max_reward:
max_reward = avg_reward
model.save(os.path.join(parent_dir, "models", "250326", name + ".zip"))
# if i % 10 == 0:
# model.save(os.path.join(parent_dir, "models", "250326", name + f"_{i}.zip"))
with open(os.path.join(parent_dir, "output", name + ".pkl"), 'wb') as f:
pickle.dump(history_data, f)
@@ -0,0 +1,77 @@
# 训练d1a3o12_250421系列模型
# 上游扰流圆柱,场景与Karman_cloak_standard一致
# obs从12逐渐减少至2,观察模型是否能够适应,具体观察量在模型名中体现
import os
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "8"
os.environ["MKL_NUM_THREADS"] = "8"
import torch
import numpy as np
from torch.nn import Module
import gymnasium as gym
from legacy_env.legacy_env_reduce_obs import CustomEnv
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.vec_env import DummyVecEnv
from sb3_contrib import RecurrentPPO
from torch.utils.tensorboard import SummaryWriter
import pickle
current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
if __name__ == '__main__':
vec_env = CustomEnv(device_id=3)
name = "d1a3o12_250421_total_force"
# model = PPO.load(os.path.join(parent_dir, "models", "d1a3o12_c0"), env=vec_env, device=torch.device("cuda:1"))
model = PPO(
"MlpPolicy",
policy_kwargs=dict(activation_fn=Sin),
env=vec_env,
device=torch.device("cuda:3"),
# n_steps=3000,
# batch_size=300,
verbose=0)
writer = SummaryWriter(log_dir=os.path.join(parent_dir, "tensorboard", name))
max_reward = 0
history_data = []
for i in range(500):
model.learn(total_timesteps=400)
test_env = model.get_env()
test_obs = test_env.reset()
list_reward = []
episolde_data = {'actions': [], 'observations': [], 'rewards': []}
for step in range(300):
test_action, _states = model.predict(observation=test_obs)
test_obs, test_rewards, test_dones, info = test_env.step(test_action)
list_reward.append(test_rewards)
episolde_data['actions'].append(test_action[0, :])
episolde_data['observations'].append(np.array(test_obs))
episolde_data['rewards'].append(test_rewards)
history_data.append(episolde_data)
avg_reward = np.mean(list_reward[-100:])
writer.add_scalar('Reward', np.mean(avg_reward), i)
if avg_reward > max_reward:
max_reward = avg_reward
model.save(os.path.join(parent_dir, "models", "250421", name + ".zip"))
# if i % 10 == 0:
# model.save(os.path.join(parent_dir, "models", "250421", name + f"_{i}.zip"))
with open(os.path.join(parent_dir, "output", name + ".pkl"), 'wb') as f:
pickle.dump(history_data, f)
+75
View File
@@ -0,0 +1,75 @@
# 训练vortex模型,基于d1a3o12_re100模型,训练vortex_taylor和vortex_lamb模型
# 上游干净来流,目标是vortex流过的时序信号于无pinball情况一致
# vortes系列模型都基于d1a3o12_re100模型迁移训练
import os
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "16"
os.environ["MKL_NUM_THREADS"] = "16"
import torch
import numpy as np
from torch.nn import Module
import gymnasium as gym
from legacy_env.legacy_env_vortex import CustomEnv
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.vec_env import DummyVecEnv
from sb3_contrib import RecurrentPPO
from torch.utils.tensorboard import SummaryWriter
import pickle
current_dir = os.path.dirname(os.path.abspath("__file__"))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
class Sin(Module):
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
if __name__ == '__main__':
vec_env = CustomEnv(device_id=3)
name = "vortex_taylor"
model = PPO.load(os.path.join(parent_dir, "models", "d1a3o12_re100"), env=vec_env, device=torch.device("cuda:3"))
# model = PPO(
# "MlpPolicy",
# policy_kwargs=dict(activation_fn=Sin),
# env=vec_env,
# device=torch.device("cuda:3"),
# n_steps=3600,
# batch_size=360,
# verbose=0)
writer = SummaryWriter(log_dir=os.path.join(parent_dir, "tensorboard", name))
max_reward = 0
history_data = []
for i in range(100):
model.learn(total_timesteps=1500)
test_env = model.get_env()
test_obs = test_env.reset()
list_reward = []
# episolde_data = {'actions': [], 'observations': [], 'rewards': []}
for step in range(150):
test_action, _states = model.predict(observation=test_obs)
test_obs, test_rewards, test_dones, info = test_env.step(test_action)
list_reward.append(test_rewards)
# episolde_data['actions'].append(test_action[0, :])
# episolde_data['observations'].append(np.array(test_obs))
# episolde_data['rewards'].append(test_rewards)
# history_data.append(episolde_data)
avg_reward = np.mean(list_reward[-130:])
writer.add_scalar('Reward', np.mean(avg_reward), i)
if avg_reward > max_reward:
max_reward = avg_reward
model.save(os.path.join(parent_dir, "models", name + ".zip"))
# with open(os.path.join(parent_dir, "output", name + ".pkl"), 'wb') as f:
# pickle.dump(history_data, f)