Skip to content

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

Open
tombonfert wants to merge 22 commits into
mainfrom
feature/time_window_event
Open

tombonfert wants to merge 22 commits into
mainfrom
feature/time_window_event

Conversation

@tombonfert

Copy link
Copy Markdown
Collaborator

Summary

Adds TimeWindowEvent, which splits each measurement container into consecutive fixed-duration windows (for example 1-minute, 10-minute or hourly segments), producing one event instance per window. Aggregations scoped to the event, such as StatsAggregator(..., event=...), compute one result per window.

Usage

ten_minute = TimeWindowEvent(name="ten_minute_windows", window_length=600_000)
report.add_event(ten_minute)

window_length uses the same time unit as the stored timestamps. The final window is clamped to the container's stop_ts.

Design

  • Windows are computed from container_metrics (start_ts/stop_ts) for every container matching the report's filters, whether or not it has channel data or a scoped aggregation.
  • The event fact is built natively in Spark (window_intervals_col). Scoped aggregations compute the same windows in the query engine (TimeWindowExpression). Both perform the same floating-point operations in the same order, so the windows are bit-identical and event_instance_id matches across event_instance_fact and stats_aggregator_fact.
  • New ContainerBoundaryEvent base class for ContainerEvent and TimeWindowEvent. The report keeps these events out of the channel solve and resolves them through the container filters.
  • window_length is normalized to a float, so 10000 and 10000.0 give the same definition hash and event_dimension attributes.

Test Plan

  • Unit tests added/updated
  • Manual testing completed
  • Documentation updated (if applicable)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • No new linter warnings introduced

…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 97.98658% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.66%. Comparing base (c0ffd61) to head (44129db).

Files with missing lines Patch % Lines
...ine/analyze/query/events/time_window_expression.py 96.15% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #110      +/-   ##
==========================================
- Coverage   89.82%   86.66%   -3.17%     
==========================================
  Files          62       35      -27     
  Lines        5770     3420    -2350     
  Branches      726      440     -286     
==========================================
- Hits         5183     2964    -2219     
+ Misses        464      386      -78     
+ Partials      123       70      -53     
Flag Coverage Δ
query_engine 86.66% <97.98%> (+0.51%) ⬆️
reporting ?

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%> (ø)
...ine/analyze/query/events/time_window_expression.py 96.15% <96.15%> (ø)

... and 29 files 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.

This branch has not been deployed

No deployments
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