MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SymbolicTrajectory.hpp
Go to the documentation of this file.
1#pragma once
2
5
6namespace MayaFlux::Kinesis {
7
8/**
9 * @class SymbolicTrajectory
10 * @brief Tracks a moving point's sequence of cells through a lattice
11 * partition over time.
12 *
13 * Named for symbolic dynamics: a continuous trajectory, observed through
14 * a partition of its space into discrete cells, becomes a sequence of
15 * symbols, one per cell occupied at each observation. This class is that
16 * observation process, holding the memory a single Lattice2D::cell_at()
17 * or Lattice3D::cell_at() call does not: which cell the point occupied
18 * last, how many times it has crossed a cell boundary, and a short
19 * window of recently visited cells for measuring the shape of that
20 * crossing sequence.
21 *
22 * Distinct from SpatialIndex, which tracks the current positions of
23 * many entities for neighbor queries and has no notion of history, and
24 * from Lattice2D/Lattice3D themselves, which are pure static geometry
25 * with no notion of a point moving through them at all. SymbolicTrajectory
26 * adds no new geometry: cell containment is entirely LatticeT::cell_at().
27 * It adds only memory of what cell_at() has returned over time for one
28 * moving point.
29 *
30 * @tparam LatticeT Lattice2D or Lattice3D
31 * @tparam CellT The corresponding cell coordinate type: glm::uvec2 for
32 * Lattice2D, glm::uvec3 for Lattice3D. Not deduced automatically
33 * since LatticeT does not expose its own coordinate type as a
34 * nested alias; specify explicitly at the call site.
35 *
36 * ```cpp
37 * SymbolicTrajectory<Lattice2D, glm::uvec2> traj(Lattice2D::ndc_quadrants(), 16);
38 * for (auto pos : incoming_positions) {
39 * traj.update(pos);
40 * }
41 * size_t crossings = traj.crossing_count();
42 * size_t distinct = traj.unique_cells_in_window(8);
43 * ```
44 */
45template <typename LatticeT, typename CellT>
47public:
48 /**
49 * @brief Construct a trajectory over a given lattice
50 * @param lattice The partition to observe the incoming positions through
51 * @param window Number of recent cell observations to retain for
52 * windowed queries (unique_cells_in_window, crossings_in_window,
53 * dominant_cell). Minimum 2.
54 */
55 explicit SymbolicTrajectory(LatticeT lattice, size_t window = 16)
57 , m_history(window < 2 ? 2 : window)
58 {
59 }
60
61 /**
62 * @brief Observe one new position, updating the cell sequence
63 * @tparam PositionT glm::vec2 for a Lattice2D-backed trajectory,
64 * glm::vec3 for a Lattice3D-backed trajectory; must match
65 * what LatticeT::cell_at accepts
66 * @param position Position in the same coordinate space as the lattice
67 * @return true if this observation crossed into a different cell
68 * than the previous observation, false if it stayed in the
69 * same cell or this is the first observation
70 */
71 template <typename PositionT>
72 bool update(const PositionT& position)
73 {
74 const CellT cell = m_lattice.cell_at(position);
75 m_history.push(cell);
76
77 if (!m_has_prior) {
78 m_has_prior = true;
79 m_current_cell = cell;
80 m_dwell = 1;
81 return false;
82 }
83
84 const bool crossed = !(cell == m_current_cell);
85 if (crossed) {
87 m_current_cell = cell;
88 m_dwell = 1;
89 } else {
90 ++m_dwell;
91 }
92 return crossed;
93 }
94
95 /**
96 * @brief Cell the most recent observation fell in
97 * @pre At least one update() call has happened
98 */
99 [[nodiscard]] const CellT& current_cell() const { return m_current_cell; }
100
101 /**
102 * @brief Total crossings observed since construction or reset()
103 *
104 * A crossing is any observation whose cell differs from the
105 * immediately preceding observation's cell, regardless of whether
106 * the sequence later returns to a previously visited cell.
107 */
108 [[nodiscard]] size_t crossing_count() const { return m_crossing_count; }
109
110 /**
111 * @brief Consecutive observations that have fallen in the current cell
112 *
113 * Counts how long the trajectory has held its present symbol without
114 * crossing. crossings_in_window answers how much movement there has
115 * been recently; this answers how settled the point is right now,
116 * which is what distinguishes resting inside a cell from passing
117 * through it. Reset to one on every crossing, since the observation
118 * that crossed is the first one in the new cell.
119 */
120 [[nodiscard]] size_t dwell_count() const { return m_dwell; }
121
122 /**
123 * @brief Crossings within the most recent @p window observations
124 * @param window Number of recent observations to examine, clamped
125 * to the trajectory's retained history capacity
126 * @return Count of adjacent-pair differences within the window
127 *
128 * Distinct from crossing_count(): this only looks at the retained
129 * window, so it answers "how much boundary-crossing has happened
130 * recently" rather than "how much has happened ever."
131 */
132 [[nodiscard]] size_t crossings_in_window(size_t window) const
133 {
134 window = std::min(window, m_history.capacity());
135 if (window < 2)
136 return 0;
137
138 const auto view = m_history.linearized_view();
139 size_t crossings = 0;
140 for (size_t i = 0; i + 1 < window; ++i) {
141 if (!(view[i] == view[i + 1]))
142 ++crossings;
143 }
144 return crossings;
145 }
146
147 /**
148 * @brief Count of distinct cells visited within the most recent
149 * @p window observations
150 * @param window Number of recent observations to examine, clamped
151 * to the trajectory's retained history capacity
152 * @return Number of unique cells, 1 if the window never left one
153 * cell, up to window if every observation was a new cell
154 *
155 * Distinguishes a trajectory pacing back and forth between two
156 * cells (high crossings_in_window, low unique_cells_in_window) from
157 * one sweeping steadily through new territory (both high).
158 */
159 [[nodiscard]] size_t unique_cells_in_window(size_t window) const
160 {
161 window = std::min(window, m_history.capacity());
162 if (window == 0)
163 return 0;
164
165 const auto view = m_history.linearized_view();
166 std::vector<CellT> seen;
167 seen.reserve(window);
168 for (size_t i = 0; i < window; ++i) {
169 bool found = false;
170 for (const auto& s : seen) {
171 if (s == view[i]) {
172 found = true;
173 break;
174 }
175 }
176 if (!found)
177 seen.push_back(view[i]);
178 }
179 return seen.size();
180 }
181
182 /**
183 * @brief Cell with the most observations within the most recent
184 * @p window observations
185 * @param window Number of recent observations to examine, clamped
186 * to the trajectory's retained history capacity
187 * @return The most-occupied cell in the window, and how many of the
188 * window's observations fell in it
189 *
190 * Ties resolve to whichever qualifying cell appears first in the
191 * scan, which is the most recent one among ties since the
192 * underlying HistoryBuffer is newest-first; not documented as a
193 * guarantee beyond "deterministic," since which specific tie-break
194 * rule matters is a caller decision this does not presume to make.
195 */
196 [[nodiscard]] std::pair<CellT, size_t> dominant_cell(size_t window) const
197 {
198 window = std::min(window, m_history.capacity());
199 const auto view = m_history.linearized_view();
200
201 CellT best {};
202 size_t best_count = 0;
203 for (size_t i = 0; i < window; ++i) {
204 size_t count = 0;
205 for (size_t j = 0; j < window; ++j) {
206 if (view[j] == view[i])
207 ++count;
208 }
209 if (count > best_count) {
210 best_count = count;
211 best = view[i];
212 }
213 }
214 return { best, best_count };
215 }
216
217 /**
218 * @brief Reset to uninitialized state
219 *
220 * Call on a known discontinuity (the tracked point teleporting
221 * rather than moving continuously) so the next update() does not
222 * count a crossing against a now-meaningless prior cell.
223 */
224 void reset()
225 {
227 m_current_cell = CellT {};
228 m_has_prior = false;
230 m_dwell = 0;
231 }
232
233 /**
234 * @brief The lattice this trajectory observes positions through
235 */
236 [[nodiscard]] const LatticeT& lattice() const { return m_lattice; }
237
238private:
239 LatticeT m_lattice;
242 bool m_has_prior { false };
243 size_t m_crossing_count { 0 };
244 size_t m_dwell { 0 };
245};
246
247} // namespace MayaFlux::Kinesis
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
std::weak_ptr< Core::Window > window
Definition Picker.cpp:23
size_t count
const LatticeT & lattice() const
The lattice this trajectory observes positions through.
size_t crossings_in_window(size_t window) const
Crossings within the most recent window observations.
bool update(const PositionT &position)
Observe one new position, updating the cell sequence.
Memory::HistoryBuffer< CellT > m_history
SymbolicTrajectory(LatticeT lattice, size_t window=16)
Construct a trajectory over a given lattice.
const CellT & current_cell() const
Cell the most recent observation fell in.
size_t unique_cells_in_window(size_t window) const
Count of distinct cells visited within the most recent window observations.
size_t crossing_count() const
Total crossings observed since construction or reset()
std::pair< CellT, size_t > dominant_cell(size_t window) const
Cell with the most observations within the most recent window observations.
size_t dwell_count() const
Consecutive observations that have fallen in the current cell.
void reset()
Reset to uninitialized state.
Tracks a moving point's sequence of cells through a lattice partition over time.
std::span< T > linearized_view()
Get mutable linearized view of entire history.
void push(const T &value)
Push new value to front of history.
void reset()
Reset buffer to initial state (all zeros)
size_t capacity() const
Get buffer capacity.
History buffer for difference equations and recursive relations.