MayaFlux 0.5.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 return false;
81 }
82
83 const bool crossed = !(cell == m_current_cell);
84 if (crossed) {
86 m_current_cell = cell;
87 }
88 return crossed;
89 }
90
91 /**
92 * @brief Cell the most recent observation fell in
93 * @pre At least one update() call has happened
94 */
95 [[nodiscard]] const CellT& current_cell() const { return m_current_cell; }
96
97 /**
98 * @brief Total crossings observed since construction or reset()
99 *
100 * A crossing is any observation whose cell differs from the
101 * immediately preceding observation's cell, regardless of whether
102 * the sequence later returns to a previously visited cell.
103 */
104 [[nodiscard]] size_t crossing_count() const { return m_crossing_count; }
105
106 /**
107 * @brief Crossings within the most recent @p window observations
108 * @param window Number of recent observations to examine, clamped
109 * to the trajectory's retained history capacity
110 * @return Count of adjacent-pair differences within the window
111 *
112 * Distinct from crossing_count(): this only looks at the retained
113 * window, so it answers "how much boundary-crossing has happened
114 * recently" rather than "how much has happened ever."
115 */
116 [[nodiscard]] size_t crossings_in_window(size_t window) const
117 {
118 window = std::min(window, m_history.capacity());
119 if (window < 2)
120 return 0;
121
122 const auto view = m_history.linearized_view();
123 size_t crossings = 0;
124 for (size_t i = 0; i + 1 < window; ++i) {
125 if (!(view[i] == view[i + 1]))
126 ++crossings;
127 }
128 return crossings;
129 }
130
131 /**
132 * @brief Count of distinct cells visited within the most recent
133 * @p window observations
134 * @param window Number of recent observations to examine, clamped
135 * to the trajectory's retained history capacity
136 * @return Number of unique cells, 1 if the window never left one
137 * cell, up to window if every observation was a new cell
138 *
139 * Distinguishes a trajectory pacing back and forth between two
140 * cells (high crossings_in_window, low unique_cells_in_window) from
141 * one sweeping steadily through new territory (both high).
142 */
143 [[nodiscard]] size_t unique_cells_in_window(size_t window) const
144 {
145 window = std::min(window, m_history.capacity());
146 if (window == 0)
147 return 0;
148
149 const auto view = m_history.linearized_view();
150 std::vector<CellT> seen;
151 seen.reserve(window);
152 for (size_t i = 0; i < window; ++i) {
153 bool found = false;
154 for (const auto& s : seen) {
155 if (s == view[i]) {
156 found = true;
157 break;
158 }
159 }
160 if (!found)
161 seen.push_back(view[i]);
162 }
163 return seen.size();
164 }
165
166 /**
167 * @brief Cell with the most observations within the most recent
168 * @p window observations
169 * @param window Number of recent observations to examine, clamped
170 * to the trajectory's retained history capacity
171 * @return The most-occupied cell in the window, and how many of the
172 * window's observations fell in it
173 *
174 * Ties resolve to whichever qualifying cell appears first in the
175 * scan, which is the most recent one among ties since the
176 * underlying HistoryBuffer is newest-first; not documented as a
177 * guarantee beyond "deterministic," since which specific tie-break
178 * rule matters is a caller decision this does not presume to make.
179 */
180 [[nodiscard]] std::pair<CellT, size_t> dominant_cell(size_t window) const
181 {
182 window = std::min(window, m_history.capacity());
183 const auto view = m_history.linearized_view();
184
185 CellT best {};
186 size_t best_count = 0;
187 for (size_t i = 0; i < window; ++i) {
188 size_t count = 0;
189 for (size_t j = 0; j < window; ++j) {
190 if (view[j] == view[i])
191 ++count;
192 }
193 if (count > best_count) {
194 best_count = count;
195 best = view[i];
196 }
197 }
198 return { best, best_count };
199 }
200
201 /**
202 * @brief Reset to uninitialized state
203 *
204 * Call on a known discontinuity (the tracked point teleporting
205 * rather than moving continuously) so the next update() does not
206 * count a crossing against a now-meaningless prior cell.
207 */
208 void reset()
209 {
211 m_current_cell = CellT {};
212 m_has_prior = false;
214 }
215
216 /**
217 * @brief The lattice this trajectory observes positions through
218 */
219 [[nodiscard]] const LatticeT& lattice() const { return m_lattice; }
220
221private:
222 LatticeT m_lattice;
225 bool m_has_prior { false };
226 size_t m_crossing_count { 0 };
227};
228
229} // namespace MayaFlux::Kinesis
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.
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.