Skip to content

feat(reporting): add TimeWindowEvent for fixed-duration windows per container - #110

Merged
tombonfert merged 28 commits into
mainfrom
feature/time_window_event
Oct 8, 2026
Merged

tombonfert merged 28 commits into
mainfrom
feature/time_window_event

Conversation

@tombonfert

@tombonfert tombonfert commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds TimeWindowEvent, which splits each container into consecutive fixed-length windows (the last one clamped to the container end) based on container_metrics.start_ts / stop_ts. It needs no expression. An aggregation scoped to the event, e.g. StatsAggregator(..., event=...), yields one result per window.

Changes

  • Reporting
    • TimeWindowEvent(name, window_length, max_windows_per_container=1_000_000) with the event type TIME_WINDOW_EVENT.
    • A shared ContainerBoundaryEvent base with ContainerEvent.
  • Query engine
    • TimeWindowExpression evaluates to Intervals.
    • tile_windows is the single window implementation, used by both the solve and the event fact (window_intervals_udf). The two sides therefore produce identical windows, and the event_instance_ids, which hash the window boundaries, match.
  • Channel time frame
    • New solver_config fields: channel_time_unit (s / ms / us / ns), channel_time_origin (epoch / container_start) and container_time_unit.
    • solvers/utils/window_bounds.with_window_bounds converts the container boundaries into the channels' time frame for the windows only. The raw start_ts / stop_ts stay unchanged for UDFs, ContainerEvent and measurement_dimension.
    • Supports TIMESTAMP and numeric boundaries, with RLE and RAW channel data.
  • Incremental runs: the definition hash covers window_length and the channel time frame, so changing either triggers a full recompute.
  • Validation: missing boundary columns (with a hint to the column mapping), TIMESTAMP_NTZ or DATE boundaries, mixed boundary types, and TIMESTAMP boundaries without channel_time_unit are rejected. Containers with null, NaN or infinite boundaries get no windows.
  • Docs: event reference, configuration, skills and API reference.

Impact

  • No behavior change for reports without a TimeWindowEvent.
  • measurement_dimension.config_hash changes for reports that set a solver_config, because of the new fields. The value is written but never compared.

Testing

  • Unit: window tiling and the UDF, window bounds (units, origins, time zones, INT overflow with ANSI on and off), definition hashes, and solver container metadata.
  • Integration: the event fact and per-window statistics joined by event_instance_id across 7 time bases, including ns epochs beyond 2^53, TIMESTAMP boundaries, relative seconds and RAW data.

Follow-ups

#113 event identity validation · #114 report-level validation step · #117 StatsAggregator O(windows × samples) · #118 mapInArrow for the event fact

…ows per container

Introduce `TimeWindowEvent`, which splits each measurement container into consecutive fixed-duration windows (one event instance per slice), complementing the existing `ContainerEvent` (one instance per container).

- Add `TumblingWindowsExpression` in the query engine to derive window boundaries from `container_metrics.start_ts` / `stop_ts` with no channel selectors; final window is clamped to `stop_ts`.
- Add `TimeWindowEvent` reporting class wired into `EventType`, producing `event_instance_fact` rows and `event_dimension` metadata with `window_length` surfaced in attributes.
- Update event reference docs with `TimeWindowEvent` usage, parameters, and comparison table.
- Add unit tests for `TumblingWindowsExpression` and `TimeWindowEvent`, plus integration tests verifying end-to-end report behavior and aggregation joins.
…TumblingWindowsExpression

Replace the locally-defined `_START_TS_COL` / `_STOP_TS_COL` literals in `TumblingWindowsExpression` with `SolverConfig.start_ts_col` / `stop_ts_col` from a default `SolverConfig` instance. This keeps the internal container-metric timestamp column names consistent with the rest of the query engine and avoids duplicating config-invariant literals.
…wExpression

Rename the query-engine expression class and module from `TumblingWindowsExpression` to `TimeWindowExpression` to align with the reporting `TimeWindowEvent` naming. Update all imports, exports, docstrings, and tests accordingly.
Generate the `time_window_event` API reference page and register it in the pydoc loader and API sidebar. Update the events skill README and SKILL.md to include `TimeWindowEvent` alongside the other event types.
…oat for stable hashes

