# 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.*
