A protected frame is on screen, so it anchors the walk
`domain/select`: the choosing half of a selection, pure and handed a plain vector by each site. `pose/held-frame` was already the shared reading half; nothing chose automatically at all. The greedy walk rather than the DP, deliberately — it is what the prototype shipped, and having two implementations is how the DP gets tested. Several tests pin its exact output for that comparison and will need rewriting when it lands. Protection is settled before the walk and is not unioned onto its result, because a protected frame anchors the walk like any other kept frame. The anchor is what is on screen, so measuring the next frame's drift from a frame that is no longer displayed holds a one-frame closure across two frames and shows it twice as long as it was measured. The test that says so is about the picture rather than the algorithm: the frames the mouth was measured shut on and the frames the selection shows it shut on are the same list. A strict local extremum compares against the nearest DIFFERING samples, not the immediate neighbours, and a run of equal samples is one extremum at the frame it begins. Immediate neighbours find nothing at all on a closure lasting more than one frame, which is most of them. One width for the whole signal, so `distance` is never comparing the prefix two samples happen to share; a non-finite tolerance falls back to nought, since NaN compares false against everything and would quietly collapse a performance to a single pose. THE EXTREMA HALF HAS NO CALLER under the plan as it now stands, and this is the commit to revert if it stays that way: `segments`, `turns`, the `:extrema` branch of `propose` and the multi-component refusal that only guards it, plus the three tests over them. Plate selections take no extrema by design, and the performance half gets its closures from the `[:vis]` cut `flow/freeze` already stores, so nothing asks this code for anything. It is correct and tested and speculative; docs/frame-selection.md says so beside the build order. Also corrects `ring/subsample-slots`, which claimed every even budget lands on the cardinal positions. The corners do; the lip centres survive only multiples of 4, so the aperture pair cannot be read off a subsampled ring at verts 6, 10, 14 or 18. That is why the performance signal reads a stored cut instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
14673385d4
commit
02069e88f0
3 changed files with 413 additions and 3 deletions
213
frontend/test/arthur/domain/select_test.cljs
Normal file
213
frontend/test/arthur/domain/select_test.cljs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
(ns arthur.domain.select-test
|
||||
"The one-frame closure is the feature, so it is the first test in the file.
|
||||
|
||||
Both the cadence and a selection are sets of native frames read the same way —
|
||||
hold the latest one at or before now — so they are compared through the same
|
||||
reading function over the same native frames. Nothing here goes near a store,
|
||||
a clip or an output grid: `select` is handed a vector of numbers."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[clojure.set :as set]
|
||||
[arthur.domain.cadence :as cadence]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.domain.select :as select]))
|
||||
|
||||
;; A mouth over 30 native frames that is fully shut for exactly ONE of them.
|
||||
;;
|
||||
;; It is already nearly shut either side of frame 13 — the aperture wobbles
|
||||
;; between 0.015 and 0.03 — and at 13 it reaches 0. That is the shape of the
|
||||
;; case a walk cannot see on displacement alone: the closure moves the signal by
|
||||
;; 0.015, less than any tolerance worth using on the talking either side of it,
|
||||
;; while being the only frame that reads as a closed mouth.
|
||||
(def aperture
|
||||
[0.30 0.30 0.26 0.20 0.14 0.10 0.07 0.04 0.03 0.025 0.015 0.015
|
||||
0.03 0.0 0.03
|
||||
0.06 0.09 0.15 0.22 0.30 0.36 0.40 0.43 0.43 0.40 0.36 0.30 0.24 0.18 0.12])
|
||||
|
||||
(def closure 13)
|
||||
(def tol 0.02)
|
||||
|
||||
(defn held
|
||||
"The sample `signal` shows at native frame `f` when read through a selection of
|
||||
native `frames`. This is the existing shared reading half, not a second one."
|
||||
[signal frames f]
|
||||
(nth signal (pose/held-frame (mapv #(vector % %) frames) f (first frames))))
|
||||
|
||||
(defn shut
|
||||
"The frames on which `signal` read through the selection `frames` shows a fully
|
||||
shut mouth. With `frames` nil it is the frames the mouth was MEASURED shut on,
|
||||
which is what a selection is answerable to: the picture should show a closure
|
||||
for exactly as long as it happened, and no longer."
|
||||
[signal frames]
|
||||
(filterv #(zero? (if frames (held signal frames %) (nth signal %)))
|
||||
(range (count signal))))
|
||||
|
||||
(deftest a-one-frame-closure-survives-only-because-it-is-protected
|
||||
(let [grid (mapv #(cadence/frame % 12 30) (range 12))
|
||||
walk (select/propose 30 aperture {:tolerance tol})
|
||||
kept (select/propose 30 aperture {:tolerance tol :protect :extrema})]
|
||||
(testing "a uniform 12-from-30 cadence drops it"
|
||||
(is (= [0 2 5 7 10 12 15 17 20 22 25 27] grid))
|
||||
(is (not (some #{closure} grid)))
|
||||
;; Not one of the twelve frames a cadence keeps is a shut mouth, and
|
||||
;; holding over them never reaches one either: at the closure it is still
|
||||
;; showing frame 12, which is open.
|
||||
(is (= 0.03 (held aperture grid closure)))
|
||||
(is (empty? (shut aperture grid))))
|
||||
(testing "the walk alone drops it too, because displacement is all it sees"
|
||||
(is (= [0 2 3 4 5 6 7 10 15 16 17 18 19 20 21 22 24 25 26 27 28 29] walk))
|
||||
(is (not (some #{closure} walk)))
|
||||
;; It holds frame 10 straight across the closure and on to 15.
|
||||
(is (= 0.015 (held aperture walk closure)))
|
||||
(is (empty? (shut aperture walk))))
|
||||
(testing ":protect :extrema keeps it, for exactly as long as it happened"
|
||||
(is (some #{closure} kept))
|
||||
(is (zero? (held aperture kept closure)))
|
||||
(is (= [closure] (shut aperture nil) (shut aperture kept)))
|
||||
;; "At or before", so it does not show early either.
|
||||
(is (= 0.015 (held aperture kept 12))))
|
||||
(testing "the protected frame anchors the walk, so the reopening is kept too"
|
||||
;; Frame 14 is the mouth coming back open. Without it the closure at 13
|
||||
;; would hold across 14 and read twice as long as it was measured, which
|
||||
;; is why protection is settled before the walk and not unioned onto it.
|
||||
(is (= #{closure 14} (set/difference (set kept) (set walk))))
|
||||
(is (= 0.03 (held aperture kept 14))))))
|
||||
|
||||
(deftest extrema-are-turns-worth-more-than-the-tolerance
|
||||
;; One shape, two depths, one tolerance. Neither dip moves the signal far
|
||||
;; enough for the walk to keep anything, so what separates them is prominence
|
||||
;; alone — the tolerance doing its job rather than an accident of the data.
|
||||
(testing "a shallow wobble is not a closure"
|
||||
(let [s [0.015 0.025 0.01 0.01 0.025 0.015]]
|
||||
(is (= [0] (select/propose 6 s {:tolerance tol})))
|
||||
(is (= [0] (select/propose 6 s {:tolerance tol :protect :extrema})))))
|
||||
(testing "a two-frame closure is one extremum, at the frame it begins"
|
||||
;; The hold reads from the frame the closure begins on, so naming the second
|
||||
;; frame of the run instead would show it late. Frame 4 is kept because the
|
||||
;; protected frame 2 anchored the walk, and that is what makes the closure
|
||||
;; the same two frames long on screen as it was measured.
|
||||
(let [s [0.015 0.03 0.0 0.0 0.03 0.015]
|
||||
kept (select/propose 6 s {:tolerance tol :protect :extrema})]
|
||||
(is (= [0] (select/propose 6 s {:tolerance tol})))
|
||||
(is (= [0 2 4] kept))
|
||||
(is (= [2 3] (shut s nil) (shut s kept)))))
|
||||
(testing "a monotone ramp has no extrema to protect"
|
||||
(is (= (select/propose 8 [0 1 2 3 4 5 6 7] {:tolerance 2})
|
||||
(select/propose 8 [0 1 2 3 4 5 6 7] {:tolerance 2 :protect :extrema}))))
|
||||
(testing "the ends are not extrema; frame 0 is kept for being the start"
|
||||
(is (= [0] (select/propose 3 [0.0 0.5 1.0] {:tolerance 2 :protect :extrema})))))
|
||||
|
||||
(deftest a-motionless-take-proposes-one-frame
|
||||
(is (= [0] (select/propose 30 (vec (repeat 30 0.2)) {:tolerance tol})))
|
||||
(is (= [0] (select/propose 30 (vec (repeat 30 0.2))
|
||||
{:tolerance tol :protect :extrema})))
|
||||
(is (= [0] (select/propose 30 (vec (repeat 30 [1 2 3 4])) {:tolerance tol})))
|
||||
(is (= [] (select/propose 0 [] {:tolerance tol}))))
|
||||
|
||||
;; A head sliding right at 0.004 stage units a frame, as four transformed
|
||||
;; corners. The tolerance is a distance a person can reason about: "the head has
|
||||
;; moved this far".
|
||||
(def slide
|
||||
(mapv (fn [f] (let [x (* 0.004 f)]
|
||||
[[(- x 0.5) -0.5] [(+ x 0.5) -0.5]
|
||||
[(+ x 0.5) 0.5] [(- x 0.5) 0.5]]))
|
||||
(range 10)))
|
||||
|
||||
(deftest distance-is-the-largest-change-over-components
|
||||
(testing "landmark pairs and a bare number both answer without being declared"
|
||||
(is (= [0 3 6 9] (select/propose 10 slide {:tolerance 0.01})))
|
||||
(is (= (select/propose 10 slide {:tolerance 0.01})
|
||||
(select/propose 10 (mapv flatten slide) {:tolerance 0.01}))))
|
||||
(testing "one component moving is enough"
|
||||
(is (= [0 2] (select/propose 3 [[0 0] [0 0.5] [0 1]] {:tolerance 0.6})))))
|
||||
|
||||
(deftest a-signal-of-two-shapes-is-a-reader-bug
|
||||
;; Comparing the prefix two samples happen to share would let a reader that
|
||||
;; drops a component on one frame read as no movement at all — a signal that is
|
||||
;; quietly the wrong shape rather than one that says so.
|
||||
(is (thrown? ExceptionInfo
|
||||
(select/propose 3 [[0 0] [0 0 0] [0 0]] {:tolerance 0.01})))
|
||||
(testing "an absent measurement is not a second shape"
|
||||
(is (= [0] (select/propose 3 [[0 0] nil [0 0]] {:tolerance 0.01})))))
|
||||
|
||||
(deftest a-tolerance-that-is-not-a-number-fails-towards-the-cadence
|
||||
;; A slider that has been cleared reads as NaN, and every comparison against
|
||||
;; NaN is false. Taken at face value that proposes frame 0 alone and collapses
|
||||
;; a whole performance to one pose. Nought proposes every frame that changes,
|
||||
;; which is only the cadence back again: visible, and undone by moving the
|
||||
;; slider somewhere.
|
||||
(is (= (select/propose 30 aperture {:tolerance 0})
|
||||
(select/propose 30 aperture {:tolerance js/NaN})
|
||||
(select/propose 30 aperture {})))
|
||||
(is (< 1 (count (select/propose 30 aperture {:tolerance js/NaN})))))
|
||||
|
||||
(deftest extrema-are-refused-on-a-multi-component-signal
|
||||
;; The magnitude of a vector of landmarks has local maxima that mean nothing,
|
||||
;; so asking for them is a mistake rather than a thing to silently ignore.
|
||||
(is (thrown? ExceptionInfo
|
||||
(select/propose 10 slide {:tolerance 0.01 :protect :extrema})))
|
||||
(testing "protected frames are not: a plate selection is fed in this way"
|
||||
;; 6 and 9 give way to 8: frame 5 is on screen from 5 onwards, so that is
|
||||
;; where the head's next 0.01 of travel is measured from.
|
||||
(is (= [0 3 6 9] (select/propose 10 slide {:tolerance 0.01})))
|
||||
(is (= [0 3 5 8] (select/propose 10 slide {:tolerance 0.01 :protect [5]})))))
|
||||
|
||||
(deftest protect-carries-frames-and-extrema-through-one-input
|
||||
;; Named frames and the signal's own extrema arrive by the ONE input, which is
|
||||
;; what lets the plate selection constrain the performance later without
|
||||
;; `propose` growing a second parameter for it.
|
||||
(let [walk (select/propose 30 aperture {:tolerance tol})
|
||||
kept (select/propose 30 aperture {:tolerance tol :protect #{:extrema 8 9}})]
|
||||
(is (every? (set kept) [closure 8 9]))
|
||||
(testing "and both kinds anchor the walk, so neither is a union"
|
||||
;; 10 was kept only because the walk had drifted from 7 by then. Pinning 8
|
||||
;; and 9 re-anchors it, and 10 is within tolerance of 9.
|
||||
(is (some #{10} walk))
|
||||
(is (not (some #{10} kept)))))
|
||||
(testing "a frame outside the signal is not a frame"
|
||||
(is (= [0 5] (select/propose 6 (vec (repeat 6 0.2))
|
||||
{:tolerance tol :protect [5 6 30 -1]})))))
|
||||
|
||||
(deftest the-hand-outranks-the-proposal-in-both-directions
|
||||
(is (= [0 12 30 47] (select/effective [0 12 30] #{47} #{})))
|
||||
(is (= [0 12 47] (select/effective [0 12 30] #{47} #{30})))
|
||||
(testing "a drop beats a proposal, always"
|
||||
(is (= [0] (select/effective [0 4 8] #{} #{4 8}))))
|
||||
(testing "a drop of a frame nobody proposed is harmless"
|
||||
(is (= [0 4] (select/effective [0 4] #{} #{9}))))
|
||||
(testing "something has to be on screen at the start"
|
||||
(is (= [0] (select/effective [] #{} #{})))
|
||||
(is (= [0 4] (select/effective [4] #{} #{0}))))
|
||||
(testing "the result is a frame list in the shape the two sites already store"
|
||||
(let [fs (select/effective [30 0 12] [47 12] #{})]
|
||||
(is (vector? fs))
|
||||
(is (= fs (vec (sort (distinct fs))))))))
|
||||
|
||||
(deftest re-proposing-at-another-tolerance-costs-nobody-their-closure
|
||||
;; Step 4's test belongs with storage, but the half of it that is this
|
||||
;; namespace's is assertable now: the proposal is a value, so holding a hand
|
||||
;; decision across two of them is composition and not state.
|
||||
(let [pinned #{closure} dropped #{2}
|
||||
at (fn [t] (select/effective (select/propose 30 aperture {:tolerance t})
|
||||
pinned dropped))]
|
||||
(doseq [t [0.01 0.02 0.05 0.2]]
|
||||
(is (some #{closure} (at t)) (str "closure survived tolerance " t))
|
||||
(is (not (some #{2} (at t))) (str "drop survived tolerance " t)))
|
||||
(is (not= (at 0.01) (at 0.2)))))
|
||||
|
||||
(deftest an-absent-measurement-is-not-a-change
|
||||
;; A frame where the face was not found says nothing about the signal. It is
|
||||
;; neither a frame worth keeping nor a reason to move the anchor.
|
||||
(let [s (assoc (vec (repeat 10 0.2)) 4 nil 5 nil)]
|
||||
(is (= [0] (select/propose 10 s {:tolerance tol})))
|
||||
(is (= [0] (select/propose 10 s {:tolerance tol :protect :extrema}))))
|
||||
(testing "a signal that starts absent begins where it begins"
|
||||
(is (= [0 3] (select/propose 6 [nil nil nil 0.2 0.2 0.2] {:tolerance tol}))))
|
||||
(testing "a measurement either side of a gap is not compared across it"
|
||||
(is (= [0] (select/propose 5 [0.2 0.2 nil 0.2 0.2]
|
||||
{:tolerance tol :protect :extrema}))))
|
||||
(testing "a protected frame with no measurement is still kept"
|
||||
;; A plate frame is a frame somebody draws on. The face not having been found
|
||||
;; there is a different fact, and not a reason to drop the frame — but there
|
||||
;; is nothing to anchor on either, so the anchor stays where it was.
|
||||
(is (= [0 2] (select/propose 5 [0.2 0.2 nil 0.2 0.2]
|
||||
{:tolerance tol :protect [2]})))))
|
||||
Loading…
Add table
Add a link
Reference in a new issue