44on the timestamps' own clock, in seconds. Its start is computed by
55multiplication, never by accumulating ``hop``, so every backend -- the C++
66batch transform, the streaming transform, the JAX transform and the JAX packer
7- -- puts the same sample in the same window. The grid runs while a window ends
8- no later than one median sample period past the last timestamp, the bound the
9- batch transforms have always used .
7+ -- puts the same sample in the same window. The grid runs through the last
8+ window holding at least ``min_samples`` samples: every window with enough data
9+ is transformed, at the end of a recording as anywhere else .
1010
1111Timestamps are measured from the origin once, by :func:`relative_seconds`,
1212and every backend is handed those relative times. That is what makes the
@@ -137,22 +137,38 @@ def median_spacing(t_s: NDArray[np.float64]) -> float:
137137 return float (positive [positive .size // 2 ])
138138
139139
140- def window_count (last_relative_s : float , dt_median_s : float , window_s : float , hop_s : float ) -> int :
141- """How many windows fit: those with ``k * hop + window <= last + dt_median`` .
140+ def last_window_start (last_relative_s : float , hop_s : float ) -> int :
141+ """Index of the last window starting at or before the last sample; -1 if none .
142142
143- ``last_relative_s`` is the last timestamp minus the origin. The estimate is
144- corrected against the exact comparison, so the count agrees with a loop
145- that tests each window in turn.
143+ Windows starting later hold no samples. The estimate is corrected against
144+ the exact comparison, so it agrees with a loop that tests each start.
146145 """
147- limit = last_relative_s + dt_median_s
148- if not math .isfinite (limit ) or window_s > limit :
149- return 0
150- n = int (math .floor ((limit - window_s ) / hop_s )) + 1
151- while n > 0 and (n - 1 ) * hop_s + window_s > limit :
152- n -= 1
153- while n * hop_s + window_s <= limit :
154- n += 1
155- return n
146+ if not math .isfinite (last_relative_s ) or last_relative_s < 0.0 :
147+ return - 1
148+ k = int (math .floor (last_relative_s / hop_s ))
149+ while k >= 0 and k * hop_s > last_relative_s :
150+ k -= 1
151+ while (k + 1 ) * hop_s <= last_relative_s :
152+ k += 1
153+ return k
154+
155+
156+ def window_count (
157+ t_relative_s : NDArray [np .float64 ], window_s : float , hop_s : float , min_samples : int
158+ ) -> int :
159+ """How many windows the grid has: through the last one with ``min_samples`` samples.
160+
161+ ``t_relative_s`` are sorted timestamps minus the origin. Windows before
162+ that last one stay on the grid whatever they hold; with
163+ ``empty_windows="keep"`` those without enough samples are NaN rows.
164+ """
165+ t = np .asarray (t_relative_s , dtype = np .float64 )
166+ starts = np .arange (last_window_start (float (t [- 1 ]), hop_s ) + 1 , dtype = np .int64 ) * hop_s
167+ counts = np .searchsorted (t , starts + window_s , side = "left" ) - np .searchsorted (
168+ t , starts , side = "left"
169+ )
170+ enough = np .flatnonzero (counts >= min_samples )
171+ return int (enough [- 1 ]) + 1 if enough .size else 0
156172
157173
158174@dataclass (frozen = True )
@@ -164,7 +180,7 @@ class WindowGrid:
164180 seconds. A result's ``times`` are measured from it.
165181 window_s: Window duration in seconds.
166182 hop_s: Window hop in seconds (``window_s - overlap_s``).
167- dt_median_s: Upper median sample spacing, which sets the grid's end .
183+ dt_median_s: Upper median sample spacing, which sets the transform size .
168184 first: Index of each window's first sample, shape ``(n_windows,)``.
169185 stop: One past each window's last sample, shape ``(n_windows,)``.
170186 min_samples: Fewest samples a window needs to be transformed.
@@ -263,7 +279,7 @@ def grid_from_relative(
263279 min_samples : int ,
264280) -> WindowGrid :
265281 """Build the grid from timestamps already expressed relative to the origin."""
266- n = window_count (float ( t_relative_s [ - 1 ]), dt_median_s , window_s , hop_s )
282+ n = window_count (t_relative_s , window_s , hop_s , min_samples )
267283 starts = np .arange (n , dtype = np .int64 ) * hop_s
268284 first = np .searchsorted (t_relative_s , starts , side = "left" ).astype (np .int64 )
269285 stop = np .searchsorted (t_relative_s , starts + window_s , side = "left" ).astype (np .int64 )
0 commit comments