第一轮分析工作暂存
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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.*
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user