feat(body): runtime body add/remove, unified action/obs, FRC_REGION flag
- Add runtime body topology sync (add_body/remove_body + sync_bodies) with recompile, DDF patch (feq + BFS inward fill), and commit. - Unify action/obs flow: set_body/set_force are now host-only; run() auto-uploads action and downloads obs via CUDA stream. - Add read_body(id) -> BodyTelemetry and read_bodies() for DRL loops. - Add FRC_REGION flag (0x0800) for force_region cells. - Extract equilibrium helpers (lbm/equilibrium.py) and DDF patch module (body/ddf_patch.py). - Merge recompile / _runtime_recompile into single _recompile(). - Add n_objects to checkpoint; validate on load. - Add test suite: 40 unit + 19 integration tests (59 total). - Add conftest.py and docs/tests_overview.md for test documentation. - Update README.md and CONFIG.md for new API. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d5b7e98750
commit
987566c0e6
28 files changed
+2112
-86
No files matched your search
@@ -35,37 +35,62 @@ force = sim.read_force(0) # [fx, fy] on body 0
|
||||
sim.close()
|
||||
```
|
||||
|
||||
### Multi-body control loop
|
||||
### DRL control loop
|
||||
|
||||
```python
|
||||
from CelerisLab import Simulation
|
||||
|
||||
sim = Simulation()
|
||||
# Three rotating cylinders
|
||||
sim.add_body("circle", center=(1006, 150), radius=10)
|
||||
sim.add_body("circle", center=(1015, 140), radius=10)
|
||||
sim.add_body("circle", center=(1015, 160), radius=10)
|
||||
# Downstream velocity sensor
|
||||
sim.add_body("sensor", center=(1050, 150), radius=10)
|
||||
sim.add_body("circle", center=(256, 128), radius=10)
|
||||
sim.add_body("sensor", center=(300, 128), radius=10)
|
||||
sim.initialize()
|
||||
|
||||
for step in range(100):
|
||||
# Set body rotation speeds (implicit GPU upload)
|
||||
sim.set_body(0, omega=0.002)
|
||||
sim.set_body(1, omega=-0.001)
|
||||
sim.set_body(2, omega=0.001)
|
||||
for episode in range(100):
|
||||
# Step the simulation (auto uploads action, downloads obs)
|
||||
sim.run(100)
|
||||
|
||||
# Advance 10 LBM steps
|
||||
sim.run(10)
|
||||
# Read individual body telemetry (primary API)
|
||||
data = sim.read_body(0)
|
||||
print(f"step={sim.stepper.step_count} "
|
||||
f"force=({data.force[0]:.4f},{data.force[1]:.4f}) "
|
||||
f"sensor=({data.sensor[0]:.4f},{data.sensor[1]:.4f})")
|
||||
|
||||
# Read telemetry
|
||||
fx, fy = sim.read_force(0)
|
||||
ux, uy = sim.read_sensor(3)
|
||||
print(f"force=({fx:.4f},{fy:.4f}) sensor=({ux:.4f},{uy:.4f})")
|
||||
# DRL policy inference (replace with your model)
|
||||
action_omega = 0.001 * (0.5 - data.force[0])
|
||||
|
||||
# Set action (host-only, will be auto-uploaded next run)
|
||||
sim.set_body(0, omega=action_omega)
|
||||
|
||||
sim.close()
|
||||
```
|
||||
|
||||
### Async control (performance-oriented, custom stream)
|
||||
|
||||
```python
|
||||
import pycuda.driver as cuda
|
||||
|
||||
stream = cuda.Stream()
|
||||
sim.set_body(0, omega=0.002) # host-only
|
||||
sim.run(100, stream=stream) # action uploaded, steps run, obs downloaded on stream
|
||||
# stream is synced inside run() -- obs is ready
|
||||
data = sim.read_body(0)
|
||||
```
|
||||
|
||||
### Manual stream control (max overlap)
|
||||
|
||||
```python
|
||||
import pycuda.driver as cuda
|
||||
|
||||
stream = cuda.Stream()
|
||||
# Skip transfers for the first batch, just enqueue kernels
|
||||
sim.run(100, stream=stream, upload_act=False, sync_obs=False)
|
||||
|
||||
# ... other GPU work can overlap with the kernel launches ...
|
||||
|
||||
# Later: sync and read
|
||||
stream.synchronize()
|
||||
obs = sim.read_bodies(stream=stream) # sync already done, just read pinned buffer
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
@@ -105,10 +130,11 @@ sim = Simulation(
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `sim.add_body(type="circle", center=(x,y), radius=r)` | int body_id | Add a cylinder body |
|
||||
| `sim.add_body(type="circle", center=(x,y), radius=r)` | int body_id | Add a cylinder body (primary API) |
|
||||
| `sim.add_body(type="sensor", center=(x,y), radius=r)` | int body_id | Add a velocity sensor |
|
||||
| `sim.add_cylinder(center, radius)` | int body_id | Backward-compat alias |
|
||||
| `sim.add_sensor(center, radius)` | int body_id | Backward-compat alias |
|
||||
| `sim.add_body(type="force_region", center=(x,y), radius=r)` | int body_id | Add a force application region |
|
||||
| `sim.add_cylinder(center, radius)` | int body_id | Convenience wrapper (deprecated) |
|
||||
| `sim.add_sensor(center, radius)` | int body_id | Convenience wrapper (deprecated) |
|
||||
| `sim.add_object(obj)` | int body_id | Add pre-configured SimObject |
|
||||
|
||||
Future geometry types (polygon, mesh) will use the same `add_body()` function with a different `type` parameter.
|
||||
@@ -118,12 +144,64 @@ Future geometry types (polygon, mesh) will use the same `add_body()` function wi
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `sim.initialize()` | Recompile if needed, flow field + sync objects to GPU |
|
||||
| `sim.run(steps, checkpoint_interval=0)` | Run N LBM steps |
|
||||
| `sim.set_body(id, omega=...)` | Set body rotation speed (implicit GPU upload, ~1 μs) |
|
||||
| `sim.read_force(id)` -> ndarray | Force vector [fx, fy] (2D) |
|
||||
| `sim.read_torque(id)` -> ndarray | Torque [tz] (2D) |
|
||||
| `sim.read_sensor(id)` -> ndarray | Area-averaged velocity via GPU sensor kernel |
|
||||
| `sim.set_force(id, fx=..., fy=...)` | Set force density on a force_region object (notice: see persistence note below) |
|
||||
| `sim.run(steps, *, upload_act=True, sync_obs=True, stream=None)` | Run N LBM steps. See stream subsection below. |
|
||||
| `sim.set_body(id, omega=...)` | Set body rotation speed (host array only, uploaded at next `run()`) |
|
||||
| `sim.read_body(id)` -> BodyTelemetry | Unified telemetry: {force, torque, sensor} from pinned buffer |
|
||||
| `sim.read_bodies()` -> ndarray | Flat array of all bodies' telemetry (batch DRL read) |
|
||||
| `sim.read_force(id)` -> ndarray | Force vector [fx, fy] (backward-compat) |
|
||||
| `sim.read_torque(id)` -> ndarray | Torque [tz] (backward-compat) |
|
||||
| `sim.read_sensor(id)` -> ndarray | Area-averaged velocity (backward-compat) |
|
||||
| `sim.set_force(id, fx=..., fy=...)` | Set force density on a force_region object |
|
||||
|
||||
**Action/obs transfer model:** `set_body()` / `set_force()` are host-only — they modify
|
||||
the host action array without triggering GPU upload. The GPU buffer is automatically
|
||||
updated at the start of the next ``run()`` call when ``upload_act=True`` (the default).
|
||||
Similarly, after the step group, telemetry is downloaded to a pinned host buffer when
|
||||
``sync_obs=True``. Both transfers run on the same CUDA stream as the kernels, so
|
||||
they overlap with computation when possible.
|
||||
|
||||
``run()`` parameters:
|
||||
- ``steps``: Number of LBM steps.
|
||||
- ``upload_act`` (default True): Upload host action array to ``action_gpu`` before stepping.
|
||||
- ``sync_obs`` (default True): Download ``obs_gpu`` to host pinned buffer after stepping.
|
||||
- ``stream`` (default None): CUDA stream for all operations. ``None`` uses an internal stream.
|
||||
- ``checkpoint_interval`` (default 0): If >0, save an HDF5 checkpoint every N steps.
|
||||
|
||||
Use ``upload_act=False, sync_obs=False`` to skip all transfers and enqueue pure
|
||||
kernel launches on a user-provided stream, then sync and read later.
|
||||
|
||||
#### Runtime body topology sync
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `sim.remove_body(id)` | Stage a body for removal (committed at next `sync_bodies()`) |
|
||||
| `sim.sync_bodies()` | Commit pending add/remove edits: recompile kernel, rebuild flags/compact lists, patch DDF, re-upload to GPU |
|
||||
|
||||
`sync_bodies()` applies all staged body edits (added via `add_body()` and removed via `remove_body()`) to a running simulation without full reinitialization. The GPU flow field is preserved; only the body-related topology is rebuilt.
|
||||
|
||||
**Limitations:**
|
||||
- Requires `streaming: "double_buffer"` (esopull raises `NotImplementedError`)
|
||||
- Abrupt body introduction causes a transient; force readback is finite but may take 50+ steps to settle
|
||||
- Verified for `"circle"` type bodies; sensors and force_regions are also expected to work
|
||||
(they produce no curved links so the DDF patch is simpler)
|
||||
|
||||
```python
|
||||
# Add a body to an already-initialized simulation
|
||||
sim = Simulation()
|
||||
sim.initialize()
|
||||
sim.run(500)
|
||||
sim.add_body("circle", center=(256, 128), radius=10)
|
||||
sim.sync_bodies() # recompile + patch
|
||||
sim.run(500)
|
||||
force = sim.read_force(0)
|
||||
|
||||
# Remove the same body at runtime
|
||||
sim.remove_body(0)
|
||||
sim.sync_bodies() # recompile + patch flags/DDF
|
||||
sim.run(500)
|
||||
```
|
||||
|
||||
**Note:** If `run()` is called without a preceding `sync_bodies()`, any staged edits are silently discarded.
|
||||
|
||||
### force_region usage
|
||||
|
||||
@@ -138,7 +216,10 @@ sim.set_force(fr_id, fx=0.001, fy=0.0)
|
||||
sim.set_force(fr_id, fx=0.0, fy=0.0) # disable force
|
||||
```
|
||||
|
||||
**Persistence note:** `set_force()` writes the action buffer directly but does not update the object's state record. If `sync_to_gpu()` is called afterward, the force will be reset to zero. For the common usage pattern (initialize -> set_force -> run -> set_force -> run ...), this is not an issue. A future update will add proper force storage in the object state.
|
||||
**Persistence note:** `set_force()` only updates the host action array. The GPU
|
||||
buffer is synced at the next `run()` call. If `sync_to_gpu()` is called manually
|
||||
before `run()`, the force will be reset to zero. For the common usage pattern
|
||||
(initialize -> set_force -> run -> set_force -> run ...), this is not an issue.
|
||||
|
||||
### Comparison: body types
|
||||
|
||||
@@ -146,7 +227,7 @@ sim.set_force(fr_id, fx=0.0, fy=0.0) # disable force
|
||||
|------|-------------|-------------------|----------|-----------------|
|
||||
| `"circle"` | OBSTACLE + BC_CURVED | Yes (Bouzidi) | force/torque | `set_body(id, omega=...)` |
|
||||
| `"sensor"` | FLUID + SENSOR_FLAG | No | area-averaged velocity | None needed |
|
||||
| `"force_region"` | None (zero mask) | **No** | None | `set_force(id, fx=..., fy=...)` |
|
||||
| `"force_region"` | FLUID + FRC_REGION | No | None | `set_force(id, fx=..., fy=...)` |
|
||||
|
||||
#### Data access
|
||||
|
||||
@@ -296,8 +377,9 @@ Stores `f_i - w_i` instead of `f_i` to improve FP16 accuracy. Supported with th
|
||||
| SRT | double_buffer | any | cylinder | Expected to work (f-feq style) |
|
||||
|
||||
**Known limitations (ddf_shifting):**
|
||||
- Must use `zou_he_local` inlet scheme when combining MRT + shifting
|
||||
- Regularized inlet shows suppressed vortex shedding with MRT -- root cause under investigation
|
||||
- Verified configuration: **D2Q9 + MRT + double_buffer + zou_he_local** only
|
||||
- `regularized` inlet with `ddf_shifting` is **known incompatible / unsolved** -- use `zou_he_local`
|
||||
- `esopull + ddf_shifting` has not been jointly validated
|
||||
- MRT shifts to physical space before collision, shifts back after (SRT/TRT are shift-invariant natively)
|
||||
- D3Q19 MRT shifting patch has a `compute_feq` inconsistency (not in scope for 2D-only)
|
||||
- Host `upload_ddf()` path is asymmetric (repaired)
|
||||
@@ -363,6 +445,8 @@ conda run -n pycuda_3_10 python tests/validation/run_perf_baseline.py \
|
||||
|
||||
## Project Layout
|
||||
|
||||
See [docs/tests_overview.md](docs/tests_overview.md) for a complete guide to the test suite.
|
||||
|
||||
```
|
||||
src/CelerisLab/
|
||||
simulation.py High-level API
|
||||
@@ -400,7 +484,7 @@ ref/ External reference implementations (FluidX3D)
|
||||
```python
|
||||
sim.set_body(0, omega=0.002)
|
||||
sim.run(10)
|
||||
force = sim.read_force(0)
|
||||
data = sim.read_body(0)
|
||||
```
|
||||
|
||||
### Async control (performance-oriented)
|
||||
|
||||
Reference in new issue
Block a user