feat(sim): two-agent hand-off scenario with interaction event and per-frame possession GT - #76
Merged
Conversation
Extend the possession GT sidecar (bamdadd#73) with an InteractionEvent model — frame, time (frame / fps), giver id, receiver id, object id — carried as a sorted, opt-in events list on PossessionTimeline. A two-agent hand-off is the same possession channel with a holder id that changes, plus this interaction record; the byte-golden analytic manifest is untouched. Refs bamdadd#63 Co-Authored-By: Kimi K3 <noreply@kimi.com>
One fluent call stages the full hand-off: the giver carries the object over [start, frame) and the receiver over [frame, end) via the existing attach machinery (object tracks holder + offset on every held frame), and an InteractionEvent is baked into the scene's possession sidecar. The hand-off frame is the single frame where the holder id changes. Refs bamdadd#63 Co-Authored-By: Kimi K3 <noreply@kimi.com>
examples/handoff.py stages two agents on converging-then-diverging waypoint paths (both at the exchange point exactly at the hand-off frame) with one parcel that is unheld, carried by the giver, then carried by the receiver. It writes manifest.json plus the possession.json sidecar (segments + interaction event); the manifest stays byte-golden. Refs bamdadd#63 Co-Authored-By: Kimi K3 <noreply@kimi.com>
Cover the bamdadd#63 acceptance criteria against the constructed scene: two agents converge to the exchange point at the hand-off frame then diverge; the per-frame possession timeline is None -> giver -> receiver with the hand-off frame the single holder-id change; the InteractionEvent records frame, time, giver, receiver, object; the held object tracks its holder + offset geometrically; Scene and sidecar round-trip through JSON losslessly; the analytic manifest never carries the GT; and the example emits valid sidecars. Fixes bamdadd#63 Co-Authored-By: Kimi K3 <noreply@kimi.com>
The _time_non_negative validator only checked value < 0.0, so NaN and +/-inf passed validation (both comparisons are False), serialised to JSON null via model_dump_json, and then failed to reload — a model-admissible value did not round-trip. Add a math.isfinite check (matches the file's explicit-validator conventions; FiniteFloat is not used anywhere in src) and keep the negative-time rejection. frame needs no guard: pydantic rejects non-finite input for int fields natively. Pin it with a test: NaN, +inf and -inf are each rejected at construction, and a finite time still validates and round-trips. Co-Authored-By: Kimi K3 <noreply@kimi.com>
Owner
|
Merged — thank you, this is a really clean contribution. The additive |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stages the two-agent exchange primitive from #63: two agents converge, a carried parcel changes hands at a defined frame, then they diverge — with the interaction event and per-frame possession as opt-in ground truth. Built on the possession sidecar #73 introduced rather than a second representation.
Related Issues and Pull Requests
Fixes #63
Relates to #73
Changes
src/multicam_sim/possession.py— newInteractionEvent(frame, time, giver id, receiver id, object id) carried onPossessionTimeline.events, plusholder_at_frame(...)returning the holder entity id orNonewhile the object is unheld.timerejects negative and non-finite values.src/multicam_sim/dsl/builder.py—SceneBuilder.handoff(...): two agent entities on converging-then-diverging paths and one object entity, with the holder changing at the requested frame.examples/handoff.py— runnable 31-frame scene emitting the manifest plus the possession sidecar.src/multicam_sim/__init__.py— exports the new model.tests/test_handoff.py— 9 tests covering scene shape, converge/diverge monotonicity, the possession timeline, the interaction event, geometry-vs-possession agreement, JSON round-trip, manifest exclusion, and the example's sidecars.On your open design note — new sidecar model vs. an opt-in field — I took neither and extended #73's existing possession channel, so the tree keeps one possession representation instead of two. Happy to split single-carry and hand-off into separate models if you would rather keep them distinct.
Testing
.github/workflows/ci.yml:ruff checkclean,ruff format --checkclean (75 files),mypy srcclean (31 files),pytest284 passed, 4 skipped against an upstreammainbaseline of 275 passed, 4 skipped — the 9 added tests, nothing else moved. Both figures measured with a plainuv sync; syncing--extra overlay --extra overlay-mp4un-skips one test on both sides and the delta is unchanged.builder.pyreverted tomainand the tests kept, all 8 behaviour tests fail onAttributeError: 'SceneBuilder' object has no attribute 'handoff'; they pass once it is restored. The 9th fails withDID NOT RAISE ValidationErrorbefore the non-finite guard.upstream/mainworktree with its own venv and compared withcmp:smoke,pose_smoke,multi_entity,mtmc,assembly_station,hand_dose_response,rasterize_demo— all 7 byte-identical, andtests/fixtures/manifest_golden/is untouched.holder centre + offsetwhile held and to the staging point while unheld, with holder changes exactly[(3, None, giver), (15, giver, receiver)]— one holder-to-holder change, at the hand-off frame.giver == receiver, a hand-off frame at or outside the path bounds, an end beyondnum_frames, unknown object or holder ids, and an overlapping double hand-off of the same object (overlapping possession segments).model_dump(mode="json"),model_dump_json, the sidecarto_json, and write-to-file-and-reload, including awkward finite floats and unicode ids.Follow-ups / Known Limitations
attachsemantics are unchanged, so with non-defaultobject_point/holder_pointthe tracked point is not the centre. That is existing opt-in behaviour, not something this PR alters.Footer
Generated by Claude Opus 5 (brief, review), Kimi K3 (implementation, verification)