Store `window_length` as a float in `TimeWindowExpression` so the string representation (and downstream event definition hash) is identical whether an int or float is passed. This prevents spurious full recomputes in incremental mode when the same window is described as `10` vs `10.0`. Remove the now-unreachable `window_count <= 0` guard and add unit tests covering hash stability across int/float window lengths.
…ner_metrics

Introduce `ContainerBoundaryEvent` as a shared base for `ContainerEvent` and `TimeWindowEvent`, routing both through the solver's filter pipeline instead of the centralized channel solve. `TimeWindowEvent` now materializes windows natively in Spark from `container_metrics.start_ts` / `stop_ts` via `window_intervals_col`, so every filtered container gets windows regardless of channel coverage.

Keep the query-engine `TimeWindowExpression` bit-identical to the new Spark helper by casting boundaries to double before computing window counts and clamping. Normalize `TimeWindowEvent.window_length` through the expression so int and float inputs produce identical attributes and hashes. Update docs, skills, and tests to reflect that `TimeWindowEvent` no longer requires a co-solved aggregation and that scoped aggregations still match by `event_instance_id` even with rounded double boundaries.
@tombonfert
tombonfert requested a review from a team as a code owner October 1, 2026 13:03
@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.32636% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (c0ffd61) to head (986c125).

Files with missing lines Patch % Lines
...ine/analyze/query/events/time_window_expression.py 94.93% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #110      +/-   ##
==========================================
+ Coverage   89.82%   90.13%   +0.31%     
==========================================
  Files          62       66       +4     
  Lines        5770     5982     +212     
  Branches      726      750      +24     
==========================================
+ Hits         5183     5392     +209     
- Misses        464      468       +4     
+ Partials      123      122       -1     
Flag Coverage Δ
query_engine 86.64% <97.33%> (+0.48%) ⬆️
reporting 94.80% <100.00%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ery_engine/analyze/query/solvers/default_solver.py 94.93% <100.00%> (+0.05%) ⬆️
...uery_engine/analyze/query/solvers/solver_config.py 100.00% <100.00%> (ø)
...ngine/analyze/query/solvers/utils/window_bounds.py 100.00% <100.00%> (ø)
src/impulse_reporting/core/report.py 93.42% <100.00%> (+0.08%) ⬆️
src/impulse_reporting/core/report_utils.py 97.81% <100.00%> (ø)
...pulse_reporting/events/container_boundary_event.py 100.00% <100.00%> (ø)
src/impulse_reporting/events/container_event.py 100.00% <100.00%> (ø)
src/impulse_reporting/events/event.py 82.22% <ø> (ø)
src/impulse_reporting/events/event_types.py 100.00% <100.00%> (ø)
src/impulse_reporting/events/time_window_event.py 100.00% <100.00%> (ø)
... and 1 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…d drop redundant interval guard

Validate that `TimeWindowEvent` and `TimeWindowExpression` require a finite, strictly positive `window_length`, rejecting `inf`, `-inf`, and `nan` to prevent bogus or crashing Spark window generation. Remove the now-unnecessary `start_ts < end_ts` filter from `TimeWindowEvent.determine_events` since finite positive windows guarantee valid intervals. Rename `container_event_cls` to `boundary_event_cls` in `dispatch_events` for clarity. Update docs, skills, and tests accordingly.
… via solver_config.epoch_unit

Add an opt-in `solver_config.epoch_unit` setting (`"s"`, `"ms"`, `"us"`, `"ns"`) that converts `TIMESTAMP`-typed `container_metrics.start_ts` / `stop_ts` into epoch numbers. This lets `ContainerEvent` and `TimeWindowEvent` share the same time base as the channel sample timestamps, which is required for `TimeWindowEvent` when boundaries are `TIMESTAMP` columns.

- Implement `SolverConfig.normalize_container_boundaries` and `require_epoch_boundaries` for conversion and fail-fast validation.
- Apply normalization in `ContainerBoundaryEvent` event resolution and in the default solver's container metadata path.
- Raise a clear `TypeError` in `TimeWindowExpression` when unconverted datetime boundaries reach pandas.
- Add unit and integration tests covering all epoch units, timezone independence, TIMESTAMP_NTZ/DATE rejection, and `TimeWindowEvent` aggregation joins with TIMESTAMP boundaries.
- Update configuration, schema, API, and event reference docs.
…on, include epoch_unit in definition hashes, and add per-container window limit

