feat(eval): publish cycle-mean wake acquisition

Add deterministic phase-filtered V5 and Legacy acquisition with complete-cycle mean fields, then evaluate controlled wakes against target and zero baselines offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-08-08 15:50:49 +08:00
co-authored by Cursor
parent b144d62920
commit 61e82ec90a
21 changed files with 5022 additions and 574 deletions
+57
View File
@@ -9,8 +9,11 @@ import numpy as np
import pytest
from drl_pinball.acquisition import (
accumulate_mean_fields,
assign_phase,
assign_periodic_phase,
cleanup_scratch,
complete_cycle_field_indices,
create_scratch,
default_reproduction_mapping,
decode_legacy_physical_velocity,
@@ -20,6 +23,7 @@ from drl_pinball.acquisition import (
publish_role_output,
publish_selected_fields,
select_phase_fields,
smooth_center_uy,
validate_output_storage,
validate_modern_fields,
write_boundary_artifacts,
@@ -184,6 +188,59 @@ def test_selection_is_global_eight_with_metadata_and_index_tie():
assert np.allclose(result["phase_error"], [0.0] * 8, atol=1e-15)
def test_smooth_center_uy_binomial_and_endpoint_copy():
assert np.allclose(smooth_center_uy([1.0, 5.0, 1.0]), [1.0, 3.0, 1.0])
assert np.allclose(smooth_center_uy([2.0, 4.0]), [2.0, 4.0])
def test_assign_phase_default_behavior_unchanged_with_jitter():
times = np.arange(12, dtype=float)
sensors = np.zeros((12, 6))
sensors[:, 3] = [-1, 1, -0.1, 0.1, -1, 1, -1, 1, -1, 1, -1, 1]
result = assign_phase(times, sensors)
assert len(result["crossing_times"]) == 6
assert set(result) == {"phase", "cycle_id", "crossing_times"}
def test_assign_periodic_phase_filters_jitter_with_minimum_interval():
times = np.arange(20, dtype=float)
sensors = np.zeros((20, 6))
# Deep brief negative between rises so smoothing keeps a close false crossing.
sensors[:, 3] = [-2, -2, -1, 1, 2, -3, 1, 2, 2, -2, -1, 1, 2, 2, -2, -1, 1, 2, 2, -2]
raw = assign_phase(times, sensors)
filtered = assign_periodic_phase(times, sensors, minimum_crossing_interval=4.0)
assert len(raw["crossing_times"]) >= 4
assert filtered["rejected_crossing_count"] >= 1
assert 5.75 in filtered["rejected_crossing_times"]
assert filtered["accepted_crossing_count"] == len(filtered["crossing_times"])
assert filtered["complete_cycle_count"] == filtered["accepted_crossing_count"] - 1
assert np.all(np.diff(filtered["crossing_times"]) >= 4.0 - 1e-12)
assert filtered["smoothing_kernel"] == [0.25, 0.5, 0.25]
first, last = filtered["crossing_times"][0], filtered["crossing_times"][-1]
valid = (times >= first) & (times < last)
assert np.all(filtered["cycle_id"][valid] >= 0)
assert np.all(filtered["cycle_id"][~valid] < 0)
def test_complete_cycle_mean_excludes_outside_first_last_crossing():
times = np.arange(10, dtype=float)
crossings = np.array([2.0, 6.0, 9.0])
selected = complete_cycle_field_indices(times, crossings)
assert selected["field_indices"].tolist() == [2, 3, 4, 5, 6, 7, 8]
assert selected["mean_field_count"] == 7
assert selected["complete_cycle_count"] == 2
ux = np.arange(10, dtype=np.float32).reshape(10, 1, 1)
uy = (2 * np.arange(10)).astype(np.float32).reshape(10, 1, 1)
mean = accumulate_mean_fields(ux, uy, selected["field_indices"])
assert mean["mean_ux"].shape == (1, 1)
assert mean["mean_uy"].shape == (1, 1)
assert float(mean["mean_ux"][0, 0]) == pytest.approx(5.0)
assert float(mean["mean_uy"][0, 0]) == pytest.approx(10.0)
# Eight-slot average of selected phase snapshots must remain a different concept.
eight = accumulate_mean_fields(ux, uy, np.array([2, 3, 4, 5, 6, 7, 8, 8]))
assert float(eight["mean_ux"][0, 0]) != pytest.approx(float(mean["mean_ux"][0, 0]))
def test_pooled_32_bins_population_std_and_original_indices():
result = pooled_phase_bins(
np.array([np.pi / 32, 3 * np.pi / 32, np.pi / 32, np.nan]),