feat(obs): unified zero_obs control and time-normalised readback
- Replace split zero_force_segment / zero_sensor_segment with unified zero_obs_async() — a single memset covers all three obs segments (force, torque, sensor), resetting the step accumulator. - Add _obs_accum_steps counter so read_*(normalize=True) returns the physically meaningful per-step average for all telemetry fields. - Sensor now always applies area-normalisation internally; the normalize parameter only controls the additional time-normalisation step. - run() gains zero_obs=True parameter (default) to control reset-on-step. - 7 new integration tests covering accumulation, zeroing, and normalise. - Fix bug in test_sensor_accuracy.py (undefined loop variable i). - Bump version to 0.4.0 for the API change. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7a609b2c76
commit
04c2bc75ea
10 files changed
+322
-58
No files matched your search
@@ -144,26 +144,41 @@ 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, *, upload_act=True, sync_obs=True, stream=None)` | Run N LBM steps. See stream subsection below. |
|
||||
| `sim.run(steps, *, upload_act=True, sync_obs=True, zero_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_body(id, *, normalize=True)` -> 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.read_force(id, *, normalize=True)` -> ndarray | Force vector [fx, fy] |
|
||||
| `sim.read_torque(id, *, normalize=True)` -> ndarray | Torque [tz] |
|
||||
| `sim.read_sensor(id, *, normalize=True)` -> ndarray | Area-averaged velocity; time-normalised when normalize=True |
|
||||
| `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.
|
||||
|
||||
**Obs telemetry model:** GPU kernels accumulate force, torque, and sensor readings into
|
||||
the ``obs_gpu`` buffer via ``atomicAdd``. By default, ``run(zero_obs=True)`` clears the
|
||||
entire ``obs_gpu`` buffer (all three segments) and resets an internal step counter before
|
||||
stepping. After the step group, telemetry is downloaded to a pinned host buffer when
|
||||
``sync_obs=True``.
|
||||
|
||||
All three readback methods accept a ``normalize`` keyword:
|
||||
- ``normalize=True`` (default): divides the raw GPU value by the accumulated step count,
|
||||
yielding a **per-step average** — the physically meaningful quantity for most use cases.
|
||||
- ``normalize=False``: returns the raw GPU-accumulated sum (no time division).
|
||||
|
||||
**Sensor special handling:** Area-normalisation (dividing by the number of sensor cells)
|
||||
is **always applied internally** in ``read_sensor()``, regardless of the ``normalize`` flag.
|
||||
The ``normalize`` parameter only controls the additional time-normalisation step.
|
||||
|
||||
``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.
|
||||
- ``zero_obs`` (default True): Zero all obs segments (force, torque, sensor) on GPU and
|
||||
reset the step accumulator before the step group. Set ``False`` to accumulate
|
||||
telemetry across multiple ``run()`` calls.
|
||||
- ``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.
|
||||
|
||||
@@ -226,7 +241,7 @@ before `run()`, the force will be reset to zero. For the common usage pattern
|
||||
| Type | Flag overlay | Produces cut-links | Readback | Runtime control |
|
||||
|------|-------------|-------------------|----------|-----------------|
|
||||
| `"circle"` | OBSTACLE + BC_CURVED | Yes (Bouzidi) | force/torque | `set_body(id, omega=...)` |
|
||||
| `"sensor"` | FLUID + SENSOR_FLAG | No | area-averaged velocity | None needed |
|
||||
| `"sensor"` | FLUID + SENSOR_FLAG | No | area-averaged velocity (always); optional per-step average | None needed |
|
||||
| `"force_region"` | FLUID + FRC_REGION | No | None | `set_force(id, fx=..., fy=...)` |
|
||||
|
||||
#### Data access
|
||||
@@ -265,7 +280,7 @@ When fine-grained control is needed (e.g., custom async patterns), step manually
|
||||
|
||||
```python
|
||||
stream = cuda.Stream()
|
||||
sim.bodies.zero_force_segment_async(stream)
|
||||
sim.bodies.zero_obs_async(stream)
|
||||
sim.stepper.step(
|
||||
1,
|
||||
action_gpu=sim.bodies.action_gpu,
|
||||
@@ -273,7 +288,8 @@ sim.stepper.step(
|
||||
stream=stream,
|
||||
)
|
||||
stream.synchronize()
|
||||
force = sim.read_force(0)
|
||||
sim.bodies.increment_obs_steps(1) # manually track steps for normalize
|
||||
force = sim.read_force(0) # normalize=True: divides by 1 step
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -490,11 +506,20 @@ data = sim.read_body(0)
|
||||
### Async control (performance-oriented)
|
||||
|
||||
```python
|
||||
sim.set_body(0, omega=0.002) # implicit H2D, ~1 μs
|
||||
sim.set_body(0, omega=0.002) # host-only, ~1 μs
|
||||
sim.stepper.step(10, ..., stream=sim.stream)
|
||||
sim.bodies.increment_obs_steps(10) # track steps for normalize
|
||||
sim.bodies.download_obs_full_async(sim.stream)
|
||||
sim.stream.synchronize()
|
||||
force = sim.read_force(0)
|
||||
force = sim.read_force(0) # per-step average force
|
||||
```
|
||||
|
||||
Use ``sim.run()`` for the common case -- it stores the step count automatically:
|
||||
|
||||
```python
|
||||
sim.set_body(0, omega=0.002)
|
||||
sim.run(10, stream=sim.stream)
|
||||
force = sim.read_force(0) # per-step average force
|
||||
```
|
||||
|
||||
## Vortex initialization
|
||||
|
||||
Reference in new issue
Block a user