Change `TimeWindowEvent` `event_instance_id` generation to hash the window's position (`container_id::event_name::window_index`) instead of its boundaries, so event facts and scoped aggregations join reliably despite double rounding of epoch timestamps. Propagate `solver_config.epoch_unit` into `ContainerBoundaryEvent` subclasses and fold it into the definition hashes of `ContainerEvent`, `TimeWindowEvent`, and aggregations scoped to a `TimeWindowEvent`, forcing a full recompute when the unit changes in incremental mode. Add `max_windows_per_container` (default 1,000,000) to `TimeWindowEvent` and `TimeWindowExpression` to fail fast when `window_length` is in the wrong unit for the boundaries, and reject non-finite container boundaries by yielding no windows. Update docs, skills, and tests.
…owEvent and clarify epoch_unit semantics

Extend `TimeWindowEvent` integration tests to cover `data_type=RAW` with both `Rle` and `Interval` raw encoders, including TIMESTAMP container boundaries converted via `epoch_unit="us"`. Clarify in docs and code that `epoch_unit` describes the existing unit of channel sample timestamps (`tstart`/`tend` or `timestamp` for RAW data) and that channel timestamps are never converted themselves.
…er_container and strengthen multi-event tests

Expose `validate_max_windows` from `TimeWindowExpression` with a configurable parameter name so `TimeWindowEvent` can validate `max_windows_per_container` with an error message that names the caller's parameter. Harden the `test_multiple_time_window_events_coexist` integration test to verify that two events with different window lengths each tile every container, that scoped stats join only to their own event's windows, and that stats values match the underlying samples. Reuse the tiling assertion across other TimeWindowEvent tests.
…oundaryEvent

Move `get_id`, `as_spark_row`, and `determine_metadata_df` from `ContainerEvent` and `TimeWindowEvent` into their common base class `ContainerBoundaryEvent` to eliminate duplication. Remove redundant `window_length` and `Intervals` validation from `TimeWindowEvent` now handled by `TimeWindowExpression`. Update API docs to reflect the removed methods on subclasses.
…ope boundary helper

Refactor `test_time_window_event_in_report` to reuse the shared `_assert_windows_for_all_containers` helper. Update `_container_boundaries` to filter on the report's `vehicle_key` scope so boundary expectations match the containers actually processed. Strengthen `_assert_windows_tile_containers` to compute exact expected windows using the same double arithmetic as the event, covering edge cases like zero-span containers and fractional windows, and drop the `itertools.pairwise` dependency. Remove a redundant `event_instance_id` subset check in the aggregation coverage test.
…r_metrics start_ts/stop_ts

Clarify across configuration docs, API reference, skills, and docstrings that `solver_config.epoch_unit` is the epoch unit of the `channels` table timestamps (which are never converted) and that only `TIMESTAMP`-typed `container_metrics.start_ts`/`stop_ts` are converted to epoch numbers. Update the impulse-config skill example to use `start_ts`/`stop_ts` column names and add the `epoch_unit` description. Tighten the TimeWindowExpression error message to name the converted columns explicitly.
…nit/channel_time_origin for TimeWindowEvent

Rename `solver_config.epoch_unit` to `channel_time_unit` and add `channel_time_origin` (`"epoch"` default, `"container_start"`). The new settings describe the time frame of channel sample timestamps and are used only to compute `TimeWindowEvent` windows from `container_metrics.start_ts`/`stop_ts`; the raw boundary columns are no longer converted in place and remain visible unchanged to `ContainerEvent`, `measurement_dimension`, and UDFs.

- Add `SolverConfig.with_window_bounds` to derive prefixed `__window_start`/`__window_stop` columns in the channel time frame, supporting both absolute epoch and container-start-relative origins.
- Remove boundary conversion from `ContainerBoundaryEvent` and `ContainerEvent`; drop `epoch_unit` from their definition hashes.
- Update `TimeWindowExpression` and `TimeWindowEvent` to read the derived window-bound columns and include the channel time frame in definition hashes.
- Update configuration docs, API references, skills, and tests.
…for numeric boundaries

