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:
Frank14fandCursor committed 2026-06-21 00:50:20 +08:00
1 parent 7a609b2c76
commit 04c2bc75ea
10 files changed
+322 -58

No files matched your search

+6 -7
View File
@@ -65,10 +65,8 @@ def test_sensor_accuracy() -> dict:
# Get macroscopic field after one more step (with sensor accumulation)
import pycuda.driver as cuda
stream = cuda.Stream()
sim.bodies.zero_sensor_segment_async(stream)
sim.stepper.step(1, action_gpu=sim.bodies.action_gpu,
obs_gpu=sim.bodies.obs_gpu, stream=stream)
stream.synchronize()
sim.run(1, zero_obs=True, upload_act=False, sync_obs=True, stream=stream)
# stream.synchronize() is called inside run()
macro = sim.get_macroscopic()
ux = macro["ux"]
@@ -76,7 +74,8 @@ def test_sensor_accuracy() -> dict:
results = {}
all_pass = True
for sid in sensor_ids:
for idx, sid in enumerate(sensor_ids):
pos = positions[idx]
cells_arr, _ = sim.bodies.get(sid).get_sensor_list(
sim.lbm_cfg.nx, sim.lbm_cfg.ny
)
@@ -97,7 +96,7 @@ def test_sensor_accuracy() -> dict:
if not passed:
all_pass = False
results[f"sensor_{sid}_pos{positions[i]}"] = {
results[f"sensor_{sid}_pos{pos}"] = {
"sensor_reading": [sensor_reading_x, sensor_reading_y],
"manual_average": [sensor_ux_mean, sensor_uy_mean],
"diff": [float(diff_ux), float(diff_uy)],
@@ -106,7 +105,7 @@ def test_sensor_accuracy() -> dict:
}
status = "PASS" if passed else "FAIL"
print(
f" Sensor {sid} @ {positions[sid]}: "
f" Sensor {sid} @ {pos}: "
f"reading=({sensor_reading_x:.8f},{sensor_reading_y:.8f}) "
f"manual=({sensor_ux_mean:.8f},{sensor_uy_mean:.8f}) "
f"diff=({diff_ux:.2e},{diff_uy:.2e}) "