Add `solver_config.container_time_unit` to convert numeric `container_metrics.start_ts`/`stop_ts` into the channel time unit used by `TimeWindowEvent` windows. This supports cases like epoch-ms boundaries with µs channel samples. The setting requires `channel_time_unit`, is rejected for `TIMESTAMP` boundaries, and is included in `TimeWindowEvent` definition hashes. Update docs, API references, skills, and tests.
…on in a single pandas UDF-backed function

Replace the native-Spark `window_intervals_col` with a scalar pandas UDF (`window_intervals_udf`) that delegates to a shared `tile_windows` implementation. This ensures the reporting event fact and the solve-side `TimeWindowExpression` produce identical windows from the same `container_metrics` bounds, keeping `event_instance_id` values consistent. Update docstrings, API references, and tests to reflect the new UDF and shared tiling logic.
… by window boundaries

Revert `TimeWindowEvent` `event_instance_id` generation from the window-index hash back to the standard interval-event hash (`container_id::event_name::start_ts::end_ts`). Since the reporting event fact and the solve-side `TimeWindowExpression` now share the same `tile_windows` implementation and read the same Spark-computed window-bound columns, they produce identical windows and matching ids without relying on positional indexing.

- Remove the `TimeWindowEvent` special case from `generate_event_instance_id_column` and drop the `window_index_col` parameter.
- Stop emitting `interval_index` in `StatsAggregator` and remove the `posexplode`/`window_index` column from `TimeWindowEvent.determine_events`.
- Update docstrings, API references, skills, and tests to describe boundary-based ids and remove references to window position/index hashing.
- Strengthen `TimeWindowEvent` unit and integration tests to verify stats values join to the correct windows under the new id scheme.
…lverConfig to a standalone utility

Move `SolverConfig.with_window_bounds` into a new `solvers.utils.window_bounds` module as a standalone function that takes a `SolverConfig` argument. This decouples the window-bound computation from the configuration model and makes it easier to share between the query engine solve path and the reporting event fact. Update all call sites, docstrings, API references, and tests to reference the new location.
…s to ContainerBoundaryEvent

Hoist `as_dict`, `required_channels`, and shared SHA-256 hashing/normalization helpers into `ContainerBoundaryEvent` to remove duplication between `ContainerEvent` and `TimeWindowEvent`. Subclasses now only customize `description`, `attributes`, and `required_channels` initialization. Update API reference docs to drop the subclass `as_dict` entries that are now inherited.
…s references in TimeWindowEvent docs

Update configuration docs, event reference, and skills to consistently refer to `container_metrics.start_ts`/`stop_ts` when describing the source of `TimeWindowEvent` container boundaries and the columns that remain unchanged for `ContainerEvent`, `measurement_dimension`, and UDFs.
…vent int overflow

When converting integer container boundaries from a coarser unit to a finer channel unit (e.g. epoch seconds to ms), multiply by a long-typed factor so Spark widens the result instead of keeping int*int as int. This avoids ARITHMETIC_OVERFLOW under ANSI mode and silent wrapping otherwise. Doubles and decimals retain their original types. Add unit tests covering int overflow behavior and double fraction preservation.
…nal window lengths

Add a note to the TimeWindowEvent docs explaining that when `window_length` is not a whole number in the channel unit or not a multiple of 256 ns for nanosecond epochs, rounding can produce a final window only a few ulps long.
Replace hard-coded timestamps in window_bounds tests with the shared basic_narrow_db fixture's container_metrics boundaries. This makes the tests data-driven and consistent with the rest of the suite while still covering TIMESTAMP, numeric, and null boundary cases.
…w_intervals_udf

Change the pandas UDF return type from `array<array<double>>` to `struct<starts: array<double>, ends: array<double>>` so each row reliably carries one array per column, even when all rows have the same window count. Update `TimeWindowEvent.determine_events` to zip the struct fields and adjust unit tests accordingly.
…sage

When `with_window_bounds` cannot find required `container_metrics` columns, include the unmapped physical column name in the error and point users to `solver_config.container_metrics.column_name_mapping`. Add a unit test verifying the message for an unmapped `stop_ts` physical name.
@tombonfert
tombonfert merged commit a44c2a5 into main Oct 8, 2026
6 checks passed
@tombonfert
tombonfert deleted the feature/time_window_event branch October 8, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant