diff --git a/docs/impulse/docs/config/configuration.md b/docs/impulse/docs/config/configuration.md
index 3ad82f76..ccab0582 100644
--- a/docs/impulse/docs/config/configuration.md
+++ b/docs/impulse/docs/config/configuration.md
@@ -141,6 +141,7 @@ Two independent filter families:
| `drop_implausible_data` | `bool` | `false` | When `true`, drops `channels` rows where `is_plausible = false`. Requires `data_type = "RAW"`; combining with `"RLE"` raises a validation error. |
| `max_channels_per_batch` | `int` | `500` | Caps how many distinct channels are read in one solve batch, to bound per-solve memory. Counts channel selections (each distinct `channel(...)` / `poi_channel(...)` in your expressions). Applies to events, aggregations, and calculated channels; for calculated channels it counts the *input* channels they read, not the number of calculated channels. The cap is a packing target, not a hard limit: an expression that on its own reads more channels than the cap still runs, in a batch of its own. Must be `>= 1`. The old name `batch_size` is a **deprecated alias** (still accepted, warns, removed in a future release); setting both raises a validation error. |
| `max_containers_per_batch` | `int` | `null` | Caps how many containers are solved at once, to bound per-solve memory. `null` disables the cap; when set it must be `>= 1`. On an **incremental** run, `Report.run()` commits at most this many upserted containers per iteration and loops until the population is drained (see [incremental](#incremental-optional)). On a **full** run, and when a changed definition recomputes over all containers, the population is split into chunks of about this size and solved chunk by chunk. Chunk sizes are approximate (the cap is a memory heuristic, not an exact bound). |
+| `enable_pushdown` | `bool` | `false` | When `true`, supported expression subtrees (currently `channel.resample(rate)` on a physical channel) are computed natively in Spark before the pandas evaluation, so fewer rows are shipped to it. A channel is only pushed down when every use of it in a solve batch is; results are unchanged. Meant for downsampling: upsampling produces more rows than the raw channel. See [Spark pushdown](../references/query_engine/query_solvers.md#spark-pushdown-opt-in). |
| `solver_config` | `SolverConfig` | `null` | Per-table column mappings, per-table equality filters, and project scoping. Set `project_id` to scope reads by project — it is applied to `container_tags` (if configured), `container_metrics`, and `channel_mapping` (if configured), so it works in both narrow EAV and wide-only data models. Omit it when you don't need project scoping. See [Solver column mappings and filters](#solver-column-mappings-and-filters). |
| `measurement_db` | `str` | `"MeasurementDB"` | Registered name of the MeasurementDB implementation to use. The default is the built-in reader; a custom implementation can reshape a different source schema at read time. See [Custom measurement DB](../extensibility/custom_measurement_db.md). |
| `measurement_db_config` | `dict` | `null` | Keyword arguments for that implementation's config class, merged with the `source` tables (e.g. extra source tables, prefilter keys). |
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/metadata/time_series_expression.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/metadata/time_series_expression.md
index e20fc5c0..5a3c9a7e 100644
--- a/docs/impulse/docs/references/api/impulse_query_engine/analyze/metadata/time_series_expression.md
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/metadata/time_series_expression.md
@@ -353,6 +353,10 @@ def build(cache: SeriesCache)
Build the time series from cache.
+A result the cache already holds for this node (see
+:meth:`SeriesCache.materialized`) is returned as-is, without building the
+operands.
+
**Arguments**:
- `cache` (`SeriesCache`): Cache containing time series data.
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/context.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/context.md
new file mode 100644
index 00000000..a2624b32
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/context.md
@@ -0,0 +1,267 @@
+---
+sidebar_label: context
+title: impulse_query_engine.analyze.query.optimizer.context
+---
+
+What a pushdown rule sees of the samples it computes on.
+
+A rule never handles DataFrames. The pipeline (:mod:`.pipeline`) evaluates the
+rule's hooks with a :class:`SeriesContext`, whose properties are Spark columns
+describing the current sample of one of the rule's series: its times and value,
+its neighbours, the run (e.g. continuous block) it belongs to, and facts about
+the whole series. Columns returned by a hook become part of the shared pass
+over the channel data.
+
+
+#### ELEMENT\_TYPE
+
+Element type of the arrays returned by :meth:`PushdownRule.emit`.
+
+
+#### BLOCK
+
+Name of the built-in run of continuous samples.
+
+
+#### run\_col
+
+```python
+def run_col(run: str, name: str) -> str
+```
+
+Name of the pipeline column *name* of the run *run*.
+
+
+## Sample
+
+```python
+class Sample()
+```
+
+One sample as columns: ``tstart`` and ``tend`` as double, and its value as double.
+
+
+#### RunBreak
+
+A run definition: whether the sample *cur* starts a new run after *prev*. It
+
+is called with ``prev`` columns that are null before the series' first
+sample; a null result counts as a break.
+
+
+## Run
+
+```python
+class Run()
+```
+
+Where the current sample sits in its run, a maximal stretch without a break.
+
+**Arguments**:
+
+- `starts` (`Column`): The sample starts its run.
+- `id` (`Column`): Number of the run within the series, from 1.
+- `start` (`Column`): ``tstart`` of the run's first sample.
+- `next_starts` (`Column`): The next sample starts a new run; null for the series' last sample.
+- `is_last` (`Column`): The run is the last of its series.
+
+#### block\_break
+
+```python
+def block_break(prev: Sample, cur: Sample) -> Column
+```
+
+Continuous blocks: a sample continues its block when it starts where the previous ended.
+
+
+## SeriesContext
+
+```python
+class SeriesContext()
+```
+
+Columns a rule's hooks build on, for the current sample of one of its series.
+
+Every series is one channel's samples of one variant, ordered by
+``(tstart, tend, value)``; all variants of a channel have the same samples.
+Rules running in series order (``ordered``) see neighbours, runs and series
+facts; per-sample rules see only the sample.
+
+**Arguments**:
+
+- `rule` (`PushdownRule`): The rule the hooks belong to.
+- `variants` (`Mapping[int, Pushdown]`): The rule's variants in this solve.
+- `key` (`Sequence[str]`): Columns identifying a series.
+- `order` (`Sequence[str]`): The channel-data ``tstart``, ``tend`` and value columns: the series order.
+- `ordered` (`bool`): Whether the pipeline runs the rule in series order.
+- `runs` (`Mapping[str, str]`): The rule's run names to pipeline run names.
+- `kept` (`bool`): Whether the hook runs on the kept samples (``kept_columns``, ``emit``).
+
+#### kept\_stage
+
+```python
+def kept_stage() -> SeriesContext
+```
+
+This context, for the hooks that run on the kept samples.
+
+
+#### active
+
+```python
+def active() -> Column
+```
+
+The sample belongs to one of the rule's variants.
+
+
+#### name
+
+```python
+def name(column: str) -> str
+```
+
+Pipeline name of the rule's column *column*.
+
+
+#### col
+
+```python
+def col(column: str) -> Column
+```
+
+The rule's column *column*, defined by an earlier hook entry.
+
+
+#### param
+
+```python
+def param(name: str) -> Column
+```
+
+The pushdown parameter *name* of the sample's variant.
+
+
+#### tstart
+
+```python
+def tstart() -> Column
+```
+
+The sample's ``tstart`` as double.
+
+
+#### tend
+
+```python
+def tend() -> Column
+```
+
+The sample's ``tend`` as double.
+
+
+#### value
+
+```python
+def value() -> Column
+```
+
+The sample's value as double.
+
+
+#### sample
+
+```python
+def sample() -> Sample
+```
+
+The current sample.
+
+
+#### element
+
+```python
+def element(tstart: Column | None = None,
+ tend: Column | None = None,
+ value: Column | None = None) -> Column
+```
+
+One emitted row; ``tstart`` / ``tend`` left out keep the sample's own times.
+
+
+#### prev
+
+```python
+def prev() -> Sample
+```
+
+The previous sample of the series; null columns for its first sample.
+
+
+#### next
+
+```python
+def next() -> Sample
+```
+
+The next sample of the series; null columns for its last sample.
+
+
+#### n
+
+```python
+def n() -> Column
+```
+
+Number of samples in the series.
+
+
+#### last
+
+```python
+def last() -> Sample
+```
+
+The series' last sample.
+
+
+#### in\_chunk
+
+```python
+def in_chunk() -> WindowSpec
+```
+
+Window over the sample's chunk in series order, for local neighbour access.
+
+Chunks are consecutive slices of a channel, the same for all its series;
+use :attr:`prev`, :attr:`next` and runs for anything that must see across
+chunk boundaries.
+
+
+#### block
+
+```python
+def block() -> Run
+```
+
+The continuous block the sample belongs to (gaps end blocks).
+
+
+#### run
+
+```python
+def run(name: str) -> Run
+```
+
+The sample's run of the rule's run definition *name* (see :meth:`PushdownRule.runs`).
+
+
+#### block\_end
+
+```python
+def block_end() -> Column
+```
+
+``nanmax`` of the ``tend`` values of the sample's block (kept samples only).
+
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pipeline.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pipeline.md
new file mode 100644
index 00000000..76d806eb
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pipeline.md
@@ -0,0 +1,75 @@
+---
+sidebar_label: pipeline
+title: impulse_query_engine.analyze.query.optimizer.pipeline
+---
+
+Spark execution of a pushdown plan: one shared pass for all rules.
+
+
+#### CHUNK\_ROWS
+
+Average number of rows per chunk of a channel, counting a sample once per variant.
+
+
+#### apply\_pushdowns
+
+```python
+def apply_pushdowns(rows: DataFrame,
+ variants: DataFrame,
+ plan: PushdownPlan,
+ config,
+ include_raw: bool = True) -> DataFrame
+```
+
+The rows a solve ships: raw rows (when *include_raw*) and all derived rows.
+
+**Arguments**:
+
+- `rows` (`pyspark.sql.DataFrame`): Channel data ``(container_id, channel_id, tstart, tend, value)``, one row
+per sample.
+- `variants` (`pyspark.sql.DataFrame`): ``(container_id, channel_id, VARIANTS_COL)``: the variant ids each matched
+channel ships, one row per channel (see :meth:`PushdownPlan.channel_variants`).
+Channels without a row are dropped.
+- `plan` (`PushdownPlan`): The solve's pushdown plan.
+- `config` (`SolverConfig`): Provides the internal column names.
+- `include_raw` (`bool`): Whether to keep the raw rows (variant 0).
+
+**Returns**:
+
+`pyspark.sql.DataFrame`: Rows with the columns and types of *rows* plus :data:`VARIANT_COL`.
+
+## SeriesPipeline
+
+```python
+class SeriesPipeline()
+```
+
+The ordered pass shared by all rules that need their series in order.
+
+Chunking and everything that crosses chunk edges runs once per channel; its
+samples fan out into one series per variant only for the rules' hooks. The
+shuffle therefore carries each sample once, while a task after it processes
+about k rows per shuffled sample of a channel with k variants (chunks are k
+times smaller to keep the rows per chunk at :data:`CHUNK_ROWS`).
+
+Rule runs are computed for every channel of the pass; a rule only reads its
+own runs, on its own series.
+
+**Arguments**:
+
+- `rules` (`Sequence of (PushdownRule, Mapping[int, Pushdown])`): Ordered rules with their variants in this solve.
+- `config` (`SolverConfig`): Provides the internal column names.
+
+#### run
+
+```python
+def run(rows: DataFrame, variants: DataFrame) -> DataFrame
+```
+
+Replace the samples of the rules' variants with their derived rows.
+
+*rows* holds the channel data, one row per sample; *variants* the variant
+ids of each channel (see :func:`apply_pushdowns`). Only channels shipping a
+variant of the rules are processed.
+
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/plan.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/plan.md
new file mode 100644
index 00000000..06e1006e
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/plan.md
@@ -0,0 +1,147 @@
+---
+sidebar_label: plan
+title: impulse_query_engine.analyze.query.optimizer.plan
+---
+
+The outcome of query optimization: which subtrees Spark computes ahead of the UDF.
+
+
+## PushdownPlan
+
+```python
+class PushdownPlan()
+```
+
+Pushdowns selected for one solve call, shipped to the solver and the UDF.
+
+Variant ``0`` is the raw channel data; variants ``1..n`` are derived series,
+one per distinct :attr:`Pushdown.variant_key`.
+
+**Arguments**:
+
+- `rules` (`tuple of PushdownRule`): Rules used to recognize pushed-down nodes during evaluation.
+- `variant_ids` (`Mapping[tuple, int]`): Variant key to variant id.
+- `pushdowns_by_variant` (`Mapping[int, Pushdown]`): Variant id to a representative pushdown (rule and parameters).
+- `variants_by_selector` (`Mapping[int, tuple of int]`): Selector id to the variant ids derived from it.
+- `raw_selector_ids` (`frozenset of int`): Selectors whose raw channel data is still required.
+
+#### build
+
+```python
+def build(rules: Iterable[PushdownRule], pushdowns: Iterable[Pushdown],
+ selector_ids: Iterable[int]) -> PushdownPlan
+```
+
+Assign variant ids to *pushdowns*, in discovery order.
+
+**Arguments**:
+
+- `rules` (`Iterable[PushdownRule]`): The optimizer's rules.
+- `pushdowns` (`Iterable[Pushdown]`): Accepted pushdowns; each one's selector is pushed down entirely.
+- `selector_ids` (`Iterable[int]`): Ids of all selectors in the solve.
+
+**Raises**:
+
+- `ValueError`: If a pushdown computes on another pushdown's result (``source``),
+which the pipeline cannot compute.
+
+#### is\_empty
+
+```python
+def is_empty() -> bool
+```
+
+``True`` when nothing is pushed down.
+
+
+#### pushed\_selector\_ids
+
+```python
+def pushed_selector_ids() -> frozenset[int]
+```
+
+Selectors whose raw channel data is replaced by derived variants.
+
+
+#### variants\_by\_rule
+
+```python
+def variants_by_rule() -> list[tuple[PushdownRule, dict[int, Pushdown]]]
+```
+
+Group the variants by the rule that computes them.
+
+**Raises**:
+
+- `ValueError`: If two different rules share a name: their pipeline columns would
+collide.
+
+#### lookup
+
+```python
+def lookup(node: TimeSeriesExpression,
+ classify: Classifier | None = None) -> tuple[Pushdown, int] | None
+```
+
+Return ``(pushdown, variant)`` when *node* is a pushed-down subtree.
+
+Pass a *classify* shared by the lookups of one evaluation so that every
+node is classified once.
+
+
+#### native\_targets
+
+```python
+def native_targets(
+ selections: Iterable[Any]
+) -> dict[int, dict[int, list[tuple[int, bool]]]] | None
+```
+
+Sources of calculated channels that are pushed down as a whole.
+
+**Arguments**:
+
+- `selections` (`Iterable`): The selections of a ``solve_calculated_channels`` call.
+
+**Returns**:
+
+`dict or None`: ``{variant: {selector_id: [(channel_id, uses_alias), ...]}}``: the
+calculated channels (by output ``channel_id``) whose rows are the
+given variant's rows of the channel resolved for ``selector_id``.
+``None`` unless every selection is a ``CalculatedChannel`` whose
+whole expression is a pushdown emitting its result rows.
+
+#### channel\_variants
+
+```python
+def channel_variants(selector_ids: Column) -> Column
+```
+
+Variant ids a matched channel ships, given its ``selector_ids`` array.
+
+A channel ships only its raw data (``[0]``) unless every one of its
+selectors is pushed down; then it ships the derived variants instead.
+
+
+#### channel\_selector\_ids
+
+```python
+def channel_selector_ids(selector_ids: Column | str) -> Column
+```
+
+A matched channel's distinct, non-null ``selector_ids``, as ``long``.
+
+Variant tagging (:meth:`channel_variants`) and the native calculated-channel
+rows both read a channel's selectors through this, so they agree on which
+selectors it carries.
+
+
+#### describe
+
+```python
+def describe() -> str
+```
+
+One-line summary of the plan for logging.
+
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pushdown.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pushdown.md
new file mode 100644
index 00000000..1a3bc2c3
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/pushdown.md
@@ -0,0 +1,261 @@
+---
+sidebar_label: pushdown
+title: impulse_query_engine.analyze.query.optimizer.pushdown
+---
+
+Building blocks of Spark pushdown rules.
+
+
+#### VARIANT\_COL
+
+Column tagging each channel-data row with the series variant it belongs to.
+
+
+#### VARIANTS\_COL
+
+Column listing the variant ids a matched channel ships, one row per channel.
+
+
+#### RAW\_VARIANT
+
+Variant id of the unmodified (raw) channel data.
+
+
+#### check\_name
+
+```python
+def check_name(kind: str, name: str) -> str
+```
+
+Return *name* if it is a valid rule, rule column or run name, else raise.
+
+Names are lowercase letters, digits and single underscores, starting with a
+letter. Pipeline columns join them with ``__``, which a name never contains,
+so names of different rules cannot collide.
+
+
+## PushdownContext
+
+```python
+class PushdownContext()
+```
+
+Plan-time facts about the channel data a pushdown would run on.
+
+**Arguments**:
+
+- `tstart_type, tend_type` (`pyspark.sql.types.DataType`): Spark types of the channel-data ``tstart`` / ``tend`` columns.
+
+#### from\_schema
+
+```python
+def from_schema(schema: T.StructType, config: SolverConfig) -> PushdownContext
+```
+
+Build the context from the column-mapped channel-data schema.
+
+
+## Pushdown
+
+```python
+class Pushdown()
+```
+
+A subtree that a :class:`PushdownRule` computes natively in Spark.
+
+Expression nodes overload ``==``, so pushdowns compare by identity; use
+:attr:`variant_key` to identify equivalent pushdowns.
+
+A pushdown reads the samples of its channel, possibly absorbing pushed
+operands into that pass, or computes on the result of another pushdown, its
+:attr:`source`. The pipeline computes the former only.
+
+**Arguments**:
+
+- `rule` (`PushdownRule`): The rule that matched the subtree.
+- `selector` (`TimeSeriesSelector`): The physical channel selector the subtree reads.
+- `params` (`tuple of (str, Any)`): Canonical, hashable operation parameters (e.g. ``(("sample_rate", 1e6),)``).
+- `consumed` (`tuple of TimeSeriesSelector`): Every selector occurrence inside the subtree (its ``get_selectors()``).
+- `source` (`Pushdown`): The pushdown whose result this one computes on; ``None`` when it reads
+the channel's samples.
+
+#### variant\_key
+
+```python
+def variant_key() -> tuple
+```
+
+Identity of the derived series, independent of the input channel.
+
+
+#### describe
+
+```python
+def describe() -> str
+```
+
+Human-readable ``rule(param=value, ...)`` label.
+
+
+#### Operand
+
+Classification of an operand of the node a rule matches: the selector of a
+
+channel, the :class:`Pushdown` computing the operand, or ``None`` when the
+operand is evaluated in pandas or is not an expression.
+
+
+## PushdownRule
+
+```python
+class PushdownRule(abc.ABC)
+```
+
+Recognizes a subtree and computes it in Spark ahead of the pandas UDF.
+
+Planning and decoding run in Python: :meth:`match` recognizes a node from
+its operands' classification, :meth:`decode` turns one
+container/channel/variant slice of the derived rows back into the object the
+subtree evaluates to. The result must equal evaluating the subtree in pandas
+on the same channel data.
+
+The Spark side is a set of hooks returning columns, evaluated by the shared
+pushdown pipeline (:mod:`.pipeline`) for the samples of the rule's variants,
+one series (channel and variant) at a time, with a :class:`SeriesContext`:
+
+1. :meth:`row_columns` per sample;
+2. :meth:`keep` selects the samples that go on;
+3. :meth:`kept_columns` per kept sample;
+4. :meth:`emit` the derived rows of a kept sample.
+
+Rules never handle DataFrames, so every rule shares the pipeline's reads and
+shuffles. Derived rows have the channel-data shape
+``(container_id, channel_id, variant, tstart, tend, value)``.
+
+
+#### name
+
+Unique rule name, part of every :attr:`Pushdown.variant_key`; lowercase
+
+letters, digits and single underscores (see :func:`check_name`).
+
+
+#### ordered
+
+Whether the rule needs its series in order (neighbours, runs, series
+
+facts). Per-sample rules set ``False`` and skip the series scan.
+
+
+#### match
+
+```python
+def match(node: TimeSeriesExpression, operand: Operand) -> Pushdown | None
+```
+
+Return the pushdown for *node*, or ``None`` if the rule does not apply.
+
+Nodes are classified bottom-up: *operand* returns the classification of
+any operand of *node*. A pushdown reading the channel's samples sets no
+``source``; when it absorbs a pushed operand into that pass, its
+``consumed`` includes the operand's and :meth:`supports` covers both. A
+pushdown computing on a pushed operand's result sets ``source`` to it.
+
+Purely syntactic: it runs on the driver while planning and again inside
+the UDF to recognize the node, so it must not depend on data or schemas.
+
+
+#### supports
+
+```python
+def supports(pushdown: Pushdown, context: PushdownContext) -> bool
+```
+
+Whether *pushdown* can run on channel data described by *context*.
+
+
+#### result\_type
+
+```python
+def result_type(pushdown: Pushdown) -> type
+```
+
+Core-model class :meth:`decode` returns for *pushdown*.
+
+
+#### runs
+
+```python
+def runs() -> Mapping[str, RunBreak]
+```
+
+Run definitions beyond the built-in continuous blocks, by name.
+
+Each maps ``(prev, cur)`` samples to whether ``cur`` starts a new run;
+
+
+#### row\_columns
+
+```python
+def row_columns(ctx: SeriesContext) -> Mapping[str, Column]
+```
+
+Columns computed for every sample, in order; refer to them via ``ctx.col``.
+
+Column names follow :func:`check_name` and only need to be unique within
+the rule.
+
+
+#### keep
+
+```python
+def keep(ctx: SeriesContext) -> Column
+```
+
+Whether a sample can emit rows; the others are dropped early.
+
+
+#### kept\_columns
+
+```python
+def kept_columns(ctx: SeriesContext) -> Mapping[str, Column]
+```
+
+Columns computed for the kept samples, in order (``ctx.block_end`` available).
+
+
+#### emit
+
+```python
+def emit(ctx: SeriesContext) -> Column
+```
+
+The derived rows of a kept sample.
+
+**Returns**:
+
+`Column`: An array of :data:`~.context.ELEMENT_TYPE` structs built with
+:meth:`SeriesContext.element`; empty or null emits nothing. Times
+left out keep the sample's own ``tstart`` / ``tend``.
+
+#### decode
+
+```python
+def decode(rows: pd.DataFrame, pushdown: Pushdown,
+ cache: TimeSeriesCache) -> Any
+```
+
+Build the subtree's result from one container/channel/variant slice.
+
+
+#### emits\_result\_rows
+
+```python
+def emits_result_rows(pushdown: Pushdown) -> bool
+```
+
+Whether *pushdown*'s derived rows are its result's samples as they are.
+
+Holds when the result is a :class:`SampleSeries` built by the default
+
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/query_optimizer.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/query_optimizer.md
new file mode 100644
index 00000000..31d21612
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/query_optimizer.md
@@ -0,0 +1,58 @@
+---
+sidebar_label: query_optimizer
+title: impulse_query_engine.analyze.query.optimizer.query_optimizer
+---
+
+Rule-based optimizer choosing the subtrees Spark computes ahead of the pandas UDF.
+
+
+#### default\_rules
+
+```python
+def default_rules() -> tuple[PushdownRule, ...]
+```
+
+All built-in pushdown rules.
+
+
+## QueryOptimizer
+
+```python
+class QueryOptimizer()
+```
+
+Plans which subtrees of a solve's selections are computed natively in Spark.
+
+Nodes are classified bottom-up (:class:`~.traversal.Classifier`), and the
+topmost classified nodes whose pushdown the channel data supports are the
+candidates; a node that is not supported leaves its operands as candidates.
+
+All selections of a solve share the raw data of their physical channels, so
+a subtree is pushed down only when **every** occurrence of its input selector,
+across all selections, sits inside a pushed-down subtree. Then the raw rows
+of that selector are no longer needed and are replaced by the (smaller)
+derived rows. Selectors used anywhere else keep their raw data, and their
+subtrees are evaluated in pandas as before.
+
+**Arguments**:
+
+- `rules` (`Sequence[PushdownRule]`): Rules to apply, in priority order. Defaults to :func:`default_rules`.
+
+#### optimize
+
+```python
+def optimize(selections: Iterable[Any],
+ context: PushdownContext) -> PushdownPlan
+```
+
+Build the pushdown plan for the *selections* of one solve call.
+
+**Arguments**:
+
+- `selections` (`Iterable`): The query's selections.
+- `context` (`PushdownContext`): Plan-time facts about the channel data.
+
+**Returns**:
+
+`PushdownPlan`: The plan; empty when nothing can be pushed down.
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/resample_rule.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/resample_rule.md
new file mode 100644
index 00000000..af0cdf38
--- /dev/null
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/optimizer/resample_rule.md
@@ -0,0 +1,61 @@
+---
+sidebar_label: resample_rule
+title: impulse_query_engine.analyze.query.optimizer.resample_rule
+---
+
+Pushdown rule computing ``SampleSeries.resample`` natively in Spark.
+
+
+## ResamplePushdownRule
+
+```python
+class ResamplePushdownRule(PushdownRule)
+```
+
+Pushes ``channel.resample(sample_rate)`` on a physical channel down to Spark.
+
+Reproduces :meth:`SampleSeries.resample`: per continuous block of samples, a
+regular grid starting at the block's first ``tstart`` (built like
+``numpy.arange``), each grid point holding the value of the last sample that
+starts at or before it. Meant for downsampling: a rate finer than the native
+sample spacing yields more rows than the raw channel.
+
+Each sample is assigned the bucket ``c`` of the first grid point at or after its
+``tstart``. The last sample of a bucket holds the value of grid points
+``c, c + 1, …`` up to the next sample's bucket, so only those samples are kept
+and every row carries just the grid points it covers. Values are looked up
+within a block; this matches the pandas evaluation unless intervals overlap.
+
+Grid points are computed as ``t_min + k * delta`` in IEEE double arithmetic.
+numpy builds that fuse this into a multiply-add can differ by one ulp for
+fractional grids (e.g. seconds-as-double with a rate of ``0.1``); integral
+grids are exact.
+
+
+## \_Grid
+
+```python
+class _Grid()
+```
+
+The numpy.arange grid of the sample's block.
+
+``numpy.arange(t_min, t_max + rate, rate)`` has length
+``ceil(((t_max + rate) - t_min) / rate)`` and is filled as ``t_min``,
+``t_min + rate``, then ``t_min + k * ((t_min + rate) - t_min)``.
+
+
+#### first\_index
+
+```python
+def first_index(name: str, x: Column, strict: Column,
+ active: Column) -> dict[str, Column]
+```
+
+Columns ending in *name*: the smallest ``k >= 0`` with ``point(k) >= x`` (``> x`` if *strict*).
+
+Estimated from ``(x - t_min) / delta`` and corrected by one step against the
+exact grid points; a result that still is not exact raises instead of
+misplacing a sample. Only evaluated where *active*.
+
+
diff --git a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/solvers/default_solver.md b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/solvers/default_solver.md
index 9df87eef..463e0f3c 100644
--- a/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/solvers/default_solver.md
+++ b/docs/impulse/docs/references/api/impulse_query_engine/analyze/query/solvers/default_solver.md
@@ -48,6 +48,11 @@ silver layer.
``RawEncoder.RLE`` (default) run-length encodes equal-valued runs;
``RawEncoder.INTERVAL`` only derives ``tend`` and drops exact
duplicates. Only consulted when ``is_raw_data`` is ``True``.
+- `enable_pushdown` (`bool`): Whether to compute supported expression subtrees (e.g.
+``channel.resample(rate)``) natively in Spark before the pandas UDF, so
+fewer rows are shipped to it (see :class:`QueryOptimizer`). A channel
+is only pushed down when every use of it in the solve is. Default
+``False``.
#### filter\_container\_tags
diff --git a/docs/impulse/docs/references/api/impulse_reporting/config/config_parser.md b/docs/impulse/docs/references/api/impulse_reporting/config/config_parser.md
index 5c92a6b9..c28b8873 100644
--- a/docs/impulse/docs/references/api/impulse_reporting/config/config_parser.md
+++ b/docs/impulse/docs/references/api/impulse_reporting/config/config_parser.md
@@ -211,6 +211,11 @@ project/toolbox filtering is applied.
- `measurement_db` (`str, default="MeasurementDB"`): Registered name of the MeasurementDB implementation to use.
- `measurement_db_config` (`dict`): Keyword arguments for that implementation's config class,
merged with the ``source`` tables.
+- `enable_pushdown` (`bool, default=False`): Compute supported expression subtrees (currently
+``channel.resample(rate)`` on a physical channel) natively in Spark
+before the pandas UDF, so fewer rows are shipped to it. A channel is
+only pushed down when every use of it in a solve batch is. Meant for
+downsampling.
#### validate\_drop\_implausible\_data\_requires\_raw
diff --git a/docs/impulse/docs/references/api/sidebar.json b/docs/impulse/docs/references/api/sidebar.json
index 3f08df0f..92c8d3f9 100644
--- a/docs/impulse/docs/references/api/sidebar.json
+++ b/docs/impulse/docs/references/api/sidebar.json
@@ -30,6 +30,18 @@
"label": "impulse_query_engine.analyze.query.channels",
"type": "category"
},
+ {
+ "items": [
+ "references/api/impulse_query_engine/analyze/query/optimizer/context",
+ "references/api/impulse_query_engine/analyze/query/optimizer/pipeline",
+ "references/api/impulse_query_engine/analyze/query/optimizer/plan",
+ "references/api/impulse_query_engine/analyze/query/optimizer/pushdown",
+ "references/api/impulse_query_engine/analyze/query/optimizer/query_optimizer",
+ "references/api/impulse_query_engine/analyze/query/optimizer/resample_rule"
+ ],
+ "label": "impulse_query_engine.analyze.query.optimizer",
+ "type": "category"
+ },
{
"items": [
"references/api/impulse_query_engine/analyze/query/solvers/default_solver",
diff --git a/docs/impulse/docs/references/query_engine/img/solve_flow.svg b/docs/impulse/docs/references/query_engine/img/solve_flow.svg
new file mode 100644
index 00000000..9f6b5b79
--- /dev/null
+++ b/docs/impulse/docs/references/query_engine/img/solve_flow.svg
@@ -0,0 +1,244 @@
+
+ How a query is solved
+ Five stages: plan on the driver, resolve channels from the metadata tables, load the channel data, compute pushed-down subtrees natively in Spark, and evaluate the rest of each container's expressions in a pandas UDF, which consumes the precomputed subtree results. Example: eng_rpm.where(event).histogram2d(veh_spd.where(event), bins) with event = veh_spd > 0. Spark computes the event intervals and the filtered veh_spd samples; the UDF filters the raw eng_rpm rows to the event and computes the 2D histogram. Calculated channels that are pushed down entirely skip the UDF.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ Plan · driver
+
+
+ query.select(e1, …, en)
+ .solve(spark, solver)
+
+
+ Infer result types
+ solve(): build on an empty cache
+
+
+ Plan pushdowns (opt-in)
+ QueryOptimizer picks subtrees for Spark
+
+
+
+
+
+
+
+
+ 2
+ Resolve channels · Spark, metadata tables (no sample data)
+
+
+ container_tags
+ container filters
+
+
+ container_metrics
+ project, metric filters
+
+
+ channel_tags
+ channel tags (EAV)
+
+
+ channel_metrics
+ match each selector
+
+
+ channel_mapping
+ alias resolution
+
+
+
+
+
+
+
+ channel matches: container_id · channel_id · selector_ids
+
+
+
+
+
+ 3
+ Load channel data · Spark, channels table
+
+
+ channels
+ column mapping, filters
+
+
+ Raw mode: encode
+ RLE or intervals
+
+
+ Add POI channels
+ when POI channels are selected
+
+
+ Join channel matches
+ keep matched channels
+
+
+
+
+
+
+
+
+
+ 4
+ Compute subtrees natively · Spark pushdown (opt-in)
+
+
+
+
+ Split the expression at its pushed-down subtrees
+ eng_rpm.where( veh_spd > 0 ).histogram2d( veh_spd.where(veh_spd > 0) , bins)
+
+
+ eng_rpm
+
+
+ ① veh_spd > 0
+
+ ② veh_spd.where(veh_spd > 0)
+
+
+
+
+
+ .where(·)
+
+
+
+
+
+ .histogram2d(…)
+ rest: pandas UDF
+
+
+ Rules
+ e.g. resample(…) or filter
+ conditions like veh_spd > 0
+ each reads one channel;
+ all share the reads
+ of the channel data
+
+
+
+
+
+
+ 5
+ Evaluate per container · pandas UDF (GROUPED_MAP)
+
+
+ results of pushed-down subtrees (derived rows) and raw rows still needed
+
+
+ Group by container
+ repartition by container_id
+
+
+ TimeSeriesCache
+ one container's rows
+
+
+ Evaluate the rest of the expression
+
+ eng_rpm
+
+ ① event intervals
+
+ ② filtered veh_spd
+
+
+
+
+
+ .where(·)
+
+
+
+
+
+ .histogram2d(…)
+ Precomputed results arrive ready-made; pandas evaluates only the rest.
+
+
+ Encode each result
+ solve(): serialized value · calculated channels: rows
+
+
+
+
+
+
+
+
+
+
+ solve() · toPandas()
+ one row per container, one column per selected expression
+
+
+ solve_calculated_channels()
+ rows: container_id, channel_id, tstart, tend, value, identity
+
+
+
+ native: every calculated channel pushed down
+
+
+
+ optional table or opt-in step
+
+ computed natively in Spark
+
+ raw channel rows
+
+ evaluated in pandas
+
+ no pandas UDF
+
diff --git a/docs/impulse/docs/references/query_engine/index.md b/docs/impulse/docs/references/query_engine/index.md
index 8156a6c4..73a1a335 100644
--- a/docs/impulse/docs/references/query_engine/index.md
+++ b/docs/impulse/docs/references/query_engine/index.md
@@ -12,6 +12,9 @@ silver-layer data. It has two parts:
- **[Query Solvers](query_solvers.md)** — the `DefaultSolver` that knows how your silver tables are
laid out, reads them, and evaluates the expressions per container.
+[How a Query Is Solved](solving.md) walks through a `solve()` call stage by stage, from the
+metadata tables to the per-container results.
+
## Two ways to use it
- **Directly, for ad-hoc analysis.** Build a query against a `MeasurementDB` and solve it
diff --git a/docs/impulse/docs/references/query_engine/query_solvers.md b/docs/impulse/docs/references/query_engine/query_solvers.md
index c71cd297..f2f2426b 100644
--- a/docs/impulse/docs/references/query_engine/query_solvers.md
+++ b/docs/impulse/docs/references/query_engine/query_solvers.md
@@ -126,6 +126,9 @@ Solver tuning lives under the `query_engine` section of your report config:
— adapt the solver to a silver layer whose physical column names
diverge from Impulse's internal names, scope reads by `project_id`, or
apply per-table equality filters.
+- [`query_engine.enable_pushdown`](../../config/configuration.md#query_engine-optional)
+ — compute supported expressions natively in Spark before the pandas UDF
+ (see [Spark pushdown](#spark-pushdown-opt-in)). Off by default.
If `query_engine` is omitted from your config entirely, the engine runs
with `DefaultSolver` and `data_type = "RLE"`.
@@ -142,6 +145,8 @@ Every selection must be a [`CalculatedChannel`](../report/channel.md) (identity
need not match across selections), and each wrapped expression must evaluate to a `SampleSeries`. The stage reuses the same
metadata filter pipeline as `solve()` to resolve the input channels, then runs a grouped-map UDF per
container. Only `DefaultSolver` implements it — the base `QuerySolver` raises `NotImplementedError`.
+The `channel_id` column of `channels` must be an integer or long column to hold the calculated
+channels' ids.
```python
from impulse_query_engine.analyze.query.channels.calculated_channel import CalculatedChannel
@@ -152,12 +157,89 @@ cc = CalculatedChannel(
{"channel_name": "speed_kmh", "data_key": "CALC"},
)
df = db.query.select(cc).solve_calculated_channels(spark, solver=DefaultSolver(spark))
-# df: [container_id, channel_id, tstart, tend, value, channel_name, data_key]
+# df: [container_id, channel_id, tstart, tend, value, identity]
```
The reporting layer builds on this stage to persist derived channels to the gold layer — see
[Channels](../report/channel.md).
+## Spark pushdown (opt-in)
+
+Every selection of a solve is evaluated in one pandas UDF per container, and by default the raw
+rows of every channel the selections reference are shipped into it. With pushdown enabled, a
+rule-based optimizer finds expression subtrees that Spark can compute natively, computes them
+**before** the UDF, and ships their (smaller) result instead of the raw rows.
+
+Supported today:
+
+| Expression | Computed in Spark as |
+|------------------------------------|---------------------------------------------------------------------------|
+| `channel.resample(sample_rate)` | The resampled series, identical to `SampleSeries.resample`, on a physical `channel()` or `channel_with_alias()` selection |
+
+The subtree can sit anywhere in an expression, e.g. `veh_spd.resample(1e6).cumtrapz()`, inside a
+`CalculatedChannel`, an aggregation, or an event condition; only the pushed-down part runs in Spark.
+
+```python
+solver = DefaultSolver(spark, enable_pushdown=True)
+```
+
+In a report config, set `query_engine.enable_pushdown` to `true`.
+
+:::note When a channel is pushed down
+All selections of a solve share the raw data of their channels, so a channel is pushed down only
+if **every** use of it in the solve is a pushed-down subtree. If the same channel is also used in
+any other way, e.g. `rpm.resample(1e6)` next to `rpm.max()`, its raw rows are still needed: they are
+shipped as before and the resample runs in pandas. Results are the same either way.
+:::
+
+:::caution Intended for downsampling
+`resample` is pushed down regardless of the rate. A rate finer than the channel's native sample
+spacing (upsampling) produces **more** rows than the raw channel, so enable pushdown for workloads
+that downsample.
+:::
+
+Results match the pandas evaluation exactly for integer timestamp columns with an integral
+`sample_rate` (e.g. µs or ns epochs), and for double timestamps whose grid is exact in binary
+floating point. For double timestamps with a fractional rate such as `0.1`, the grid points can
+differ in the last bit from numpy builds that fuse multiply-add operations. The order of samples
+that share the same `tstart` is unspecified, as in the pandas evaluation, and channels whose
+`[tstart, tend)` intervals overlap are not supported (the RLE and interval encoders never
+produce overlaps).
+
+#### How Spark runs pushed-down subtrees
+
+All pushed-down subtrees of a solve share one pass over the channel data:
+
+- Spark cuts each pushed channel into chunks, equal time slices, and processes the chunks in
+ parallel. A chunk holds about 100 000 samples on average, divided by the number of pushed-down
+ expressions that read the channel (e.g. about 50 000 for `spd.resample(1e6)` next to
+ `spd.resample(5e6)`), as each of them computes on every sample of the chunk. A task holds one
+ chunk at a time, not a whole channel or container; with strongly varying sampling density a
+ chunk can hold more samples.
+- Within a chunk the samples are ordered by `tstart`, then `tend`, then value; samples with equal
+ times are ordered by value. For `resample`, each sample is assigned the grid bucket it falls
+ into, and only the last sample of each bucket emits rows: the grid points up to the next
+ sample's bucket.
+- What crosses a chunk boundary (neighbouring samples, gaps, the start and end of a continuous
+ block) comes from one summary row per chunk.
+- Each sample is shuffled once, however many pushed-down expressions read its channel: chunks,
+ neighbours and continuous blocks are computed once per channel, and only the expressions' own
+ computations run per expression.
+
+The channel data is read **twice**, however many expressions are pushed down: once in full, and
+once with only `container_id`, `channel_id`, `tstart` and `tend` (the channel boundaries that
+define the chunks). Channels that are also used outside pushed-down subtrees, e.g. `rpm.max()`
+next to `spd.resample(1e6)`, are read once more for their raw rows.
+
+### Calculated channels without the pandas UDF
+
+When every calculated channel of a `solve_calculated_channels` call (in a report: of a solve
+batch) is a pushed-down expression as a whole, e.g.
+`CalculatedChannel(veh_spd.resample(1e6), {...})`, its rows are produced entirely in Spark and no
+pandas UDF runs. If any calculated channel of the call needs the pandas evaluation, e.g.
+`veh_spd.resample(1e6) * 3.6`, all of them are evaluated in the UDF as usual; their pushed-down
+parts still run in Spark. Either way the rows are identical.
+
## API reference
Auto-generated symbol-level docs:
@@ -167,3 +249,9 @@ Auto-generated symbol-level docs:
— abstract base class defining the six-stage solver pipeline.
- [`SolverConfig`](../api/impulse_query_engine/analyze/query/solvers/solver_config.md)
— per-table column mappings, filters, and project scoping.
+- [`QueryOptimizer`](../api/impulse_query_engine/analyze/query/optimizer/query_optimizer.md)
+ and [`PushdownRule`](../api/impulse_query_engine/analyze/query/optimizer/pushdown.md)
+ — the Spark pushdown optimizer and the interface for its rules;
+ [`pipeline`](../api/impulse_query_engine/analyze/query/optimizer/pipeline.md) and
+ [`SeriesContext`](../api/impulse_query_engine/analyze/query/optimizer/context.md) — how rules
+ run in Spark and what they can use.
diff --git a/docs/impulse/docs/references/query_engine/solving.md b/docs/impulse/docs/references/query_engine/solving.md
new file mode 100644
index 00000000..aa6728fe
--- /dev/null
+++ b/docs/impulse/docs/references/query_engine/solving.md
@@ -0,0 +1,151 @@
+---
+sidebar_position: 3
+title: How a Query Is Solved
+---
+
+# How a Query Is Solved
+
+One `solve()` call evaluates **every selected expression for every matching container**. The
+expressions themselves are lazy ([Evaluation](tsal/evaluation.md)): nothing is computed until the
+solver runs them through five stages. Query planning (stage 1) runs on the driver; query
+execution (stages 2 to 5) runs in Spark. With [Spark pushdown](query_solvers.md#spark-pushdown-opt-in)
+enabled, Spark computes supported subtrees natively in stage 4, and the pandas UDF in stage 5
+consumes their results instead of evaluating them.
+
+
+
+The example computes a 2D histogram of engine rpm and vehicle speed while the vehicle is
+moving:
+
+```python
+eng_rpm = db.query.channel(channel_name="Engine RPM")
+veh_spd = db.query.channel(channel_name="Vehicle Speed")
+event = veh_spd > 0
+result = db.query.select(
+ eng_rpm.where(event)
+ .histogram2d(veh_spd.where(event), [0, 1000, 2000, 3000, 4000], [0, 50, 100, 150, 200])
+ .alias("rpm_vs_speed"),
+).solve(spark, solver=DefaultSolver(spark, enable_pushdown=True))
+```
+
+## 1. Plan (driver)
+
+- `select(...)` stores the expressions; `solve()` starts by **building each expression once
+ against an empty cache**. This infers every result's type, and with it the schema of the output
+ DataFrame, before any data is read (see
+ [Planning: type inference before solve](tsal/evaluation.md#planning-type-inference-before-solve)).
+ `solve_calculated_channels()` skips this step: its results are always sample rows.
+- With pushdown enabled, the optimizer also picks the subtrees Spark computes natively in
+ stage 4 (see stage 4 for the example).
+
+## 2. Resolve channels (metadata tables)
+
+The solver finds the physical channels each expression reads, using only the metadata tables. No
+sample data is read in this stage. It covers the first five stages of the `QuerySolver` pipeline
+(container tags, container metrics, channel tags, channel metrics, alias resolution); the sixth,
+`solve`, covers stages 3 to 5 here.
+
+1. **Containers.** `container_tags` (optional) and `container_metrics` select the containers of
+ the configured project that match the query's container filters. Incremental report runs pass
+ their pre-filtered containers here.
+2. **Channels.** Every selector of every expression, e.g. `channel(channel_name="Engine RPM")`, is
+ matched against the channel attributes on `channel_tags` (optional, EAV) or `channel_metrics`.
+ A selector should match at most one channel per container
+ (see [Channel selection](tsal/defining_expressions.md#channel-selection)).
+3. **Aliases.** Alongside the direct selectors, selectors written with `channel_with_alias(...)`
+ are resolved through `channel_mapping` (optional), by alias priority.
+4. **Container metadata.** Container tags and metrics that expressions read are attached.
+
+The result is a small table of channel matches: `container_id`, `channel_id` and the
+`selector_ids` of the selectors that read the channel, plus the container metadata and, for
+aliased channels, their source and target units.
+
+## 3. Load channel data (Spark)
+
+- The `channels` table is read with its column mapping and filters, keeping only `container_id`,
+ `channel_id`, `tstart`, `tend` and `value`. Raw data is first encoded into RLE or interval
+ samples; points-in-time data from `poi_channels` is added when an expression selects POI
+ channels.
+- Conversion factors for aliased channels are looked up in `unit_conversion` (optional).
+- The samples are joined with the channel matches from stage 2, which keeps only the matched
+ channels and attaches their selector ids, conversion factors and container metadata. With
+ pushdown, the rows first carry only which results they feed; the rest is attached after
+ stage 4.
+
+## 4. Compute subtrees natively (Spark pushdown, opt-in)
+
+Pushed-down subtrees are computed **in Spark, before any pandas code runs**, and only the rest of
+each expression is left for the pandas UDF. The optimizer is rule-based: each rule computes one
+kind of subtree on the rows of one channel, e.g. `resample(...)` or filter conditions such as
+`veh_spd > 0`. All rules share the same reads of the channel data; rules that need a channel's
+samples in order run in parallel per chunk of the channel (see
+[How Spark runs pushed-down subtrees](query_solvers.md#how-spark-runs-pushed-down-subtrees)).
+
+The example expression forms this tree (`event` is used twice, so it appears twice):
+
+```
+histogram2d(rpm_bins, speed_bins) pandas UDF
+├── where pandas UDF
+│ ├── eng_rpm raw rows
+│ └── veh_spd > 0 ① Spark: condition → Intervals
+└── veh_spd.where(veh_spd > 0) ② Spark: filter → SampleSeries
+```
+
+The optimizer cuts out a subtree when
+
+1. **a rule matches it.** It takes the largest matching subtree: ② as a whole, not just the event
+ inside it.
+2. **it reads a single channel.** `eng_rpm.where(veh_spd > 0)` and `histogram2d` combine two
+ channels and stay in pandas.
+3. **every use of its channel is cut out.** All three uses of `veh_spd` lie in ① or ②, so its raw
+ rows are no longer needed. `eng_rpm` is read by pandas directly, so its raw rows are shipped as
+ before.
+
+The results of cut-out subtrees are **derived rows** that replace the raw rows of their channel.
+Here, instead of the full `veh_spd` channel, the UDF receives the event intervals ① (usually a few
+rows) and the samples with `veh_spd > 0` ②. A channel can ship several derived results like this.
+
+:::note Rules available today
+The optimizer currently implements the `resample` rule; rules for further operations such as
+filter conditions follow the same flow. Until a rule covers a subtree, it is evaluated in stage 5
+as before.
+:::
+
+## 5. Evaluate per container (pandas UDF)
+
+- The rows are **grouped by container**. Each container's rows reach one call of a pandas
+ `GROUPED_MAP` UDF, so one task holds one container's rows in memory.
+- A `TimeSeriesCache` serves the container's channels, its container tags and metrics, and the
+ results of pushed-down subtrees.
+- Each expression is **evaluated bottom-up, except for its pushed-down subtrees**: those arrive as
+ finished results from stage 4. In the example, the UDF clips the raw `eng_rpm` rows to the
+ precomputed event intervals ① and computes the 2D histogram against the precomputed `veh_spd`
+ samples ②. Every other leaf loads its channel from the cache as a `SampleSeries` (or
+ `PointsInTimeSeries` for POI channels), multiplied by the conversion factor for aliased
+ selectors, and each operation then works on its children's results.
+- Each result is encoded for the output. `solve()` serializes it into its column: `SampleSeries`
+ as binary, `Intervals` and points as arrays, histograms as structs, scalars as doubles (see
+ [From expression to result](tsal/evaluation.md#from-expression-to-result)).
+ `solve_calculated_channels()` emits the samples of each calculated channel as rows.
+
+## Results
+
+| Call | Output |
+|------|--------|
+| `solve()` | One row per container, one column per selected expression, named by its alias. |
+| `toPandas()` | The same, as a pandas DataFrame with `SampleSeries` objects deserialized. |
+| `solve_calculated_channels()` | Silver-shaped rows `container_id, channel_id, tstart, tend, value` plus the channel's `identity`. |
+
+When **every** calculated channel of a `solve_calculated_channels()` call is pushed down as a
+whole, e.g. `CalculatedChannel(veh_spd.resample(1e6), {...})`, its rows come straight from stage 4
+and no pandas UDF runs (see
+[Calculated channels without the pandas UDF](query_solvers.md#calculated-channels-without-the-pandas-udf)).
+
+:::note Where the work happens
+Stages 2 to 4 are distributed Spark jobs; for rules that need samples in order, stage 4 even
+splits each channel into chunks that run in parallel. Stage 5 runs in parallel across containers,
+but each container is evaluated in a single task: the more channels and samples a container's
+expressions read, the more memory that task needs. Every pushed-down subtree moves work from
+stage 5 into stage 4 and usually reduces the rows that reach stage 5 (resampling to a finer rate
+than the channel's own adds rows).
+:::
diff --git a/docs/impulse/docs/references/query_engine/tsal/defining_expressions.md b/docs/impulse/docs/references/query_engine/tsal/defining_expressions.md
index dda8ed93..25980249 100644
--- a/docs/impulse/docs/references/query_engine/tsal/defining_expressions.md
+++ b/docs/impulse/docs/references/query_engine/tsal/defining_expressions.md
@@ -31,6 +31,12 @@ Each tag passed to `channel(...)` must be resolvable by the solver — either as
table is configured — see [Query Solvers](../query_solvers.md#how-defaultsolver-adapts).
:::
+:::note When a selection matches several channels
+A selection should identify at most one channel per container. If several channels of a container
+match its tags (e.g. the same `channel_name` under two `data_key`s), one of them is used, and which
+one is unspecified; add tags such as `data_key` to select a specific channel.
+:::
+
### Points-in-Time (POI) channels
Not every channel can be interpreted as a continuous signal within its `[tstart, tend)` intervals.
@@ -95,6 +101,9 @@ channel-identifying columns (by default `channel_name` and `data_key`). See
[Query Solvers](../query_solvers.md) for details.
:::
+If several mapping entries of an alias resolve in a container, the one with the lowest `priority`
+wins; without priorities, or on ties, the lowest `channel_id` is used.
+
When the `channel_mapping` table carries `source_unit` and `target_unit` columns and the report config sets
`source.unit_conversion_table`, values returned from `channel_with_alias()` are automatically converted from source
to target unit before any expression is evaluated. Constants and parameters in expressions over an aliased selector
@@ -177,6 +186,10 @@ type) at execution time.
distance_km = veh_spd.resample(1e6).cumtrapz() / 3600 / 1e6
```
+When downsampling large channels, `.resample(...)` on a physical channel can be computed in Spark
+before the data reaches the pandas evaluation — see
+[Spark pushdown](../query_solvers.md#spark-pushdown-opt-in).
+
### Filtering
| Method | Signature | Description |
diff --git a/docs/impulse/docs/references/query_engine/tsal/evaluation.md b/docs/impulse/docs/references/query_engine/tsal/evaluation.md
index a8734e14..99345aeb 100644
--- a/docs/impulse/docs/references/query_engine/tsal/evaluation.md
+++ b/docs/impulse/docs/references/query_engine/tsal/evaluation.md
@@ -63,7 +63,12 @@ following per container:
| `PointsInTimeSeries` | `ArrayType(ArrayType(DoubleType))` | `[[tstart, value], ...]`|
| scalar | `DoubleType` | the value |
+With [Spark pushdown](../query_solvers.md#spark-pushdown-opt-in) enabled, some subtrees (e.g.
+`channel.resample(rate)`) are computed natively in Spark before step 1. The solver ships their
+results instead of the raw channel rows, and step 2 uses those results in place of evaluating the
+subtrees in pandas. The tree itself is unchanged, and so is every result.
+
`toPandas()` deserializes the binary `SampleSeries` columns back into objects; the array-backed types
are returned as nested lists. See the [Core Data Model](core_data_model.md) for the semantics of each
-result class, and [Query Solvers](../query_solvers.md) for the full solver pipeline and the
-`DefaultSolver` that reads the silver layer.
+result class, [How a Query Is Solved](../solving.md) for the full flow of a `solve()` call, and
+[Query Solvers](../query_solvers.md) for the `DefaultSolver` that reads the silver layer.
diff --git a/docs/impulse/pydoc-markdown.yml b/docs/impulse/pydoc-markdown.yml
index f52dda2b..7fa9bb8d 100644
--- a/docs/impulse/pydoc-markdown.yml
+++ b/docs/impulse/pydoc-markdown.yml
@@ -24,6 +24,12 @@ loaders:
- impulse_query_engine.analyze.query.solvers.default_solver
- impulse_query_engine.analyze.query.solvers.solver_config
- impulse_query_engine.analyze.query.solvers.query_solver
+ - impulse_query_engine.analyze.query.optimizer.pushdown
+ - impulse_query_engine.analyze.query.optimizer.context
+ - impulse_query_engine.analyze.query.optimizer.pipeline
+ - impulse_query_engine.analyze.query.optimizer.plan
+ - impulse_query_engine.analyze.query.optimizer.query_optimizer
+ - impulse_query_engine.analyze.query.optimizer.resample_rule
- impulse_query_engine.model.series.sample_series
- impulse_query_engine.model.series.intervals
- impulse_query_engine.model.series.points_in_time
diff --git a/resources/e2e_job.yml b/resources/e2e_job.yml
index 84d5678a..c61ecf06 100644
--- a/resources/e2e_job.yml
+++ b/resources/e2e_job.yml
@@ -58,6 +58,12 @@ resources:
depends_on: [{task_key: setup}]
notebook_task: {notebook_path: ../tests/e2e/e2e_events.py, base_parameters: *params}
+ - task_key: pushdown
+ environment_key: e2e_env
+ disable_auto_optimization: true
+ depends_on: [{task_key: setup}]
+ notebook_task: {notebook_path: ../tests/e2e/e2e_pushdown.py, base_parameters: *params}
+
# Runs only when all tests passed. Otherwise it ends as "upstream failed", which fails
# the job run, and the schema is kept for debugging.
- task_key: teardown
@@ -70,6 +76,7 @@ resources:
- {task_key: container_filter}
- {task_key: incremental_updates}
- {task_key: events}
+ - {task_key: pushdown}
notebook_task: {notebook_path: ../tests/e2e/teardown.py, base_parameters: *params}
environments:
diff --git a/src/impulse_query_engine/analyze/metadata/time_series_expression.py b/src/impulse_query_engine/analyze/metadata/time_series_expression.py
index 6a2b75bd..8b335011 100644
--- a/src/impulse_query_engine/analyze/metadata/time_series_expression.py
+++ b/src/impulse_query_engine/analyze/metadata/time_series_expression.py
@@ -1226,6 +1226,10 @@ def build(self, cache: SeriesCache):
"""
Build the time series from cache.
+ A result the cache already holds for this node (see
+ :meth:`SeriesCache.materialized`) is returned as-is, without building the
+ operands.
+
Parameters
----------
cache : SeriesCache
@@ -1236,6 +1240,10 @@ def build(self, cache: SeriesCache):
Any
Built time series object.
"""
+ # Looked up on the type so caches without the hook (or test doubles) are skipped.
+ materialized = getattr(type(cache), "materialized", None)
+ if materialized is not None and (result := materialized(cache, self)) is not None:
+ return result
argsb = [a.build(cache) if isinstance(a, TimeSeriesExpression) else a for a in self.args]
kwargsb = {
k: a.build(cache) if isinstance(a, TimeSeriesExpression) else a
diff --git a/src/impulse_query_engine/analyze/query/optimizer/__init__.py b/src/impulse_query_engine/analyze/query/optimizer/__init__.py
new file mode 100644
index 00000000..5c2b8c31
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/__init__.py
@@ -0,0 +1,37 @@
+from .context import ELEMENT_TYPE, Run, RunBreak, Sample, SeriesContext
+from .pipeline import SeriesPipeline, apply_pushdowns
+from .plan import PushdownPlan
+from .pushdown import (
+ RAW_VARIANT,
+ VARIANT_COL,
+ VARIANTS_COL,
+ Operand,
+ Pushdown,
+ PushdownContext,
+ PushdownRule,
+)
+from .query_optimizer import QueryOptimizer, default_rules
+from .resample_rule import ResamplePushdownRule
+from .traversal import Classifier
+
+__all__ = [
+ "ELEMENT_TYPE",
+ "RAW_VARIANT",
+ "VARIANT_COL",
+ "VARIANTS_COL",
+ "Classifier",
+ "Operand",
+ "Pushdown",
+ "PushdownContext",
+ "PushdownPlan",
+ "PushdownRule",
+ "QueryOptimizer",
+ "ResamplePushdownRule",
+ "Run",
+ "RunBreak",
+ "Sample",
+ "SeriesContext",
+ "SeriesPipeline",
+ "apply_pushdowns",
+ "default_rules",
+]
diff --git a/src/impulse_query_engine/analyze/query/optimizer/context.py b/src/impulse_query_engine/analyze/query/optimizer/context.py
new file mode 100644
index 00000000..062125fe
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/context.py
@@ -0,0 +1,282 @@
+"""What a pushdown rule sees of the samples it computes on.
+
+A rule never handles DataFrames. The pipeline (:mod:`.pipeline`) evaluates the
+rule's hooks with a :class:`SeriesContext`, whose properties are Spark columns
+describing the current sample of one of the rule's series: its times and value,
+its neighbours, the run (e.g. continuous block) it belongs to, and facts about
+the whole series. Columns returned by a hook become part of the shared pass
+over the channel data.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Mapping, Sequence
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+from pyspark.sql import Column, Window, WindowSpec
+
+from .pushdown import VARIANT_COL
+
+if TYPE_CHECKING:
+ from .pushdown import Pushdown, PushdownRule
+
+#: Element type of the arrays returned by :meth:`PushdownRule.emit`.
+ELEMENT_TYPE = T.StructType(
+ [
+ T.StructField("tstart", T.DoubleType()),
+ T.StructField("tend", T.DoubleType()),
+ T.StructField("value", T.DoubleType()),
+ ]
+)
+
+#: Name of the built-in run of continuous samples.
+BLOCK = "block"
+
+# Columns the pipeline provides, shared by all rules.
+TS, TE = "_pd_ts", "_pd_te"
+CHUNK = "_pd_chunk"
+PREV_TS, PREV_TE, PREV_VALUE = "_pd_prev_ts", "_pd_prev_te", "_pd_prev_value"
+NEXT_TS, NEXT_TE, NEXT_VALUE = "_pd_next_ts", "_pd_next_te", "_pd_next_value"
+COUNT = "_pd_n"
+LAST = "_pd_last"
+BLOCK_END = "_pd_block_end"
+
+
+def run_col(run: str, name: str) -> str:
+ """Name of the pipeline column *name* of the run *run*."""
+ return f"_pd_run__{run}__{name}"
+
+
+@dataclass(frozen=True)
+class Sample:
+ """One sample as columns: ``tstart`` and ``tend`` as double, and its value as double."""
+
+ tstart: Column
+ tend: Column
+ value: Column
+
+
+#: A run definition: whether the sample *cur* starts a new run after *prev*. It
+#: is called with ``prev`` columns that are null before the series' first
+#: sample; a null result counts as a break.
+RunBreak = Callable[[Sample, Sample], Column]
+
+
+@dataclass(frozen=True)
+class Run:
+ """Where the current sample sits in its run, a maximal stretch without a break.
+
+ Attributes
+ ----------
+ starts : Column
+ The sample starts its run.
+ id : Column
+ Number of the run within the series, from 1.
+ start : Column
+ ``tstart`` of the run's first sample.
+ next_starts : Column
+ The next sample starts a new run; null for the series' last sample.
+ is_last : Column
+ The run is the last of its series.
+ """
+
+ starts: Column
+ id: Column
+ start: Column
+ next_starts: Column
+ is_last: Column
+
+
+def block_break(prev: Sample, cur: Sample) -> Column:
+ """Continuous blocks: a sample continues its block when it starts where the previous ended."""
+ return prev.tend.isNull() | ((cur.tstart - prev.tend) != 0)
+
+
+class SeriesContext:
+ """Columns a rule's hooks build on, for the current sample of one of its series.
+
+ Every series is one channel's samples of one variant, ordered by
+ ``(tstart, tend, value)``; all variants of a channel have the same samples.
+ Rules running in series order (``ordered``) see neighbours, runs and series
+ facts; per-sample rules see only the sample.
+
+ Parameters
+ ----------
+ rule : PushdownRule
+ The rule the hooks belong to.
+ variants : Mapping[int, Pushdown]
+ The rule's variants in this solve.
+ key : Sequence[str]
+ Columns identifying a series.
+ order : Sequence[str]
+ The channel-data ``tstart``, ``tend`` and value columns: the series order.
+ ordered : bool
+ Whether the pipeline runs the rule in series order.
+ runs : Mapping[str, str]
+ The rule's run names to pipeline run names.
+ kept : bool
+ Whether the hook runs on the kept samples (``kept_columns``, ``emit``).
+ """
+
+ def __init__(
+ self,
+ rule: PushdownRule,
+ variants: Mapping[int, Pushdown],
+ key: Sequence[str],
+ order: Sequence[str],
+ ordered: bool,
+ runs: Mapping[str, str],
+ kept: bool = False,
+ ):
+ self._rule = rule
+ self._variants = variants
+ self._key = tuple(key)
+ self._order = tuple(order)
+ self._ordered = ordered
+ self._runs = dict(runs)
+ self._kept = kept
+
+ def kept_stage(self) -> SeriesContext:
+ """This context, for the hooks that run on the kept samples."""
+ return SeriesContext(
+ self._rule,
+ self._variants,
+ self._key,
+ self._order,
+ self._ordered,
+ self._runs,
+ kept=True,
+ )
+
+ # --- always available -------------------------------------------------------
+
+ @property
+ def active(self) -> Column:
+ """The sample belongs to one of the rule's variants."""
+ return F.col(VARIANT_COL).isin(sorted(self._variants))
+
+ def name(self, column: str) -> str:
+ """Pipeline name of the rule's column *column*."""
+ return f"_pd_rule__{self._rule.name}__{column}"
+
+ def col(self, column: str) -> Column:
+ """The rule's column *column*, defined by an earlier hook entry."""
+ return F.col(self.name(column))
+
+ def param(self, name: str) -> Column:
+ """The pushdown parameter *name* of the sample's variant."""
+ return F.try_element_at(
+ F.create_map(
+ *[
+ item
+ for vid, pushdown in sorted(self._variants.items())
+ for item in (F.lit(vid), F.lit(dict(pushdown.params)[name]))
+ ]
+ ),
+ F.col(VARIANT_COL),
+ )
+
+ @property
+ def tstart(self) -> Column:
+ """The sample's ``tstart`` as double."""
+ return F.col(TS)
+
+ @property
+ def tend(self) -> Column:
+ """The sample's ``tend`` as double."""
+ return F.col(TE)
+
+ @property
+ def value(self) -> Column:
+ """The sample's value as double."""
+ return F.col(self._order[2]).cast("double")
+
+ @property
+ def sample(self) -> Sample:
+ """The current sample."""
+ return Sample(self.tstart, self.tend, self.value)
+
+ @staticmethod
+ def element(
+ tstart: Column | None = None, tend: Column | None = None, value: Column | None = None
+ ) -> Column:
+ """One emitted row; ``tstart`` / ``tend`` left out keep the sample's own times."""
+
+ def field(col: Column | None, name: str) -> Column:
+ return (F.lit(None) if col is None else col).cast("double").alias(name)
+
+ return F.struct(field(tstart, "tstart"), field(tend, "tend"), field(value, "value"))
+
+ # --- rules running in series order --------------------------------------------
+
+ def _require_ordered(self, what: str) -> None:
+ if not self._ordered:
+ raise TypeError(f"{self._rule.name}: {what} needs an ordered rule")
+
+ @property
+ def prev(self) -> Sample:
+ """The previous sample of the series; null columns for its first sample."""
+ self._require_ordered("prev")
+ return Sample(F.col(PREV_TS), F.col(PREV_TE), F.col(PREV_VALUE))
+
+ @property
+ def next(self) -> Sample:
+ """The next sample of the series; null columns for its last sample."""
+ self._require_ordered("next")
+ return Sample(F.col(NEXT_TS), F.col(NEXT_TE), F.col(NEXT_VALUE))
+
+ @property
+ def n(self) -> Column:
+ """Number of samples in the series."""
+ self._require_ordered("n")
+ return F.col(COUNT)
+
+ @property
+ def last(self) -> Sample:
+ """The series' last sample."""
+ self._require_ordered("last")
+ last = F.col(LAST)
+ return Sample(last.getField("tstart"), last.getField("tend"), last.getField("value"))
+
+ @property
+ def in_chunk(self) -> WindowSpec:
+ """Window over the sample's chunk in series order, for local neighbour access.
+
+ Chunks are consecutive slices of a channel, the same for all its series;
+ use :attr:`prev`, :attr:`next` and runs for anything that must see across
+ chunk boundaries.
+ """
+ self._require_ordered("in_chunk")
+ return Window.partitionBy(*self._key, CHUNK).orderBy(*self._order)
+
+ @property
+ def block(self) -> Run:
+ """The continuous block the sample belongs to (gaps end blocks)."""
+ self._require_ordered("block")
+ return _run(BLOCK)
+
+ def run(self, name: str) -> Run:
+ """The sample's run of the rule's run definition *name* (see :meth:`PushdownRule.runs`)."""
+ self._require_ordered("run")
+ return _run(self._runs[name])
+
+ @property
+ def block_end(self) -> Column:
+ """``nanmax`` of the ``tend`` values of the sample's block (kept samples only)."""
+ self._require_ordered("block_end")
+ if not self._kept:
+ raise TypeError(f"{self._rule.name}: block_end is available to kept samples only")
+ return F.col(BLOCK_END)
+
+
+def _run(run: str) -> Run:
+ return Run(
+ starts=F.col(run_col(run, "starts")),
+ id=F.col(run_col(run, "id")),
+ start=F.col(run_col(run, "start")),
+ next_starts=F.col(run_col(run, "next_starts")),
+ is_last=F.col(run_col(run, "is_last")),
+ )
diff --git a/src/impulse_query_engine/analyze/query/optimizer/pipeline.py b/src/impulse_query_engine/analyze/query/optimizer/pipeline.py
new file mode 100644
index 00000000..3d9083a9
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/pipeline.py
@@ -0,0 +1,660 @@
+"""Spark execution of a pushdown plan: one shared pass for all rules.
+
+:func:`apply_pushdowns` turns the channel data of a solve into the rows the
+solver ships: raw rows where still needed, plus every rule's derived rows, each
+tagged with its variant. Rules contribute columns only (see
+:class:`~.pushdown.PushdownRule`), so the number of reads of the channel data
+does not grow with the number of rules:
+
+* **Ordered rules** (:class:`SeriesPipeline`) read the channel data once in full
+ and once narrowly (channel boundaries, aggregated before their shuffle). Each
+ sample is shuffled once, by channel and chunk, however many variants its
+ channel ships, and processed per chunk in parallel.
+* **Raw rows and per-sample rules** share one more read, only when present.
+
+The ordered pass, per channel (its samples ordered by ``(tstart, tend, value)``):
+
+1. Channel boundaries give each sample its chunk: equal time slices holding
+ about :data:`CHUNK_ROWS` / k samples on average for a channel with k variants.
+2. A window per chunk derives each sample's neighbours and run breaks.
+3. One summary row per chunk, scanned in chunk order, carries what crosses chunk
+ boundaries: neighbours at chunk edges, run numbers and starts, the channel's
+ sample count and last sample, and block ends.
+4. The samples fan out into one row per variant: a series, one channel's samples
+ of one variant. Stages 1 to 3 are the same for every variant of a channel,
+ so they run once; a chunk again holds about :data:`CHUNK_ROWS` rows.
+5. Rules add per-sample columns (``row_columns``) and select samples (``keep``).
+6. Kept samples get their block end; rules add ``kept_columns``.
+7. Rules ``emit`` the derived rows.
+
+Windows never span more than a chunk of samples (or the chunk summaries of a
+channel), so the memory per task is bounded by the chunk size.
+
+Keeping one full read for the chunk pass and its summaries relies on both
+seeing the same plan below the shuffle:
+
+* The same projected rows: the raw sample columns and a chunk id that cannot be
+ null. Windows over the samples read raw columns only; a derived window input
+ is computed before the shuffle, and each consumer would keep only the inputs
+ it uses.
+* The same runtime filters: the full read is joined with *variants* like the
+ narrow read, so both copies are pruned alike, and the summaries are joined on
+ keys the planner cannot trace to the scan, so no filter on the chunk pass is
+ built from its own summaries.
+* The value as the last sort key, and summaries that depend on the chunk window.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from functools import reduce
+
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+from pyspark.sql import Column, DataFrame, Window
+
+from .context import (
+ BLOCK,
+ BLOCK_END,
+ CHUNK,
+ COUNT,
+ LAST,
+ NEXT_TE,
+ NEXT_TS,
+ NEXT_VALUE,
+ PREV_TE,
+ PREV_TS,
+ PREV_VALUE,
+ TE,
+ TS,
+ RunBreak,
+ Sample,
+ SeriesContext,
+ block_break,
+ run_col,
+)
+from .plan import PushdownPlan
+from .pushdown import (
+ RAW_VARIANT,
+ VARIANT_COL,
+ VARIANTS_COL,
+ Pushdown,
+ PushdownRule,
+ check_name,
+)
+
+#: Average number of rows per chunk of a channel, counting a sample once per variant.
+CHUNK_ROWS = 100_000
+
+_ELEMENT = "_pd_element"
+_KEPT = "_pd_kept"
+
+
+def apply_pushdowns(
+ rows: DataFrame, variants: DataFrame, plan: PushdownPlan, config, include_raw: bool = True
+) -> DataFrame:
+ """The rows a solve ships: raw rows (when *include_raw*) and all derived rows.
+
+ Parameters
+ ----------
+ rows : pyspark.sql.DataFrame
+ Channel data ``(container_id, channel_id, tstart, tend, value)``, one row
+ per sample.
+ variants : pyspark.sql.DataFrame
+ ``(container_id, channel_id, VARIANTS_COL)``: the variant ids each matched
+ channel ships, one row per channel (see :meth:`PushdownPlan.channel_variants`).
+ Channels without a row are dropped.
+ plan : PushdownPlan
+ The solve's pushdown plan.
+ config : SolverConfig
+ Provides the internal column names.
+ include_raw : bool
+ Whether to keep the raw rows (variant 0).
+
+ Returns
+ -------
+ pyspark.sql.DataFrame
+ Rows with the columns and types of *rows* plus :data:`VARIANT_COL`.
+ """
+ groups = plan.variants_by_rule()
+ ordered = [(rule, variants) for rule, variants in groups if rule.ordered]
+ per_sample = [(rule, variants) for rule, variants in groups if not rule.ordered]
+ key = [config.container_id_col, config.channel_id_col]
+ schema = _output_schema(rows)
+ parts = []
+ row_variants = ([RAW_VARIANT] if include_raw else []) + _variant_ids(per_sample)
+ if row_variants:
+ # Only the channels shipping a row-branch variant, so the join drops the others.
+ shipping = variants.select(*key, _shipped(row_variants).alias(VARIANTS_COL))
+ tagged = rows.join(F.broadcast(shipping.where(F.size(VARIANTS_COL) > 0)), on=key).select(
+ *rows.columns, F.explode(VARIANTS_COL).alias(VARIANT_COL)
+ )
+ parts.append(_row_branch(tagged, per_sample, config, schema))
+ if ordered:
+ parts.append(SeriesPipeline(ordered, config).run(rows, variants))
+ if not parts:
+ return rows.withColumn(VARIANT_COL, F.lit(None).cast(T.IntegerType())).limit(0)
+ return reduce(lambda a, b: a.unionByName(b), parts)
+
+
+def _variant_ids(groups: Sequence[tuple[PushdownRule, Mapping[int, Pushdown]]]) -> list[int]:
+ return sorted(vid for _, variants in groups for vid in variants)
+
+
+def _shipped(ids: Sequence[int]) -> Column:
+ """The variant ids among *ids* that a channel ships, from its :data:`VARIANTS_COL`."""
+ return F.filter(F.col(VARIANTS_COL), lambda vid: vid.isin(sorted(ids)))
+
+
+def _fan_out(df: DataFrame, variants: DataFrame, key: Sequence[str], ids: Sequence[int]):
+ """*df* with one row per variant among *ids* its channel ships, in :data:`VARIANT_COL`.
+
+ Channels shipping none of *ids* are dropped. Unlike ``explode``, ``explode_outer``
+ adds no inferred filter to the broadcast side of the join, so the pipeline's joins
+ with *variants* share one broadcast.
+ """
+ return (
+ df.join(F.broadcast(variants), on=key)
+ .withColumn(VARIANT_COL, F.explode_outer(_shipped(ids)))
+ .where(F.col(VARIANT_COL).isNotNull())
+ .drop(VARIANTS_COL)
+ )
+
+
+def _opaque_join(df: DataFrame, small: DataFrame, on: Sequence[str]) -> DataFrame:
+ """*df* joined with the broadcast *small* on keys the planner cannot trace to a scan.
+
+ *df*'s keys are copied behind an always-true non-deterministic guard, so no
+ runtime filter built from *small* reaches *df*'s scans. When *small* is
+ computed from *df*, such a filter would make the two copies of *df*'s plan
+ differ and keep them from sharing their shuffle. The keys must not be null.
+ """
+ guarded = {c: f"_pd_join_{c}" for c in on}
+ # spark_partition_id() is never negative; Spark folds comparisons on rand() away.
+ guard = F.spark_partition_id() >= 0
+ left = df.select("*", *[F.when(guard, F.col(c)).alias(g) for c, g in guarded.items()])
+ right = small.select(*[F.col(c).alias(guarded.get(c, c)) for c in small.columns])
+ return left.join(F.broadcast(right), on=list(guarded.values())).drop(*guarded.values())
+
+
+def _output_schema(rows: DataFrame) -> T.StructType:
+ """The columns and types of the shipped rows: those of *rows* plus the variant."""
+ return T.StructType([*rows.schema.fields, T.StructField(VARIANT_COL, T.IntegerType())])
+
+
+@dataclass(frozen=True)
+class _Run:
+ """A run definition of the pipeline."""
+
+ name: str
+ breaks: RunBreak
+
+
+class SeriesPipeline:
+ """The ordered pass shared by all rules that need their series in order.
+
+ Chunking and everything that crosses chunk edges runs once per channel; its
+ samples fan out into one series per variant only for the rules' hooks. The
+ shuffle therefore carries each sample once, while a task after it processes
+ about k rows per shuffled sample of a channel with k variants (chunks are k
+ times smaller to keep the rows per chunk at :data:`CHUNK_ROWS`).
+
+ Rule runs are computed for every channel of the pass; a rule only reads its
+ own runs, on its own series.
+
+ Parameters
+ ----------
+ rules : Sequence of (PushdownRule, Mapping[int, Pushdown])
+ Ordered rules with their variants in this solve.
+ config : SolverConfig
+ Provides the internal column names.
+ """
+
+ def __init__(self, rules: Sequence[tuple[PushdownRule, Mapping[int, Pushdown]]], config):
+ self.rules = list(rules)
+ self.config = config
+ self.channel_key = [config.container_id_col, config.channel_id_col]
+ self.key = [*self.channel_key, VARIANT_COL]
+ self.order = [config.tstart_col, config.tend_col, config.value_col]
+ self.variant_ids = _variant_ids(self.rules)
+ self.runs = [_Run(BLOCK, block_break)]
+ self.rule_runs: dict[str, dict[str, str]] = {}
+ for rule, _ in self.rules:
+ names = {}
+ for name, breaks in rule.runs().items():
+ names[name] = f"{rule.name}__{check_name('run', name)}"
+ self.runs.append(_Run(names[name], breaks))
+ self.rule_runs[rule.name] = names
+
+ def _context(self, rule: PushdownRule, variants: Mapping[int, Pushdown]) -> SeriesContext:
+ return SeriesContext(rule, variants, self.key, self.order, True, self.rule_runs[rule.name])
+
+ def run(self, rows: DataFrame, variants: DataFrame) -> DataFrame:
+ """Replace the samples of the rules' variants with their derived rows.
+
+ *rows* holds the channel data, one row per sample; *variants* the variant
+ ids of each channel (see :func:`apply_pushdowns`). Only channels shipping a
+ variant of the rules are processed.
+ """
+ df = self._chunked(rows, variants)
+ df = self._chunk_scan(df)
+ df = _opaque_join(df, self._chunk_summaries(df), [*self.channel_key, CHUNK])
+ df = self._resolve_neighbours_and_runs(df)
+ # The rules' columns read the variant (SeriesContext.active, .param).
+ df = _fan_out(df, variants, self.channel_key, self.variant_ids)
+ contexts = [(rule, self._context(rule, variants)) for rule, variants in self.rules]
+ for rule, ctx in contexts:
+ df = _with_rule_columns(df, ctx, rule.row_columns(ctx))
+ df = _keep(df, contexts, extra=_flag(run_col(BLOCK, "ends")))
+ df = self._with_block_end(df)
+ kept = [(rule, ctx.kept_stage()) for rule, ctx in contexts]
+ for rule, ctx in kept:
+ df = _with_rule_columns(df, ctx, rule.kept_columns(ctx))
+ return _emit(df, kept, _output_schema(rows), self.order, raw=None)
+
+ # --- stages ----------------------------------------------------------------------
+
+ def _chunked(self, rows: DataFrame, variants: DataFrame) -> DataFrame:
+ """Channel boundaries and each sample's chunk, with the raw sample columns.
+
+ A channel with k variants gets k times as many chunks: its samples fan out
+ per chunk before the rules' hooks (see :meth:`run`).
+ """
+ ts, te, value = self.order
+ ts_d, te_d = F.col(ts).cast("double"), F.col(te).cast("double")
+ matched = rows.join(F.broadcast(variants), on=self.channel_key)
+ # The time range spans the finite times; a channel without a finite tend is
+ # still cut by its tstart range.
+ series = (
+ matched.groupBy(*self.channel_key).agg(
+ F.min(_nan_free(ts_d)).alias("_pd_first"),
+ F.greatest(F.max(_nan_free(te_d)), F.max(_nan_free(ts_d))).alias("_pd_end"),
+ F.count(F.lit(1)).alias("_pd_count"),
+ F.max(F.size(_shipped(self.variant_ids))).alias("_pd_k"),
+ )
+ # Only the channels shipping a variant of the rules; filtering the aggregate
+ # keeps the broadcast of *variants* the same as for the other joins.
+ .where(F.col("_pd_k") > 0)
+ )
+ first, end = F.col("_pd_first"), F.col("_pd_end")
+ n_chunks = F.ceil(F.col("_pd_count") * F.col("_pd_k") / F.lit(CHUNK_ROWS))
+ position = F.least(F.floor((ts_d - first) / (end - first) * n_chunks), n_chunks - 1)
+ # Null and NaN tstarts go where the series order puts them: first and last.
+ chunk = F.coalesce(
+ F.when(ts_d.isNull(), F.lit(0).cast("long"))
+ .when(F.isnan(ts_d), n_chunks - 1)
+ .when(end > first, position),
+ F.lit(0).cast("long"),
+ )
+ return matched.join(F.broadcast(series), on=self.channel_key).select(
+ *self.channel_key, ts, te, value, chunk.alias(CHUNK)
+ )
+
+ def _chunk_scan(self, df: DataFrame) -> DataFrame:
+ """Neighbours within the chunk, and each run's breaks and segments."""
+ ts, te, value = self.order
+ in_chunk = Window.partitionBy(*self.channel_key, CHUNK).orderBy(*self.order)
+ running = in_chunk.rowsBetween(Window.unboundedPreceding, Window.currentRow)
+ # The neighbour windows read the raw columns and cast their results.
+ df = (
+ df.withColumn("_pd_first_in_chunk", F.row_number().over(in_chunk) == 1)
+ .withColumn("_pd_last_in_chunk", F.lead(F.lit(True)).over(in_chunk).isNull())
+ .withColumn("_pd_lag_ts", F.lag(ts).over(in_chunk).cast("double"))
+ .withColumn("_pd_lag_te", F.lag(te).over(in_chunk).cast("double"))
+ .withColumn("_pd_lag_value", F.lag(value).over(in_chunk))
+ .withColumn("_pd_lead_ts", F.lead(ts).over(in_chunk).cast("double"))
+ .withColumn("_pd_lead_te", F.lead(te).over(in_chunk).cast("double"))
+ .withColumn("_pd_lead_value", F.lead(value).over(in_chunk))
+ .withColumn(TS, F.col(ts).cast("double"))
+ .withColumn(TE, F.col(te).cast("double"))
+ )
+ prev = Sample(
+ F.col("_pd_lag_ts"), F.col("_pd_lag_te"), F.col("_pd_lag_value").cast("double")
+ )
+ cur = Sample(F.col(TS), F.col(TE), F.col(value).cast("double"))
+ for run in self.runs:
+ brk = F.col(run_col(run.name, "brk"))
+ df = (
+ df.withColumn(
+ run_col(run.name, "brk"),
+ F.when(F.col("_pd_first_in_chunk"), F.lit(0).cast("long")).otherwise(
+ _breaks(run, prev, cur)
+ ),
+ )
+ .withColumn(run_col(run.name, "seg"), F.sum(brk).over(running))
+ .withColumn(
+ run_col(run.name, "seg_start"),
+ F.last(F.when(brk == 1, F.col(TS)), ignorenulls=True).over(running),
+ )
+ .withColumn(run_col(run.name, "lead_brk"), F.lead(brk).over(in_chunk))
+ )
+ return df
+
+ def _chunk_summaries(self, df: DataFrame) -> DataFrame:
+ """One row per chunk with everything that crosses chunk boundaries."""
+ order = F.struct(*self.order)
+ sample = F.struct(
+ F.col(TS).alias("tstart"),
+ F.col(TE).alias("tend"),
+ F.col(self.order[2]).cast("double").alias("value"),
+ )
+ block_brk, block_seg = F.col(run_col(BLOCK, "brk")), F.col(run_col(BLOCK, "seg"))
+ # Head and tail are taken via the chunk window's flags, so the summaries always
+ # run on the chunk-partitioned samples and share their read.
+ head = F.when(F.col("_pd_first_in_chunk"), F.lit(1))
+ tail = F.when(F.col("_pd_last_in_chunk"), F.lit(1))
+ aggregates = [
+ F.max_by(sample, head).alias("_pd_head"),
+ F.max_by(sample, tail).alias("_pd_tail"),
+ F.max_by(block_brk, tail).alias("_pd_tail_brk"),
+ F.count(F.lit(1)).alias("_pd_chunk_count"),
+ # The block of the chunk's first sample: its last sample within the chunk.
+ F.max_by(sample, F.when(block_seg == 0, order)).alias("_pd_seg0_last"),
+ F.sum(F.when(block_seg == 0, 1).otherwise(0)).alias("_pd_seg0_count"),
+ ]
+ for run in self.runs:
+ brk = F.col(run_col(run.name, "brk"))
+ aggregates += [
+ F.sum(brk).alias(run_col(run.name, "inner")),
+ F.max(F.when(brk == 1, F.col(TS))).alias(run_col(run.name, "last_start")),
+ ]
+ chunks = df.groupBy(*self.channel_key, CHUNK).agg(*aggregates)
+
+ by_chunk = Window.partitionBy(*self.channel_key).orderBy(CHUNK)
+ before = by_chunk.rowsBetween(Window.unboundedPreceding, -1)
+ so_far = by_chunk.rowsBetween(Window.unboundedPreceding, Window.currentRow)
+ everything = by_chunk.rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing)
+ later = (
+ Window.partitionBy(*self.channel_key)
+ .orderBy(F.col(CHUNK).desc())
+ .rowsBetween(Window.unboundedPreceding, -1)
+ )
+ head, tail = F.col("_pd_head"), F.col("_pd_tail")
+ for field in ("tstart", "tend", "value"):
+ chunks = chunks.withColumn(
+ f"_pd_prev_tail_{field}", F.lag(tail.getField(field)).over(by_chunk)
+ ).withColumn(f"_pd_next_head_{field}", F.lead(head.getField(field)).over(by_chunk))
+ prev_tail = Sample(*(F.col(f"_pd_prev_tail_{f}") for f in ("tstart", "tend", "value")))
+ head_sample = Sample(*(head.getField(f) for f in ("tstart", "tend", "value")))
+ keep = [*self.channel_key, CHUNK, "_pd_head", "_pd_tail"]
+ keep += [f"_pd_prev_tail_{f}" for f in ("tstart", "tend", "value")]
+ keep += [f"_pd_next_head_{f}" for f in ("tstart", "tend", "value")]
+ for run in self.runs:
+ head_brk = F.col(run_col(run.name, "head_brk"))
+ inner = F.col(run_col(run.name, "inner"))
+ starts_here = F.coalesce(
+ F.col(run_col(run.name, "last_start")),
+ F.when(head_brk == 1, head.getField("tstart")),
+ )
+ chunks = (
+ chunks.withColumn(
+ run_col(run.name, "head_brk"),
+ _breaks(run, prev_tail, head_sample),
+ )
+ .withColumn(run_col(run.name, "breaks"), head_brk + inner)
+ .withColumn(
+ run_col(run.name, "base"),
+ F.sum(run_col(run.name, "breaks")).over(so_far) - inner,
+ )
+ .withColumn(
+ run_col(run.name, "head_start"),
+ F.when(head_brk == 1, head.getField("tstart")).otherwise(
+ F.last(starts_here, ignorenulls=True).over(before)
+ ),
+ )
+ .withColumn(run_col(run.name, "next_head_brk"), F.lead(head_brk).over(by_chunk))
+ .withColumn(
+ run_col(run.name, "total"), F.sum(run_col(run.name, "breaks")).over(everything)
+ )
+ )
+ keep += [
+ run_col(run.name, c)
+ for c in ("head_brk", "base", "head_start", "next_head_brk", "total")
+ ]
+
+ # Block ends, from the block's last sample (see _block_end_at), wrapped in a
+ # struct: a block's end may be null, and must not be mistaken for none.
+ head_brk = F.col(run_col(BLOCK, "head_brk")) == 1
+ count = F.col("_pd_chunk_count")
+ tail_single = F.when(count == 1, head_brk).otherwise(F.col("_pd_tail_brk") == 1)
+ seg0_single = (F.col("_pd_seg0_count") == 1) & head_brk
+ tail_ends = F.coalesce(F.col(run_col(BLOCK, "next_head_brk")) == 1, F.lit(True))
+ tail_end = _found(_block_end_at(tail, tail_single))
+ chunks = (
+ chunks.withColumn(COUNT, F.sum(count).over(everything))
+ .withColumn(LAST, F.last(tail).over(everything))
+ # The end of the block the chunk starts in, if that block ends here.
+ .withColumn(
+ "_pd_first_end",
+ F.when(
+ F.col(run_col(BLOCK, "inner")) > 0,
+ _found(_block_end_at(F.col("_pd_seg0_last"), seg0_single)),
+ ).when(tail_ends, tail_end),
+ )
+ .withColumn(
+ "_pd_tail_block_end",
+ F.when(tail_ends, tail_end).otherwise(
+ F.last("_pd_first_end", ignorenulls=True).over(later)
+ ),
+ )
+ )
+ return chunks.select(*keep, COUNT, LAST, "_pd_tail_block_end")
+
+ def _resolve_neighbours_and_runs(self, df: DataFrame) -> DataFrame:
+ """Neighbours and run facts of every sample, across chunk boundaries."""
+ first, last = F.col("_pd_first_in_chunk"), F.col("_pd_last_in_chunk")
+ df = (
+ df.withColumn(
+ PREV_TS,
+ F.when(first, F.col("_pd_prev_tail_tstart")).otherwise(F.col("_pd_lag_ts")),
+ )
+ .withColumn(
+ PREV_TE, F.when(first, F.col("_pd_prev_tail_tend")).otherwise(F.col("_pd_lag_te"))
+ )
+ .withColumn(
+ PREV_VALUE,
+ F.when(first, F.col("_pd_prev_tail_value")).otherwise(
+ F.col("_pd_lag_value").cast("double")
+ ),
+ )
+ .withColumn(
+ NEXT_TS,
+ F.when(last, F.col("_pd_next_head_tstart")).otherwise(F.col("_pd_lead_ts")),
+ )
+ .withColumn(
+ NEXT_TE, F.when(last, F.col("_pd_next_head_tend")).otherwise(F.col("_pd_lead_te"))
+ )
+ .withColumn(
+ NEXT_VALUE,
+ F.when(last, F.col("_pd_next_head_value")).otherwise(
+ F.col("_pd_lead_value").cast("double")
+ ),
+ )
+ )
+ for run in self.runs:
+
+ def c(name: str, run: _Run = run) -> Column:
+ return F.col(run_col(run.name, name))
+
+ df = (
+ df.withColumn(
+ run_col(run.name, "starts"),
+ F.when(first, c("head_brk") == 1).otherwise(c("brk") == 1),
+ )
+ .withColumn(run_col(run.name, "id"), c("base") + c("seg"))
+ .withColumn(
+ run_col(run.name, "start"), F.coalesce(c("seg_start"), c("head_start"))
+ )
+ .withColumn(
+ run_col(run.name, "next_starts"),
+ F.when(last, c("next_head_brk") == 1).otherwise(c("lead_brk") == 1),
+ )
+ .withColumn(run_col(run.name, "is_last"), c("id") == c("total"))
+ )
+ # The last sample of a block (also the series' last sample).
+ return df.withColumn(
+ run_col(BLOCK, "ends"),
+ F.coalesce(F.col(run_col(BLOCK, "next_starts")), F.lit(True)),
+ )
+
+ def _with_block_end(self, df: DataFrame) -> DataFrame:
+ """Each kept sample's block end: the nearest block end at or after it.
+
+ Every block's last sample is kept, so within a chunk a reverse scan over
+ the kept samples finds it; a block that continues past the chunk ends
+ where the chunk summary says.
+ """
+ reverse = (
+ Window.partitionBy(*self.key, CHUNK)
+ .orderBy(*[F.col(c).desc() for c in self.order])
+ .rowsBetween(Window.unboundedPreceding, Window.currentRow)
+ )
+ here = F.when(
+ F.col(run_col(BLOCK, "ends")),
+ _found(
+ _block_end_at(
+ F.struct(F.col(TS).alias("tstart"), F.col(TE).alias("tend")),
+ F.col(run_col(BLOCK, "starts")),
+ )
+ ),
+ )
+ found = F.coalesce(
+ F.last(here, ignorenulls=True).over(reverse), F.col("_pd_tail_block_end")
+ )
+ return df.withColumn(BLOCK_END, found.getField("end"))
+
+
+def _row_branch(
+ rows: DataFrame,
+ rules: Sequence[tuple[PushdownRule, Mapping[int, Pushdown]]],
+ config,
+ schema: T.StructType,
+) -> DataFrame:
+ """Raw rows unchanged, and the derived rows of per-sample rules.
+
+ *rows* are tagged with :data:`VARIANT_COL`, one copy per variant; *schema* gives
+ the output columns.
+ """
+ if not rules:
+ return rows
+ key = [config.container_id_col, config.channel_id_col, VARIANT_COL]
+ order = [config.tstart_col, config.tend_col, config.value_col]
+ df = rows.withColumn(TS, F.col(order[0]).cast("double")).withColumn(
+ TE, F.col(order[1]).cast("double")
+ )
+ contexts = [
+ (rule, SeriesContext(rule, variants, key, order, False, {})) for rule, variants in rules
+ ]
+ for rule, ctx in contexts:
+ df = _with_rule_columns(df, ctx, rule.row_columns(ctx))
+ df = _keep(df, contexts, extra=F.col(VARIANT_COL) == RAW_VARIANT)
+ kept = [(rule, ctx.kept_stage()) for rule, ctx in contexts]
+ for rule, ctx in kept:
+ df = _with_rule_columns(df, ctx, rule.kept_columns(ctx))
+ return _emit(df, kept, schema, order, raw=F.col(VARIANT_COL) == RAW_VARIANT)
+
+
+# --- helpers shared by both branches ------------------------------------------------
+
+
+def _with_rule_columns(df: DataFrame, ctx: SeriesContext, columns: Mapping[str, Column]):
+ """Add a rule's columns, defined for the rule's samples only."""
+ for name, column in columns.items():
+ df = df.withColumn(ctx.name(check_name("column", name)), F.when(ctx.active, column))
+ return df
+
+
+def _keep_col(rule: PushdownRule) -> str:
+ return f"_pd_keep__{rule.name}"
+
+
+def _keep(df: DataFrame, contexts, extra: Column) -> DataFrame:
+ """Drop the samples no rule keeps (*extra* marks samples kept regardless).
+
+ The filter reads a flag column behind an always-true non-deterministic guard.
+ Spark pushes filters through deterministic projections by substituting their
+ column definitions, without a cost check; rule columns that refer to each other
+ several times would then multiply into one huge condition. The guard stops the
+ pushdown, so the rule columns are computed once, in their own projections.
+ """
+ keep = extra
+ for rule, ctx in contexts:
+ df = df.withColumn(
+ _keep_col(rule), F.coalesce(F.when(ctx.active, rule.keep(ctx)), F.lit(False))
+ )
+ keep = keep | F.col(_keep_col(rule))
+ # spark_partition_id() is never negative; Spark folds comparisons on rand() away.
+ guarded = F.when(F.spark_partition_id() >= 0, keep)
+ return df.withColumn(_KEPT, guarded).where(F.col(_KEPT)).drop(_KEPT)
+
+
+def _emit(
+ df: DataFrame, contexts, schema: T.StructType, order: Sequence[str], raw: Column | None
+) -> DataFrame:
+ """Explode the rules' emitted rows into rows with the columns and types of *schema*.
+
+ *raw* marks rows passed through unchanged.
+ """
+ elements = None
+ for rule, ctx in contexts:
+ emitted = F.when(F.col(_keep_col(rule)), rule.emit(ctx))
+ elements = (
+ F.when(ctx.active, emitted) if elements is None else elements.when(ctx.active, emitted)
+ )
+ original = F.lit(False) if raw is None else raw
+ if raw is not None:
+ # One placeholder element keeps each raw row.
+ elements = F.when(raw, F.array(SeriesContext.element())).otherwise(elements)
+ element = F.col(_ELEMENT)
+ fields = dict(zip(order, ("tstart", "tend", "value"), strict=True))
+
+ def out(col: str) -> Column:
+ if col not in df.columns:
+ # Columns beyond the channel-data shape (e.g. POI string values) are not
+ # carried through the series scan; derived rows have none.
+ return F.lit(None).cast(schema[col].dataType).alias(col)
+ if col not in fields:
+ return F.col(col)
+ derived = element.getField(fields[col]).cast(schema[col].dataType)
+ # Times left out of an element keep the sample's own.
+ keep = (
+ original
+ if fields[col] == "value"
+ else original | element.getField(fields[col]).isNull()
+ )
+ return F.when(keep, F.col(col)).otherwise(derived).alias(col)
+
+ return df.withColumn(_ELEMENT, F.explode(elements)).select(
+ *[out(c) for c in schema.fieldNames()]
+ )
+
+
+def _breaks(run: _Run, prev: Sample, cur: Sample) -> Column:
+ """1 where *cur* starts a new run after *prev* (a null break counts as one)."""
+ return F.coalesce(run.breaks(prev, cur), F.lit(True)).cast("long")
+
+
+def _flag(name: str) -> Column:
+ return F.coalesce(F.col(name), F.lit(False))
+
+
+def _nan_free(col: Column) -> Column:
+ """Map NaN to null so ``min`` / ``max`` behave like ``numpy.nanmin`` / ``nanmax``."""
+ return F.when(~F.isnan(col), col)
+
+
+def _found(end: Column) -> Column:
+ """*end* in a struct that is not null even when *end* is."""
+ return F.struct(end.alias("end"))
+
+
+def _block_end_at(last_sample: Column, single: Column) -> Column:
+ """``nanmax`` of a block's ``tend`` values, from its last sample.
+
+ Within a block every ``tend`` equals the next sample's ``tstart``, so the
+ maximum is the last ``tend`` or, for blocks of several samples, the last
+ ``tstart`` if larger (NaN ``tend`` ignored).
+ """
+ tend = _nan_free(last_sample.getField("tend"))
+ return F.when(single, tend).otherwise(F.greatest(last_sample.getField("tstart"), tend))
diff --git a/src/impulse_query_engine/analyze/query/optimizer/plan.py b/src/impulse_query_engine/analyze/query/optimizer/plan.py
new file mode 100644
index 00000000..295b918f
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/plan.py
@@ -0,0 +1,230 @@
+"""The outcome of query optimization: which subtrees Spark computes ahead of the UDF."""
+
+from __future__ import annotations
+
+from collections.abc import Iterable, Mapping
+from dataclasses import dataclass, field
+from typing import Any
+
+import pyspark.sql.functions as F
+from pyspark.sql import Column
+
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesExpression
+
+from .pushdown import RAW_VARIANT, Pushdown, PushdownRule
+from .traversal import Classifier
+
+
+@dataclass(frozen=True, eq=False)
+class PushdownPlan:
+ """Pushdowns selected for one solve call, shipped to the solver and the UDF.
+
+ Variant ``0`` is the raw channel data; variants ``1..n`` are derived series,
+ one per distinct :attr:`Pushdown.variant_key`.
+
+ Parameters
+ ----------
+ rules : tuple of PushdownRule
+ Rules used to recognize pushed-down nodes during evaluation.
+ variant_ids : Mapping[tuple, int]
+ Variant key to variant id.
+ pushdowns_by_variant : Mapping[int, Pushdown]
+ Variant id to a representative pushdown (rule and parameters).
+ variants_by_selector : Mapping[int, tuple of int]
+ Selector id to the variant ids derived from it.
+ raw_selector_ids : frozenset of int
+ Selectors whose raw channel data is still required.
+ """
+
+ rules: tuple[PushdownRule, ...] = ()
+ variant_ids: Mapping[tuple, int] = field(default_factory=dict)
+ pushdowns_by_variant: Mapping[int, Pushdown] = field(default_factory=dict)
+ variants_by_selector: Mapping[int, tuple[int, ...]] = field(default_factory=dict)
+ raw_selector_ids: frozenset[int] = frozenset()
+
+ @staticmethod
+ def build(
+ rules: Iterable[PushdownRule],
+ pushdowns: Iterable[Pushdown],
+ selector_ids: Iterable[int],
+ ) -> PushdownPlan:
+ """Assign variant ids to *pushdowns*, in discovery order.
+
+ Parameters
+ ----------
+ rules : Iterable[PushdownRule]
+ The optimizer's rules.
+ pushdowns : Iterable[Pushdown]
+ Accepted pushdowns; each one's selector is pushed down entirely.
+ selector_ids : Iterable[int]
+ Ids of all selectors in the solve.
+
+ Raises
+ ------
+ ValueError
+ If a pushdown computes on another pushdown's result (``source``),
+ which the pipeline cannot compute.
+ """
+ variant_ids: dict[tuple, int] = {}
+ pushdowns_by_variant: dict[int, Pushdown] = {}
+ variants_by_selector: dict[int, list[int]] = {}
+ for pushdown in pushdowns:
+ if pushdown.source is not None:
+ raise ValueError(f"pushdown {pushdown.describe()} has a source")
+ key = pushdown.variant_key
+ if key not in variant_ids:
+ variant_ids[key] = len(variant_ids) + 1
+ pushdowns_by_variant[variant_ids[key]] = pushdown
+ variants = variants_by_selector.setdefault(pushdown.selector.selector_id, [])
+ if variant_ids[key] not in variants:
+ variants.append(variant_ids[key])
+ return PushdownPlan(
+ rules=tuple(rules),
+ variant_ids=variant_ids,
+ pushdowns_by_variant=pushdowns_by_variant,
+ variants_by_selector={sid: tuple(v) for sid, v in variants_by_selector.items()},
+ raw_selector_ids=frozenset(selector_ids) - variants_by_selector.keys(),
+ )
+
+ @property
+ def is_empty(self) -> bool:
+ """``True`` when nothing is pushed down."""
+ return not self.variant_ids
+
+ @property
+ def pushed_selector_ids(self) -> frozenset[int]:
+ """Selectors whose raw channel data is replaced by derived variants."""
+ return frozenset(self.variants_by_selector)
+
+ def variants_by_rule(self) -> list[tuple[PushdownRule, dict[int, Pushdown]]]:
+ """Group the variants by the rule that computes them.
+
+ Raises
+ ------
+ ValueError
+ If two different rules share a name: their pipeline columns would
+ collide.
+ """
+ groups: dict[str, tuple[PushdownRule, dict[int, Pushdown]]] = {}
+ for variant, pushdown in self.pushdowns_by_variant.items():
+ rule = pushdown.rule
+ group = groups.setdefault(rule.name, (rule, {}))
+ if group[0] is not rule:
+ raise ValueError(f"two different pushdown rules are named {rule.name!r}")
+ group[1][variant] = pushdown
+ return list(groups.values())
+
+ def lookup(
+ self, node: TimeSeriesExpression, classify: Classifier | None = None
+ ) -> tuple[Pushdown, int] | None:
+ """Return ``(pushdown, variant)`` when *node* is a pushed-down subtree.
+
+ Pass a *classify* shared by the lookups of one evaluation so that every
+ node is classified once.
+ """
+ if classify is None:
+ classify = Classifier(self.rules)
+ pushdown = classify(node)
+ if pushdown is None:
+ return None
+ variant = self.variant_ids.get(pushdown.variant_key)
+ if variant in self.variants_by_selector.get(pushdown.selector.selector_id, ()):
+ return pushdown, variant
+ return None
+
+ def native_targets(
+ self, selections: Iterable[Any]
+ ) -> dict[int, dict[int, list[tuple[int, bool]]]] | None:
+ """Sources of calculated channels that are pushed down as a whole.
+
+ Parameters
+ ----------
+ selections : Iterable
+ The selections of a ``solve_calculated_channels`` call.
+
+ Returns
+ -------
+ dict or None
+ ``{variant: {selector_id: [(channel_id, uses_alias), ...]}}``: the
+ calculated channels (by output ``channel_id``) whose rows are the
+ given variant's rows of the channel resolved for ``selector_id``.
+ ``None`` unless every selection is a ``CalculatedChannel`` whose
+ whole expression is a pushdown emitting its result rows.
+ """
+ from impulse_query_engine.analyze.query.channels.calculated_channel import (
+ CalculatedChannel,
+ )
+
+ selections = list(selections)
+ if not selections:
+ return None
+ targets: dict[int, dict[int, list[tuple[int, bool]]]] = {}
+ classify = Classifier(self.rules)
+ for selection in selections:
+ if not isinstance(selection, CalculatedChannel):
+ return None
+ hit = self.lookup(selection.expr, classify)
+ if hit is None or not hit[0].rule.emits_result_rows(hit[0]):
+ return None
+ pushdown, variant = hit
+ by_selector = targets.setdefault(variant, {})
+ by_selector.setdefault(pushdown.selector.selector_id, []).append(
+ (selection.channel_id, pushdown.selector.uses_alias)
+ )
+ return targets
+
+ def channel_variants(self, selector_ids: Column) -> Column:
+ """Variant ids a matched channel ships, given its ``selector_ids`` array.
+
+ A channel ships only its raw data (``[0]``) unless every one of its
+ selectors is pushed down; then it ships the derived variants instead.
+ """
+ ids = PushdownPlan.channel_selector_ids(selector_ids)
+ pushed = sorted(self.variants_by_selector)
+ needs_raw = F.exists(ids, lambda sid: ~sid.isin(pushed)) | (F.size(ids) == 0)
+ mapping = F.create_map(
+ *[
+ item
+ for sid in pushed
+ for item in (
+ F.lit(sid).cast("long"),
+ F.array(*[F.lit(v) for v in self.variants_by_selector[sid]]),
+ )
+ ]
+ )
+ no_variants = F.array().cast("array")
+ derived = F.array_distinct(
+ F.flatten(
+ F.transform(
+ ids, lambda sid: F.coalesce(F.try_element_at(mapping, sid), no_variants)
+ )
+ )
+ )
+ return F.when(F.coalesce(needs_raw, F.lit(True)), F.array(F.lit(RAW_VARIANT))).otherwise(
+ derived
+ )
+
+ @staticmethod
+ def channel_selector_ids(selector_ids: Column | str) -> Column:
+ """A matched channel's distinct, non-null ``selector_ids``, as ``long``.
+
+ Variant tagging (:meth:`channel_variants`) and the native calculated-channel
+ rows both read a channel's selectors through this, so they agree on which
+ selectors it carries.
+ """
+ return F.array_distinct(
+ F.filter(
+ F.transform(selector_ids, lambda sid: sid.cast("long")),
+ lambda sid: sid.isNotNull(),
+ )
+ )
+
+ def describe(self) -> str:
+ """One-line summary of the plan for logging."""
+ variants = ", ".join(
+ f"{vid}={pushdown.describe()}" for vid, pushdown in self.pushdowns_by_variant.items()
+ )
+ return (
+ f"variants [{variants}]; pushed selectors {sorted(self.pushed_selector_ids)}; "
+ f"raw selectors {sorted(self.raw_selector_ids)}"
+ )
diff --git a/src/impulse_query_engine/analyze/query/optimizer/pushdown.py b/src/impulse_query_engine/analyze/query/optimizer/pushdown.py
new file mode 100644
index 00000000..f05e9590
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/pushdown.py
@@ -0,0 +1,230 @@
+"""Building blocks of Spark pushdown rules."""
+
+from __future__ import annotations
+
+import abc
+import re
+from collections.abc import Callable, Mapping
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, ClassVar
+
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+from pyspark.sql import Column
+
+from impulse_query_engine.analyze.metadata.time_series_expression import (
+ TimeSeriesExpression,
+ TimeSeriesSelector,
+)
+from impulse_query_engine.model.series.sample_series import SampleSeries
+
+if TYPE_CHECKING:
+ import pandas as pd
+
+ from impulse_query_engine.analyze.query.solvers.default_solver import TimeSeriesCache
+ from impulse_query_engine.analyze.query.solvers.solver_config import SolverConfig
+
+ from .context import RunBreak, SeriesContext
+
+#: Column tagging each channel-data row with the series variant it belongs to.
+VARIANT_COL = "__impulse_variant"
+#: Column listing the variant ids a matched channel ships, one row per channel.
+VARIANTS_COL = "__impulse_variants"
+#: Variant id of the unmodified (raw) channel data.
+RAW_VARIANT = 0
+
+_NAME = re.compile(r"[a-z][a-z0-9]*(_[a-z0-9]+)*")
+
+
+def check_name(kind: str, name: str) -> str:
+ """Return *name* if it is a valid rule, rule column or run name, else raise.
+
+ Names are lowercase letters, digits and single underscores, starting with a
+ letter. Pipeline columns join them with ``__``, which a name never contains,
+ so names of different rules cannot collide.
+ """
+ if not isinstance(name, str) or not _NAME.fullmatch(name):
+ raise ValueError(
+ f"{kind} name {name!r} must consist of lowercase letters, digits and single "
+ "underscores, starting with a letter"
+ )
+ return name
+
+
+@dataclass(frozen=True)
+class PushdownContext:
+ """Plan-time facts about the channel data a pushdown would run on.
+
+ Parameters
+ ----------
+ tstart_type, tend_type : pyspark.sql.types.DataType
+ Spark types of the channel-data ``tstart`` / ``tend`` columns.
+ """
+
+ tstart_type: T.DataType
+ tend_type: T.DataType
+
+ @staticmethod
+ def from_schema(schema: T.StructType, config: SolverConfig) -> PushdownContext:
+ """Build the context from the column-mapped channel-data schema."""
+ return PushdownContext(
+ tstart_type=schema[config.tstart_col].dataType,
+ tend_type=schema[config.tend_col].dataType,
+ )
+
+
+@dataclass(frozen=True, eq=False)
+class Pushdown:
+ """A subtree that a :class:`PushdownRule` computes natively in Spark.
+
+ Expression nodes overload ``==``, so pushdowns compare by identity; use
+ :attr:`variant_key` to identify equivalent pushdowns.
+
+ A pushdown reads the samples of its channel, possibly absorbing pushed
+ operands into that pass, or computes on the result of another pushdown, its
+ :attr:`source`. The pipeline computes the former only.
+
+ Parameters
+ ----------
+ rule : PushdownRule
+ The rule that matched the subtree.
+ selector : TimeSeriesSelector
+ The physical channel selector the subtree reads.
+ params : tuple of (str, Any)
+ Canonical, hashable operation parameters (e.g. ``(("sample_rate", 1e6),)``).
+ consumed : tuple of TimeSeriesSelector
+ Every selector occurrence inside the subtree (its ``get_selectors()``).
+ source : Pushdown, optional
+ The pushdown whose result this one computes on; ``None`` when it reads
+ the channel's samples.
+ """
+
+ rule: PushdownRule
+ selector: TimeSeriesSelector
+ params: tuple[tuple[str, Any], ...]
+ consumed: tuple[TimeSeriesSelector, ...]
+ source: Pushdown | None = None
+
+ @property
+ def variant_key(self) -> tuple:
+ """Identity of the derived series, independent of the input channel."""
+ key = (self.rule.name, self.params)
+ return key if self.source is None else (*key, self.source.variant_key)
+
+ def describe(self) -> str:
+ """Human-readable ``rule(param=value, ...)`` label."""
+ params = ", ".join(f"{k}={v}" for k, v in self.params)
+ label = f"{self.rule.name}({params})"
+ return label if self.source is None else f"{label} on {self.source.describe()}"
+
+
+#: Classification of an operand of the node a rule matches: the selector of a
+#: channel, the :class:`Pushdown` computing the operand, or ``None`` when the
+#: operand is evaluated in pandas or is not an expression.
+Operand = Callable[[Any], "TimeSeriesSelector | Pushdown | None"]
+
+
+class PushdownRule(abc.ABC):
+ """Recognizes a subtree and computes it in Spark ahead of the pandas UDF.
+
+ Planning and decoding run in Python: :meth:`match` recognizes a node from
+ its operands' classification, :meth:`decode` turns one
+ container/channel/variant slice of the derived rows back into the object the
+ subtree evaluates to. The result must equal evaluating the subtree in pandas
+ on the same channel data.
+
+ The Spark side is a set of hooks returning columns, evaluated by the shared
+ pushdown pipeline (:mod:`.pipeline`) for the samples of the rule's variants,
+ one series (channel and variant) at a time, with a :class:`SeriesContext`:
+
+ 1. :meth:`row_columns` per sample;
+ 2. :meth:`keep` selects the samples that go on;
+ 3. :meth:`kept_columns` per kept sample;
+ 4. :meth:`emit` the derived rows of a kept sample.
+
+ Rules never handle DataFrames, so every rule shares the pipeline's reads and
+ shuffles. Derived rows have the channel-data shape
+ ``(container_id, channel_id, variant, tstart, tend, value)``.
+ """
+
+ #: Unique rule name, part of every :attr:`Pushdown.variant_key`; lowercase
+ #: letters, digits and single underscores (see :func:`check_name`).
+ name: ClassVar[str]
+ #: Whether the rule needs its series in order (neighbours, runs, series
+ #: facts). Per-sample rules set ``False`` and skip the series scan.
+ ordered: ClassVar[bool] = True
+
+ @abc.abstractmethod
+ def match(self, node: TimeSeriesExpression, operand: Operand) -> Pushdown | None:
+ """Return the pushdown for *node*, or ``None`` if the rule does not apply.
+
+ Nodes are classified bottom-up: *operand* returns the classification of
+ any operand of *node*. A pushdown reading the channel's samples sets no
+ ``source``; when it absorbs a pushed operand into that pass, its
+ ``consumed`` includes the operand's and :meth:`supports` covers both. A
+ pushdown computing on a pushed operand's result sets ``source`` to it.
+
+ Purely syntactic: it runs on the driver while planning and again inside
+ the UDF to recognize the node, so it must not depend on data or schemas.
+ """
+
+ def supports(self, pushdown: Pushdown, context: PushdownContext) -> bool:
+ """Whether *pushdown* can run on channel data described by *context*."""
+ return True
+
+ def result_type(self, pushdown: Pushdown) -> type:
+ """Core-model class :meth:`decode` returns for *pushdown*."""
+ return SampleSeries
+
+ def runs(self) -> Mapping[str, RunBreak]:
+ """Run definitions beyond the built-in continuous blocks, by name.
+
+ Each maps ``(prev, cur)`` samples to whether ``cur`` starts a new run;
+ :meth:`SeriesContext.run` then describes the sample's run. Names follow
+ :func:`check_name` and only need to be unique within the rule.
+ """
+ return {}
+
+ def row_columns(self, ctx: SeriesContext) -> Mapping[str, Column]:
+ """Columns computed for every sample, in order; refer to them via ``ctx.col``.
+
+ Column names follow :func:`check_name` and only need to be unique within
+ the rule.
+ """
+ return {}
+
+ def keep(self, ctx: SeriesContext) -> Column:
+ """Whether a sample can emit rows; the others are dropped early."""
+ return F.lit(True)
+
+ def kept_columns(self, ctx: SeriesContext) -> Mapping[str, Column]:
+ """Columns computed for the kept samples, in order (``ctx.block_end`` available)."""
+ return {}
+
+ @abc.abstractmethod
+ def emit(self, ctx: SeriesContext) -> Column:
+ """The derived rows of a kept sample.
+
+ Returns
+ -------
+ Column
+ An array of :data:`~.context.ELEMENT_TYPE` structs built with
+ :meth:`SeriesContext.element`; empty or null emits nothing. Times
+ left out keep the sample's own ``tstart`` / ``tend``.
+ """
+
+ def decode(self, rows: pd.DataFrame, pushdown: Pushdown, cache: TimeSeriesCache) -> Any:
+ """Build the subtree's result from one container/channel/variant slice."""
+ return cache.to_sample_series(rows, uses_alias=pushdown.selector.uses_alias)
+
+ def emits_result_rows(self, pushdown: Pushdown) -> bool:
+ """Whether *pushdown*'s derived rows are its result's samples as they are.
+
+ Holds when the result is a :class:`SampleSeries` built by the default
+ :meth:`decode`: the variant's rows, with values multiplied by the
+ conversion factor for aliased selectors, are then exactly the result, so
+ it can be emitted without the pandas evaluation.
+ """
+ return (
+ type(self).decode is PushdownRule.decode and self.result_type(pushdown) is SampleSeries
+ )
diff --git a/src/impulse_query_engine/analyze/query/optimizer/query_optimizer.py b/src/impulse_query_engine/analyze/query/optimizer/query_optimizer.py
new file mode 100644
index 00000000..009d837d
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/query_optimizer.py
@@ -0,0 +1,120 @@
+"""Rule-based optimizer choosing the subtrees Spark computes ahead of the pandas UDF."""
+
+from __future__ import annotations
+
+from collections import Counter
+from collections.abc import Iterable, Sequence
+from functools import partial
+from typing import Any
+
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesExpression
+
+from .plan import PushdownPlan
+from .pushdown import Pushdown, PushdownContext, PushdownRule, check_name
+from .resample_rule import ResamplePushdownRule
+from .traversal import Classifier, discover
+
+
+def default_rules() -> tuple[PushdownRule, ...]:
+ """All built-in pushdown rules."""
+ return (ResamplePushdownRule(),)
+
+
+class QueryOptimizer:
+ """Plans which subtrees of a solve's selections are computed natively in Spark.
+
+ Nodes are classified bottom-up (:class:`~.traversal.Classifier`), and the
+ topmost classified nodes whose pushdown the channel data supports are the
+ candidates; a node that is not supported leaves its operands as candidates.
+
+ All selections of a solve share the raw data of their physical channels, so
+ a subtree is pushed down only when **every** occurrence of its input selector,
+ across all selections, sits inside a pushed-down subtree. Then the raw rows
+ of that selector are no longer needed and are replaced by the (smaller)
+ derived rows. Selectors used anywhere else keep their raw data, and their
+ subtrees are evaluated in pandas as before.
+
+ Parameters
+ ----------
+ rules : Sequence[PushdownRule], optional
+ Rules to apply, in priority order. Defaults to :func:`default_rules`.
+ """
+
+ def __init__(self, rules: Sequence[PushdownRule] | None = None):
+ self.rules: tuple[PushdownRule, ...] = (
+ tuple(rules) if rules is not None else default_rules()
+ )
+ names = [check_name("rule", rule.name) for rule in self.rules]
+ duplicates = sorted({name for name in names if names.count(name) > 1})
+ if duplicates:
+ raise ValueError(f"pushdown rule names must be unique: {duplicates}")
+
+ def optimize(self, selections: Iterable[Any], context: PushdownContext) -> PushdownPlan:
+ """Build the pushdown plan for the *selections* of one solve call.
+
+ Parameters
+ ----------
+ selections : Iterable
+ The query's selections.
+ context : PushdownContext
+ Plan-time facts about the channel data.
+
+ Returns
+ -------
+ PushdownPlan
+ The plan; empty when nothing can be pushed down.
+ """
+ selections = list(selections)
+ if not all(isinstance(s, TimeSeriesExpression) for s in selections):
+ return PushdownPlan(rules=self.rules)
+
+ classify = Classifier(self.rules)
+ accept = partial(_accepted, context=context)
+ total: Counter[int] = Counter()
+ candidates: list[tuple[TimeSeriesExpression, Pushdown]] = []
+ for selection in selections:
+ expected = Counter(s.selector_id for s in selection.get_selectors())
+ total.update(expected)
+ found, leaves = discover(selection, classify, accept)
+ walked = Counter(s.selector_id for s in leaves)
+ walked.update(s.selector_id for _, p in found for s in p.consumed)
+ # A walk that disagrees with get_selectors() missed or double-counted part
+ # of the tree; its candidates can't be trusted to cover all occurrences.
+ if walked == expected:
+ candidates.extend(found)
+
+ # Candidates read a single selector, so the operands of a dropped candidate
+ # read one whose raw rows are shipped anyway and need not be reconsidered.
+ while True:
+ consumed = Counter(s.selector_id for _, p in candidates for s in p.consumed)
+ fully_pushed = {sid for sid, count in consumed.items() if count == total[sid]}
+ kept = [
+ (node, pushdown)
+ for node, pushdown in candidates
+ if all(s.selector_id in fully_pushed for s in pushdown.consumed)
+ ]
+ if len(kept) == len(candidates):
+ break
+ candidates = kept
+
+ return PushdownPlan.build(self.rules, [p for _, p in candidates], total)
+
+
+def _accepted(node: TimeSeriesExpression, pushdown: Pushdown, context: PushdownContext) -> bool:
+ """Whether the pipeline can compute *pushdown* for *node* on the channel data."""
+ if pushdown.source is not None:
+ # Computing on another pushdown's result needs a pipeline stage per rule.
+ return False
+ if any(s.selector_id != pushdown.selector.selector_id for s in pushdown.consumed):
+ return False
+ return pushdown.rule.supports(pushdown, context) and _has_result_type(node, pushdown)
+
+
+def _has_result_type(node: TimeSeriesExpression, pushdown: Pushdown) -> bool:
+ """Whether *node* evaluates to the type the rule decodes into."""
+ try:
+ actual = node.evaluation_type()
+ except Exception:
+ # Only disqualifies the candidate; the query's own type inference reports errors.
+ return False
+ return actual is pushdown.rule.result_type(pushdown)
diff --git a/src/impulse_query_engine/analyze/query/optimizer/resample_rule.py b/src/impulse_query_engine/analyze/query/optimizer/resample_rule.py
new file mode 100644
index 00000000..d71f1020
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/resample_rule.py
@@ -0,0 +1,201 @@
+"""Pushdown rule computing ``SampleSeries.resample`` natively in Spark."""
+
+from __future__ import annotations
+
+import math
+import numbers
+from collections.abc import Mapping
+from typing import Any
+
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+from pyspark.sql import Column
+
+from impulse_query_engine.analyze.metadata.time_series_expression import (
+ SeriesType,
+ TimeSeriesExpression,
+ TimeSeriesOp,
+ TimeSeriesSelector,
+)
+
+from .context import SeriesContext
+from .pushdown import Operand, Pushdown, PushdownContext, PushdownRule
+
+_RATE = "sample_rate"
+_DEFAULT_RATE = 1.0
+_TIME_TYPES = (T.ByteType, T.ShortType, T.IntegerType, T.LongType, T.DoubleType)
+
+
+class ResamplePushdownRule(PushdownRule):
+ """Pushes ``channel.resample(sample_rate)`` on a physical channel down to Spark.
+
+ Reproduces :meth:`SampleSeries.resample`: per continuous block of samples, a
+ regular grid starting at the block's first ``tstart`` (built like
+ ``numpy.arange``), each grid point holding the value of the last sample that
+ starts at or before it. Meant for downsampling: a rate finer than the native
+ sample spacing yields more rows than the raw channel.
+
+ Each sample is assigned the bucket ``c`` of the first grid point at or after its
+ ``tstart``. The last sample of a bucket holds the value of grid points
+ ``c, c + 1, …`` up to the next sample's bucket, so only those samples are kept
+ and every row carries just the grid points it covers. Values are looked up
+ within a block; this matches the pandas evaluation unless intervals overlap.
+
+ Grid points are computed as ``t_min + k * delta`` in IEEE double arithmetic.
+ numpy builds that fuse this into a multiply-add can differ by one ulp for
+ fractional grids (e.g. seconds-as-double with a rate of ``0.1``); integral
+ grids are exact.
+ """
+
+ name = "resample"
+
+ def match(self, node: TimeSeriesExpression, operand: Operand) -> Pushdown | None:
+ if (
+ type(node) is not TimeSeriesOp
+ or node.optype != "cls"
+ or not isinstance(node.operation, str)
+ or node.operation != "resample"
+ or not node.args
+ ):
+ return None
+ selector = operand(node.args[0])
+ if not isinstance(selector, TimeSeriesSelector):
+ return None
+ if selector.series_type is not SeriesType.CONTINUOUS:
+ return None
+ rate = _sample_rate(node.args[1:], node.kwargs)
+ if rate is None:
+ return None
+ return Pushdown(
+ rule=self, selector=selector, params=((_RATE, rate),), consumed=(selector,)
+ )
+
+ def supports(self, pushdown: Pushdown, context: PushdownContext) -> bool:
+ time_types = (context.tstart_type, context.tend_type)
+ if not all(isinstance(t, _TIME_TYPES) for t in time_types):
+ return False
+ # Integral time columns receive the grid cast back from double; an integral
+ # rate keeps every grid point integral, so that cast is exact.
+ rate = dict(pushdown.params)[_RATE]
+ return rate.is_integer() or not any(isinstance(t, T.IntegralType) for t in time_types)
+
+ def row_columns(self, ctx: SeriesContext) -> Mapping[str, Column]:
+ grid = _Grid(ctx)
+ resampled = ctx.col("resampled")
+ return {
+ # A single sample is returned as it is.
+ "resampled": ctx.n >= 2,
+ **grid.first_index("c", ctx.tstart, F.lit(False), resampled),
+ **grid.first_index(
+ "next_c", ctx.next.tstart, F.lit(False), resampled & _next_in_block(ctx)
+ ),
+ }
+
+ def keep(self, ctx: SeriesContext) -> Column:
+ # The last sample of each bucket.
+ return ~ctx.col("resampled") | ~_next_in_block(ctx) | (ctx.col("next_c") != ctx.col("c"))
+
+ def kept_columns(self, ctx: SeriesContext) -> Mapping[str, Column]:
+ grid = _Grid(ctx)
+ t_max = ctx.block_end
+ # The last block keeps a grid point at t_max when the series ends with a
+ # zero-duration sample.
+ inclusive = ctx.block.is_last & (ctx.last.tstart == ctx.last.tend)
+ length = F.greatest(F.ceil(((t_max + grid.rate) - grid.t_min) / grid.rate), F.lit(0))
+ n_grid = ctx.col("n_grid")
+ return {
+ **grid.first_index("grid_end", t_max, inclusive, ctx.col("resampled")),
+ "n_grid": F.least(ctx.col("grid_end"), length),
+ # Grid points up to the next sample's bucket, or up to the block's end.
+ "c_stop": F.when(_next_in_block(ctx), F.least(ctx.col("next_c"), n_grid)).otherwise(
+ n_grid
+ ),
+ }
+
+ def emit(self, ctx: SeriesContext) -> Column:
+ grid = _Grid(ctx)
+ c, c_stop, n_grid = ctx.col("c"), ctx.col("c_stop"), ctx.col("n_grid")
+ points = F.transform(
+ F.sequence(c, c_stop - 1),
+ lambda k: ctx.element(
+ grid.point(k),
+ F.when(k == n_grid - 1, ctx.block_end).otherwise(grid.point(k + 1)),
+ ctx.value,
+ ),
+ )
+ return F.when(~ctx.col("resampled"), F.array(ctx.element(value=ctx.value))).when(
+ c_stop > c, points
+ )
+
+
+def _next_in_block(ctx: SeriesContext) -> Column:
+ """The next sample exists and continues the block."""
+ return F.coalesce(~ctx.block.next_starts, F.lit(False))
+
+
+class _Grid:
+ """The numpy.arange grid of the sample's block.
+
+ ``numpy.arange(t_min, t_max + rate, rate)`` has length
+ ``ceil(((t_max + rate) - t_min) / rate)`` and is filled as ``t_min``,
+ ``t_min + rate``, then ``t_min + k * ((t_min + rate) - t_min)``.
+ """
+
+ def __init__(self, ctx: SeriesContext):
+ self.ctx = ctx
+ self.rate = ctx.param(_RATE)
+ self.t_min = ctx.block.start
+ self.delta = (self.t_min + self.rate) - self.t_min
+
+ def point(self, k: Column) -> Column:
+ return (
+ F.when(k == 0, self.t_min)
+ .when(k == 1, self.t_min + self.rate)
+ .otherwise(self.t_min + k.cast("double") * self.delta)
+ )
+
+ def first_index(
+ self, name: str, x: Column, strict: Column, active: Column
+ ) -> dict[str, Column]:
+ """Columns ending in *name*: the smallest ``k >= 0`` with ``point(k) >= x`` (``> x`` if *strict*).
+
+ Estimated from ``(x - t_min) / delta`` and corrected by one step against the
+ exact grid points; a result that still is not exact raises instead of
+ misplacing a sample. Only evaluated where *active*.
+ """
+
+ def reached(k: Column) -> Column:
+ g = self.point(k)
+ return F.when(strict, g > x).otherwise(g >= x)
+
+ estimate, step = self.ctx.col(f"{name}_estimate"), self.ctx.col(f"{name}_step")
+ return {
+ f"{name}_estimate": F.when(
+ active,
+ F.when(
+ self.delta > 0,
+ F.greatest(F.ceil((x - self.t_min) / self.delta), F.lit(0)),
+ ).otherwise(
+ F.raise_error(
+ F.lit("resample: sample_rate is below the resolution of the timestamps")
+ )
+ ),
+ ),
+ f"{name}_step": F.when((estimate > 0) & reached(estimate - 1), estimate - 1)
+ .when(~reached(estimate), estimate + 1)
+ .otherwise(estimate),
+ name: F.when(~active, F.lit(None))
+ .when(reached(step) & ((step == 0) | ~reached(step - 1)), step)
+ .otherwise(F.raise_error(F.lit("resample: grid index could not be determined"))),
+ }
+
+
+def _sample_rate(args: tuple, kwargs: dict[str, Any]) -> float | None:
+ """The resample rate of a ``resample(...)`` call, or ``None`` if unsupported."""
+ if len(args) > 1 or set(kwargs) - {_RATE} or (args and kwargs):
+ return None
+ rate = args[0] if args else kwargs.get(_RATE, _DEFAULT_RATE)
+ if isinstance(rate, bool) or not isinstance(rate, numbers.Real):
+ return None
+ rate = float(rate)
+ return rate if math.isfinite(rate) and rate > 0 else None
diff --git a/src/impulse_query_engine/analyze/query/optimizer/traversal.py b/src/impulse_query_engine/analyze/query/optimizer/traversal.py
new file mode 100644
index 00000000..9db5d358
--- /dev/null
+++ b/src/impulse_query_engine/analyze/query/optimizer/traversal.py
@@ -0,0 +1,141 @@
+"""Bottom-up classification of TSAL expression trees into pushdowns."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterable, Iterator, Sequence
+from typing import Any
+
+from impulse_query_engine.analyze.metadata.time_series_expression import (
+ TimeSeriesAliasSelector,
+ TimeSeriesExpression,
+ TimeSeriesOp,
+ TimeSeriesSelector,
+)
+
+from .pushdown import Pushdown, PushdownRule
+
+#: Plan-time check whether a classified node's pushdown is planned.
+Accept = Callable[[TimeSeriesExpression, Pushdown], bool]
+
+
+def child_expressions(node: TimeSeriesExpression) -> Iterator[TimeSeriesExpression]:
+ """Yield the direct sub-expressions of *node*.
+
+ Operations yield their operands; other node types (aggregations, events,
+ calculated channels, …) yield every expression held in their attributes,
+ including inside lists, tuples and dicts. The walk is best-effort: the
+ optimizer checks the leaves it finds against ``get_selectors()``.
+ """
+ if isinstance(node, TimeSeriesSelector):
+ return
+ if isinstance(node, TimeSeriesOp):
+ # Covers TimeSeriesUDF, whose ``optype`` attribute holds its first operand.
+ values: Iterable = (*node.args, *node.kwargs.values())
+ elif isinstance(node, TimeSeriesAliasSelector):
+ values = node._aliases
+ else:
+ values = (value for key, value in vars(node).items() if key != "_alias")
+ yield from _expressions_in(values)
+
+
+def _expressions_in(values: Iterable) -> Iterator[TimeSeriesExpression]:
+ for value in values:
+ if isinstance(value, TimeSeriesExpression):
+ yield value
+ elif isinstance(value, list | tuple):
+ yield from _expressions_in(value)
+ elif isinstance(value, dict):
+ yield from _expressions_in(value.values())
+
+
+class Classifier:
+ """Classifies expression nodes bottom-up into the pushdowns computing them.
+
+ A node is classified after its operands: the first rule whose
+ :meth:`~.pushdown.PushdownRule.match` returns a pushdown for the node, given
+ its operands' classification, computes it. Only plain :class:`TimeSeriesOp`
+ nodes can be pushed down: their ``build`` serves precomputed results from the
+ cache, other node types (UDFs, aggregations, events, …) would evaluate on
+ missing raw rows instead.
+
+ Classification is syntactic, so the driver and the UDF classify alike.
+ Results are memoized per node object: use one classifier per optimization or
+ per container, and never store it on a plan or a node.
+
+ Parameters
+ ----------
+ rules : Sequence[PushdownRule]
+ Rules in priority order.
+ """
+
+ def __init__(self, rules: Sequence[PushdownRule]):
+ self.rules = tuple(rules)
+ # Nodes are not hashable; holding each node keeps its id from being reused.
+ self._memo: dict[int, tuple[TimeSeriesOp, Pushdown | None]] = {}
+
+ def __call__(self, node: TimeSeriesExpression) -> Pushdown | None:
+ """The pushdown computing *node*, or ``None``."""
+ if type(node) is not TimeSeriesOp:
+ return None
+ stack: list[tuple[TimeSeriesOp, bool]] = [(node, False)]
+ while stack:
+ current, operands_classified = stack.pop()
+ if id(current) in self._memo:
+ continue
+ if operands_classified:
+ self._memo[id(current)] = (current, self._match(current))
+ else:
+ stack.append((current, True))
+ stack.extend(
+ (child, False)
+ for child in child_expressions(current)
+ if type(child) is TimeSeriesOp
+ )
+ return self._memo[id(node)][1]
+
+ def operand(self, value: Any) -> TimeSeriesSelector | Pushdown | None:
+ """Classification of an operand: its selector for a channel, else its pushdown."""
+ if isinstance(value, TimeSeriesSelector):
+ return value
+ if isinstance(value, TimeSeriesExpression):
+ return self(value)
+ return None
+
+ def _match(self, node: TimeSeriesOp) -> Pushdown | None:
+ for rule in self.rules:
+ pushdown = rule.match(node, self.operand)
+ if pushdown is not None:
+ return pushdown
+ return None
+
+
+def discover(
+ root: TimeSeriesExpression, classify: Classifier, accept: Accept | None = None
+) -> tuple[list[tuple[TimeSeriesExpression, Pushdown]], list[TimeSeriesSelector]]:
+ """Find the topmost subtrees of *root* that are pushed down.
+
+ A classified node is a candidate when *accept* (by default every pushdown)
+ accepts its pushdown; otherwise the walk continues into its operands, whose
+ pushdowns can still be candidates. The walk does not descend into a
+ candidate and keeps no visited set, so a node referenced twice is counted
+ twice, like ``get_selectors()`` does.
+
+ Returns
+ -------
+ tuple
+ ``(candidates, leaves)``: the ``(node, pushdown)`` candidates and the
+ selectors reached outside of them.
+ """
+ candidates: list[tuple[TimeSeriesExpression, Pushdown]] = []
+ leaves: list[TimeSeriesSelector] = []
+ stack: list[TimeSeriesExpression] = [root]
+ while stack:
+ node = stack.pop()
+ pushdown = classify(node)
+ if pushdown is not None and (accept is None or accept(node, pushdown)):
+ candidates.append((node, pushdown))
+ elif isinstance(node, TimeSeriesSelector):
+ leaves.append(node)
+ else:
+ stack.extend(child_expressions(node))
+ return candidates, leaves
diff --git a/src/impulse_query_engine/analyze/query/solvers/default_solver.py b/src/impulse_query_engine/analyze/query/solvers/default_solver.py
index f9e6d7b0..bae40a38 100644
--- a/src/impulse_query_engine/analyze/query/solvers/default_solver.py
+++ b/src/impulse_query_engine/analyze/query/solvers/default_solver.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import logging
from collections.abc import Iterable
from functools import partial
from typing import TYPE_CHECKING, Any
@@ -7,7 +8,7 @@
import pandas as pd
import pyspark.sql.functions as F
import pyspark.sql.types as T
-from pyspark.sql import DataFrame, Window
+from pyspark.sql import Column, DataFrame, Window
from impulse_query_engine.analyze.metadata.metric_expression import MetricExpression
from impulse_query_engine.analyze.metadata.tag_expression import TagExpression
@@ -16,6 +17,16 @@
SeriesValueType,
TimeSeriesExpression,
)
+from impulse_query_engine.analyze.query.optimizer import (
+ RAW_VARIANT,
+ VARIANT_COL,
+ VARIANTS_COL,
+ Classifier,
+ PushdownContext,
+ PushdownPlan,
+ QueryOptimizer,
+ apply_pushdowns,
+)
from impulse_query_engine.model.series.points_in_time_series import PointsInTimeSeries
from impulse_query_engine.model.series.sample_series import SampleSeries
@@ -29,6 +40,8 @@
if TYPE_CHECKING:
from impulse_query_engine.measurement_db import MeasurementDB
+logger = logging.getLogger(__name__)
+
class TimeSeriesCache(SeriesCache):
def __init__(
@@ -37,6 +50,7 @@ def __init__(
col_map: dict[str, str],
container_tag_cols=None,
container_metric_cols=None,
+ plan: PushdownPlan | None = None,
):
"""
Initialize the TimeSeriesCache.
@@ -52,7 +66,9 @@ def __init__(
the caller's frame is reordered by ``(cid, ch, ts)`` and its
index reset. The sort makes each channel's rows contiguous,
which the constructor exploits to build a ``(cid, ch)`` →
- ``(start, stop)`` range index for :meth:`load_blob`.
+ ``(start, stop)`` range index for :meth:`load_blob`. When *pdf*
+ carries the pushdown variant column, rows are additionally keyed
+ by variant (raw data is variant ``0``).
col_map : dict[str, str]
Mapping with keys ``"cid"``, ``"ch"``, ``"ts"``, ``"te"``,
``"val"``, ``"conv"`` to the actual column names in *pdf*. The
@@ -65,6 +81,9 @@ def __init__(
container_metric_cols : list of str, optional
Columns on *pdf* holding container-level metric values, exposed
via :attr:`container_metrics`. These are constant per container.
+ plan : PushdownPlan, optional
+ Pushdowns whose derived rows *pdf* carries; :meth:`materialized`
+ serves the matching subtrees from them.
"""
self._cid_col = col_map["cid"]
self._ch_col = col_map["ch"]
@@ -78,28 +97,34 @@ def __init__(
self._value_string_col = col_map.get("value_string")
self._container_tag_cols = list(container_tag_cols or [])
self._container_metric_cols = list(container_metric_cols or [])
+ self._plan = plan
+ # Classifies each node once for all lookups of this container.
+ self._classify = Classifier(plan.rules) if plan is not None else None
+ has_variants = VARIANT_COL in pdf.columns
# *pdf* holds channel data for a whole container, so avoid creating unnecessary copies of the data.
meta_cols = [
- c for c in pdf.columns if c not in (self._ts_col, self._te_col, self._val_col)
+ c
+ for c in pdf.columns
+ if c not in (self._ts_col, self._te_col, self._val_col, VARIANT_COL)
]
meta_source = pdf
if "selector_ids" in pdf.columns:
meta_source = pdf.loc[pdf["selector_ids"].notna()]
first_rows = ~meta_source.duplicated(subset=[self._cid_col, self._ch_col])
self.mdf = meta_source.loc[first_rows, meta_cols].reset_index(drop=True)
- pdf.sort_values(
- [self._cid_col, self._ch_col, self._ts_col], inplace=True, ignore_index=True
- )
+ group_cols = [self._cid_col, self._ch_col] + ([VARIANT_COL] if has_variants else [])
+ pdf.sort_values([*group_cols, self._ts_col], inplace=True, ignore_index=True)
self.pdf = pdf
- # Each (cid, ch) group is a contiguous block after the sort, so its
+ # Each (cid, ch, variant) group is a contiguous block after the sort, so its
# row positions collapse to a (start, stop) range
self._ranges = {
- key: (int(positions[0]), int(positions[-1]) + 1)
- for key, positions in pdf.groupby(
- [self._cid_col, self._ch_col], sort=False
- ).indices.items()
+ (key if has_variants else (*key, RAW_VARIANT)): (
+ int(positions[0]),
+ int(positions[-1]) + 1,
+ )
+ for key, positions in pdf.groupby(group_cols, sort=False).indices.items()
}
def resolve(self, selection):
@@ -169,8 +194,7 @@ def load_blob(
-------
SampleSeries or PointsInTimeSeries
"""
- lo, hi = self._ranges.get((mid, cid), (0, 0))
- s = self.pdf.iloc[lo:hi]
+ s = self._slice(mid, cid, RAW_VARIANT)
if series_type == SeriesType.POINTS_IN_TIME:
# The selector's declared value type picks the column; the constructor
@@ -185,12 +209,72 @@ def load_blob(
)
return PointsInTimeSeries(s[self._ts_col], s[value_col], value_type=value_type)
- values = s[self._val_col]
- if self._has_conversion and len(s) > 0 and uses_alias:
- factor = s[self._conv_col].iloc[0]
+ return self.to_sample_series(s, uses_alias=uses_alias)
+
+ def materialized(self, expression):
+ """Serve a pushed-down subtree from its derived rows.
+
+ Returns ``None`` (evaluate normally) when *expression* is not pushed down
+ or its channel carries no derived rows in this container.
+
+ Parameters
+ ----------
+ expression : TimeSeriesExpression
+ The node about to be built.
+
+ Returns
+ -------
+ Any or None
+ The rule-decoded result, or ``None``.
+ """
+ if self._plan is None:
+ return None
+ hit = self._plan.lookup(expression, self._classify)
+ if hit is None:
+ return None
+ pushdown, variant = hit
+ candidates = self.resolve(pushdown.selector)
+ if len(candidates) == 0:
+ return None
+ rows = self._slice(
+ candidates[self._cid_col].iloc[0], candidates[self._ch_col].iloc[0], variant
+ )
+ if len(rows) == 0:
+ return None
+ return pushdown.rule.decode(rows, pushdown, self)
+
+ def to_sample_series(self, rows: pd.DataFrame, uses_alias: bool = False) -> SampleSeries:
+ """Build a :class:`SampleSeries` from channel-data *rows* of one channel.
+
+ Applies the per-channel conversion factor when *uses_alias* is ``True``
+ (see :meth:`load_blob`).
+ """
+ values = rows[self._val_col]
+ if self._has_conversion and len(rows) > 0 and uses_alias:
+ factor = rows[self._conv_col].iloc[0]
if pd.notna(factor):
values = values * factor
- return SampleSeries(s[self._ts_col], s[self._te_col], values)
+ return SampleSeries(rows[self._ts_col], rows[self._te_col], values)
+
+ @property
+ def tstart_col(self) -> str:
+ """Name of the ``tstart`` column of the cached rows."""
+ return self._ts_col
+
+ @property
+ def tend_col(self) -> str:
+ """Name of the ``tend`` column of the cached rows."""
+ return self._te_col
+
+ @property
+ def value_col(self) -> str:
+ """Name of the numeric value column of the cached rows."""
+ return self._val_col
+
+ def _slice(self, mid, cid, variant: int) -> pd.DataFrame:
+ """Rows of one container/channel/variant (empty when absent)."""
+ lo, hi = self._ranges.get((mid, cid, variant), (0, 0))
+ return self.pdf.iloc[lo:hi]
@property
def container_tags(self) -> dict:
@@ -256,6 +340,12 @@ class DefaultSolver(QuerySolver):
``RawEncoder.RLE`` (default) run-length encodes equal-valued runs;
``RawEncoder.INTERVAL`` only derives ``tend`` and drops exact
duplicates. Only consulted when ``is_raw_data`` is ``True``.
+ enable_pushdown : bool, optional
+ Whether to compute supported expression subtrees (e.g.
+ ``channel.resample(rate)``) natively in Spark before the pandas UDF, so
+ fewer rows are shipped to it (see :class:`QueryOptimizer`). A channel
+ is only pushed down when every use of it in the solve is. Default
+ ``False``.
"""
def __init__(
@@ -265,12 +355,14 @@ def __init__(
is_raw_data: bool = False,
drop_implausible_data: bool = False,
raw_encoder: RawEncoder = RawEncoder.RLE,
+ enable_pushdown: bool = False,
):
super().__init__(config=config)
self.spark = spark
self.is_raw_data = is_raw_data
self.drop_implausible_data: bool = drop_implausible_data
self.raw_encoder: RawEncoder = raw_encoder
+ self.optimizer: QueryOptimizer | None = QueryOptimizer() if enable_pushdown else None
# In RAW mode an is_plausible channels filter drops samples before raw
# encoding, bridging intervals instead of splitting them. Reject it.
self.config.reject_implausible_channels_filter_in_raw(is_raw=self.is_raw_data)
@@ -714,8 +806,9 @@ def filter_aliased_channel_metrics(
resolved = resolved.withColumn(source_unit_col, F.col("_map_source_unit"))
resolved = resolved.withColumn(target_unit_col, F.col("_map_target_unit"))
+ # Ties in priority (e.g. no priorities configured) go to the lowest channel_id.
dedup_window = Window.partitionBy(container_id_col, channel_alias_col).orderBy(
- F.col(alias_priority_col).asc_nulls_last()
+ F.col(alias_priority_col).asc_nulls_last(), F.col(channel_id_col).asc()
)
resolved = resolved.withColumn("_rank", F.row_number().over(dedup_window))
resolved = resolved.where(F.col("_rank") == 1).drop("_rank")
@@ -806,12 +899,13 @@ def resolve_channel_selections(
agg_exprs = [F.flatten(F.collect_list("selector_ids")).alias("selector_ids")]
if has_unit_cols:
- # collect_set serves a dual purpose: (a) it deduplicates so we
- # can detect a conflict by size > 1, and (b) the single
- # remaining element materializes the scalar unit value the
- # downstream code expects.
- agg_exprs.append(F.collect_set(source_unit_col).alias("_source_units"))
- agg_exprs.append(F.collect_set(target_unit_col).alias("_target_units"))
+ # Distinct units: size > 1 is a conflict, else the single element is
+ # the unit. Not collect_set: Databricks may plan copies of several
+ # collect_set differently, which blocks exchange reuse.
+ source_units = F.array_distinct(F.collect_list(source_unit_col))
+ target_units = F.array_distinct(F.collect_list(target_unit_col))
+ agg_exprs.append(source_units.alias("_source_units"))
+ agg_exprs.append(target_units.alias("_target_units"))
grouped = merged.groupBy(
self.config.container_id_col,
@@ -1007,6 +1101,7 @@ def _solve_udf(
col_map: dict[str, str],
container_tag_cols=None,
container_metric_cols=None,
+ plan: PushdownPlan | None = None,
) -> pd.DataFrame:
"""
UDF to solve for a single container by applying selections.
@@ -1024,6 +1119,8 @@ def _solve_udf(
container_metric_cols : list of str, optional
Columns on *pdf* holding container-level metric values, exposed to
expressions via ``cache.container_metrics``.
+ plan : PushdownPlan, optional
+ Pushdowns whose derived rows *pdf* carries.
Returns
-------
@@ -1037,6 +1134,7 @@ def _solve_udf(
col_map=col_map,
container_tag_cols=container_tag_cols,
container_metric_cols=container_metric_cols,
+ plan=plan,
)
for s in selections:
res = s.build(cache)
@@ -1075,7 +1173,7 @@ def solve(self, query, channels_df, selections, dtypes) -> DataFrame:
DataFrame containing results for each container.
"""
col_map = self.config.col_map
- q, joined_df, container_count = self._prepare_channels_join(query, channels_df)
+ q, joined_df, container_count, plan, _ = self._prepare_channels_join(query, channels_df)
container_tag_cols, container_metric_cols = self._container_meta_cols(query)
schema = self._build_solve_output_schema(q, selections, dtypes)
@@ -1086,6 +1184,7 @@ def solve(self, query, channels_df, selections, dtypes) -> DataFrame:
col_map=col_map,
container_tag_cols=container_tag_cols,
container_metric_cols=container_metric_cols,
+ plan=plan,
),
schema,
F.PandasUDFType.GROUPED_MAP,
@@ -1098,7 +1197,9 @@ def solve(self, query, channels_df, selections, dtypes) -> DataFrame:
container_meta_cols=[*container_tag_cols, *container_metric_cols],
)
- def _prepare_channels_join(self, query, channels_df) -> tuple[DataFrame, DataFrame, int]:
+ def _prepare_channels_join(
+ self, query, channels_df, native_selections=None
+ ) -> tuple[DataFrame, DataFrame, int, PushdownPlan | None, dict | None]:
"""Shared prelude for :meth:`solve` and :meth:`solve_calculated_channels`.
Applies optional per-channel unit conversion, reads and column-maps the
@@ -1110,20 +1211,31 @@ def _prepare_channels_join(self, query, channels_df) -> tuple[DataFrame, DataFra
*channels_df* (attached by :meth:`attach_container_metadata`) ride
through the broadcast join into ``joined_df``.
+ With pushdown enabled, subtrees chosen by the optimizer are computed on
+ the channel data before the join (see :meth:`_apply_pushdowns`), and
+ ``joined_df`` carries their derived rows instead of the raw rows they
+ replace.
+
Parameters
----------
query : QueryBuilder
Query object containing database and filter information.
channels_df : pyspark.sql.DataFrame
Channel-match DataFrame from the filter pipeline.
+ native_selections : list, optional
+ Calculated-channel selections that may be solved without the UDF
+ (see :meth:`PushdownPlan.native_targets`).
Returns
-------
- tuple[DataFrame, DataFrame, int]
- ``(channels_q, joined_df, container_count)`` — the column-mapped
- channel-data DataFrame (authoritative for output ``container_id`` /
- ``channel_id`` types), the join fed to the grouped-map UDF, and the
- number of distinct containers.
+ tuple[DataFrame, DataFrame, int, PushdownPlan or None, dict or None]
+ ``(channels_q, joined_df, container_count, plan, native_targets)`` —
+ the column-mapped channel-data DataFrame (authoritative for output
+ ``container_id`` / ``channel_id`` types), the join fed to the
+ grouped-map UDF, the number of distinct containers, the pushdown
+ plan the UDF must receive (``None`` when nothing is pushed down),
+ and the native targets of *native_selections* (``None`` unless
+ they are solved natively; ``joined_df`` then holds no raw rows).
"""
source_unit_col = self.config.source_unit_col
target_unit_col = self.config.target_unit_col
@@ -1168,12 +1280,57 @@ def _prepare_channels_join(self, query, channels_df) -> tuple[DataFrame, DataFra
if DefaultSolver._query_contains_poi_selections(query.selections):
q = self._union_poi_channel_data(query, q)
- joined_df = q.join(
+ container_count = channels_df.select(self.config.container_id_col).distinct().count()
+ plan = self._plan_pushdowns(query, q) if container_count > 0 else None
+ channel_data = q
+ targets = None
+ if plan is not None:
+ if native_selections is not None:
+ targets = plan.native_targets(native_selections)
+ # Raw rows are only read by the UDF, for selectors that are not pushed down.
+ include_raw = targets is None and bool(plan.raw_selector_ids)
+ channel_data = self._apply_pushdowns(q, channels_df, plan, include_raw)
+
+ joined_df = channel_data.join(
F.broadcast(channels_df),
on=[self.config.container_id_col, self.config.channel_id_col],
)
- container_count = channels_df.select(self.config.container_id_col).distinct().count()
- return q, joined_df, container_count
+ return q, joined_df, container_count, plan, targets
+
+ def _plan_pushdowns(self, query, channels_q: DataFrame) -> PushdownPlan | None:
+ """Plan the pushdowns for *query*, or ``None`` when disabled or nothing applies."""
+ if self.optimizer is None:
+ return None
+ plan = self.optimizer.optimize(
+ query.selections, PushdownContext.from_schema(channels_q.schema, self.config)
+ )
+ if plan.is_empty:
+ return None
+ logger.info("Spark pushdown plan: %s", plan.describe())
+ return plan
+
+ def _apply_pushdowns(
+ self,
+ channels_q: DataFrame,
+ channels_df: DataFrame,
+ plan: PushdownPlan,
+ include_raw: bool,
+ ) -> DataFrame:
+ """Replace raw channel data with the derived rows of *plan* where possible.
+
+ Each matched channel ships the variants of
+ :meth:`PushdownPlan.channel_variants`: its raw data (variant ``0``)
+ while any of its selectors still needs it, otherwise the derived
+ variants of its selectors, computed by their rules (see
+ :func:`~impulse_query_engine.analyze.query.optimizer.apply_pushdowns`).
+ The result carries a :data:`VARIANT_COL` column and keeps the
+ channel-data columns and types; raw rows are kept only if *include_raw*.
+ """
+ key = [self.config.container_id_col, self.config.channel_id_col]
+ variants = channels_df.select(
+ *key, plan.channel_variants(F.col("selector_ids")).alias(VARIANTS_COL)
+ )
+ return apply_pushdowns(channels_q, variants, plan, self.config, include_raw=include_raw)
@staticmethod
def _query_contains_poi_selections(selections: Iterable[Any]) -> bool:
@@ -1382,6 +1539,7 @@ def _solve_calculated_channels_udf(
col_map: dict[str, str],
container_tag_cols=None,
container_metric_cols=None,
+ plan: PushdownPlan | None = None,
) -> pd.DataFrame:
"""
UDF to solve calculated channels for a single container.
@@ -1409,6 +1567,8 @@ def _solve_calculated_channels_udf(
container_metric_cols : list of str, optional
Columns on *pdf* holding container-level metric values, exposed to
expressions via ``cache.container_metrics``.
+ plan : PushdownPlan, optional
+ Pushdowns whose derived rows *pdf* carries.
Returns
-------
@@ -1421,6 +1581,7 @@ def _solve_calculated_channels_udf(
col_map=col_map,
container_tag_cols=container_tag_cols,
container_metric_cols=container_metric_cols,
+ plan=plan,
)
cid_col = col_map["cid"]
ch_col = col_map["ch"]
@@ -1470,9 +1631,24 @@ def solve_calculated_channels(self, query, channels_df, selections) -> DataFrame
-------
pyspark.sql.DataFrame
Narrow DataFrame of calculated-channel samples.
+
+ Notes
+ -----
+ When every selection is a pushdown as a whole (see
+ :meth:`PushdownPlan.native_targets`), the rows are produced in Spark from
+ the derived rows and no pandas UDF runs.
"""
+ self._validate_calculated_channel_id_type(query)
col_map = self.config.col_map
- q, joined_df, container_count = self._prepare_channels_join(query, channels_df)
+ q, joined_df, container_count, plan, targets = self._prepare_channels_join(
+ query, channels_df, native_selections=selections
+ )
+
+ if targets is not None:
+ logger.info("Calculated channels solved natively in Spark, without the pandas UDF")
+ native = self._native_calculated_channel_rows(joined_df, q, targets)
+ return self._attach_identity(native, selections)
+
container_tag_cols, container_metric_cols = self._container_meta_cols(query)
schema = self._build_calculated_channels_udf_schema(q)
@@ -1483,6 +1659,7 @@ def solve_calculated_channels(self, query, channels_df, selections) -> DataFrame
col_map=col_map,
container_tag_cols=container_tag_cols,
container_metric_cols=container_metric_cols,
+ plan=plan,
),
schema,
F.PandasUDFType.GROUPED_MAP,
@@ -1495,8 +1672,14 @@ def solve_calculated_channels(self, query, channels_df, selections) -> DataFrame
container_meta_cols=[*container_tag_cols, *container_metric_cols],
)
- # Identity is constant per channel_id, so attach it here via a
- # channel_id-keyed CASE instead of the UDF shipping a copy per row.
+ return self._attach_identity(res, selections)
+
+ def _attach_identity(self, rows: DataFrame, selections) -> DataFrame:
+ """Add the ``identity`` map of each calculated channel to its rows.
+
+ Identity is constant per channel_id, so it is attached via a
+ channel_id-keyed CASE instead of being shipped with every row.
+ """
channel_id_col = self.config.channel_id_col
identity_expr = None
for cc in selections:
@@ -1507,4 +1690,123 @@ def solve_calculated_channels(self, query, channels_df, selections) -> DataFrame
if identity_expr is None
else identity_expr.when(branch, id_map)
)
- return res.withColumn("identity", identity_expr)
+ return rows.withColumn("identity", identity_expr)
+
+ def _validate_calculated_channel_id_type(self, query) -> None:
+ """Raise unless the ``channels`` table's ``channel_id`` can hold calculated-channel ids.
+
+ :attr:`CalculatedChannel.channel_id` is a 31-bit integer, which only fits
+ ``IntegerType`` and ``LongType`` columns. Only the table schema is read, so
+ the check runs no Spark job.
+ """
+ channels = self._apply_column_mapping(
+ query.db.channels(self.spark), self.config.channels.column_name_mapping
+ )
+ dtype = channels.schema[self.config.channel_id_col].dataType
+ if not isinstance(dtype, T.IntegerType | T.LongType):
+ raise ValueError(
+ "Calculated channels need an integer or long channel_id column, got "
+ f"{dtype.simpleString()}."
+ )
+
+ def _native_calculated_channel_rows(
+ self,
+ joined_df: DataFrame,
+ channels_q: DataFrame,
+ targets: dict[int, dict[int, list[tuple[int, bool]]]],
+ ) -> DataFrame:
+ """Calculated-channel rows taken directly from the pushed-down rows.
+
+ *targets* (:meth:`PushdownPlan.native_targets`) maps each variant and
+ selector to the calculated channels built from them. Every derived row
+ is emitted once per such channel, so the result equals the pandas
+ evaluation: values get the conversion factor for aliased selectors, and
+ the round-trip through float64 and Arrow is mirrored (integral times via
+ double, NaN as null).
+
+ Returns
+ -------
+ pyspark.sql.DataFrame
+ ``[container_id, channel_id, tstart, tend, value]`` as typed by
+ :meth:`_build_calculated_channels_udf_schema`.
+ """
+ cfg = self.config
+ cid, ch = cfg.container_id_col, cfg.channel_id_col
+ ts, te, val, conv = cfg.tstart_col, cfg.tend_col, cfg.value_col, cfg.conversion_factor_col
+ target_type = T.ArrayType(
+ T.StructType(
+ [
+ T.StructField("_cc_channel_id", T.LongType()),
+ T.StructField("_cc_uses_alias", T.BooleanType()),
+ ]
+ )
+ )
+ by_variant = F.create_map(
+ *[
+ item
+ for variant, by_selector in sorted(targets.items())
+ for item in (
+ F.lit(variant),
+ F.create_map(
+ *[
+ entry
+ for selector_id, channels in sorted(by_selector.items())
+ for entry in (
+ F.lit(selector_id).cast("long"),
+ F.array(
+ *[
+ F.struct(
+ F.lit(channel_id).cast("long").alias("_cc_channel_id"),
+ F.lit(uses_alias).alias("_cc_uses_alias"),
+ )
+ for channel_id, uses_alias in channels
+ ]
+ ),
+ )
+ ]
+ ),
+ )
+ ]
+ )
+ by_selector = F.try_element_at(by_variant, F.col(VARIANT_COL))
+ no_targets = F.array().cast(target_type)
+ channel_targets = F.flatten(
+ F.transform(
+ PushdownPlan.channel_selector_ids("selector_ids"),
+ lambda sid: F.coalesce(F.try_element_at(by_selector, sid), no_targets),
+ )
+ )
+ has_conversion = conv in joined_df.columns
+ # joined_df holds no raw rows here; any other row without targets is
+ # dropped by inline.
+ rows = joined_df.select(
+ cid,
+ ts,
+ te,
+ val,
+ *([conv] if has_conversion else []),
+ F.inline(channel_targets),
+ )
+
+ value = F.col(val).cast("double")
+ if has_conversion:
+ factor = F.col(conv)
+ # Like TimeSeriesCache.to_sample_series: a null or NaN factor leaves values as they are.
+ applies = F.col("_cc_uses_alias") & factor.isNotNull() & ~F.isnan(factor)
+ value = F.when(applies, value * factor).otherwise(value)
+
+ def as_udf_output(col: Column, dtype: T.DataType) -> Column:
+ # The UDF emits float64 arrays; Arrow casts them back and masks NaN as null.
+ col = col.cast("double")
+ if isinstance(dtype, T.IntegralType):
+ return col.cast(dtype)
+ return F.when(F.isnan(col), F.lit(None)).otherwise(col).cast(dtype)
+
+ schema = channels_q.schema
+ return rows.select(
+ cid,
+ F.col("_cc_channel_id").cast(schema[ch].dataType).alias(ch),
+ as_udf_output(F.col(ts), schema[ts].dataType).alias(ts),
+ as_udf_output(F.col(te), schema[te].dataType).alias(te),
+ as_udf_output(value, T.DoubleType()).alias(val),
+ )
diff --git a/src/impulse_query_engine/analyze/query/solvers/series_cache.py b/src/impulse_query_engine/analyze/query/solvers/series_cache.py
index 33def4b8..fbf252a3 100644
--- a/src/impulse_query_engine/analyze/query/solvers/series_cache.py
+++ b/src/impulse_query_engine/analyze/query/solvers/series_cache.py
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
+from typing import Any
import pandas as pd
@@ -70,6 +71,25 @@ def load_blob(
"""
pass
+ def materialized(self, expression) -> Any | None:
+ """Return the precomputed result of *expression*, if the cache holds one.
+
+ Consulted by :meth:`TimeSeriesOp.build` before evaluating the node, so a
+ subtree computed ahead of time (e.g. pushed down to Spark) replaces its
+ in-memory evaluation. Default: no precomputed results.
+
+ Parameters
+ ----------
+ expression : TimeSeriesExpression
+ The node about to be built.
+
+ Returns
+ -------
+ Any or None
+ The precomputed result, or ``None`` to evaluate *expression* normally.
+ """
+ return None
+
@property
def container_tags(self) -> dict:
"""Container-level tag values available to UDFs.
diff --git a/src/impulse_reporting/config/config_parser.py b/src/impulse_reporting/config/config_parser.py
index 79b27472..a6301ca3 100644
--- a/src/impulse_reporting/config/config_parser.py
+++ b/src/impulse_reporting/config/config_parser.py
@@ -395,6 +395,12 @@ class QueryEngine(BaseModel):
measurement_db_config : dict, optional
Keyword arguments for that implementation's config class,
merged with the ``source`` tables.
+ enable_pushdown : bool, default=False
+ Compute supported expression subtrees (currently
+ ``channel.resample(rate)`` on a physical channel) natively in Spark
+ before the pandas UDF, so fewer rows are shipped to it. A channel is
+ only pushed down when every use of it in a solve batch is. Meant for
+ downsampling.
Notes
-----
@@ -423,6 +429,7 @@ class QueryEngine(BaseModel):
measurement_db_config: dict[str, Any] | None = None
max_channels_per_batch: int = 500
max_containers_per_batch: int | None = None
+ enable_pushdown: bool = False
@model_validator(mode="before")
@classmethod
diff --git a/src/impulse_reporting/core/report.py b/src/impulse_reporting/core/report.py
index f8d9bdab..bd2d625d 100644
--- a/src/impulse_reporting/core/report.py
+++ b/src/impulse_reporting/core/report.py
@@ -362,6 +362,7 @@ def create_solver(spark: SparkSession, config: ImpulseConfig) -> QuerySolver:
is_raw_data=config.query_engine.data_type is DataType.RAW,
drop_implausible_data=config.query_engine.drop_implausible_data,
raw_encoder=config.query_engine.raw_encoder,
+ enable_pushdown=config.query_engine.enable_pushdown,
)
case _:
raise ValueError(
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index de7eabfc..02c0b664 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -15,6 +15,7 @@ The job is defined in `resources/e2e_job.yml`. Each task is a notebook in this f
| `pipeline` | `e2e_pipeline` | Container event, per-container statistics, an RPM x speed heatmap and a calculated channel; the calculated channel's samples must give the same statistics as the StatsAggregator |
| `container_filter` | `e2e_container_filter` | Baseline report on (`experiment_1` or `experiment_2`) and longer than 30 minutes; must equal those containers' `rpm_histogram` rows, with the containers selected independently in SQL |
| `events` | `e2e_events` | Points-in-time, sequence and 5-minute window events with point values and window statistics; point events have zero length; windows tile each container without gaps or overlaps; each slow_down has one value per channel, with speed at most 100 |
+| `pushdown` | `e2e_pushdown` | A report of `resample` expressions (calculated channels, one channel at two rates, an event condition, a histogram next to a raw-channel event, a `diff` above a resample, and a channel also used raw) runs without Spark pushdown, with it, and with it on small chunks; the pushdown runs log a pushdown plan with both rates and solve the calculated channels without the pandas UDF, and their gold tables equal the run without pushdown; the calculated channels lie on their 1 s or 5 s grid |
| `teardown` | `teardown` | Drops the schema; runs only if all tests passed, otherwise it ends as upstream failed, which fails the job run |
Shared code (report config, baseline report, comparison) is in `e2e_utils.py`. When a test fails, its schema is kept for debugging.
@@ -37,7 +38,7 @@ The reports read the silver tables in `.` (bundle
## Expected results
-`expected//.csv` holds the expected gold rows of a test (without `_` bookkeeping columns). `incremental` and `container_filter` reuse `expected/rpm_histogram/`; `incremental_updates` compares with full reference runs instead of expected files.
+`expected//.csv` holds the expected gold rows of a test (without `_` bookkeeping columns). `incremental` and `container_filter` reuse `expected/rpm_histogram/`; `incremental_updates` compares with full reference runs instead of expected files. `pushdown` also compares with a reference run without pushdown, and has no expected file for `calculated_channel_fact` (too many rows).
To refresh them after an intended change, run the job. On a mismatch, the test uploads its actual rows as CSV to `/actual//` and logs the path. Review them, then copy them into `expected/`.
diff --git a/tests/e2e/e2e_pushdown.py b/tests/e2e/e2e_pushdown.py
new file mode 100644
index 00000000..fecc9b3b
--- /dev/null
+++ b/tests/e2e/e2e_pushdown.py
@@ -0,0 +1,211 @@
+# Databricks notebook source
+# MAGIC %md
+# MAGIC # e2e: pushdown
+# MAGIC Runs a report of `resample` expressions without Spark pushdown, with pushdown, and with
+# MAGIC pushdown on small chunks (so channels span many chunks). The pushdown runs must log a
+# MAGIC pushdown plan and solve the calculated channels without the pandas UDF, and their gold
+# MAGIC tables must equal those of the run without pushdown. Covers calculated channels computed
+# MAGIC in Spark (one channel at two rates, so its samples fan out into two variants), pushed
+# MAGIC subtrees inside events and aggregations, a nested op above a pushdown, and a channel also
+# MAGIC used raw, which is not pushed down.
+
+# COMMAND ----------
+
+import logging
+from contextlib import contextmanager
+
+import pyspark.sql.functions as F
+from e2e_utils import channel, check, config, event_ids, gold, new_report, params
+from pyspark.sql import Window
+from pyspark.testing import assertDataFrameEqual
+
+from impulse_query_engine.analyze.query.optimizer import pipeline
+from impulse_reporting.aggregations.histogram import HistogramDuration
+from impulse_reporting.aggregations.stats_aggregator import StatsAggregator
+from impulse_reporting.channels.calculated_channel import CalculatedChannel
+from impulse_reporting.core.page import Page
+from impulse_reporting.events.basic_event import BasicEvent
+from impulse_reporting.events.container_event import ContainerEvent
+
+p = params()
+RATE = 1.0 # The source timestamps are epoch seconds; an integral rate gives an exact grid.
+SLOW_RATE = 5 * RATE # A second rate on speed: two pushed-down variants of one channel.
+SOLVER_LOGGER = "impulse_query_engine.analyze.query.solvers.default_solver"
+PLAN_LOG = "Spark pushdown plan"
+NATIVE_LOG = "Calculated channels solved natively in Spark"
+#: Rate of each calculated channel, by channel id (filled by build()).
+CALCULATED_RATES = {}
+
+
+def build(prefix, pushdown):
+ cfg = config(
+ p,
+ calculated_channels={"emit_channel_metrics": True},
+ query_engine={"enable_pushdown": pushdown},
+ )
+ cfg["unity_sink"]["table_prefix"] = prefix
+ report = new_report(cfg)
+ rpm, speed, coolant = (
+ channel(report, name)
+ for name in ["Engine RPM", "Vehicle Speed Sensor", "Engine Coolant Temperature"]
+ )
+
+ # Only resamples, so the calculated channels are computed in Spark without the UDF.
+ for name, source, rate in [
+ ("speed_1s", speed, RATE),
+ ("coolant_1s", coolant, RATE),
+ ("speed_5s", speed, SLOW_RATE),
+ ]:
+ calculated = CalculatedChannel(
+ name=name, expr=source.resample(rate), identity={"channel_name": name}
+ )
+ report.add_calculated_channel(calculated)
+ CALCULATED_RATES[calculated.get_id()] = rate
+
+ high_rpm = BasicEvent(name="high_rpm", expr=rpm > 2000)
+ fast = BasicEvent(name="fast", expr=speed.resample(RATE) > 100)
+ container = ContainerEvent(name="container", desc="Whole measurement")
+ for event in [high_rpm, fast, container]:
+ report.add_event(event)
+
+ page = Page(page_number=1)
+ page.add_aggregation(
+ HistogramDuration(
+ name="speed_at_high_rpm",
+ base_expr=speed.resample(RATE),
+ bins=[float(x) for x in range(0, 201, 20)],
+ event=high_rpm,
+ channel_name="Vehicle Speed Sensor",
+ bins_unit="km/h",
+ values_unit="s",
+ )
+ )
+ page.add_aggregation(
+ StatsAggregator(
+ name="resampled_stats",
+ # The speed change per second: an op above the pushed-down resample.
+ input_expressions=[coolant.resample(RATE), speed.resample(RATE).diff()],
+ channel_names=["coolant_1s", "speed_1s_diff"],
+ statistics=["min", "mean", "max"],
+ event=container,
+ )
+ )
+ # rpm is also used raw in the same solve, so its resample runs in pandas.
+ page.add_aggregation(
+ StatsAggregator(
+ name="raw_and_resampled_rpm",
+ input_expressions=[rpm, rpm.resample(RATE)],
+ channel_names=["Engine RPM", "rpm_1s"],
+ statistics=["min", "mean", "max"],
+ event=container,
+ )
+ )
+ report.add_page(page)
+ return report
+
+
+@contextmanager
+def solver_logs():
+ """Messages the solver logs at INFO and above while the block runs."""
+ messages = []
+ handler = logging.Handler(logging.INFO)
+ handler.emit = lambda record: messages.append(record.getMessage())
+ logger = logging.getLogger(SOLVER_LOGGER)
+ level = logger.level
+ logger.setLevel(logging.INFO)
+ logger.addHandler(handler)
+ try:
+ yield messages
+ finally:
+ logger.removeHandler(handler)
+ logger.setLevel(level)
+
+
+def run(prefix, pushdown, chunk_rows=None):
+ """Run the report into `_*`; returns the solver's log messages."""
+ report = build(prefix, pushdown)
+ default_chunk_rows = pipeline.CHUNK_ROWS
+ if chunk_rows is not None:
+ pipeline.CHUNK_ROWS = chunk_rows
+ try:
+ with solver_logs() as messages:
+ report.run()
+ finally:
+ pipeline.CHUNK_ROWS = default_chunk_rows
+ return messages
+
+
+ref, chunked = f"{p.prefix}_ref", f"{p.prefix}_chunked"
+logs = {
+ ref: run(ref, pushdown=False),
+ p.prefix: run(p.prefix, pushdown=True),
+ # About 40 chunks per channel instead of one.
+ chunked: run(chunked, pushdown=True, chunk_rows=1000),
+}
+
+# COMMAND ----------
+
+assert not any(PLAN_LOG in m or NATIVE_LOG in m for m in logs[ref]), "pushdown without opt-in"
+for prefix in [p.prefix, chunked]:
+ plans = [m for m in logs[prefix] if m.startswith(PLAN_LOG)]
+ assert plans, f"{prefix}: no pushdown plan"
+ assert all(f"resample(sample_rate={RATE})" in m for m in plans), plans
+ assert any(f"resample(sample_rate={SLOW_RATE})" in m for m in plans), plans
+ assert any(NATIVE_LOG in m for m in logs[prefix]), f"{prefix}: calculated channels in UDF"
+ print(f"{prefix}: {plans}")
+
+# COMMAND ----------
+
+# Sample rows, events and pandas-computed aggregations are identical (compared in Spark, as
+# the calculated channels have many rows); the calculated channel metrics are sums in Spark,
+# whose order is not deterministic.
+EXACT = [
+ "calculated_channel_fact",
+ "event_instance_fact",
+ "histogram_fact",
+ "stats_aggregator_fact",
+]
+for prefix in [p.prefix, chunked]:
+ for table in EXACT:
+ actual, expected = gold(p, table, prefix=prefix), gold(p, table, prefix=ref)
+ for name, diff in [
+ ("extra", actual.exceptAll(expected)),
+ ("missing", expected.exceptAll(actual)),
+ ]:
+ assert diff.count() == 0, f"{prefix}_{table}: {name} rows {diff.take(5)}"
+ print(f"{prefix}_{table}: equal to {ref} ({actual.count()} rows)")
+ assertDataFrameEqual(
+ gold(p, "calculated_channel_metrics", prefix=prefix),
+ gold(p, "calculated_channel_metrics", prefix=ref),
+ rtol=1e-12,
+ atol=0,
+ )
+
+# COMMAND ----------
+
+# The calculated channels lie on their grids (1 s or 5 s): every sample lasts exactly the rate,
+# except the last one of each continuous block (whose end isn't the next sample's start).
+series = Window.partitionBy("container_id", "channel_id").orderBy("tstart")
+rate = F.create_map(
+ *[x for cid, r in CALCULATED_RATES.items() for x in (F.lit(cid).cast("long"), F.lit(r))]
+)[F.col("channel_id").cast("long")]
+samples = gold(p, "calculated_channel_fact").withColumn("next", F.lead("tstart").over(series))
+duration = F.col("tend") - F.col("tstart")
+off_grid = samples.where(
+ (duration != rate) & ((duration > rate) | (F.col("next") == F.col("tend")))
+)
+assert off_grid.count() == 0, f"samples off the grid: {off_grid.take(5)}"
+n_series = samples.select("container_id", "channel_id").distinct().count()
+n_containers = gold(p, "measurement_dimension").count()
+assert n_series == len(CALCULATED_RATES) * n_containers, "missing series"
+
+ids = event_ids(p)
+fast = gold(p, "event_instance_fact").where(F.col("event_id") == ids["fast"])
+assert fast.agg(F.sum(F.col("end_ts") - F.col("start_ts"))).first()[0] > 0, "no fast events"
+assert gold(p, "histogram_fact").agg(F.sum("hist_value")).first()[0] > 0, "empty histogram"
+
+# COMMAND ----------
+
+# The calculated channel samples are covered by their metrics and the comparison above.
+for table in [*EXACT[1:], "calculated_channel_metrics"]:
+ check(p, table, gold(p, table))
diff --git a/tests/e2e/expected/pushdown/calculated_channel_metrics.csv b/tests/e2e/expected/pushdown/calculated_channel_metrics.csv
new file mode 100644
index 00000000..39df8254
--- /dev/null
+++ b/tests/e2e/expected/pushdown/calculated_channel_metrics.csv
@@ -0,0 +1,244 @@
+container_id,channel_id,channel_name,value_type,duration,min,max,mean
+14,1698605908,speed_5s,double,4895.842000007629,0.0,201.0,77.609734954561
+19,1698605908,speed_5s,double,3789.2969999313354,0.0,170.0,68.12873205892228
+21,1604839375,coolant_1s,double,3527.805000066757,41.0,93.0,81.94178674855078
+28,1698605908,speed_5s,double,3254.2829999923706,0.0,130.0,61.87230797090236
+56,1604839375,coolant_1s,double,2314.15700006485,62.0,95.0,89.53733216897544
+56,22179408,speed_1s,double,2314.15700006485,0.0,126.0,80.90851225511194
+67,1698605908,speed_5s,double,1448.0329999923706,0.0,88.0,24.840594102613352
+74,1604839375,coolant_1s,double,1296.5739998817444,18.0,95.0,59.68428026208551
+81,1604839375,coolant_1s,double,731.2820000648499,22.0,79.0,53.439409149490345
+6,1604839375,coolant_1s,double,5081.025000095367,25.0,94.0,85.90773515201593
+7,22179408,speed_1s,double,4971.943000078201,0.0,182.0,75.07407063880844
+9,22179408,speed_1s,double,4663.890000104904,0.0,191.0,75.63278722098201
+17,1698605908,speed_5s,double,3945.3499999046326,0.0,125.0,49.534261853757954
+24,22179408,speed_1s,double,3292.1470000743866,0.0,126.0,59.34151785919213
+25,1698605908,speed_5s,double,3282.6349999904633,0.0,125.0,59.476609492242424
+26,22179408,speed_1s,double,3738.061000108719,0.0,125.0,51.42987233071066
+27,1698605908,speed_5s,double,3238.0629999637604,0.0,126.0,60.4450870789699
+31,1698605908,speed_5s,double,3556.890000104904,0.0,128.0,56.04334123184041
+47,22179408,speed_1s,double,2917.132999897003,0.0,128.0,67.05693569916306
+54,1604839375,coolant_1s,double,2352.483999967575,67.0,94.0,89.93323482747094
+55,1604839375,coolant_1s,double,3004.49799990654,34.0,94.0,84.60425601861019
+59,1604839375,coolant_1s,double,3440.821000099182,25.0,101.0,86.59994838482683
+67,1604839375,coolant_1s,double,1448.0329999923706,27.0,95.0,69.42530660544675
+6,1698605908,speed_5s,double,5081.025000095367,0.0,196.0,73.55799272646463
+9,1698605908,speed_5s,double,4663.890000104904,0.0,190.0,75.65144117722842
+11,1698605908,speed_5s,double,6304.121000051498,0.0,182.0,59.99646897591437
+15,22179408,speed_1s,double,4031.986999988556,0.0,126.0,48.8248101991794
+25,22179408,speed_1s,double,3282.6349999904633,0.0,126.0,59.51042379081669
+26,1604839375,coolant_1s,double,3738.061000108719,14.0,94.0,80.88859170596483
+29,1698605908,speed_5s,double,3401.3180000782013,0.0,125.0,59.21822070014302
+36,1698605908,speed_5s,double,2921.0889999866486,0.0,130.0,68.21428583686111
+38,22179408,speed_1s,double,2858.1579999923706,0.0,128.0,69.8085970056703
+38,1698605908,speed_5s,double,2858.1579999923706,0.0,128.0,69.78795434000935
+39,22179408,speed_1s,double,2791.648999929428,0.0,130.0,71.91842527662877
+42,1698605908,speed_5s,double,2709.4609999656677,0.0,130.0,73.73975857284222
+48,1698605908,speed_5s,double,2593.265000104904,0.0,126.0,75.181286907475
+49,22179408,speed_1s,double,2373.802999973297,0.0,125.0,79.56557473477143
+50,22179408,speed_1s,double,2875.108999967575,0.0,125.0,64.31790933911914
+51,1604839375,coolant_1s,double,3914.0039999485016,31.0,94.0,85.44686515384528
+58,1604839375,coolant_1s,double,1914.832999944687,11.0,95.0,71.05970651180458
+62,1698605908,speed_5s,double,1835.1440000534058,0.0,128.0,55.63323640925664
+77,22179408,speed_1s,double,1233.3819999694824,0.0,87.0,29.38829981376156
+79,1698605908,speed_5s,double,1127.119999885559,0.0,86.0,32.201540211942984
+80,1698605908,speed_5s,double,1204.9849998950958,0.0,89.0,30.834408729763986
+5,22179408,speed_1s,double,5398.681999921799,0.0,216.0,66.43102890764727
+6,22179408,speed_1s,double,5081.025000095367,0.0,198.0,73.56803007129152
+9,1604839375,coolant_1s,double,4663.890000104904,42.0,96.0,88.33248425676416
+18,1604839375,coolant_1s,double,4335.055000066757,17.0,94.0,80.69768318984516
+18,1698605908,speed_5s,double,4335.055000066757,0.0,197.0,87.88123795295176
+20,1604839375,coolant_1s,double,3700.523999929428,30.0,94.0,84.58550302593561
+24,1698605908,speed_5s,double,3292.1470000743866,0.0,125.0,59.36247682609076
+30,1604839375,coolant_1s,double,3214.0999999046326,34.0,94.0,83.29358140675642
+57,1604839375,coolant_1s,double,2048.8069999217987,73.0,92.0,88.20734359065447
+60,22179408,speed_1s,double,1853.6219999790192,0.0,107.0,61.20341687856753
+62,1604839375,coolant_1s,double,1835.1440000534058,88.0,91.0,89.70193074767752
+66,1604839375,coolant_1s,double,1490.2360000610352,-1.0,64.0,45.183517242335085
+69,1698605908,speed_5s,double,1243.1129999160767,0.0,65.0,14.479777784654486
+76,1604839375,coolant_1s,double,1227.5350000858307,11.0,84.0,57.1490059364652
+78,1604839375,coolant_1s,double,862.3629999160767,88.0,91.0,89.44107063957185
+80,1604839375,coolant_1s,double,1204.9849998950958,16.0,91.0,62.131615743914985
+2,1604839375,coolant_1s,double,6522.724999904633,17.0,98.0,86.48472072621517
+4,22179408,speed_1s,double,5986.755000114441,0.0,190.0,63.68441668194404
+11,22179408,speed_1s,double,6304.121000051498,0.0,184.0,59.97711655548986
+12,22179408,speed_1s,double,5138.710000038147,0.0,194.0,82.68086737660735
+27,22179408,speed_1s,double,3238.0629999637604,0.0,126.0,60.50469061355282
+29,22179408,speed_1s,double,3401.3180000782013,0.0,126.0,59.23439090239954
+30,22179408,speed_1s,double,3214.0999999046326,0.0,130.0,61.07744003168066
+30,1698605908,speed_5s,double,3214.0999999046326,0.0,130.0,61.083693726309086
+35,1698605908,speed_5s,double,2939.588000059128,0.0,131.0,67.90747546798558
+37,1604839375,coolant_1s,double,3100.582999944687,15.0,94.0,78.68194046069054
+40,1604839375,coolant_1s,double,3131.84299993515,29.0,94.0,84.2353432147225
+46,1604839375,coolant_1s,double,2491.7430000305176,11.0,94.0,77.08755838797703
+51,1698605908,speed_5s,double,3914.0039999485016,0.0,215.0,95.3997492094829
+52,22179408,speed_1s,double,2443.24799990654,0.0,125.0,78.59947087129342
+55,1698605908,speed_5s,double,3004.49799990654,0.0,125.0,64.95727273104659
+1,22179408,speed_1s,double,8083.973999977112,0.0,203.0,49.36829015050111
+10,1698605908,speed_5s,double,4837.854000091553,0.0,125.0,45.55222212076461
+14,1604839375,coolant_1s,double,4895.842000007629,31.0,93.0,85.51840970358967
+16,1604839375,coolant_1s,double,4374.618999958038,12.0,94.0,79.50146401315618
+17,1604839375,coolant_1s,double,3945.3499999046326,29.0,94.0,84.91004600337592
+20,1698605908,speed_5s,double,3700.523999929428,0.0,127.0,52.71010267835578
+23,1698605908,speed_5s,double,4696.368999958038,0.0,206.0,79.58275851053003
+43,1604839375,coolant_1s,double,2630.640000104904,12.0,94.0,75.0400358853205
+43,22179408,speed_1s,double,2630.640000104904,0.0,131.0,75.90092144574616
+46,22179408,speed_1s,double,2491.7430000305176,0.0,132.0,63.55952439640056
+47,1698605908,speed_5s,double,2917.132999897003,0.0,128.0,67.03842437314471
+60,1604839375,coolant_1s,double,1853.6219999790192,87.0,91.0,89.50172041549492
+63,1604839375,coolant_1s,double,1625.7799999713898,2.0,70.0,51.822878863979106
+65,1604839375,coolant_1s,double,2216.9100000858307,24.0,94.0,76.26011430381257
+66,1698605908,speed_5s,double,1490.2360000610352,0.0,103.0,26.599813719690356
+70,1698605908,speed_5s,double,1239.236999988556,0.0,103.0,31.4104565957597
+74,1698605908,speed_5s,double,1296.5739998817444,0.0,84.0,28.116405236665955
+1,1698605908,speed_5s,double,8083.973999977112,0.0,202.0,49.36235247677775
+3,1604839375,coolant_1s,double,6491.930000066757,31.0,101.0,86.7964519026567
+22,1698605908,speed_5s,double,3521.632999897003,0.0,210.0,68.53354679691459
+23,22179408,speed_1s,double,4696.368999958038,0.0,208.0,79.59106279837461
+25,1604839375,coolant_1s,double,3282.6349999904633,33.0,94.0,85.00035794413705
+26,1698605908,speed_5s,double,3738.061000108719,0.0,125.0,51.404993121945125
+27,1604839375,coolant_1s,double,3238.0629999637604,31.0,94.0,84.65791740302966
+34,1698605908,speed_5s,double,3547.53200006485,0.0,130.0,60.29402976381607
+35,1604839375,coolant_1s,double,2939.588000059128,9.0,95.0,77.25033440079919
+45,1698605908,speed_5s,double,2761.46799993515,0.0,113.0,40.436825631375164
+47,1604839375,coolant_1s,double,2917.132999897003,54.0,94.0,88.95273887062815
+50,1604839375,coolant_1s,double,2875.108999967575,12.0,94.0,79.43062367363801
+54,22179408,speed_1s,double,2352.483999967575,0.0,126.0,79.84793945573661
+54,1698605908,speed_5s,double,2352.483999967575,0.0,125.0,79.82306532267843
+61,22179408,speed_1s,double,2315.7400000095367,0.0,126.0,83.33857859657142
+62,22179408,speed_1s,double,1835.1440000534058,0.0,128.0,55.72260269331676
+65,22179408,speed_1s,double,2216.9100000858307,0.0,102.0,18.82928490534107
+70,1604839375,coolant_1s,double,1239.236999988556,25.0,94.0,72.15030700325742
+71,1604839375,coolant_1s,double,1184.8540000915527,24.0,95.0,73.02575676121619
+73,1698605908,speed_5s,double,980.5539999008179,0.0,126.0,51.848240897637886
+76,22179408,speed_1s,double,1227.5350000858307,0.0,92.0,30.264717500847112
+76,1698605908,speed_5s,double,1227.5350000858307,0.0,92.0,30.32092771073536
+77,1698605908,speed_5s,double,1233.3819999694824,0.0,87.0,29.508295078816232
+78,1698605908,speed_5s,double,862.3629999160767,0.0,118.0,57.69854342613318
+2,1698605908,speed_5s,double,6522.724999904633,0.0,192.0,57.23068196274685
+3,1698605908,speed_5s,double,6491.930000066757,0.0,184.0,58.166831742812526
+13,1698605908,speed_5s,double,4708.522000074387,0.0,178.0,80.93516394188654
+16,1698605908,speed_5s,double,4374.618999958038,0.0,186.0,86.55153740328743
+44,22179408,speed_1s,double,3321.704999923706,0.0,197.0,78.15775332426057
+45,1604839375,coolant_1s,double,2761.46799993515,89.0,93.0,90.3648305900915
+48,22179408,speed_1s,double,2593.265000104904,0.0,126.0,75.19324094996536
+53,22179408,speed_1s,double,3152.9649999141693,0.0,130.0,62.329585011362255
+5,1604839375,coolant_1s,double,5398.681999921799,30.0,93.0,86.50359106161959
+10,1604839375,coolant_1s,double,4837.854000091553,8.0,93.0,74.14057927361539
+11,1604839375,coolant_1s,double,6304.121000051498,30.0,93.0,86.09463730157664
+12,1604839375,coolant_1s,double,5138.710000038147,8.0,95.0,82.9118961763229
+12,1698605908,speed_5s,double,5138.710000038147,0.0,194.0,82.61898414132114
+19,22179408,speed_1s,double,3789.2969999313354,0.0,172.0,68.11316188851836
+21,22179408,speed_1s,double,3527.805000066757,0.0,115.0,42.330854454022855
+33,1604839375,coolant_1s,double,4302.207999944687,75.0,103.0,90.50106921824994
+39,1698605908,speed_5s,double,2791.648999929428,0.0,130.0,71.91985812151725
+40,22179408,speed_1s,double,3131.84299993515,0.0,126.0,65.2788150632817
+40,1698605908,speed_5s,double,3131.84299993515,0.0,126.0,65.24912008814982
+46,1698605908,speed_5s,double,2491.7430000305176,0.0,132.0,63.664270351339255
+52,1604839375,coolant_1s,double,2443.24799990654,64.0,93.0,89.34073331886282
+53,1698605908,speed_5s,double,3152.9649999141693,0.0,130.0,62.31436124579514
+64,1698605908,speed_5s,double,1545.9430000782013,0.0,101.0,25.11088701073474
+69,1604839375,coolant_1s,double,1243.1129999160767,90.0,92.0,90.62423930893844
+72,22179408,speed_1s,double,1156.648999929428,0.0,98.0,33.5365352863027
+73,1604839375,coolant_1s,double,980.5539999008179,4.0,72.0,46.54092278189158
+73,22179408,speed_1s,double,980.5539999008179,0.0,126.0,51.827844264711985
+75,1698605908,speed_5s,double,1268.12700009346,0.0,92.0,28.597293486636033
+13,1604839375,coolant_1s,double,4708.522000074387,17.0,94.0,80.0719329761365
+15,1604839375,coolant_1s,double,4031.986999988556,29.0,100.0,86.09125401444594
+18,22179408,speed_1s,double,4335.055000066757,0.0,199.0,87.89761606118773
+19,1604839375,coolant_1s,double,3789.2969999313354,16.0,93.0,77.2196362542339
+28,22179408,speed_1s,double,3254.2829999923706,0.0,130.0,61.916864636687215
+31,22179408,speed_1s,double,3556.890000104904,0.0,128.0,56.04643382115289
+37,22179408,speed_1s,double,3100.582999944687,0.0,129.0,64.29597272627645
+39,1604839375,coolant_1s,double,2791.648999929428,55.0,96.0,88.40019071146253
+41,22179408,speed_1s,double,2719.5569999217987,0.0,128.0,73.4248261778451
+41,1698605908,speed_5s,double,2719.5569999217987,0.0,127.0,73.41453038334593
+58,1698605908,speed_5s,double,1914.832999944687,0.0,91.0,30.25329101894181
+68,22179408,speed_1s,double,1308.3269999027252,0.0,102.0,30.248554071682708
+1,1604839375,coolant_1s,double,8083.973999977112,20.0,93.0,81.47659307165077
+3,22179408,speed_1s,double,6491.930000066757,0.0,184.0,58.180078958971535
+4,1698605908,speed_5s,double,5986.755000114441,0.0,188.0,63.690596991644256
+28,1604839375,coolant_1s,double,3254.2829999923706,16.0,94.0,80.05025592424866
+32,1604839375,coolant_1s,double,3080.598999977112,13.0,94.0,78.1569788861685
+32,1698605908,speed_5s,double,3080.598999977112,0.0,130.0,64.76337881090085
+34,1604839375,coolant_1s,double,3547.53200006485,14.0,94.0,74.21999745200145
+36,1604839375,coolant_1s,double,2921.0889999866486,5.0,95.0,76.04038870429954
+44,1604839375,coolant_1s,double,3321.704999923706,31.0,97.0,85.31638420615018
+45,22179408,speed_1s,double,2761.46799993515,0.0,114.0,40.462898720037316
+52,1698605908,speed_5s,double,2443.24799990654,0.0,125.0,78.59824299757773
+58,22179408,speed_1s,double,1914.832999944687,0.0,91.0,30.277836240379585
+64,1604839375,coolant_1s,double,1545.9430000782013,28.0,94.0,75.63724535841438
+75,1604839375,coolant_1s,double,1268.12700009346,30.0,94.0,72.03738821251518
+79,22179408,speed_1s,double,1127.119999885559,0.0,86.0,32.19000639123062
+8,22179408,speed_1s,double,4819.085999965668,0.0,201.0,70.55196773878329
+8,1698605908,speed_5s,double,4819.085999965668,0.0,200.0,70.53308448996792
+15,1698605908,speed_5s,double,4031.986999988556,0.0,126.0,48.867965100224595
+16,22179408,speed_1s,double,4374.618999958038,0.0,188.0,86.59131229568415
+21,1698605908,speed_5s,double,3527.805000066757,0.0,115.0,42.295421656575826
+22,1604839375,coolant_1s,double,3521.632999897003,40.0,93.0,86.71665559683699
+38,1604839375,coolant_1s,double,2858.1579999923706,9.0,95.0,77.4673975336255
+42,1604839375,coolant_1s,double,2709.4609999656677,12.0,94.0,76.7494228555472
+56,1698605908,speed_5s,double,2314.15700006485,0.0,125.0,80.87141753771259
+59,1698605908,speed_5s,double,3440.821000099182,0.0,170.0,73.25722552633054
+64,22179408,speed_1s,double,1545.9430000782013,0.0,101.0,25.02097425198945
+68,1604839375,coolant_1s,double,1308.3269999027252,10.0,74.0,52.49620163606516
+74,22179408,speed_1s,double,1296.5739998817444,0.0,85.0,28.1595959839778
+80,22179408,speed_1s,double,1204.9849998950958,0.0,91.0,30.916567428842082
+10,22179408,speed_1s,double,4837.854000091553,0.0,125.0,45.56131706244726
+13,22179408,speed_1s,double,4708.522000074387,0.0,179.0,80.964472501982
+22,22179408,speed_1s,double,3521.632999897003,0.0,211.0,68.50799047119791
+23,1604839375,coolant_1s,double,4696.368999958038,26.0,94.0,85.88982616138162
+32,22179408,speed_1s,double,3080.598999977112,0.0,131.0,64.76110652554333
+35,22179408,speed_1s,double,2939.588000059128,0.0,131.0,67.90203252836285
+37,1698605908,speed_5s,double,3100.582999944687,0.0,129.0,64.24598212773328
+41,1604839375,coolant_1s,double,2719.5569999217987,7.0,95.0,75.75952443698638
+50,1698605908,speed_5s,double,2875.108999967575,0.0,125.0,64.4132100738054
+53,1604839375,coolant_1s,double,3152.9649999141693,40.0,94.0,80.60631342221673
+61,1698605908,speed_5s,double,2315.7400000095367,0.0,126.0,83.34764697212304
+63,22179408,speed_1s,double,1625.7799999713898,0.0,99.0,24.299720749852515
+67,22179408,speed_1s,double,1448.0329999923706,0.0,89.0,24.94349231004425
+71,1698605908,speed_5s,double,1184.8540000915527,0.0,111.0,32.77197021489537
+77,1604839375,coolant_1s,double,1233.3819999694824,30.0,95.0,71.54822269117298
+81,1698605908,speed_5s,double,731.2820000648499,0.0,100.0,43.062763748160876
+5,1698605908,speed_5s,double,5398.681999921799,0.0,215.0,66.42917660369602
+14,22179408,speed_1s,double,4895.842000007629,0.0,201.0,77.59278179308238
+31,1604839375,coolant_1s,double,3556.890000104904,14.0,94.0,79.9971604410171
+48,1604839375,coolant_1s,double,2593.265000104904,37.0,94.0,85.42180610184076
+49,1604839375,coolant_1s,double,2373.802999973297,17.0,94.0,79.0921866724866
+59,22179408,speed_1s,double,3440.821000099182,0.0,170.0,73.26245683595097
+61,1604839375,coolant_1s,double,2315.7400000095367,51.0,95.0,87.06961921460437
+66,22179408,speed_1s,double,1490.2360000610352,0.0,103.0,26.60987923951365
+71,22179408,speed_1s,double,1184.8540000915527,0.0,111.0,32.749182597182205
+72,1698605908,speed_5s,double,1156.648999929428,0.0,97.0,33.592732110061654
+78,22179408,speed_1s,double,862.3629999160767,0.0,118.0,57.63002355717546
+17,22179408,speed_1s,double,3945.3499999046326,0.0,126.0,49.57785747898871
+20,22179408,speed_1s,double,3700.523999929428,0.0,127.0,52.736855646314346
+24,1604839375,coolant_1s,double,3292.1470000743866,29.0,94.0,84.96984794432757
+29,1604839375,coolant_1s,double,3401.3180000782013,9.0,95.0,75.96046944173662
+33,1698605908,speed_5s,double,4302.207999944687,0.0,127.0,66.98072245779491
+42,22179408,speed_1s,double,2709.4609999656677,0.0,131.0,73.70875609670358
+43,1698605908,speed_5s,double,2630.640000104904,0.0,131.0,75.90928442965848
+49,1698605908,speed_5s,double,2373.802999973297,0.0,125.0,79.57644463424228
+51,22179408,speed_1s,double,3914.0039999485016,0.0,218.0,95.40920244458448
+55,22179408,speed_1s,double,3004.49799990654,0.0,125.0,64.97391577763489
+57,1698605908,speed_5s,double,2048.8069999217987,0.0,129.0,75.40369298120333
+65,1698605908,speed_5s,double,2216.9100000858307,0.0,101.0,18.873039500699583
+68,1698605908,speed_5s,double,1308.3269999027252,0.0,101.0,30.28677081719336
+69,22179408,speed_1s,double,1243.1129999160767,0.0,65.0,14.41140105622695
+75,22179408,speed_1s,double,1268.12700009346,0.0,92.0,28.583887889248118
+79,1604839375,coolant_1s,double,1127.119999885559,24.0,95.0,67.72829867036053
+2,22179408,speed_1s,double,6522.724999904633,0.0,193.0,57.2452464277521
+4,1604839375,coolant_1s,double,5986.755000114441,12.0,95.0,84.76381528899793
+7,1604839375,coolant_1s,double,4971.943000078201,30.0,96.0,86.36500257550907
+7,1698605908,speed_5s,double,4971.943000078201,0.0,182.0,75.07728869661797
+8,1604839375,coolant_1s,double,4819.085999965668,65.0,93.0,86.09475987773212
+33,22179408,speed_1s,double,4302.207999944687,0.0,127.0,67.01442608160897
+34,22179408,speed_1s,double,3547.53200006485,0.0,131.0,60.30107691659708
+36,22179408,speed_1s,double,2921.0889999866486,0.0,130.0,68.19785360901723
+44,1698605908,speed_5s,double,3321.704999923706,0.0,196.0,78.13457245780742
+57,22179408,speed_1s,double,2048.8069999217987,0.0,129.0,75.40388138360358
+60,1698605908,speed_5s,double,1853.6219999790192,0.0,106.0,61.185613896082224
+63,1698605908,speed_5s,double,1625.7799999713898,0.0,99.0,24.437500769291763
+70,22179408,speed_1s,double,1239.236999988556,0.0,103.0,31.349128536638887
+72,1604839375,coolant_1s,double,1156.648999929428,22.0,94.0,71.81384326508437
+81,22179408,speed_1s,double,731.2820000648499,0.0,100.0,43.26241313100778
diff --git a/tests/e2e/expected/pushdown/event_instance_fact.csv b/tests/e2e/expected/pushdown/event_instance_fact.csv
new file mode 100644
index 00000000..b875ad3c
--- /dev/null
+++ b/tests/e2e/expected/pushdown/event_instance_fact.csv
@@ -0,0 +1,3660 @@
+container_id,event_instance_id,event_id,start_ts,end_ts
+58,-392167370839906768,479635942,1.5194049718860002E9,1.519404972756E9
+58,-577730803822243282,479635942,1.519405126926E9,1.519405127677E9
+58,-1607115580174927158,479635942,1.5194051384160001E9,1.5194051391650002E9
+58,-1707683064145550443,479635942,1.5194052788140001E9,1.5194052797440002E9
+58,-8584964339849135253,479635942,1.519405314184E9,1.5194053159550002E9
+58,-1421226166730715875,479635942,1.519405385286E9,1.519405386187E9
+58,-1876304493150135480,479635942,1.519405427798E9,1.519405440457E9
+58,-6154401651170875487,479635942,1.519405468206E9,1.519405468957E9
+58,-1406917860190929118,479635942,1.5194054834180002E9,1.519405485667E9
+58,-8909632217749248406,479635942,1.5194054958960001E9,1.519405497726E9
+58,-2648942039680002738,479635942,1.519405639806E9,1.519405642565E9
+58,-4884515493043158578,479635942,1.5194059158040001E9,1.5194059174840002E9
+21,-6899411427707798469,479635942,1.5188770660960002E9,1.5188770670270002E9
+21,-7119357877423866742,479635942,1.5188771023070002E9,1.5188771031460001E9
+21,-6264600875148061610,479635942,1.518877115296E9,1.5188771170370002E9
+21,4974826582884942551,479635942,1.518877206045E9,1.5188772078460002E9
+21,1289029136727313106,479635942,1.5188772771460001E9,1.518877278647E9
+21,8101410539130513915,479635942,1.518877284736E9,1.5188772893850002E9
+21,8752606268609374984,479635942,1.5188773419160001E9,1.518877344585E9
+21,-1480847104797789343,479635942,1.518877352625E9,1.5188773534950001E9
+21,-8338341015569994825,479635942,1.5188773650170002E9,1.5188773675080001E9
+21,8154730559722274258,479635942,1.5188773958590002E9,1.5188773996950002E9
+21,-3158721213861171555,479635942,1.5188774546550002E9,1.518877455556E9
+21,-6871179186759736467,479635942,1.5188774583160002E9,1.5188774592150002E9
+21,-1829041438898315475,479635942,1.5188778007340002E9,1.518877801663E9
+21,2059476694655684232,479635942,1.5188778034940002E9,1.5188778052930002E9
+21,1694605092864503726,479635942,1.518877827973E9,1.5188778315730002E9
+21,-1745174359212764099,479635942,1.5188778352020001E9,1.5188778370310001E9
+21,-139531584369840434,479635942,1.518877842404E9,1.5188778556350002E9
+21,2607686719068508503,479635942,1.518877858335E9,1.518877861003E9
+21,-119566172197811932,479635942,1.5188779573930001E9,1.5188779609940002E9
+21,-4733250411904369960,479635942,1.5188780001430001E9,1.5188780018830001E9
+21,7255531698784217731,479635942,1.518878005243E9,1.518878007795E9
+21,513519212452121020,479635942,1.5188780291820002E9,1.518878034522E9
+21,-1176947577378637820,479635942,1.518878079433E9,1.518878080332E9
+21,1685195814674221943,479635942,1.518878254367E9,1.518878255954E9
+21,-3112809955600316429,479635942,1.518878317782E9,1.5188783232120001E9
+21,-8710966488530415760,479635942,1.5188784892610002E9,1.518878491571E9
+21,-1045224000766056863,479635942,1.5188785017110002E9,1.518878502581E9
+21,7867369122101485919,479635942,1.518878568071E9,1.5188785707710001E9
+21,-3742457767832808475,479635942,1.518878572601E9,1.518878577911E9
+21,8312151138059009802,479635942,1.518878652042E9,1.518878681831E9
+21,8414858666053075109,479635942,1.518878683661E9,1.5188786972510002E9
+21,-8219263397949537991,479635942,1.518878740693E9,1.518878756592E9
+21,-3304979534718454068,479635942,1.5188787656820002E9,1.518878791602E9
+21,-1934919521937342953,479635942,1.5188788042910001E9,1.5188788651890001E9
+21,-370981617701841497,479635942,1.5188788674410002E9,1.5188788780890002E9
+21,6355139955981713229,479635942,1.518878917661E9,1.5188789248590002E9
+21,-8952215723121251998,479635942,1.5188791500100002E9,1.518879157209E9
+21,8094389502770662682,479635942,1.518879378397E9,1.5188794001460001E9
+21,7246192116860339378,479635942,1.518879470078E9,1.518879474726E9
+21,7175156655889102509,479635942,1.518879528486E9,1.518879538325E9
+21,-5822746784247612928,479635942,1.5188796440170002E9,1.518879645818E9
+21,-7315354526747371087,479635942,1.51887967297E9,1.518879673868E9
+21,-7196778552377116883,479635942,1.5188796882680001E9,1.5188796890200002E9
+21,4440939622405681780,479635942,1.518879711009E9,1.518879714009E9
+21,6488693368657868428,479635942,1.518879715509E9,1.5188797190770001E9
+21,-5655791661006958610,479635942,1.518879783667E9,1.518879789937E9
+21,4821727960606958404,479635942,1.5188799552350001E9,1.518879957065E9
+21,-3418011628657036574,479635942,1.5188799615660002E9,1.5188799624650002E9
+21,-1241142162227330849,479635942,1.518880064736E9,1.5188800700740001E9
+21,-7531538706908136964,479635942,1.518880105864E9,1.518880106765E9
+21,-3483513624368168626,479635942,1.518880119334E9,1.518880121164E9
+21,2977822913768495977,479635942,1.518880149844E9,1.5188801505930002E9
+21,-6222786963020322569,479635942,1.5188802360070002E9,1.5188802396660001E9
+21,-1098647243308978850,479635942,1.518880248457E9,1.5188802493260002E9
+21,6724973428078230276,479635942,1.5188803934450002E9,1.518880395154E9
+21,8130984584943230121,479635942,1.518880403617E9,1.5188804067050002E9
+71,-4489390871663989789,479635942,1.501231062964E9,1.501231068962E9
+71,5206177872305096274,479635942,1.501231075533E9,1.501231076463E9
+71,-4192053480864001720,479635942,1.501231092213E9,1.501231093112E9
+71,-8254019672698524351,479635942,1.501231161123E9,1.501231161873E9
+71,-7392519746642868786,479635942,1.5012311633739998E9,1.501231164873E9
+71,-2963164630870117144,479635942,1.501231230393E9,1.501231231325E9
+71,-5990506934678448865,479635942,1.501231233184E9,1.501231235885E9
+71,-278389414754127081,479635942,1.501231236784E9,1.501231238646E9
+71,780385810968596253,479635942,1.501231261174E9,1.501231262914E9
+71,7210320572063479283,479635942,1.50123126611E9,1.501231270746E9
+71,7151462058824111706,479635942,1.501231276895E9,1.501231280134E9
+71,1586813079414789139,479635942,1.501231280884E9,1.5012312824429998E9
+71,-7038077438129468939,479635942,1.501231333235E9,1.501231335034E9
+71,-7227225762381369104,479635942,1.5012313413639998E9,1.501231344964E9
+71,-3778616821865773687,479635942,1.501231346764E9,1.5012313503639998E9
+71,-5682487224265024281,479635942,1.501231355704E9,1.501231356576E9
+71,1583533552934774866,479635942,1.501231644847E9,1.5012316457159998E9
+71,4773761074037179597,479635942,1.501231698578E9,1.501231699507E9
+71,-5059436520065358221,479635942,1.5012317148899999E9,1.501231717621E9
+71,-5819772863819020717,479635942,1.501231762383E9,1.501231764121E9
+71,6026457628452452747,479635942,1.501231765951E9,1.501231766733E9
+71,-7955541008994306897,479635942,1.501231827422E9,1.501231828351E9
+71,-5353205977548936132,479635942,1.501231921652E9,1.501231923452E9
+71,-6896668616549247688,479635942,1.5012319252519999E9,1.501231927053E9
+71,-8588831663195296280,479635942,1.501231950692E9,1.501231951593E9
+71,7368758490086608583,479635942,1.501232062743E9,1.501232064544E9
+21,-4301959432199305070,627460548,1.518879727209E9,1.518879750209E9
+71,-6675533528471306315,627460548,1.5012313008E9,1.5012313098E9
+71,8420902226317329417,627460548,1.5012313568E9,1.5012313818E9
+2,6428881338307012591,479635942,1.524466816435E9,1.5244668172749999E9
+2,3608378248033897188,479635942,1.5244669526209998E9,1.524466954122E9
+2,7900657249880286998,479635942,1.524466957274E9,1.5244669603349998E9
+2,3760264827360994613,479635942,1.5244669828679998E9,1.5244669837979999E9
+2,-8666981863875370668,479635942,1.524467046047E9,1.524467047698E9
+2,-3085541496776081601,479635942,1.5244670656369998E9,1.5244670663869998E9
+2,-2425461742533251016,479635942,1.5244670695979998E9,1.524467072837E9
+2,734577216518266441,479635942,1.5244671582189999E9,1.5244671590579998E9
+2,-1993999618294923600,479635942,1.5244671616379998E9,1.5244671633179998E9
+2,-2621726907686913263,479635942,1.5244672068479998E9,1.5244672085879998E9
+2,-3821005119971238146,479635942,1.5244672113179998E9,1.524467212218E9
+2,4089823852297291605,479635942,1.524467221248E9,1.524467222148E9
+2,6430346601165202106,479635942,1.5244673325189998E9,1.5244673397189999E9
+2,4077963347682511371,479635942,1.524467399119E9,1.524467401699E9
+2,3293682431627794236,479635942,1.5244674277689998E9,1.52446742936E9
+2,2692926594913195496,479635942,1.524467708422E9,1.524467813182E9
+2,3529383243168161374,479635942,1.524468131237E9,1.524468132077E9
+2,-939484201965019641,479635942,1.524468146238E9,1.524468160007E9
+2,293036198218521617,479635942,1.5244689333779998E9,1.5244689342779999E9
+2,1434803248378001878,479635942,1.524469130089E9,1.5244691318899999E9
+2,5048621473473296117,479635942,1.524469193449E9,1.5244692056E9
+2,-9150013433860073968,479635942,1.5244692333189998E9,1.52446923512E9
+2,3680292025329744175,479635942,1.5244693180089998E9,1.52446931882E9
+2,-1642564822255826089,479635942,1.524469413379E9,1.5244694159589999E9
+2,3758435368791722745,479635942,1.5244694192889998E9,1.5244694328799999E9
+2,-8059549587276591825,479635942,1.5244694496799998E9,1.5244694545089998E9
+2,-3029148802393051644,479635942,1.52446946162E9,1.5244694837889998E9
+2,-1591319615282683150,479635942,1.5244698628999999E9,1.52446993151E9
+2,5350367763502789966,479635942,1.524469935261E9,1.5244699869199998E9
+2,-1664548746545410241,479635942,1.524469995921E9,1.524470029941E9
+2,1717591356661177602,479635942,1.5244700796799998E9,1.524470129211E9
+2,-3427002422015830987,479635942,1.5244701392299998E9,1.5244701426799998E9
+2,-7129141920227876786,479635942,1.5244701966509998E9,1.524470280951E9
+2,-1155500824059021327,479635942,1.5244702885119998E9,1.5244704935909998E9
+2,3319824501553106817,479635942,1.524470495211E9,1.524470546181E9
+2,-2355847032375364518,479635942,1.524470644043E9,1.524470645543E9
+2,8543181478380905706,479635942,1.5244706618019998E9,1.5244706898219998E9
+2,-6228702062621336173,479635942,1.5244707075219998E9,1.524470724893E9
+2,-3110428313527381105,479635942,1.524470746102E9,1.5244707485319998E9
+2,-7532193968443324498,479635942,1.524470762872E9,1.524470820383E9
+2,1016417222900581189,479635942,1.5244708230219998E9,1.5244708361929998E9
+2,1344924229594086868,479635942,1.5244708764229999E9,1.524470880382E9
+2,562571204393351006,479635942,1.524470886712E9,1.524470893043E9
+2,5795463611740781210,479635942,1.5244709282619998E9,1.5244710126829998E9
+2,6738994380435536255,479635942,1.5244710216829998E9,1.524471113784E9
+2,-9183279540747628614,479635942,1.524471132863E9,1.5244711436629999E9
+2,-5220472668867677849,479635942,1.5244711922629998E9,1.5244713056339998E9
+2,-2628277987157342201,479635942,1.524471331314E9,1.5244713531539998E9
+2,2746166188718263427,479635942,1.524471356814E9,1.5244713586139998E9
+2,-44603451606756140,479635942,1.5244713883739998E9,1.524471391074E9
+2,-6205464925420997478,479635942,1.524471395574E9,1.5244714295949998E9
+2,6579061671960854680,479635942,1.5244714413839998E9,1.524471461185E9
+2,-1750677098349466194,479635942,1.5244723598669999E9,1.5244723632879999E9
+2,-2906537603049956629,479635942,1.5244724998479998E9,1.5244725005979998E9
+2,-5077247164212358936,479635942,1.5244725192879999E9,1.524472521599E9
+2,2051295939379277460,479635942,1.524472555648E9,1.5244725565479999E9
+2,5209212310043592781,479635942,1.5244728050379999E9,1.524472805938E9
+2,-3422328057157434933,479635942,1.5244729387789998E9,1.524472939679E9
+2,158446166795455731,479635942,1.524473063909E9,1.5244730648089998E9
+75,-969344755953198875,479635942,1.499974195436E9,1.4999741977150002E9
+75,-8184068060056686042,479635942,1.4999743014910002E9,1.4999743022410002E9
+75,1393722219846118446,479635942,1.4999743521420002E9,1.499974353671E9
+75,-4100211533129267640,479635942,1.499974427502E9,1.4999744291520002E9
+75,4462164656315198460,479635942,1.4999744306520002E9,1.499974433561E9
+75,-3375624799758144240,479635942,1.49997460399E9,1.499974605551E9
+75,4286020811146110156,479635942,1.4999747010410001E9,1.499974702601E9
+75,5930394635173492755,479635942,1.499974724476E9,1.4999747259710002E9
+75,-8132417072585646740,479635942,1.49997489469E9,1.49997489592E9
+75,-3020731302601077640,479635942,1.49997493063E9,1.4999749317440002E9
+75,9193106912931198964,479635942,1.49997493363E9,1.49997493549E9
+75,9166106347110276410,479635942,1.4999749373500001E9,1.4999749383400002E9
+75,7024209480978008053,479635942,1.49997495748E9,1.499974960811E9
+75,-7250479276706265556,479635942,1.49997498415E9,1.49997498526E9
+75,-5306326062299008932,479635942,1.4999749875440001E9,1.499974994051E9
+75,-7895199489110900965,479635942,1.4999750400140002E9,1.499975041061E9
+75,7536782177835813566,479635942,1.4999750469720001E9,1.4999750479320002E9
+75,-5088014885737652708,479635942,1.499975079796E9,1.4999750829720001E9
+75,-5622553165211996885,479635942,1.499975153621E9,1.499975156743E9
+75,-1654556088811395401,479635942,1.4999751592020001E9,1.499975160161E9
+75,3071895908825449354,479635942,1.4999751698530002E9,1.4999751708130002E9
+75,4050046434697820056,479635942,1.499975249502E9,1.499975254032E9
+2,2493439660761732656,627460548,1.524467242245E9,1.524467248245E9
+2,8946178248164710761,627460548,1.524467713245E9,1.524467818245E9
+2,-8853963897832703531,627460548,1.524469349245E9,1.524469361245E9
+2,8671949497985902729,627460548,1.524469451245E9,1.524469565245E9
+2,2811815646353338462,627460548,1.524469586245E9,1.524470042245E9
+2,-210603526432604769,627460548,1.524470054245E9,1.524470055245E9
+2,-5551831272299988146,627460548,1.524470058245E9,1.524470555245E9
+2,8446249224466709581,627460548,1.524470565245E9,1.524470588245E9
+2,-1382790657226354902,627460548,1.524470713245E9,1.524470720245E9
+2,4585644074316800636,627460548,1.524470746245E9,1.524470825245E9
+2,8762222841335602636,627460548,1.524470826245E9,1.524470850245E9
+2,-2262671936032733214,627460548,1.524470878245E9,1.524470880245E9
+2,3433822255857845541,627460548,1.524470888245E9,1.524471161245E9
+2,7559420633593932777,627460548,1.524471165245E9,1.524471358245E9
+2,6498776139947240734,627460548,1.524471390245E9,1.524471446245E9
+2,8486322444058506005,627460548,1.524471448245E9,1.524471461245E9
+13,4817799264842501958,479635942,1.521659509202E9,1.521659510102E9
+13,7147772458447570332,479635942,1.521659545352E9,1.521659546253E9
+13,-4932185226528181672,479635942,1.521660098205E9,1.521660100455E9
+13,-2997641485597601676,479635942,1.5216601363639998E9,1.521660141583E9
+13,402567769413879711,479635942,1.521660360853E9,1.521660468888E9
+13,-3411377884205898521,479635942,1.5216604747359998E9,1.5216605003569999E9
+13,8437075331488845659,479635942,1.5216605038679998E9,1.521660649636E9
+13,-3742205350777598555,479635942,1.521660656898E9,1.521660666138E9
+13,-4026177149477485256,479635942,1.521660682786E9,1.5216606934369998E9
+13,-6978322282385129884,479635942,1.5216606968569999E9,1.521660714287E9
+13,-6422077922835698539,479635942,1.521660754938E9,1.5216607617779999E9
+13,-4864554648212862312,479635942,1.5216607727259998E9,1.521660776596E9
+13,-6066192730293781880,479635942,1.521660796757E9,1.521660968027E9
+13,-6882182369509597035,479635942,1.521661029708E9,1.5216610305779998E9
+13,6136162275528816797,479635942,1.5216610350779998E9,1.521661035949E9
+13,5903075173780523616,479635942,1.521661058659E9,1.521661059468E9
+13,-1346495427689087586,479635942,1.521661061178E9,1.5216610630379999E9
+13,8717479127552464751,479635942,1.521661104438E9,1.521661108938E9
+13,-9074484878331569590,479635942,1.5216611098379998E9,1.5216611207879999E9
+13,1206920225367988278,479635942,1.521661135218E9,1.521661140768E9
+13,7078961414964755886,479635942,1.5216612035579998E9,1.521661204309E9
+13,6023006092693409346,479635942,1.521661315218E9,1.521661367898E9
+13,3245635549036903735,479635942,1.521661374228E9,1.521661383919E9
+13,8166491176997001250,479635942,1.521661394389E9,1.521661429878E9
+13,5128112370791334652,479635942,1.5216614728379998E9,1.521661704199E9
+13,8533662849561025432,479635942,1.521661713949E9,1.5216618244689999E9
+13,-7647637127377450343,479635942,1.5216618262089999E9,1.5216618329599998E9
+13,-8871088638699198633,479635942,1.5216618511999998E9,1.521661905199E9
+13,6282710171146024966,479635942,1.521661932109E9,1.521661956109E9
+13,925025361002840436,479635942,1.5216619958899999E9,1.521662005909E9
+13,809875666491140486,479635942,1.52166201761E9,1.521662179037E9
+13,-8272064035502067169,479635942,1.521662189777E9,1.521662191277E9
+13,1428235939705298331,479635942,1.521662201777E9,1.521662220678E9
+13,2770503645448460679,479635942,1.5216622576369998E9,1.5216622594369998E9
+13,8375653396621733908,479635942,1.521662280258E9,1.521662284158E9
+13,-58078453003816465,479635942,1.521662287247E9,1.521662305337E9
+13,3776595335092534106,479635942,1.521662582898E9,1.521662587429E9
+13,-7703579568060325019,479635942,1.52166263774E9,1.5216628424299998E9
+13,4496920418649241915,479635942,1.521662846931E9,1.52166298079E9
+13,-2698669699054422977,479635942,1.5216630069499998E9,1.52166311699E9
+13,-6860375553783689619,479635942,1.521663358852E9,1.521663364402E9
+13,4773233191951197115,479635942,1.521663366712E9,1.5216633674629998E9
+13,4133005825801737440,479635942,1.5216633697129998E9,1.5216633720519998E9
+13,-8174713008942603155,479635942,1.5216633909529998E9,1.5216633924819999E9
+13,-6009824077439626139,479635942,1.521663487523E9,1.521663488273E9
+13,-5975373578820469544,479635942,1.521663489113E9,1.5216634899229999E9
+13,-275726931500538122,479635942,1.5216634992519999E9,1.5216635010519998E9
+13,715807089942874627,479635942,1.5216635137419999E9,1.521663514642E9
+13,-4063175586583164999,479635942,1.5216636139429998E9,1.5216636157429998E9
+13,-5756669265794165985,479635942,1.521663616643E9,1.5216636184429998E9
+13,857151176166707288,479635942,1.521663619343E9,1.5216636238439999E9
+13,-4374425671241739461,479635942,1.521663632843E9,1.521663634643E9
+65,4719668373508124611,479635942,1.499761614769E9,1.499761615668E9
+65,-7169939569812516543,479635942,1.4997616771369998E9,1.4997616780089998E9
+65,-7273538344788941719,479635942,1.4997617179399998E9,1.4997617224399998E9
+65,5013659030936275641,479635942,1.499761745779E9,1.499761746679E9
+65,237303886416469529,479635942,1.4997617494099998E9,1.4997617503079998E9
+65,6055127308425681282,479635942,1.499761754839E9,1.499761755738E9
+65,-3934014457530951276,479635942,1.4997617864299998E9,1.4997617889789999E9
+65,-6712142563860992483,479635942,1.499761804612E9,1.4997618078509998E9
+65,7549865158803695691,479635942,1.499761809382E9,1.4997618154989998E9
+65,-4086833120984252772,479635942,1.499761850659E9,1.499761855189E9
+65,-5673687569610918985,479635942,1.49976185879E9,1.499761864189E9
+65,-4495511763572570555,479635942,1.499761869619E9,1.4997618714199998E9
+65,2338187898689186004,479635942,1.499761874179E9,1.499761875109E9
+65,7853596920018868921,479635942,1.49976267728E9,1.4997626782089999E9
+65,4385722441516789054,479635942,1.499762750083E9,1.499762750984E9
+65,-8633816536481225059,479635942,1.499762835916E9,1.499762836667E9
+65,4212563341699859262,479635942,1.499762844615E9,1.499762846415E9
+65,6015693465804936212,479635942,1.499762899905E9,1.499762901646E9
+65,-4344380978372460117,479635942,1.499762903325E9,1.4997629145449998E9
+65,-4902386205708669207,479635942,1.499762978266E9,1.4997629791959999E9
+65,8372012408235330481,479635942,1.499763347385E9,1.4997633481339998E9
+65,-5097910357593736211,479635942,1.499763542954E9,1.4997635438539999E9
+65,-6085413192047102426,479635942,1.499763582615E9,1.4997635834259999E9
+13,-3154544354092539908,627460548,1.521660277202E9,1.521660283202E9
+13,-6621488678396043684,627460548,1.521660297202E9,1.521660298202E9
+13,3953943173052118814,627460548,1.521660312202E9,1.521660332202E9
+13,5579633878529810246,627460548,1.521660352202E9,1.521660720202E9
+13,2700728227723515615,627460548,1.521660757202E9,1.521660972202E9
+13,-80149770694396452,627460548,1.521661252202E9,1.521661268202E9
+13,721999558888855777,627460548,1.521661306202E9,1.521661965202E9
+13,-1999473390221597630,627460548,1.521661990202E9,1.521662353202E9
+13,8157187037751996369,627460548,1.521662407202E9,1.521662436202E9
+13,-6997215574325460783,627460548,1.521662523202E9,1.521662591202E9
+13,-8690912574394479226,627460548,1.521662627202E9,1.521663135202E9
+65,4197275359830286825,627460548,1.499761891866E9,1.499761893866E9
+65,7571294578525124640,627460548,1.499761909866E9,1.499761926866E9
+5,8902958916463720654,479635942,1.4999294426920002E9,1.499929487813E9
+5,-7692040218201770403,479635942,1.4999295233930001E9,1.499929527172E9
+5,-3423026619375348253,479635942,1.499929624374E9,1.499929626774E9
+5,212846580323713450,479635942,1.499929647174E9,1.499929648254E9
+5,-1793682281292532537,479635942,1.4999299060110002E9,1.499930020494E9
+5,-3959351044829722336,479635942,1.4999300599720001E9,1.499930090273E9
+5,2647283471769003350,479635942,1.499930108752E9,1.499930112502E9
+5,-369599271055775771,479635942,1.4999301186230001E9,1.499930301349E9
+5,4205040294636417611,479635942,1.4999305141690001E9,1.499930515069E9
+5,4379746394465828628,479635942,1.499930555989E9,1.499930556829E9
+5,7747753480868146931,479635942,1.499930591301E9,1.499930592051E9
+5,6630432870107444104,479635942,1.499930757351E9,1.49993076008E9
+5,-6506652238997531020,479635942,1.499930770971E9,1.499930772741E9
+5,8307308487971482530,479635942,1.499930890522E9,1.499930891421E9
+5,-7411978661435902772,479635942,1.499930895053E9,1.499930896882E9
+5,8450771440901965979,479635942,1.499930971762E9,1.499930974342E9
+5,6015794412134086279,479635942,1.499930988083E9,1.499930989013E9
+5,-387730786331893414,479635942,1.499931037015E9,1.499931038757E9
+5,-1043688133102033435,479635942,1.499931134063E9,1.4999311367940001E9
+5,1096288484248534419,479635942,1.499931187343E9,1.499931188093E9
+5,7243772014294287411,479635942,1.499931198894E9,1.499931201983E9
+5,-4553159679081070131,479635942,1.4999312035440001E9,1.499931207594E9
+5,7692558223503993869,479635942,1.4999312196560001E9,1.499931220583E9
+5,-7993117733540396190,479635942,1.4999312342350001E9,1.4999312415540001E9
+5,-4860414650489795137,479635942,1.499931425418E9,1.4999314305180001E9
+5,-5068205994520341057,479635942,1.499931691371E9,1.49993170676E9
+5,5296398872499190013,479635942,1.4999317295E9,1.49993174582E9
+5,1647590290195022729,479635942,1.49993176208E9,1.49993176844E9
+5,4827762125733049996,479635942,1.49993178818E9,1.499931809181E9
+5,-898308553129896206,479635942,1.49993181077E9,1.499931853881E9
+5,1429825881893739035,479635942,1.4999318792910001E9,1.499931887361E9
+5,-8195681600547059099,479635942,1.4999319647020001E9,1.4999319764620001E9
+5,-1379040740224905692,479635942,1.499931991912E9,1.499932314707E9
+5,3695388058995681464,479635942,1.499932493538E9,1.499932508808E9
+5,-222983529212670781,479635942,1.499932509708E9,1.49993258708E9
+5,-4642433530398408947,479635942,1.499932598778E9,1.4999327054290001E9
+5,-5028526390189212073,479635942,1.4999327146100001E9,1.499932733595E9
+5,130533773238010555,479635942,1.499932737223E9,1.499932840874E9
+5,6394946711252475861,479635942,1.499932851734E9,1.499932877384E9
+5,6597089585081998261,479635942,1.499932891184E9,1.499932967144E9
+5,-3868322482483179853,479635942,1.499933007855E9,1.499933012355E9
+5,-7916319366091270320,479635942,1.499933023005E9,1.499933097046E9
+45,6266181406396054200,479635942,1.50235246699E9,1.50235246789E9
+45,7200835445480681161,479635942,1.50235251178E9,1.502352512619E9
+45,4571098392909437877,479635942,1.502352518831E9,1.50235252108E9
+45,135675698970736221,479635942,1.502352934902E9,1.502352936731E9
+45,-5577686260897590271,479635942,1.502352963848E9,1.502352964748E9
+45,8291955083287461620,479635942,1.502353260671E9,1.502353290281E9
+45,-2682605357669538559,479635942,1.502353302071E9,1.502353307471E9
+45,-7330090020582467320,479635942,1.502353457622E9,1.502353459422E9
+45,4981235174882312208,479635942,1.502353648538E9,1.502353649287E9
+45,1140921559780434240,479635942,1.502353665697E9,1.502353672958E9
+45,2100420905882065348,479635942,1.502354151427E9,1.502354152177E9
+45,-7278279068581340108,479635942,1.502354162947E9,1.502354167567E9
+45,-7012973211423787624,479635942,1.502354308806E9,1.502354311596E9
+45,5721192339350477135,479635942,1.502354374987E9,1.50235437634E9
+45,-2570074008800897080,479635942,1.502354430878E9,1.50235443439E9
+45,4215643679546429471,479635942,1.502354497749E9,1.50235449991E9
+45,925311042337340058,479635942,1.50235451881E9,1.502354541009E9
+45,-1587075061726872088,479635942,1.50235470511E9,1.502354708859E9
+45,8709899580297832521,479635942,1.502354714556E9,1.502354717106E9
+45,-1857290141244922299,479635942,1.502354826551E9,1.502354829188E9
+45,-4531323013506911281,479635942,1.502354906591E9,1.502354907728E9
+45,435295589608270350,479635942,1.502355016601E9,1.502355017952E9
+5,9034480364792910277,627460548,1.499929899381E9,1.499930317381E9
+5,3098729330709378421,627460548,1.499931296381E9,1.499931377381E9
+5,-3624193296538870209,627460548,1.499931674381E9,1.499931713381E9
+5,6466824894604173497,627460548,1.499931731381E9,1.499931865381E9
+5,-8571204180817890063,627460548,1.499931870381E9,1.499931896381E9
+5,7808981918047423877,627460548,1.499931907381E9,1.499931926381E9
+5,7809691020130174005,627460548,1.499931938381E9,1.499931946381E9
+5,-2988930365537617065,627460548,1.499931954381E9,1.499931982381E9
+5,-3322243079765283902,627460548,1.499932000381E9,1.499932324381E9
+5,8141979530607143028,627460548,1.499932406381E9,1.499932409381E9
+5,-6556680061057295706,627460548,1.499932451381E9,1.499932981381E9
+5,817601014755015407,627460548,1.499932982381E9,1.499933107381E9
+5,-5190891934745124218,627460548,1.499933112381E9,1.499933150381E9
+5,2685161687734480169,627460548,1.499933159381E9,1.499933178381E9
+45,536028785320424480,627460548,1.502352708421E9,1.502352770421E9
+45,8305880036004809305,627460548,1.502352849421E9,1.502352870421E9
+7,-1808444978587944336,479635942,1.499367547936E9,1.499367548775E9
+7,7566209710823937298,479635942,1.499367746717E9,1.4993677476160002E9
+7,-102897415790291370,479635942,1.499368004446E9,1.499368005345E9
+7,-8889937423372047749,479635942,1.499368032075E9,1.499368035555E9
+7,1312564108312011945,479635942,1.499368061115E9,1.499368062676E9
+7,-9053341801436345413,479635942,1.499368096756E9,1.4993680994550002E9
+7,6026559957067583640,479635942,1.4993681121160002E9,1.499368118476E9
+7,-6692539519040106197,479635942,1.499368144786E9,1.4993682083270001E9
+7,-6555487177726919675,479635942,1.499368216726E9,1.499368259176E9
+7,5015061172082147878,479635942,1.4993682763660002E9,1.499368292476E9
+7,-9141243021315057686,479635942,1.499368306036E9,1.4993683910870001E9
+7,1934331560186185199,479635942,1.4993684264580002E9,1.499368435877E9
+7,5106685400200901038,479635942,1.4993684847470002E9,1.499368487477E9
+7,-3637733582875247986,479635942,1.499368490177E9,1.499368492877E9
+7,-969012846056418675,479635942,1.499368580834E9,1.499368630005E9
+7,6102372668010452217,479635942,1.4993686354640002E9,1.499368694088E9
+7,2166567492366169776,479635942,1.499368768069E9,1.4993687691780002E9
+7,-1826984801005310387,479635942,1.4993692567110002E9,1.4993692612410002E9
+7,4258659392126689540,479635942,1.4993693675270002E9,1.499369406407E9
+7,-210196320991639744,479635942,1.4993694180470002E9,1.4993694730080001E9
+7,-8867247233755604431,479635942,1.499369480237E9,1.4993696713100002E9
+7,7946327702765303496,479635942,1.49936973593E9,1.499369739019E9
+7,2938349546860434379,479635942,1.4993697818890002E9,1.4993699703500001E9
+7,2007310469855512286,479635942,1.499370327676E9,1.499370338536E9
+7,4023301720356759627,479635942,1.499370344956E9,1.499370372917E9
+7,39148491874846965,479635942,1.499370380657E9,1.4993703853070002E9
+7,-7622894766729398540,479635942,1.4993703883070002E9,1.499370393707E9
+7,623124786120858274,479635942,1.499370409036E9,1.499370481727E9
+7,-1725003033276983277,479635942,1.4993707381650002E9,1.499370739065E9
+7,8423304772946172202,479635942,1.4993707504350002E9,1.499370835305E9
+7,7402190212066389388,479635942,1.4993708362050002E9,1.499370910426E9
+7,-1940219217588224784,479635942,1.499370916516E9,1.499370991216E9
+7,4543245080622306889,479635942,1.499371008342E9,1.499371062223E9
+7,-6240201281733892615,479635942,1.4993710768030002E9,1.499371243844E9
+7,-6645225289200000888,479635942,1.499371548105E9,1.4993715519160001E9
+7,-4931535407836327335,479635942,1.4993716262210002E9,1.4993716280210001E9
+7,7104521075700542246,479635942,1.499371834752E9,1.499371835502E9
+7,-7629915031770549489,479635942,1.499371879753E9,1.499371880653E9
+7,7723518591563702616,479635942,1.499371959223E9,1.4993719599740002E9
+7,-2773104361333438343,627460548,1.499367927538E9,1.499367940538E9
+7,4561401412736649794,627460548,1.499367952538E9,1.499367966538E9
+7,-9096136500403375882,627460548,1.499367979538E9,1.499367982538E9
+7,5025036125181142561,627460548,1.499368112538E9,1.499368452538E9
+7,-7040298260106543148,627460548,1.499368543538E9,1.499368553538E9
+7,1320492427382947647,627460548,1.499368569538E9,1.499368701538E9
+7,-3693230908287934432,627460548,1.499369360538E9,1.499369675538E9
+7,-4566464079515358922,627460548,1.499369687538E9,1.499369702538E9
+7,-572149208593503650,627460548,1.499369707538E9,1.499369715538E9
+7,7251964566713875602,627460548,1.499369719538E9,1.499369745538E9
+7,2893453764439219333,627460548,1.499369777538E9,1.499369982538E9
+7,9041852605052855848,627460548,1.499369987538E9,1.499370020538E9
+7,-3736564651179375684,627460548,1.499370051538E9,1.499370070538E9
+7,1783094021464126029,627460548,1.499370321538E9,1.499370489538E9
+7,-5767613228858276254,627460548,1.499370505538E9,1.499370524538E9
+7,8332374850927550445,627460548,1.499370738538E9,1.499370998538E9
+7,5231020659771374701,627460548,1.499371001538E9,1.499371264538E9
+41,-4677523405622048088,479635942,1.51988760536E9,1.51988760626E9
+41,-178695453151093496,479635942,1.5198876253E9,1.51988762629E9
+41,6127897547816621601,479635942,1.519887697543E9,1.519887698286E9
+41,8272017924394962172,479635942,1.5198877487749999E9,1.519887749648E9
+41,-3093168700136956182,479635942,1.519887752375E9,1.519887753245E9
+41,-3249193701604923720,479635942,1.519887789756E9,1.519887792065E9
+41,2573557067244083826,479635942,1.519887840665E9,1.519887841564E9
+41,-6661984088213940905,479635942,1.519887844323E9,1.519887846123E9
+41,-7720343372977107946,479635942,1.519887885034E9,1.5198878859029999E9
+41,-7440337806377233149,479635942,1.519887888514E9,1.519887890224E9
+41,5382125340492849363,479635942,1.5198879366629999E9,1.519887937623E9
+41,-1363181056850043860,479635942,1.519887961263E9,1.5198879621629999E9
+41,-7808272543654999306,479635942,1.519887964053E9,1.519887965853E9
+41,-8153572807600975955,479635942,1.519887969514E9,1.519887974102E9
+41,1588119377546676693,479635942,1.519887999483E9,1.519888000233E9
+41,788596363199245797,479635942,1.519888001733E9,1.519888004312E9
+41,8624612020489102104,479635942,1.519888006082E9,1.519888008725E9
+41,-5261895149861224231,479635942,1.519888010343E9,1.519888012925E9
+41,2439517011922788417,479635942,1.5198881569229999E9,1.519888158511E9
+41,-7986516893518690268,479635942,1.519888187281E9,1.519888190011E9
+41,5733419275909412287,479635942,1.5198882411299999E9,1.519888244491E9
+41,-2563130171662740927,479635942,1.5198882501929998E9,1.519888267533E9
+41,4394627724010566465,479635942,1.519888332514E9,1.519888369743E9
+41,-6518689595040719673,479635942,1.519888392514E9,1.519888521302E9
+41,7628191332091892683,479635942,1.519888547582E9,1.519888550282E9
+41,-1806250691208316253,479635942,1.519888552981E9,1.519888563541E9
+41,8946440547963602708,479635942,1.5198885975609999E9,1.519888669501E9
+41,-7657111425514477905,479635942,1.519888786411E9,1.5198887882389998E9
+41,-9204072247326138662,479635942,1.5198888121499999E9,1.519888812991E9
+41,4378113508704496282,479635942,1.519888854691E9,1.5198888555909998E9
+41,-4809677895435621791,479635942,1.519888886461E9,1.519888889221E9
+41,957919953322641549,479635942,1.51988908377E9,1.51988908557E9
+41,-1668987856019589076,479635942,1.519889921516E9,1.5198899224459999E9
+41,-3975455588039475088,479635942,1.519889933306E9,1.519889940477E9
+41,-74575833505549571,479635942,1.519889943026E9,1.5198899490579998E9
+41,7574304553408663397,479635942,1.519890086009E9,1.5198900875049999E9
+41,5380358890321647678,479635942,1.5198901051529999E9,1.519890106648E9
+41,509908886070930648,479635942,1.5198901811629999E9,1.519890182664E9
+41,2576910454007426840,479635942,1.519890183414E9,1.519890186654E9
+41,5982470602958907473,627460548,1.519888018198E9,1.519888079198E9
+41,8070345324659771998,627460548,1.519888108198E9,1.519888129198E9
+41,-1043996108886573113,627460548,1.519888241198E9,1.519888674198E9
+41,-2423033387007625924,627460548,1.519889144198E9,1.519889187198E9
+41,-4727060375464731483,627460548,1.519889205198E9,1.519889231198E9
+41,-6245524963796692913,627460548,1.519889253198E9,1.519889264198E9
+41,78802444463084265,627460548,1.519889275198E9,1.519889286198E9
+41,-7791822701797037495,627460548,1.519889307198E9,1.519889311198E9
+41,8764762745713966564,627460548,1.519889314198E9,1.519889437198E9
+41,-5675205819948262275,627460548,1.519889574198E9,1.519889622198E9
+41,-3654878556763806290,627460548,1.519889844198E9,1.519889847198E9
+41,-7786629805285420609,627460548,1.519889855198E9,1.519889869198E9
+72,7349084231938519861,479635942,1.5011450202689998E9,1.501145021138E9
+72,-3549146352338583379,479635942,1.5011451072389998E9,1.50114510904E9
+72,3837283931272050234,479635942,1.501145118099E9,1.5011451189989998E9
+72,-1839616692179130239,479635942,1.5011451500219998E9,1.50114515482E9
+72,-2715197297216098958,479635942,1.501145161813E9,1.5011451635219998E9
+72,8286645675138383523,479635942,1.501145169914E9,1.5011451706629999E9
+72,-6817751754951934533,479635942,1.5011452261299999E9,1.501145227931E9
+72,-4679699424704212085,479635942,1.501145230631E9,1.5011452315319998E9
+72,-2742896797415491301,479635942,1.50114526204E9,1.5011452655809999E9
+72,-6395694713084343142,479635942,1.501145273771E9,1.501145275271E9
+72,5368072267026914267,479635942,1.501145320151E9,1.501145326422E9
+72,-2126303297105770488,479635942,1.5011453291209998E9,1.501145335543E9
+72,5784208471437148692,479635942,1.501145342862E9,1.501145345592E9
+72,5823934772091517279,479635942,1.501145357501E9,1.501145362001E9
+72,-2469297329014265550,479635942,1.501145513138E9,1.501145518148E9
+72,6502904036972389269,479635942,1.50114555733E9,1.501145558079E9
+72,7214989092161437164,479635942,1.501145560418E9,1.501145562668E9
+72,-6326010689359857321,479635942,1.501145564168E9,1.5011455679789999E9
+72,-6178611041498736965,479635942,1.501145612319E9,1.5011456132189999E9
+72,-324060818884686191,479635942,1.501145617809E9,1.501145618711E9
+72,977063501159212129,479635942,1.501145670672E9,1.5011456714789999E9
+72,-8316004670462761355,479635942,1.501145676879E9,1.501145679191E9
+72,-8654916052714027138,479635942,1.5011456824299998E9,1.5011456831799998E9
+72,-3348750295688071206,479635942,1.501145687079E9,1.50114568981E9
+72,3595361889405729230,479635942,1.501145726412E9,1.50114572731E9
+72,3323330250236163804,479635942,1.501145870563E9,1.501145871402E9
+72,7793321357906836428,479635942,1.501145900623E9,1.501145901461E9
+63,-387868821401755592,479635942,1.521534681523E9,1.521534691693E9
+63,2307824192423988307,479635942,1.521534708793E9,1.521534710293E9
+63,-1080721506877563135,479635942,1.521534720072E9,1.521534720822E9
+63,1877791111076666096,479635942,1.5215347951920002E9,1.521534796002E9
+63,-1088656736121464004,479635942,1.521534797592E9,1.521534798463E9
+63,-5882842860394767340,479635942,1.5215348017020001E9,1.521534802482E9
+63,1161124801694752899,479635942,1.521534834431E9,1.521534835181E9
+63,2854158283434250227,479635942,1.521534848891E9,1.521534850691E9
+63,5860587540692531972,479635942,1.52153489665E9,1.52153489755E9
+63,1432305960810305325,479635942,1.5215349001890001E9,1.52153491141E9
+63,-2209237361749169686,479635942,1.521534915309E9,1.5215349176200001E9
+63,3840183682456072919,479635942,1.5215349198700001E9,1.5215349206200001E9
+63,-8334207964566990846,479635942,1.5215352797480001E9,1.5215352805040002E9
+63,-1356390637588517158,479635942,1.5215352828370001E9,1.521535283589E9
+63,6439699972430138574,479635942,1.521535345176E9,1.5215353460770001E9
+63,-7083250554452252151,479635942,1.521535348777E9,1.521535349707E9
+63,1371665037533329470,479635942,1.521535423029E9,1.521535423926E9
+63,-622020087765505622,479635942,1.521535482006E9,1.521535482816E9
+63,4479076988115521000,479635942,1.521535574375E9,1.521535575275E9
+64,-7708674167454275526,479635942,1.499330430144E9,1.499330431944E9
+64,-56539365439692692,479635942,1.499330441874E9,1.499330442775E9
+64,-2216180734663872570,479635942,1.4993305039160001E9,1.4993305047250001E9
+64,332371965965655320,479635942,1.499330535237E9,1.499330536015E9
+64,-8518325267389109714,479635942,1.4993306083760002E9,1.499330609247E9
+64,2953638335034800189,479635942,1.499330646236E9,1.4993306492970002E9
+64,-6806905253670888201,479635942,1.499330659016E9,1.4993306599160001E9
+64,2969987012616826320,479635942,1.499330685266E9,1.4993306888660002E9
+64,6864806943596030189,479635942,1.499330714907E9,1.4993307158070002E9
+64,3368038195188030217,479635942,1.4993307373760002E9,1.499330740407E9
+64,4971043108251322373,479635942,1.4993307419680002E9,1.4993307442180002E9
+64,-38472601267021523,479635942,1.499330840964E9,1.4993308437240002E9
+64,6644949758242835283,479635942,1.4993309151850002E9,1.499330916115E9
+64,2226283497680774849,479635942,1.499330918815E9,1.4993309197150002E9
+64,-5561867658391240094,479635942,1.4993310321560001E9,1.499331033775E9
+64,-6396572371930544009,479635942,1.499331035575E9,1.499331036505E9
+64,3308990702071930347,479635942,1.4993310960860002E9,1.499331096986E9
+64,-7436936749515992944,479635942,1.499331098697E9,1.499331102986E9
+64,-948422790415422742,627460548,1.499330778298E9,1.499330791298E9
+66,-7722668711470158764,479635942,1.519719607197E9,1.5197196102580001E9
+66,-7784765305257264729,479635942,1.5197196604480002E9,1.5197196631460001E9
+66,3200463398401226877,479635942,1.519719671367E9,1.519719672299E9
+66,-6228087251117047021,479635942,1.5197197349970002E9,1.519719735926E9
+66,4245331785510372279,479635942,1.519719768506E9,1.519719773967E9
+66,8726810908537607247,479635942,1.5197197874970002E9,1.5197197883660002E9
+66,-8304742241430141584,479635942,1.5197197927770002E9,1.519719793648E9
+66,7252332276579439706,479635942,1.519719827786E9,1.519719828716E9
+66,3727139577522669009,479635942,1.519719830516E9,1.5197198323760002E9
+66,8158661245170802947,479635942,1.519719833276E9,1.519719838706E9
+66,-3554608247727530664,479635942,1.519719844167E9,1.519719845066E9
+66,2445160304740286163,479635942,1.5197198541550002E9,1.519719855056E9
+66,8449821975889812097,479635942,1.519719857845E9,1.519719859677E9
+66,1685217310746362649,479635942,1.5197198687050002E9,1.5197198723660002E9
+66,8658326976840453268,479635942,1.519720050421E9,1.519720051167E9
+66,6615015572829396137,479635942,1.519720053509E9,1.519720055009E9
+66,-6328876798551859687,479635942,1.519720161686E9,1.5197201625270002E9
+66,3692635464107712470,479635942,1.519720236505E9,1.5197202374060001E9
+66,552857259367872954,479635942,1.519720242865E9,1.519720243765E9
+66,-8533238888322276948,479635942,1.5197204596450002E9,1.5197204605760002E9
+66,-7720810768648913084,479635942,1.5197208185040002E9,1.519720819344E9
+66,-8134504750248642783,479635942,1.5197208621230001E9,1.5197208630540001E9
+66,-3253883043109902158,627460548,1.519719809367E9,1.519719821367E9
+1,2011702610517061746,479635942,1.5223420552389998E9,1.5223420561399999E9
+1,8052312756486327108,479635942,1.5223420850289998E9,1.5223420859289997E9
+1,-7724838520056834887,479635942,1.5223420948699996E9,1.5223420957689998E9
+1,-8703191068718194937,479635942,1.5223421153289998E9,1.5223421162299998E9
+1,-7612356545069565773,479635942,1.5223422415089998E9,1.5223422423799996E9
+1,-4801122709854909103,479635942,1.5223422558789997E9,1.5223422567489998E9
+1,-3861814096722306717,479635942,1.5223426537969997E9,1.5223426547269998E9
+1,4534398901693039288,479635942,1.5223426838569996E9,1.5223426848169997E9
+1,4782429586747185523,479635942,1.5223427622479997E9,1.5223427637479997E9
+1,-1665583644534407272,479635942,1.5223430351269999E9,1.5223430360269997E9
+1,-4672661020375272926,479635942,1.5223430414569998E9,1.5223430423569996E9
+1,788012111625654361,479635942,1.5223431455559998E9,1.5223431465759997E9
+1,-5263746901235689052,479635942,1.5223431776869998E9,1.5223431785869997E9
+1,3983974241423904455,479635942,1.5223432161459997E9,1.5223432168969998E9
+1,-4191943150157127094,479635942,1.5223432829259996E9,1.5223432838259997E9
+1,5384525810414840800,479635942,1.5223432856569998E9,1.5223432865559998E9
+1,996506958026982933,479635942,1.5223434435159998E9,1.5223434443869998E9
+1,9058670877734439452,479635942,1.5223441430089998E9,1.5223441442699997E9
+1,-2188653196871002330,479635942,1.5223443272679996E9,1.5223443282899997E9
+1,1396086404134580402,479635942,1.5223445999379997E9,1.5223446017379997E9
+1,-7580848256663138879,479635942,1.5223446404379997E9,1.5223446412779996E9
+1,-2408265882598384355,479635942,1.5223446429579997E9,1.5223446438279998E9
+1,-2518356896661729862,479635942,1.5223447079679997E9,1.5223447088679998E9
+1,-976904041326757485,479635942,1.5223447648489997E9,1.5223447666179998E9
+1,-6047289796479193406,479635942,1.5223448661579998E9,1.5223448722479997E9
+1,-6050026585726393484,479635942,1.5223448757879999E9,1.5223448828979998E9
+1,-1364325958713954078,479635942,1.5223449163489997E9,1.5223449211169999E9
+1,-3518661501516058856,479635942,1.5223449218989997E9,1.5223449242369998E9
+1,406509367928660936,479635942,1.5223449462289999E9,1.5223449516279998E9
+1,-788463548014622857,479635942,1.5223450665899997E9,1.5223450719909997E9
+1,-5063114389807714829,479635942,1.5223450728899999E9,1.5223450783199997E9
+1,-6587471117942044255,479635942,1.5223450864199998E9,1.5223451135109997E9
+1,-7643593366575074942,479635942,1.5223451386199996E9,1.5223451604599998E9
+1,-6656698791426814590,479635942,1.5223451636099997E9,1.5223453503599997E9
+1,1189018417848857299,479635942,1.5223453664119997E9,1.5223454184009998E9
+1,5907070023440599095,479635942,1.5223454265299997E9,1.5223455010509996E9
+1,-178188395778287975,479635942,1.5223456265109997E9,1.5223456272609997E9
+1,4391583515375772602,479635942,1.5223460073919997E9,1.5223460100919998E9
+1,2029013473896337574,479635942,1.5223460309129999E9,1.5223460336119998E9
+1,3494940479816335368,479635942,1.5223461898819997E9,1.5223461906929998E9
+1,-2474217368813907887,479635942,1.5223461953429997E9,1.5223461983429997E9
+1,5035097814576773127,479635942,1.5223462447219996E9,1.5223462456219997E9
+1,-4602546580498750229,479635942,1.5223462555829997E9,1.5223462582819998E9
+1,-8065544650026521852,479635942,1.5223463559919999E9,1.5223463568929996E9
+1,4612658976108604600,479635942,1.5223463640919998E9,1.5223463650219998E9
+1,-793232335097168089,479635942,1.5223469105309997E9,1.5223469116109998E9
+1,7814650910290370884,479635942,1.5223470608669999E9,1.5223470637769997E9
+1,8523221661415107386,479635942,1.5223471431559997E9,1.5223471457369998E9
+1,-6124619826740612524,479635942,1.5223471502659998E9,1.5223471529979997E9
+1,442426065745810156,479635942,1.5223471583369997E9,1.5223471608569996E9
+1,507497286636504211,479635942,1.5223471816179998E9,1.5223471823679998E9
+1,1282036458393761530,479635942,1.5223471831179998E9,1.5223471861179998E9
+1,3045984571673582708,479635942,1.5223472529529998E9,1.5223472842729998E9
+1,-9124668695529772826,479635942,1.5223473116329997E9,1.5223473134329998E9
+1,-1984102843527604709,479635942,1.5223473601729999E9,1.5223475315629997E9
+1,5267385840744473615,479635942,1.5223475495939999E9,1.5223475594639997E9
+1,-4181015596023858505,479635942,1.5223475994539998E9,1.5223476754439998E9
+1,1256497883890158481,479635942,1.5223477562049997E9,1.5223477589049997E9
+1,3044774076748321148,479635942,1.5223477859849997E9,1.5223477897639997E9
+1,-1602804958307063948,479635942,1.5223477983149998E9,1.5223478001749997E9
+1,5268082680764618884,479635942,1.5223479262069998E9,1.5223479282449996E9
+1,6682364632461235758,479635942,1.5223479427959998E9,1.5223479445969996E9
+1,7441181468021537861,479635942,1.5223479545259998E9,1.5223479555149999E9
+1,3639357921960080647,479635942,1.5223480425459998E9,1.5223480458769999E9
+1,-4689540127585079298,479635942,1.5223480528969998E9,1.5223480678059998E9
+1,-8619594752486031033,479635942,1.5223486840539997E9,1.5223486933839998E9
+1,-5629077619490409960,479635942,1.5223487145049996E9,1.5223487305849998E9
+1,-5483390653874302473,479635942,1.5223487389249997E9,1.5223487507149997E9
+1,-7886269455361735245,479635942,1.5223487737259998E9,1.5223487767259998E9
+1,3660333314119397104,479635942,1.5223487862949998E9,1.5223487935849998E9
+1,-7451784041563779563,479635942,1.5223489000859997E9,1.5223489812079997E9
+1,-6862727367400738824,479635942,1.5223489902369998E9,1.5223491436569998E9
+1,5781003997132495666,479635942,1.5223491499869998E9,1.5223491607569997E9
+1,9051425867701705569,479635942,1.5223491831369998E9,1.5223492075279996E9
+1,7311614263889312027,479635942,1.5223494101199996E9,1.5223494157589998E9
+1,-8130963386133965028,479635942,1.5223495064189997E9,1.5223495072899997E9
+1,5486362317664265069,479635942,1.5223495189899998E9,1.5223495212399998E9
+1,6068915525844548076,479635942,1.5223495575689998E9,1.5223495593689997E9
+1,4301749932214998094,479635942,1.5223496488899999E9,1.5223496511399999E9
+1,352885825063062633,479635942,1.5223496541399999E9,1.5223496563909998E9
+1,972550385184240571,479635942,1.5223496737899997E9,1.5223496755899997E9
+1,3287700133110600200,479635942,1.5223496827909997E9,1.5223496863899999E9
+1,-3778493555693101334,479635942,1.5223496917899997E9,1.5223496944899998E9
+1,4803809497308140556,479635942,1.5223497316899998E9,1.5223497342099998E9
+1,-6068290437933413614,479635942,1.5223498018609998E9,1.5223498036609998E9
+1,-5647945425913720358,479635942,1.5223498072609997E9,1.5223498081609998E9
+1,2391299093138852528,479635942,1.5223498421809998E9,1.5223498429609997E9
+15,5674514831207226738,479635942,1.5010548616969998E9,1.5010548624469998E9
+15,8526628640297578421,479635942,1.5010548812259998E9,1.501054882126E9
+15,-3221350351955076309,479635942,1.5010549694559999E9,1.5010549702059999E9
+15,7979388045735057634,479635942,1.501054972517E9,1.501054974018E9
+15,-1148147588669840887,479635942,1.501055002097E9,1.501055003597E9
+15,-1087504450246344602,479635942,1.501055071247E9,1.5010550721469998E9
+15,6380196404669273142,479635942,1.501055075748E9,1.501055077518E9
+15,4447359542693422444,479635942,1.501055096418E9,1.5010550971669998E9
+15,8226393635034838095,479635942,1.501055103408E9,1.5010551057189999E9
+15,2199135515111423810,479635942,1.5010551310679998E9,1.501055131968E9
+15,-6701777319647864697,479635942,1.501055133768E9,1.501055135629E9
+15,-229050406819685918,479635942,1.5010551365279999E9,1.501055137428E9
+15,-3535700018836067696,479635942,1.5010551518579998E9,1.5010551527589998E9
+15,6496399619893984357,479635942,1.50105515456E9,1.5010551563879998E9
+15,305962883988710371,479635942,1.501055158188E9,1.501055159988E9
+15,-2838698212771148377,479635942,1.5010551968279998E9,1.5010551985979998E9
+15,7289599539836723674,479635942,1.501055201178E9,1.501055202918E9
+15,3773360981308185894,479635942,1.5010552344799998E9,1.5010552353189998E9
+15,5380653810171671020,479635942,1.501055240629E9,1.501055242189E9
+15,-1915559223150357543,479635942,1.5010552452789998E9,1.50105524678E9
+15,-3315701241537856866,479635942,1.501055492836E9,1.501055498175E9
+15,3559031427999251940,479635942,1.5010555105059998E9,1.501055515905E9
+15,-4101595302292380662,479635942,1.5010555195059998E9,1.501055583796E9
+15,-205186731877998722,479635942,1.5010556054859998E9,1.501055614636E9
+15,-4198471446518016008,479635942,1.501055645626E9,1.5010556917359998E9
+15,519371735146566519,479635942,1.501055698066E9,1.501055719067E9
+15,1980695642775245625,479635942,1.501055731908E9,1.5010557450519998E9
+15,-4622136540444779429,479635942,1.501055831929E9,1.501055833518E9
+15,4918945119565506828,479635942,1.501055834267E9,1.5010558395779998E9
+15,-1236905120090552224,479635942,1.5010559877749999E9,1.5010559885249999E9
+15,7876309239950267596,479635942,1.5010561760849998E9,1.501056176985E9
+15,91548746927325249,479635942,1.501056264435E9,1.5010562689959998E9
+15,-5242650524055454950,479635942,1.5010564831969998E9,1.5010564846969998E9
+15,-5230930335982649349,479635942,1.501056622771E9,1.501056623511E9
+15,5330014354181555602,479635942,1.5010571064439998E9,1.5010571071929998E9
+15,-2411722358562652780,479635942,1.5010572754899998E9,1.5010572772889998E9
+15,-1904021872399996187,479635942,1.50105729181E9,1.5010572927099998E9
+15,-1286669139893299188,479635942,1.5010576310549998E9,1.501057633452E9
+15,682353131493516386,479635942,1.5010577048179998E9,1.5010577066179998E9
+15,3372857720372184710,479635942,1.5010578099109998E9,1.50105781084E9
+15,5554832599010177850,479635942,1.5010578324399998E9,1.501057835889E9
+15,-533249632036496060,479635942,1.5010582746069999E9,1.5010582791379998E9
+15,-6352278968384264393,479635942,1.5010583226399999E9,1.501058324408E9
+15,-6695557396181775501,479635942,1.50105836776E9,1.5010583785589998E9
+15,2518132122871798975,479635942,1.5010584027389998E9,1.501058406339E9
+15,7805968684218304970,479635942,1.5010584502559998E9,1.5010584511269999E9
+15,-7410036492877817202,479635942,1.5010584520279999E9,1.501058455478E9
+15,-3692940415117112549,479635942,1.501058469099E9,1.5010584699669998E9
+15,9002585348644348226,479635942,1.5010584743189998E9,1.5010584759069998E9
+15,-7097406725363016431,479635942,1.501058497958E9,1.501058498797E9
+15,7724194224988043391,479635942,1.501058516797E9,1.5010585186279998E9
+15,7818753916548148637,479635942,1.5010585250159998E9,1.5010585259169998E9
+15,3125702859676108468,479635942,1.501058534087E9,1.501058539537E9
+15,-5481357432299743806,479635942,1.501058541337E9,1.501058543143E9
+15,-2092580797704714807,479635942,1.5010585485359998E9,1.501058550337E9
+15,-4034078988507495331,479635942,1.5010585692069998E9,1.5010585701069999E9
+15,7908448017415971211,479635942,1.501058572807E9,1.5010585753869998E9
+15,-5802439914802332250,479635942,1.501058585257E9,1.5010585861569998E9
+15,3985068091752069696,479635942,1.5010587099959998E9,1.5010587117359998E9
+15,7011441579650767157,479635942,1.5010587133869998E9,1.5010587167759998E9
+15,-7462698104047495543,479635942,1.501058756527E9,1.5010587574559999E9
+77,1207667927464859018,479635942,1.501530873715E9,1.501530874705E9
+77,-6434891464697196824,479635942,1.501531097579E9,1.501531098328E9
+77,1496426376066523478,479635942,1.501531145669E9,1.50153114657E9
+77,2500279291343453256,479635942,1.501531155628E9,1.501531156528E9
+77,8322933800296372175,479635942,1.501531183302E9,1.501531184112E9
+77,-4071385490699071698,479635942,1.501531294419E9,1.50153129517E9
+77,-4080601915773239018,479635942,1.501531305009E9,1.501531306508E9
+77,-5200824067668337378,479635942,1.501531314009E9,1.501531315509E9
+77,2247317944957634122,479635942,1.501531380399E9,1.5015313813E9
+77,8048481326882221158,479635942,1.501531498064E9,1.5015315059E9
+77,-554676247354202311,479635942,1.501531537165E9,1.501531540882E9
+77,-851786572561386675,479635942,1.501531610015E9,1.501531611996E9
+77,8734079615436554953,479635942,1.501531615707E9,1.501531617604E9
+77,4169351128071739799,479635942,1.501531665869E9,1.5015316678470001E9
+77,5911136307152762713,479635942,1.501531746715E9,1.501531748749E9
+77,-3565420134326535136,479635942,1.501531789944E9,1.501531792088E9
+77,-4423152424965677804,479635942,1.501531804491E9,1.501531807544E9
+77,-1156874418084337014,479635942,1.501531821184E9,1.501531823828E9
+1,8724902421903630867,627460548,1.5223449201579998E9,1.5223449881579998E9
+1,-7133278293675844760,627460548,1.5223450201579998E9,1.5223450471579998E9
+1,-7032243117222112801,627460548,1.5223450741579998E9,1.5223451211579998E9
+1,502018113555828644,627460548,1.5223451271579998E9,1.5223455041579998E9
+1,-8136262909607767,627460548,1.5223471591579998E9,1.5223471751579998E9
+1,4206817155372211865,627460548,1.5223472051579998E9,1.5223472171579998E9
+1,-5917649767893816599,627460548,1.5223472231579998E9,1.5223473521579998E9
+1,6856560965358601071,627460548,1.5223473551579998E9,1.5223475781579998E9
+1,2579696317140897087,627460548,1.5223475851579998E9,1.5223476931579998E9
+1,-1711969672346902692,627460548,1.5223477001579998E9,1.5223477671579998E9
+1,2943293789736059765,627460548,1.5223479551579998E9,1.5223479571579998E9
+1,53407197793942360,627460548,1.5223479631579998E9,1.5223479831579998E9
+1,2422378131931299845,627460548,1.5223481071579998E9,1.5223481311579998E9
+1,-8479939479382248179,627460548,1.5223482991579998E9,1.5223483071579998E9
+1,-6859386369307958961,627460548,1.5223483361579998E9,1.5223483401579998E9
+1,3096654080314498211,627460548,1.5223483841579998E9,1.5223483931579998E9
+1,-2216710971175188305,627460548,1.5223484931579998E9,1.5223485271579998E9
+1,2674867824878445166,627460548,1.5223486361579998E9,1.5223488041579998E9
+1,-3383072363441494449,627460548,1.5223488111579998E9,1.5223492321579998E9
+15,4184610140458662981,627460548,1.50105551187E9,1.50105559187E9
+15,3799577609150081280,627460548,1.50105559787E9,1.50105575587E9
+15,1069756466712498702,627460548,1.50105729287E9,1.50105741687E9
+15,4728997244984685431,627460548,1.50105742287E9,1.50105749487E9
+15,2829432426447922465,627460548,1.50105825787E9,1.50105828687E9
+15,3755894065056053234,627460548,1.50105829687E9,1.50105831487E9
+3,1514957805362266958,479635942,1.5012635685469997E9,1.5012635692959998E9
+3,7999461460725244078,479635942,1.5012636699199998E9,1.5012636707569997E9
+3,2462932405019587011,479635942,1.5012637193869998E9,1.5012637202879999E9
+3,-4336080123780192837,479635942,1.5012640550899997E9,1.5012640558369997E9
+3,-8071297743325020025,479635942,1.5012641640169997E9,1.5012641727479997E9
+3,7234574314960815222,479635942,1.5012642019379997E9,1.5012642026899998E9
+3,4541044529897639889,479635942,1.5012643153969998E9,1.5012643300369997E9
+3,7751222854500301110,479635942,1.5012644160799997E9,1.5012644169749997E9
+3,-4758976890291903048,479635942,1.5012644468549998E9,1.5012644936249998E9
+3,-6596748138873331727,479635942,1.5012645099149997E9,1.5012645843159997E9
+3,-6599927583317810016,479635942,1.5012646136249998E9,1.5012646145249999E9
+3,-5985188656802829103,479635942,1.5012646253249998E9,1.5012646262249997E9
+3,-6751922704480618538,479635942,1.5012647199439998E9,1.5012647227759998E9
+3,-1178019866244300013,479635942,1.5012648014809997E9,1.5012648033789997E9
+3,-1401126923522355579,479635942,1.5012648195489998E9,1.5012648217759998E9
+3,-1391882352226554566,479635942,1.5012652878039997E9,1.5012652885539997E9
+3,-6123491999176752515,479635942,1.5012653279429998E9,1.5012653295029998E9
+3,7670246949588577375,479635942,1.5012654261649997E9,1.5012654269149997E9
+3,-1291641994180930968,479635942,1.5012655228199997E9,1.5012655245589998E9
+3,-6246942355577032357,479635942,1.5012655678179998E9,1.5012655713879998E9
+3,-9053224774303788837,479635942,1.5012655722879999E9,1.5012655749879997E9
+3,9055867540294919037,479635942,1.5012655803879998E9,1.5012655822179997E9
+3,815741452890975614,479635942,1.5012655876479998E9,1.5012655903479998E9
+3,3539884616619483446,479635942,1.5012656227479997E9,1.5012657068389997E9
+3,-4918051360885637254,479635942,1.5012657123289998E9,1.5012657714899998E9
+3,-1650095159834422017,479635942,1.5012659234419997E9,1.5012659249999998E9
+3,2692551895520284500,479635942,1.5012659334299998E9,1.5012659357429998E9
+3,2106021580738181621,479635942,1.5012660267029998E9,1.5012660275719998E9
+3,7840673105508985180,479635942,1.5012660789269998E9,1.5012660796769998E9
+3,-2711556646462993225,479635942,1.5012664331089997E9,1.5012664339199998E9
+3,2200403446136681589,479635942,1.5012664517999997E9,1.5012664526109998E9
+3,937363912532196641,479635942,1.5012665519699998E9,1.5012665536819997E9
+3,-6019166495123879963,479635942,1.5012665763009996E9,1.5012665857219996E9
+3,-2843575162192371936,479635942,1.5012666366009998E9,1.5012666384009998E9
+3,4567288904308494367,479635942,1.5012666574509997E9,1.5012667213519998E9
+3,6464089092096721732,479635942,1.5012667527019997E9,1.5012667656319997E9
+3,8265153762712079831,479635942,1.5012667854619997E9,1.5012668892329998E9
+3,-8841473587459913087,479635942,1.5012670650429997E9,1.5012670706509998E9
+3,-1055536280048435105,479635942,1.5012670726309998E9,1.5012671600539997E9
+3,268473589104666821,479635942,1.5012671617579997E9,1.5012671767459998E9
+3,-3611918485320426074,479635942,1.5012672313889997E9,1.5012672797619998E9
+3,5613256615659636122,479635942,1.5012673297859998E9,1.5012673306089997E9
+3,1053943597106945424,479635942,1.5012680455539997E9,1.5012681595649998E9
+3,2745110061494033838,479635942,1.5012681938299997E9,1.5012682231179998E9
+3,-4759210778525383191,479635942,1.5012682288659997E9,1.5012684924809997E9
+3,-5215474932563982068,479635942,1.5012688863079998E9,1.5012688880179996E9
+3,8185311292586694395,479635942,1.5012688896889997E9,1.5012688930439997E9
+3,2925109149439138408,479635942,1.5012688938969998E9,1.5012688947219996E9
+3,-8676821592753044433,479635942,1.5012689404279997E9,1.5012689420769997E9
+3,-8481056729004665640,479635942,1.5012689438099997E9,1.5012689454879997E9
+3,-652663400770816662,479635942,1.5012689754079998E9,1.5012689762329998E9
+3,-6146173594742349990,479635942,1.5012690318659997E9,1.5012690327459998E9
+3,5327062569814547095,479635942,1.5012690402529998E9,1.5012690419029999E9
+3,5771726674692863617,479635942,1.5012690510339997E9,1.5012690526839998E9
+3,-4066089606644232973,479635942,1.5012691918049998E9,1.5012691934819999E9
+3,5090121014098658421,479635942,1.5012691984869998E9,1.5012691993119998E9
+3,-3428976067225053815,479635942,1.5012692168289998E9,1.5012692176539998E9
+3,4964843974808640673,479635942,1.5012692426249998E9,1.5012692443019998E9
+3,-1635467916613769432,479635942,1.5012693958559997E9,1.5012693967079997E9
+3,2477207882402308793,479635942,1.5012695648169997E9,1.5012695656419997E9
+3,-2404963927861004326,479635942,1.5012695681159997E9,1.5012695697669997E9
+3,3132853260534630014,479635942,1.5012695989409997E9,1.5012695997379997E9
+3,8520236112298881690,479635942,1.5012696923329997E9,1.5012696931839998E9
+37,-2259015754605110091,479635942,1.520406393157E9,1.520406394146E9
+37,6764542643902577672,479635942,1.520406406056E9,1.520406406987E9
+37,6657848334227690407,479635942,1.520406499548E9,1.520406500448E9
+37,-6408271416809763810,479635942,1.520406536207E9,1.520406536988E9
+37,5368626669846744881,479635942,1.520406539389E9,1.520406540978E9
+37,-6699428095899409690,479635942,1.520406593627E9,1.520406594527E9
+37,-4683897012566044374,479635942,1.520406596327E9,1.520406598157E9
+37,6302426830809562176,479635942,1.520406633407E9,1.520406634308E9
+37,-4147738867631198535,479635942,1.520406637937E9,1.520406639737E9
+37,-3421817422779313004,479635942,1.520406688338E9,1.520406689088E9
+37,-4513305667404281142,479635942,1.520406713867E9,1.520406714767E9
+37,-8440336818753734388,479635942,1.520406815558E9,1.520406816787E9
+37,308337843659633171,479635942,1.520406821767E9,1.520406824167E9
+37,5447834893035142644,479635942,1.520406849307E9,1.5204068505970001E9
+37,1726483753099582267,479635942,1.520406859777E9,1.520406862416E9
+37,-1749444934080090260,479635942,1.520406875106E9,1.520406878705E9
+37,-7853350339498812881,479635942,1.520406892085E9,1.520406896735E9
+37,7548293822285268972,479635942,1.520407127164E9,1.520407433254E9
+37,2433673396311040339,479635942,1.520407435954E9,1.520407479844E9
+37,-488221232316041521,479635942,1.520407527874E9,1.520407532464E9
+37,298723057501563137,479635942,1.520407865344E9,1.520407869873E9
+37,8860475477743996862,479635942,1.520407888803E9,1.520407891564E9
+37,7066039219720110445,479635942,1.520407968363E9,1.520407969263E9
+37,2549704497553875761,479635942,1.520408081264E9,1.520408082852E9
+37,-2051859332148730936,479635942,1.520408085914E9,1.520408087503E9
+37,5369472328529967685,479635942,1.520408258502E9,1.520408263843E9
+37,3009315165343892103,479635942,1.520408306143E9,1.520408306893E9
+37,742275709871709128,479635942,1.520408546864E9,1.520408628283E9
+37,-5266893512751160249,479635942,1.520408642773E9,1.520408677123E9
+37,1444672617222438358,479635942,1.520408684323E9,1.520408686122E9
+37,-7537824954834727440,479635942,1.520408967374E9,1.520408992994E9
+37,5345458963708941164,479635942,1.520409047294E9,1.520409057764E9
+37,4450078221822330443,479635942,1.520409146624E9,1.5204091493240001E9
+37,-9206478895523538727,479635942,1.520409180794E9,1.520409182534E9
+37,-1475139408965160187,479635942,1.520409184004E9,1.520409184754E9
+37,-5001335555076417755,479635942,1.520409206834E9,1.520409209834E9
+37,1676839279775904427,479635942,1.520409212083E9,1.520409215084E9
+37,9050282813543740529,479635942,1.520409227715E9,1.520409229514E9
+37,-7689783217235717284,479635942,1.520409249314E9,1.520409252014E9
+37,-7908175636611681680,479635942,1.520409259244E9,1.520409260145E9
+37,-8742079032283345525,479635942,1.520409266504E9,1.520409267404E9
+37,6129631311525069083,479635942,1.520409278774E9,1.520409283725E9
+37,-4080934468868162961,479635942,1.520409344339E9,1.520409345269E9
+37,114868866606611121,479635942,1.520409360299E9,1.52040936102E9
+3,2086326190347903593,627460548,1.5012643006019998E9,1.5012643456019998E9
+3,-3116221803458301946,627460548,1.5012643516019998E9,1.5012643966019998E9
+3,-6309253449589861540,627460548,1.5012644416019998E9,1.5012645906019998E9
+3,7847460242594550885,627460548,1.5012656246019998E9,1.5012657746019998E9
+3,-6676479350840676759,627460548,1.5012666376019998E9,1.5012669436019998E9
+3,-5773039151576390786,627460548,1.5012669816019998E9,1.5012669916019998E9
+3,7049421528852272708,627460548,1.5012669936019998E9,1.5012670076019998E9
+3,-8221261718612649608,627460548,1.5012670126019998E9,1.5012670176019998E9
+3,1712593096930041612,627460548,1.5012670186019998E9,1.5012670416019998E9
+3,-3594666096670506324,627460548,1.5012670476019998E9,1.5012673216019998E9
+3,4868339307241172958,627460548,1.5012676446019998E9,1.5012678206019998E9
+3,5064247092816624760,627460548,1.5012678296019998E9,1.5012678526019998E9
+3,-5696498078713153790,627460548,1.5012680366019998E9,1.5012681806019998E9
+3,778189447883207514,627460548,1.5012681876019998E9,1.5012685366019998E9
+37,-6028233170004681707,627460548,1.520407119296E9,1.520407499296E9
+37,-903639349529085439,627460548,1.520408306296E9,1.520408308296E9
+37,2578219410578628435,627460548,1.520408326296E9,1.520408439296E9
+37,-3445779213865074308,627460548,1.520408442296E9,1.520408472296E9
+37,-6571148882511144742,627460548,1.520408484296E9,1.520408695296E9
+37,6862737239824681180,627460548,1.520408710296E9,1.520408721296E9
+37,-4715510912133192615,627460548,1.520408961296E9,1.520408998296E9
+27,8674353898574550141,479635942,1.4994122658549998E9,1.499412266636E9
+27,695281303909472462,479635942,1.4994124804769998E9,1.499412481228E9
+27,-8864080507758269857,479635942,1.4994124844669998E9,1.4994124860279999E9
+27,5402183394216494358,479635942,1.4994126095379999E9,1.4994126110679998E9
+27,-3591895910128644059,479635942,1.499412647998E9,1.4994126498589997E9
+27,3915479474320450706,479635942,1.4994129112759998E9,1.4994129158059998E9
+27,3053381746211365606,479635942,1.4994129222569997E9,1.4994129584069998E9
+27,-2866501197743070330,479635942,1.4994129655169997E9,1.4994131448589997E9
+27,-3939067592918533534,479635942,1.4994131510679998E9,1.4994131828949997E9
+27,-4652280311614165217,479635942,1.4994131856249998E9,1.499413232126E9
+27,3207051283557539044,479635942,1.499413267465E9,1.4994132883149998E9
+27,-2732529320601835300,479635942,1.499413421366E9,1.4994134268259997E9
+27,-1363334564744447690,479635942,1.4994136946349998E9,1.4994136973349998E9
+27,3893654913370439254,479635942,1.4994138611649997E9,1.4994138620049999E9
+27,8242568482103878969,479635942,1.4994141418159997E9,1.4994141440649998E9
+27,-6144419108510199687,479635942,1.4994146467159998E9,1.499414649807E9
+27,-5078310701503483746,479635942,1.4994147685729997E9,1.499414769353E9
+27,8579090422341008492,479635942,1.4994150927549999E9,1.4994150954829998E9
+27,4039316153655645498,479635942,1.4994151016049998E9,1.4994151073939998E9
+27,-2237134870613399443,479635942,1.499415223014E9,1.4994152246649997E9
+27,8202051261902046912,479635942,1.4994152743449998E9,1.4994152750949998E9
+27,5086261691144969555,479635942,1.4994153203939998E9,1.4994153212949998E9
+27,2733625987049382211,479635942,1.4994153674659998E9,1.4994153682159998E9
+57,-6554230948777517037,479635942,1.5192423368619998E9,1.5192423386619997E9
+57,7173916698745329875,479635942,1.5192423713609996E9,1.5192423722609997E9
+57,-9090840472072964219,479635942,1.5192423740909996E9,1.5192423749909997E9
+57,-1671633887400698586,479635942,1.5192424589009998E9,1.5192424604009998E9
+57,9048040990016721025,479635942,1.5192425608099997E9,1.5192425638099997E9
+57,-5870775394791778962,479635942,1.5192425750599997E9,1.5192425813599997E9
+57,-4253372485325844225,479635942,1.5192425867599998E9,1.5192425912899997E9
+57,-7542605970773775985,479635942,1.5192426221589997E9,1.5192426434889996E9
+57,6637335121187019215,479635942,1.5192426488289998E9,1.5192426617909997E9
+57,3976298108922824775,479635942,1.5192426706709998E9,1.5192426776299996E9
+57,-4137165481925903370,479635942,1.5192426889689996E9,1.5192427179489996E9
+57,6579444281817735706,479635942,1.5192427206789997E9,1.5192427324689996E9
+57,-3611796496342995915,479635942,1.5192428705879996E9,1.5192428804269998E9
+57,7525755179545259719,479635942,1.5192429808709998E9,1.5192430006099997E9
+57,7783490503564084382,479635942,1.5192430477399998E9,1.5192430499899998E9
+57,-8609858951172947820,479635942,1.5192430557799997E9,1.5192431224389997E9
+57,-4458153139553405095,479635942,1.5192431383089998E9,1.5192434818999996E9
+57,6378783642114550610,479635942,1.5192437361459997E9,1.5192437376759996E9
+57,-4797598619547570490,479635942,1.5192437407959998E9,1.5192437438569996E9
+57,4304386528415966236,479635942,1.5192437453559997E9,1.5192437461059997E9
+57,5523042718259901713,479635942,1.5192438357779996E9,1.5192438397669997E9
+57,4986024019145050466,479635942,1.5192438496069996E9,1.5192438518579998E9
+57,-5254904973701572879,479635942,1.5192438649069996E9,1.5192438656569996E9
+57,5438032168873030028,479635942,1.5192439249979997E9,1.5192439258669996E9
+57,-993545838186621405,479635942,1.5192439863159997E9,1.5192439870649996E9
+27,-6393753909343424005,627460548,1.4994129130089998E9,1.4994132390089998E9
+27,-3070683365875284593,627460548,1.4994132460089998E9,1.4994132920089998E9
+27,2110288011233501027,627460548,1.4994138510089998E9,1.4994140440089998E9
+27,-9131597979868475747,627460548,1.4994147850089998E9,1.4994148280089998E9
+27,4045771157844725277,627460548,1.4994150130089998E9,1.4994150200089998E9
+27,1783021425772426411,627460548,1.4994150460089998E9,1.4994150610089998E9
+57,884067157795190068,627460548,1.5192426086949997E9,1.5192427496949997E9
+57,7678212195377752192,627460548,1.5192427676949997E9,1.5192428076949997E9
+57,16815563956577441,627460548,1.5192428796949997E9,1.5192428816949997E9
+57,4030803934067838860,627460548,1.5192428926949997E9,1.5192428996949997E9
+57,1020006338934776264,627460548,1.5192429056949997E9,1.5192429246949997E9
+57,2415756925561855884,627460548,1.5192429526949997E9,1.5192431266949997E9
+57,28885189448055567,627460548,1.5192431326949997E9,1.5192434946949997E9
+24,-8271726994879254810,479635942,1.502263533326E9,1.502263534226E9
+24,-323950731939568250,479635942,1.502263562246E9,1.5022635630870001E9
+24,4933679438375242511,479635942,1.502263604009E9,1.502263605508E9
+24,1297735555378282246,479635942,1.502263633468E9,1.502263634367E9
+24,-2738979816088928422,479635942,1.502263636167E9,1.502263638027E9
+24,-5467593927101091507,479635942,1.502263679998E9,1.502263680898E9
+24,-2337223226789559487,479635942,1.502263682579E9,1.502263684318E9
+24,3884426066974528872,479635942,1.502263769998E9,1.502263770898E9
+24,-8009399546736316308,479635942,1.502263773659E9,1.502263775459E9
+24,-2587155742064256033,479635942,1.502263899331E9,1.502263900259E9
+24,-5620942151316782854,479635942,1.502263902991E9,1.502263903951E9
+24,2532325572054373917,479635942,1.502263945352E9,1.50226394703E9
+24,-3798675711688482769,479635942,1.502264236828E9,1.502264276758E9
+24,-424952667515034468,479635942,1.502264301298E9,1.502264529536E9
+24,817993339488982970,479635942,1.502264789965E9,1.502264790804E9
+24,-8467590875612072123,479635942,1.502264821134E9,1.502264825033E9
+24,-397737126793059872,479635942,1.5022648295970001E9,1.502264830345E9
+24,-7702149830799955036,479635942,1.502264838775E9,1.502264840336E9
+24,1721611287864174997,479635942,1.502265010225E9,1.502265012927E9
+24,6382146468482351297,479635942,1.502265016525E9,1.502265018325E9
+24,-5782306477470861236,479635942,1.502265102175E9,1.502265138566E9
+24,-8662668889700624181,479635942,1.502265441025E9,1.502265441836E9
+24,-292978311600729389,479635942,1.502265637435E9,1.5022656383370001E9
+24,-6151445071170154327,479635942,1.502265656306E9,1.502265657986E9
+24,1900236812222391203,479635942,1.502266046843E9,1.502266050382E9
+24,8827479991731110079,479635942,1.502266056924E9,1.502266059263E9
+24,2827888587990366695,479635942,1.502266068412E9,1.502266071623E9
+24,-7122049896024673105,479635942,1.502266199362E9,1.502266201673E9
+24,1739314643299862673,479635942,1.502266244992E9,1.502266247693E9
+24,-7600160568368630556,479635942,1.502266266713E9,1.502266269443E9
+24,-2036494577029594759,479635942,1.502266288403E9,1.502266291945E9
+24,5615280465657652424,479635942,1.502266407774E9,1.502266408644E9
+24,2917463559832182088,479635942,1.502266436396E9,1.502266438795E9
+24,-4678353026325301096,479635942,1.502266442784E9,1.502266450646E9
+24,1188988666242901836,479635942,1.502266460724E9,1.502266462224E9
+24,-319100049241786755,479635942,1.502266509351E9,1.502266510281E9
+24,-2971217135755330652,479635942,1.502266512081E9,1.50226651394E9
+24,5995742182165642488,479635942,1.502266515771E9,1.502266518502E9
+24,-5073062917195522933,479635942,1.502266536651E9,1.502266540912E9
+24,-966943468313349187,479635942,1.502266594732E9,1.502266596232E9
+24,4518543085308497195,479635942,1.502266599801E9,1.502266601632E9
+24,-8897807749355510685,479635942,1.502266642553E9,1.502266644352E9
+24,1010443384744067324,479635942,1.502266747044E9,1.502266747943E9
+24,7629939448402436848,627460548,1.502264193684E9,1.502264535684E9
+24,6888774802012843780,627460548,1.502264542684E9,1.502264543684E9
+24,6209606946974175230,627460548,1.502264556684E9,1.502264566684E9
+24,-2471707800494912078,627460548,1.502264577684E9,1.502264583684E9
+24,7211745083105806118,627460548,1.502264591684E9,1.502264632684E9
+24,3481248545506522859,627460548,1.502265016684E9,1.502265019684E9
+24,-6252444754938667161,627460548,1.502265039684E9,1.502265161684E9
+24,1979807095150432588,627460548,1.502265182684E9,1.502265221684E9
+24,7295909730768766305,627460548,1.502265857684E9,1.502265858684E9
+52,-2087632722845524189,479635942,1.499455476099E9,1.499455477899E9
+52,3448233945657650516,479635942,1.499455490379E9,1.499455492179E9
+52,5992452535513510995,479635942,1.4994554989899998E9,1.499455499829E9
+52,1866691306126058830,479635942,1.499455524369E9,1.499455526681E9
+52,-3948887270595223067,479635942,1.499455528181E9,1.4994555289299998E9
+52,-2975362549562094253,479635942,1.4994555531709998E9,1.4994555548189998E9
+52,-1768576756537310847,479635942,1.49945555752E9,1.4994555593189998E9
+52,7449648592234594284,479635942,1.49945556112E9,1.4994555620189998E9
+52,-3266469965124748900,479635942,1.4994555819399998E9,1.49945558377E9
+52,-9179968419444721001,479635942,1.4994556271799998E9,1.49945562808E9
+52,-3616734105770689146,479635942,1.4994556306899998E9,1.49945563153E9
+52,3723223346697916687,479635942,1.4994556507299998E9,1.4994556531299999E9
+52,-3019095699150699055,479635942,1.4994556538799999E9,1.4994556562199998E9
+52,-1953376434088184126,479635942,1.4994556599699998E9,1.499455662281E9
+52,5755530235233645867,479635942,1.499455665281E9,1.49945566837E9
+52,-8307054000355814225,479635942,1.499455672931E9,1.499455673681E9
+52,3858400344203402424,479635942,1.4994556842099998E9,1.49945568607E9
+52,2665598858018186435,479635942,1.4994556905709999E9,1.4994557014899998E9
+52,-2233514826563582784,479635942,1.49945570329E9,1.4994557294209998E9
+52,-2500338387891296896,479635942,1.49945628757E9,1.49945628832E9
+52,-8543066368144693654,479635942,1.499456353658E9,1.4994563545579998E9
+52,8062392373497619213,479635942,1.4994568633579998E9,1.4994570707199998E9
+52,7851620437001948117,479635942,1.499457078339E9,1.49945712376E9
+52,848383177218628469,479635942,1.4994571299689999E9,1.49945713087E9
+52,-4074119981866861472,479635942,1.499457599471E9,1.4994576031E9
+52,8429349343242411215,479635942,1.499457628418E9,1.4994576293179998E9
+52,-7951357391260011843,479635942,1.4994577863909998E9,1.4994577884819999E9
+52,8846306731700379444,627460548,1.499455696949E9,1.499455779949E9
+52,-9035506030366779319,627460548,1.499455951949E9,1.499455958949E9
+52,7065988900713281098,627460548,1.499456398949E9,1.499456438949E9
+52,2437777036022907665,627460548,1.499456462949E9,1.499456488949E9
+52,-9152985168502729191,627460548,1.499456528949E9,1.499456549949E9
+52,5792270040883499398,627460548,1.499456725949E9,1.499456727949E9
+52,4714015256334737331,627460548,1.499456735949E9,1.499456751949E9
+52,-2001566054420712475,627460548,1.499456752949E9,1.499456766949E9
+52,1020457402724611751,627460548,1.499456767949E9,1.499456778949E9
+52,-4457654314229111696,627460548,1.499456789949E9,1.499457130949E9
+52,-5695599057815644292,627460548,1.499457142949E9,1.499457173949E9
+28,2989719843892638139,479635942,1.521789301609E9,1.52178930248E9
+28,-6546071482316512068,479635942,1.521789372378E9,1.521789373278E9
+28,3587260167952106219,479635942,1.521789375078E9,1.5217893759780002E9
+28,2179278434497127602,479635942,1.521789419027E9,1.521789419839E9
+28,8018325600470558660,479635942,1.521789421427E9,1.521789422989E9
+28,-934247401822315610,479635942,1.521789425328E9,1.521789426139E9
+28,3420096387648749466,479635942,1.521789539598E9,1.521789540348E9
+28,7584186644877240987,479635942,1.521789542598E9,1.521789544848E9
+28,-5670014871484998878,479635942,1.521789586398E9,1.521789587298E9
+28,-8988529172338256401,479635942,1.521789590027E9,1.521789590928E9
+28,8064245848125124982,479635942,1.5217896675770001E9,1.5217896687480001E9
+28,7025848776605415659,479635942,1.5217896712680001E9,1.5217896724090002E9
+28,5803688828261080643,479635942,1.521789707719E9,1.5217897090080001E9
+28,3311195045597475729,479635942,1.521789905116E9,1.521789906617E9
+28,-9219023194579910593,479635942,1.521789909617E9,1.521789911178E9
+28,8723088841604986933,479635942,1.521789918017E9,1.521789919517E9
+28,-7905251261920329566,479635942,1.5217899949060001E9,1.521789997487E9
+28,734507364879299218,479635942,1.5217900599160001E9,1.521790087937E9
+28,-6948859773793809975,479635942,1.521790094236E9,1.521790143647E9
+28,-4813329296749751112,479635942,1.521790163657E9,1.521790272286E9
+28,-896873121140455280,479635942,1.521790295686E9,1.521790372816E9
+28,-3176428470923601424,479635942,1.521790379206E9,1.521790380767E9
+28,6772600188298482330,479635942,1.521790381606E9,1.521790389167E9
+28,5595521599310606776,479635942,1.521790392706E9,1.5217904026660001E9
+28,-6350544209921415499,479635942,1.521790422527E9,1.521790424326E9
+28,-4537315618059827377,479635942,1.521790483487E9,1.521790485076E9
+28,8580869026912468006,479635942,1.521790545257E9,1.521790550687E9
+28,-2665185072265794832,479635942,1.521790556086E9,1.521790566736E9
+28,-8938280773641438702,479635942,1.5217905736660001E9,1.521790576157E9
+28,2330001219452104673,479635942,1.521790594997E9,1.521790598236E9
+28,6733620230882008436,479635942,1.521790651157E9,1.521790653857E9
+28,5877522186661116480,479635942,1.521790667446E9,1.521790668346E9
+28,-361088117889028396,479635942,1.521790701167E9,1.521790703417E9
+28,189455269329405450,479635942,1.521790775356E9,1.521790776256E9
+28,4518935997959129305,479635942,1.5217907843860002E9,1.5217907896660001E9
+28,5647976886843573768,479635942,1.521791032908E9,1.521791035907E9
+28,5903742365224137150,479635942,1.521791722968E9,1.521791736468E9
+28,1622335935126789139,479635942,1.521791758459E9,1.521791767459E9
+28,-5977472103327219007,479635942,1.521791928979E9,1.52179193066E9
+28,-5223558511203374263,479635942,1.5217920465800002E9,1.5217920488300002E9
+28,-6788269653895207535,479635942,1.52179207976E9,1.52179208066E9
+28,5671175831315955221,479635942,1.521792101509E9,1.52179210421E9
+28,5097287225211377241,479635942,1.521792174469E9,1.521792175219E9
+28,3736919765184195364,479635942,1.521792263662E9,1.521792264411E9
+28,-6321656678343779921,479635942,1.52179228664E9,1.52179228739E9
+28,6644070681382979192,627460548,1.521789995098E9,1.521790004098E9
+28,-8316686508540267169,627460548,1.521790005098E9,1.521790020098E9
+28,6833298294237936702,627460548,1.521790037098E9,1.521790377098E9
+28,5321933356163115789,627460548,1.521791104098E9,1.521791337098E9
+28,-7674243211080680621,627460548,1.521791375098E9,1.521791518098E9
+28,-4664527021828171774,627460548,1.521791725098E9,1.521791774098E9
+28,-192423587251135279,627460548,1.521791785098E9,1.521791794098E9
+54,5884422611613760258,479635942,1.5017895723669999E9,1.501789573268E9
+54,-6632521396907378471,479635942,1.501789576809E9,1.501789578549E9
+54,-8544994393027598490,479635942,1.501789585958E9,1.5017895876069999E9
+54,-7425676856160693953,479635942,1.501790205669E9,1.501790215599E9
+54,1021945395007737335,479635942,1.50179022505E9,1.5017902866999998E9
+54,7130584181550608,479635942,1.50179031559E9,1.501790323721E9
+54,-2003436196878806087,479635942,1.501790604481E9,1.5017906440619998E9
+54,3932072332709365639,479635942,1.501790658168E9,1.501790703751E9
+54,-1666438681689735740,479635942,1.5017907241829998E9,1.5017908194989998E9
+54,8656173152238052690,479635942,1.5017908374039998E9,1.501790954554E9
+54,-8521628532193775465,479635942,1.5017909591439998E9,1.5017909624989998E9
+54,-5315738317568988502,479635942,1.501790970034E9,1.5017909834269998E9
+54,-7434133036494917831,479635942,1.501790988432E9,1.501791000064E9
+54,-1974156482553828232,479635942,1.5017910091669998E9,1.501791017527E9
+54,2599935394869429313,479635942,1.5017910274269998E9,1.501791128957E9
+54,4104353716081471096,479635942,1.501791361688E9,1.5017913641899998E9
+54,5092652387796307429,479635942,1.5017913666109998E9,1.501791369113E9
+54,3036675870441418807,479635942,1.501791370764E9,1.5017913732659998E9
+54,9195331733543381068,479635942,1.501791395899E9,1.501791396751E9
+54,-5934748721544570706,479635942,1.5017914026079998E9,1.5017914060189998E9
+54,2244290691548681295,479635942,1.501791506781E9,1.501791508429E9
+54,-4334013784194347496,479635942,1.501791521574E9,1.501791522399E9
+54,8574502040419010817,479635942,1.50179161304E9,1.501791614745E9
+54,1306396689189238469,479635942,1.501791616395E9,1.501791618072E9
+54,1031552160557126126,479635942,1.501791692791E9,1.501791693615E9
+54,-1049759118489475353,627460548,1.501790199646E9,1.501790341646E9
+54,1258962116866064326,627460548,1.501790413646E9,1.501790451646E9
+54,6695355669994504649,627460548,1.501790599646E9,1.501791133646E9
+39,2217679845298225747,479635942,1.5018728299259999E9,1.501872830825E9
+39,7429129270833811340,479635942,1.501872833525E9,1.501872835325E9
+39,-2205412637285000343,479635942,1.501872927698E9,1.501872929197E9
+39,-2262513564006162990,479635942,1.501872964385E9,1.501872966245E9
+39,-4538657594522823140,479635942,1.501873035545E9,1.501873037105E9
+39,642871938461495812,479635942,1.5018730386069999E9,1.5018730408569999E9
+39,8388514415251228254,479635942,1.501873042356E9,1.501873043856E9
+39,6676464437231046948,479635942,1.501873111057E9,1.501873111986E9
+39,6254671758320742423,479635942,1.501873112886E9,1.501873118317E9
+39,-8723353049507850817,479635942,1.501873137966E9,1.501873140576E9
+39,9021226933992420292,479635942,1.501873150808E9,1.501873153267E9
+39,-8352077785127594621,479635942,1.501873154767E9,1.501873155518E9
+39,7394615352148428027,479635942,1.501873172408E9,1.501873174747E9
+39,-4680562287917372401,479635942,1.501873761667E9,1.501873776697E9
+39,-6306273737284460527,479635942,1.501873805587E9,1.501873875909E9
+39,-1045836146745966823,479635942,1.501874148309E9,1.50187415644E9
+39,-2208627613166407546,479635942,1.50187418806E9,1.50187420456E9
+39,8757164211530508676,479635942,1.501874211552E9,1.50187432621E9
+39,9035390723419109917,479635942,1.501874337611E9,1.501874519112E9
+39,2562574547047384209,479635942,1.501874553642E9,1.501874629783E9
+39,-6752555350224995283,479635942,1.501874640612E9,1.501874669143E9
+39,-8971213737537990398,479635942,1.501874842082E9,1.501874855134E9
+39,-260625663470644455,479635942,1.501874930793E9,1.501874934304E9
+39,722760640995362381,479635942,1.501874961154E9,1.501874961906E9
+39,7037525964559988538,479635942,1.501875001593E9,1.501875003393E9
+39,-5678376788961029492,479635942,1.501875004293E9,1.501875007053E9
+39,4831392706876999725,479635942,1.501875069934E9,1.501875071616E9
+39,-6699269542960821323,479635942,1.501875086404E9,1.501875087154E9
+39,8150811091564660034,479635942,1.501875119014E9,1.501875120845E9
+39,4774107043749094763,479635942,1.501875126304E9,1.501875127204E9
+39,5722532102437117965,479635942,1.501875221613E9,1.501875222362E9
+39,-7225941824705592368,479635942,1.501875238263E9,1.501875239763E9
+39,6598872003948840095,479635942,1.501875246362E9,1.501875247262E9
+39,-6141972856624708171,479635942,1.501875276302E9,1.501875277202E9
+39,597376076782374913,479635942,1.501875279063E9,1.501875279964E9
+39,-6964320635298983148,479635942,1.501875357725E9,1.501875358474E9
+39,6166801384617271629,627460548,1.501873755635E9,1.501873932635E9
+39,3763874668790095729,627460548,1.501873961635E9,1.501874012635E9
+39,8408206159893819107,627460548,1.501874142635E9,1.501874674635E9
+35,3807156136094092477,479635942,1.5199755269329998E9,1.519975528431E9
+35,-7868151759452467913,479635942,1.5199755753799999E9,1.519975576281E9
+35,-1012290315859943529,479635942,1.519975578981E9,1.5199755798809998E9
+35,-7445769394435248618,479635942,1.519975583511E9,1.519975584411E9
+35,6400465824860810860,479635942,1.51997568605E9,1.5199756878799999E9
+35,6285222692204463578,479635942,1.5199757052189999E9,1.519975706119E9
+35,-7941861147535292487,479635942,1.5199757224989998E9,1.5199757234299998E9
+35,5835777072089175065,479635942,1.519975726159E9,1.51997572709E9
+35,2518343011503706020,479635942,1.51997576756E9,1.5199757690619998E9
+35,2456924796795394609,479635942,1.519975771313E9,1.519975772149E9
+35,-7119773801040918960,479635942,1.519975847419E9,1.5199758482889998E9
+35,-4917539598947089040,479635942,1.5199758536299999E9,1.519975855369E9
+35,-1223175280759925677,479635942,1.5199758579789999E9,1.519975858908E9
+35,3520977062530178324,479635942,1.519976024892E9,1.519976025793E9
+35,-6735443420804731016,479635942,1.519976027622E9,1.519976029422E9
+35,-4968085838182275418,479635942,1.519976032122E9,1.519976036622E9
+35,3628193686305137308,479635942,1.5199761119819999E9,1.519976174501E9
+35,659191462920116663,479635942,1.5199761985609999E9,1.519976288681E9
+35,9124031735557573651,479635942,1.519976310402E9,1.519976364942E9
+35,3525754609373030386,479635942,1.5199763702519999E9,1.519976472701E9
+35,-4221657168373797184,479635942,1.5199766384789999E9,1.51997664658E9
+35,1349770851040280296,479635942,1.519976687228E9,1.5199766894789999E9
+35,-4433036976473232254,479635942,1.519976778909E9,1.519976779747E9
+35,-7213473412068006276,479635942,1.5199767950479999E9,1.5199767958E9
+35,-3194353193933720338,479635942,1.5199767996999998E9,1.5199768141899998E9
+35,-7303683789813629858,479635942,1.519976849619E9,1.5199768550479999E9
+35,6460727155122702223,479635942,1.5199768668079998E9,1.519976872208E9
+35,3422950762716315227,479635942,1.519976890057E9,1.519976903079E9
+35,-1528010770785603060,479635942,1.5199769521279998E9,1.519976954829E9
+35,5737490194055173790,479635942,1.5199770951069999E9,1.519977098738E9
+35,-1321838096581725488,479635942,1.5199771023679998E9,1.5199771032979999E9
+35,3269477752945050724,479635942,1.519977787863E9,1.5199778366729999E9
+35,-2299505510720164333,479635942,1.5199778563249998E9,1.5199778601629999E9
+35,-7838391747680193884,479635942,1.5199778925029998E9,1.5199778934029999E9
+35,-1584244933793512447,479635942,1.5199779636959999E9,1.519977964533E9
+35,4864248771182400045,479635942,1.5199779667849998E9,1.5199779690939999E9
+35,-8776671137839204180,479635942,1.519977980737E9,1.519977983316E9
+35,3615342812623510476,479635942,1.5199779960349998E9,1.5199779978349998E9
+35,-2640278058879556125,479635942,1.5199780803049998E9,1.519978084143E9
+35,5794457005668239110,479635942,1.5199780878969998E9,1.519978089484E9
+35,-5034104462427228891,479635942,1.519978107935E9,1.519978109824E9
+35,-6350543409292125905,479635942,1.519978125304E9,1.519978127133E9
+35,2246607572088895757,479635942,1.519978152274E9,1.5199781531729999E9
+35,3848633351862000975,627460548,1.519976105235E9,1.519976489235E9
+35,-6433962027570020609,627460548,1.519977103235E9,1.519977246235E9
+35,4375002370194676905,627460548,1.519977251235E9,1.519977253235E9
+35,-4007153983025216719,627460548,1.519977275235E9,1.519977294235E9
+35,7579346900117515166,627460548,1.519977296235E9,1.519977320235E9
+35,8057918823554949052,627460548,1.519977351235E9,1.519977361235E9
+35,839537418906605514,627460548,1.519977367235E9,1.519977591235E9
+35,4679591872902996954,627460548,1.519977752235E9,1.519977844235E9
+43,-7058008765899197432,479635942,1.518592085119E9,1.5185920860180001E9
+43,4099126706977885074,479635942,1.518592208955E9,1.5185922099160001E9
+43,6451356444034115562,479635942,1.518592246516E9,1.518592247356E9
+43,-8150602038896385731,479635942,1.518592293576E9,1.518592294476E9
+43,-7102617389349683731,479635942,1.518592296275E9,1.518592298076E9
+43,9174022562754907217,479635942,1.518592305276E9,1.5185923070770001E9
+43,2422301470465107897,479635942,1.518592337076E9,1.5185923379750001E9
+43,1950901516163481914,479635942,1.518592373376E9,1.518592374276E9
+43,-1833478260161155518,479635942,1.518592378206E9,1.518592378961E9
+43,-4565122457542768112,479635942,1.518592399566E9,1.518592400466E9
+43,6979453964667639134,479635942,1.518592402326E9,1.5185924032250001E9
+43,-4413016090813513212,479635942,1.518592437756E9,1.518592438685E9
+43,6171648788862839373,479635942,1.5185925448860002E9,1.518592546685E9
+43,7712676961613195260,479635942,1.5185925493860002E9,1.518592551184E9
+43,1500305998960258232,479635942,1.5185928114620001E9,1.518592812362E9
+43,1899578016838962269,479635942,1.518592840353E9,1.51859311926E9
+43,321256945866508792,479635942,1.518593540729E9,1.518593544839E9
+43,6349644486090024992,479635942,1.518593561039E9,1.518593569379E9
+43,6853817542902001778,479635942,1.518593806197E9,1.5185938198170002E9
+43,8057354203374075125,479635942,1.518593848826E9,1.518593865926E9
+43,5428921319659140702,479635942,1.518593953105E9,1.518593979116E9
+43,3058655587346765347,479635942,1.518594042176E9,1.518594043076E9
+43,7483600543748994185,479635942,1.518594063086E9,1.518594063992E9
+43,1438897541422177990,479635942,1.518594345956E9,1.5185943468270001E9
+43,-1532292184569246003,479635942,1.518594395185E9,1.518594397045E9
+43,4400534172488044475,479635942,1.518594397945E9,1.518594400647E9
+43,7399425679228370350,479635942,1.518594401546E9,1.518594403346E9
+43,-8437452438337175529,479635942,1.518594429535E9,1.518594432265E9
+43,1270742671164561227,479635942,1.518594436766E9,1.518594438535E9
+43,115487077398780973,479635942,1.518594444535E9,1.51859444874E9
+43,-4808354739683817918,479635942,1.518594513205E9,1.518594516924E9
+43,-6308949655469903080,627460548,1.518592797006E9,1.518592818006E9
+43,6464573402665774094,627460548,1.518592823006E9,1.518593365006E9
+43,8346798350924835948,627460548,1.518593422006E9,1.518593428006E9
+43,5938567433821079553,627460548,1.518593526006E9,1.518593755006E9
+43,2886888829017893948,627460548,1.518593770006E9,1.518593828006E9
+43,-1980224228425834260,627460548,1.518593836006E9,1.518594001006E9
+49,-9107542811376712554,479635942,1.518804968773E9,1.518804969672E9
+49,-5067644735208933170,479635942,1.518804990734E9,1.518804991493E9
+49,-2666009975662904868,479635942,1.518805017073E9,1.518805017823E9
+49,8073829210336210285,479635942,1.518805099033E9,1.518805099902E9
+49,7943529268706742413,479635942,1.518805102481E9,1.518805104251E9
+49,6032250005349690304,479635942,1.518805105931E9,1.51880510758E9
+49,6436874953734956466,479635942,1.518805170249E9,1.518805171151E9
+49,-767164038784487233,479635942,1.518805213763E9,1.518805215912E9
+49,-294130005838402358,479635942,1.518805234882E9,1.518805237411E9
+49,-8760229126709107486,479635942,1.518805558009E9,1.518805580669E9
+49,1651676104084734951,479635942,1.518806150329E9,1.518806177031E9
+49,-8979553314074613263,479635942,1.518806213688E9,1.5188062625E9
+49,-4842058215165844819,479635942,1.518806934844E9,1.518806937346E9
+49,-4469580179019911241,479635942,1.518806961325E9,1.518806973811E9
+49,-7820837760682198275,479635942,1.518807123414E9,1.518807125945E9
+49,1050792728431251470,479635942,1.518807127595E9,1.518807133451E9
+49,-3998395617141563823,479635942,1.518807148384E9,1.518807149209E9
+49,3287360646335771100,627460548,1.518805310421E9,1.518805316421E9
+49,7000134458797106677,627460548,1.518805534421E9,1.518805895421E9
+49,-3741305235141471234,627460548,1.518805908421E9,1.518805912421E9
+49,4991928393789004847,627460548,1.518806082421E9,1.518806182421E9
+49,7899776172822746806,627460548,1.518806194421E9,1.518806288421E9
+49,-6784152684712676730,627460548,1.518806306421E9,1.518806322421E9
+49,-5414965408042151121,627460548,1.518806348421E9,1.518806358421E9
+49,3777595674135698594,627460548,1.518806383421E9,1.518806424421E9
+49,-9136935570379168161,627460548,1.518807131421E9,1.518807135421E9
+22,-5896597027850248888,479635942,1.499780633591E9,1.499780635152E9
+22,7186125788303790293,479635942,1.499780675052E9,1.499780675951E9
+22,2412148044735908029,479635942,1.499780729262E9,1.4997807300740001E9
+22,7798991882888057475,479635942,1.499780765203E9,1.499780766792E9
+22,5202673324534620856,479635942,1.4997807774120002E9,1.4997807792120001E9
+22,-386642346121350473,479635942,1.499780835433E9,1.499780837232E9
+22,5929840982891225472,479635942,1.4997808722740002E9,1.4997808737740002E9
+22,-8981032894100369185,479635942,1.499780875423E9,1.499780877013E9
+22,-3855955331048401240,479635942,1.499780955161E9,1.499780956066E9
+22,7475747809140334547,479635942,1.499780994852E9,1.499780995635E9
+22,-3494481583210250767,479635942,1.499781001721E9,1.499781020111E9
+22,1236147292822658510,479635942,1.4997810276720002E9,1.4997810291720002E9
+22,6112863226965943190,479635942,1.4997810445010002E9,1.499781058931E9
+22,5773179323339585353,479635942,1.4997810652610002E9,1.499781067961E9
+22,-1771613916691416206,479635942,1.499781195612E9,1.499781204642E9
+22,-4236842894713427019,479635942,1.499781210072E9,1.499781225252E9
+22,-4308372485789840397,479635942,1.4997812468530002E9,1.499781248502E9
+22,9081538531300407835,479635942,1.499781275444E9,1.499781311772E9
+22,4834188891837984225,479635942,1.4997813127020001E9,1.499781328993E9
+22,-6345126932983930831,479635942,1.499781341713E9,1.499781388753E9
+22,-4819556096174284400,479635942,1.499781394004E9,1.49978141278E9
+22,-763926201650936003,479635942,1.49978142823E9,1.499781461801E9
+22,58292236850751263,479635942,1.4997815183500001E9,1.499781562211E9
+22,6683831598837137130,479635942,1.499781660341E9,1.499781664842E9
+22,1411212696096643537,479635942,1.499781669161E9,1.499781682812E9
+22,52852672845617987,479635942,1.499781699071E9,1.4997817017710001E9
+22,3345397891700601777,479635942,1.499781721601E9,1.499781725951E9
+22,-8932913868609031800,479635942,1.499781729371E9,1.4997817302410002E9
+22,2474915859971147000,479635942,1.4997817659120002E9,1.4997817689720001E9
+22,-866635493092282493,479635942,1.499781823902E9,1.4997818266030002E9
+22,6164177422194639818,479635942,1.4997818302020001E9,1.499781833832E9
+22,-723593460536740098,479635942,1.499781971082E9,1.499781971983E9
+22,3678401050055269458,479635942,1.49978202892E9,1.49978202967E9
+22,2490252344147428514,479635942,1.4997823096920002E9,1.499782364473E9
+22,2117734938487868256,479635942,1.499782408152E9,1.499782417124E9
+22,-6768875195471919041,479635942,1.499782447063E9,1.499782629254E9
+22,6544906540127044360,479635942,1.49978276045E9,1.4997828143E9
+22,-999100488552378757,479635942,1.4997828341230001E9,1.499782839434E9
+22,-7604241434750071025,479635942,1.499782840934E9,1.499782845522E9
+22,1092395173625558393,479635942,1.499782975785E9,1.499782976685E9
+22,2869856165843789039,479635942,1.4997832687680001E9,1.499783270565E9
+22,8334409195797582672,479635942,1.4997832714950001E9,1.4997832769550002E9
+22,-8968791445652546090,479635942,1.499783327355E9,1.4997833306560001E9
+22,-6438788932495869939,479635942,1.499783362993E9,1.499783367163E9
+22,4075747738832979129,479635942,1.499783368963E9,1.499783369862E9
+22,718849825638016006,479635942,1.499783548542E9,1.499783580584E9
+22,-5774266889087139995,479635942,1.499783632273E9,1.499783633174E9
+22,-3306266905293366488,479635942,1.499783636833E9,1.499783642263E9
+22,-493087465170145080,479635942,1.499783647783E9,1.4997836505440001E9
+22,6371821713090872612,479635942,1.499783658734E9,1.499783663263E9
+22,-27518988888910856,479635942,1.499783669625E9,1.499783672384E9
+22,4849384397952781469,479635942,1.499783705444E9,1.499783706283E9
+22,-1560713681876793463,479635942,1.499783707933E9,1.499783710183E9
+22,-5068703575023954692,479635942,1.499783710934E9,1.499783713333E9
+22,-4295601171174149999,479635942,1.499783731273E9,1.499783735925E9
+22,8426616595836198065,479635942,1.499783738234E9,1.499783739763E9
+22,5419412095421874800,479635942,1.499783773844E9,1.499783774744E9
+22,-1174591054879262699,479635942,1.4997838017150002E9,1.499783802584E9
+22,1657710588205256678,479635942,1.499783824605E9,1.499783825443E9
+22,-6449559305410223238,479635942,1.499783827184E9,1.499783829644E9
+22,-602043046948197464,479635942,1.499783836068E9,1.499783837805E9
+22,5981615962974144895,479635942,1.4997838429650002E9,1.499783844645E9
+22,8690613384222675243,479635942,1.499783919134E9,1.499783921835E9
+22,3937160509130180812,479635942,1.499783934435E9,1.499783935335E9
+76,-1105845952950245550,479635942,1.51967927791E9,1.51967927881E9
+76,5920097817553487994,479635942,1.519679416328E9,1.519679417229E9
+76,462367718546954289,479635942,1.519679536747E9,1.519679538548E9
+76,5487179622702546582,479635942,1.519679539448E9,1.519679541248E9
+76,-2146354098996149394,479635942,1.519679617797E9,1.519679618934E9
+76,1041142898128823602,479635942,1.519679622384E9,1.519679623643E9
+76,6098506547500618988,479635942,1.519679795939E9,1.519679800588E9
+76,2768182684889614369,479635942,1.519679808506E9,1.519679810698E9
+76,7782395365222991857,479635942,1.5196798130970001E9,1.519679814178E9
+76,-4659682481312204476,479635942,1.51967982408E9,1.51967982978E9
+76,-4019722658362444328,479635942,1.519679830921E9,1.519679833227E9
+76,-4261328116098121612,479635942,1.519679862357E9,1.519679863497E9
+76,-9158845563864338448,479635942,1.519679864758E9,1.519679868208E9
+76,3904467482979114234,479635942,1.519679884889E9,1.519679886026E9
+76,-7630807212104375517,479635942,1.519679902107E9,1.519679903337E9
+76,9151136906136994793,479635942,1.519679913599E9,1.519679915907E9
+76,-2019607882880990654,479635942,1.519679977825E9,1.519679979057E9
+76,-8537295039923796871,479635942,1.519679986945E9,1.519679989255E9
+76,-3207974231983701376,479635942,1.519680019345E9,1.519680020425E9
+76,-2048748740986875253,479635942,1.519680035425E9,1.519680038967E9
+22,7453634497762763387,627460548,1.499781109611E9,1.499781157611E9
+22,-2667840623629106412,627460548,1.499781162611E9,1.499781165611E9
+22,3658548711854299695,627460548,1.499781248611E9,1.499781569611E9
+22,7959932615450571977,627460548,1.499781578611E9,1.499781586611E9
+22,-1526987754415272387,627460548,1.499781661611E9,1.499781708611E9
+22,4489693828107356701,627460548,1.499781752611E9,1.499781755611E9
+22,1694102862087938930,627460548,1.499781787611E9,1.499781807611E9
+22,-263350719581045661,627460548,1.499781906611E9,1.499781925611E9
+22,5648384458542277604,627460548,1.499781938611E9,1.499781948611E9
+22,-5089699600337277020,627460548,1.499782264611E9,1.499782374611E9
+22,8132696178219463756,627460548,1.499782378611E9,1.499782423611E9
+22,-7087653142422353105,627460548,1.499782435611E9,1.499782648611E9
+22,-5877691627726887504,627460548,1.499782685611E9,1.499782692611E9
+22,4479280310250551514,627460548,1.499782696611E9,1.499782732611E9
+22,-147885727688475139,627460548,1.499782742611E9,1.499782820611E9
+22,6931678911227148041,627460548,1.499783542611E9,1.499783588611E9
+16,-4835422748163153092,479635942,1.519755878227E9,1.519755879127E9
+16,-7944667711158368039,479635942,1.519755945459E9,1.5197559463860002E9
+16,-4320676910727870764,479635942,1.519756041246E9,1.519756042088E9
+16,-8873097009693721787,479635942,1.519756340344E9,1.519756341183E9
+16,7768673216496133832,479635942,1.5197563470340002E9,1.519756347874E9
+16,487294542318201677,479635942,1.5197565150600002E9,1.5197565159610002E9
+16,1187681031301874890,479635942,1.519756562249E9,1.5197565848690002E9
+16,-2429833818515115371,479635942,1.5197565999290001E9,1.5197566077910001E9
+16,-7069086383233093899,479635942,1.519756612289E9,1.5197566167910001E9
+16,4810758493534637137,479635942,1.5197566206890001E9,1.5197566252800002E9
+16,-6181135641813650112,479635942,1.519756640249E9,1.519756649338E9
+16,2666886561404715274,479635942,1.519756799766E9,1.519756809911E9
+16,5460807173925225660,479635942,1.5197568137900002E9,1.5197571293470001E9
+16,8842098101795433501,479635942,1.519757182647E9,1.519757186937E9
+16,6777686955071808335,479635942,1.519757210946E9,1.519757244275E9
+16,-547117406604438075,479635942,1.5197572486490002E9,1.519757411887E9
+16,-6209365452399336460,479635942,1.519757583704E9,1.519757587884E9
+16,1441354691417098818,479635942,1.5197575903590002E9,1.5197576984880002E9
+16,-8258231792585524440,479635942,1.51975774774E9,1.5197579262710001E9
+16,1299417524337361056,479635942,1.519757933779E9,1.519757937079E9
+16,4041459375818170055,479635942,1.5197579454120002E9,1.519757974644E9
+16,5052791538369553896,479635942,1.519757987078E9,1.519758160184E9
+16,-2195595709962290905,479635942,1.5197582259940002E9,1.519758255911E9
+16,-1133735532446012360,479635942,1.5197582659220002E9,1.5197584638920002E9
+16,-3815594941682790112,479635942,1.519758734631E9,1.519758735486E9
+16,5036161273076758188,479635942,1.519758770436E9,1.5197587712610002E9
+16,8578113621891167676,479635942,1.519758855493E9,1.5197588571490002E9
+16,431103479953852746,479635942,1.51975886465E9,1.519758867125E9
+16,-4952489109208497132,479635942,1.5197588879160001E9,1.519758890473E9
+16,-2812082221183416367,479635942,1.519759236144E9,1.5197594859290001E9
+16,566906191349775020,479635942,1.519759494427E9,1.519759521736E9
+16,-6045810387841523693,479635942,1.5197598429850001E9,1.519759845515E9
+16,6971406564904154602,479635942,1.5197598912220001E9,1.5197598920470002E9
+16,5060414737808162330,479635942,1.5197598953760002E9,1.5197598969980001E9
+16,-6554071425759889315,479635942,1.51975990874E9,1.5197599228760002E9
+16,-1996093016827180782,479635942,1.5197599489450002E9,1.5197599540340002E9
+16,4728952328184572349,479635942,1.5197599807080002E9,1.519759981533E9
+16,-3353886951813461091,479635942,1.5197599998200002E9,1.5197600014970002E9
+16,1527556662811698572,479635942,1.519760134568E9,1.519760136246E9
+16,7928295504514018886,479635942,1.519760141223E9,1.519760142048E9
+70,-509005689115366972,479635942,1.50157709735E9,1.501577098851E9
+70,6521682553365299603,479635942,1.501577108901E9,1.50157710971E9
+70,-622821019216253274,479635942,1.501577157081E9,1.501577160741E9
+70,-1147642693879407564,479635942,1.501577163445E9,1.501577164341E9
+70,-1719432998864520753,479635942,1.501577200435E9,1.501577202683E9
+70,2532846129743573755,479635942,1.501577267092E9,1.501577267994E9
+70,-8385069588646240591,479635942,1.501577270783E9,1.501577271682E9
+70,-5994758805273643702,479635942,1.501577317525E9,1.501577318333E9
+70,-946746684094057234,479635942,1.501577389944E9,1.501577391775E9
+70,1782019380705962809,479635942,1.50157759844E9,1.501577599971E9
+70,5956479429352131992,479635942,1.501577602674E9,1.50157760357E9
+70,-2490928093214390957,479635942,1.501577677821E9,1.50157767869E9
+70,-606288794264288998,479635942,1.501577742022E9,1.501577742921E9
+70,8741690489709291574,479635942,1.501577752881E9,1.501577754683E9
+70,6523620502606192215,479635942,1.5015777583109999E9,1.501577760171E9
+70,3166832361691151802,479635942,1.501577880652E9,1.5015778815839999E9
+70,-1251625012631277163,479635942,1.501578082579E9,1.501578083331E9
+16,-3131788387669208619,627460548,1.519756664446E9,1.519756686446E9
+16,3589590186528271340,627460548,1.519756738446E9,1.519756779446E9
+16,7497853340005543603,627460548,1.519756802446E9,1.519757137446E9
+16,-7063621413719097680,627460548,1.519757174446E9,1.519757449446E9
+16,-7700552614807772297,627460548,1.519757584446E9,1.519757715446E9
+16,4778570380157291994,627460548,1.519757727446E9,1.519758597446E9
+16,3734631109805916415,627460548,1.519758650446E9,1.519758691446E9
+16,2645177865872206883,627460548,1.519759068446E9,1.519759084446E9
+16,-6695814739707253795,627460548,1.519759119446E9,1.519759122446E9
+16,-5285543817577578533,627460548,1.519759137446E9,1.519759139446E9
+16,-762924391285812763,627460548,1.519759158446E9,1.519759186446E9
+16,-7868963532725186035,627460548,1.519759191446E9,1.519759572446E9
+70,1818956261512233195,627460548,1.501577403043E9,1.501577428043E9
+4,5698522733215146848,479635942,1.521442429307E9,1.521442430267E9
+4,1454533965849290447,479635942,1.521442450246E9,1.521442451116E9
+4,7853551631203065571,479635942,1.5214425267450001E9,1.521442527676E9
+4,1833410277002282101,479635942,1.521442529476E9,1.521442531276E9
+4,-6451212302676283085,479635942,1.521442562625E9,1.521442564425E9
+4,-3997782922552825098,479635942,1.521442566195E9,1.521442567036E9
+4,406136036182675561,479635942,1.521442611375E9,1.521442612305E9
+4,8935088791150433135,479635942,1.521442614106E9,1.521442615815E9
+4,5883141217375091076,479635942,1.521442617436E9,1.521442618336E9
+4,-1562504477348617903,479635942,1.521442681996E9,1.521442682895E9
+4,6442162853111072300,479635942,1.521442686405E9,1.521442688085E9
+4,-8281095471555881789,479635942,1.521442690636E9,1.521442691505E9
+4,-1648692117170179720,479635942,1.5214427591230001E9,1.521442760023E9
+4,-8679815270693998385,479635942,1.521442800613E9,1.521442801484E9
+4,-238919976005673680,479635942,1.521442803343E9,1.521442804213E9
+4,2547097415411733833,479635942,1.521442834483E9,1.521442835233E9
+4,17839593406518140,479635942,1.521442838233E9,1.521442839013E9
+4,-1597116562559993379,479635942,1.521442842194E9,1.521442843813E9
+4,1919742531973120014,479635942,1.5214430213830001E9,1.521443022253E9
+4,-8071599083672122831,479635942,1.521443034493E9,1.521443036233E9
+4,-2867097201446592630,479635942,1.521443058912E9,1.521443059663E9
+4,-2856142782889317323,479635942,1.521443063473E9,1.5214430725640001E9
+4,9220563198211307167,479635942,1.521443084053E9,1.521443089513E9
+4,8817697574276404783,479635942,1.521443121072E9,1.521443335302E9
+4,-3238860579118206654,479635942,1.521443450382E9,1.521443451282E9
+4,-3561508015441693492,479635942,1.521443725092E9,1.521443725903E9
+4,6822797498750745699,479635942,1.521443922311E9,1.521443924111E9
+4,4953013065632445643,479635942,1.521444020111E9,1.521444021011E9
+4,7760988766985286192,479635942,1.521444029112E9,1.521444036342E9
+4,-8762038282842783786,479635942,1.521444243252E9,1.521444244182E9
+4,-5350319228689018845,479635942,1.521444258852E9,1.521444260682E9
+4,-8742950905512247522,479635942,1.521444288552E9,1.521444300402E9
+4,1676183236839755511,479635942,1.521444361247E9,1.5214443621460001E9
+4,8139167809038270383,479635942,1.521444488117E9,1.521444489016E9
+4,-2565920918797393891,479635942,1.521444492647E9,1.521444494446E9
+4,-4350399611172187771,479635942,1.521444516048E9,1.521444522167E9
+4,-1153579934777545451,479635942,1.521444577727E9,1.521444580427E9
+4,-8234476568187380094,479635942,1.521444621857E9,1.521444623657E9
+4,-4186649922188925357,479635942,1.521444655367E9,1.521444656957E9
+4,2371002038867060936,479635942,1.521444671537E9,1.521444672287E9
+4,-5163969143044480876,479635942,1.521444682127E9,1.521444683747E9
+4,5899276795438100606,479635942,1.521444707088E9,1.521444713387E9
+4,6165936064654459822,479635942,1.521444744048E9,1.521444744948E9
+4,-805466746891542436,479635942,1.5214447485180001E9,1.521444752808E9
+4,-338115955708042987,479635942,1.521444754548E9,1.521444757069E9
+4,4645870033043675307,479635942,1.521444802098E9,1.521444803658E9
+4,-2250719218250569279,479635942,1.521444817158E9,1.521444818058E9
+4,-1888591562771703483,479635942,1.521444875058E9,1.5214448766790001E9
+4,5349594770159211086,479635942,1.521445000129E9,1.52144500634E9
+4,-5677458873443610006,479635942,1.5214455159520001E9,1.521445536323E9
+4,3345784560919517059,479635942,1.521445543102E9,1.521445607963E9
+4,4918967835272334586,479635942,1.521445616992E9,1.521445657583E9
+4,212834596276290437,479635942,1.521445685993E9,1.521445689593E9
+4,7430485492785476126,479635942,1.521445734864E9,1.521445886598E9
+4,-2978637804348362758,479635942,1.521445888848E9,1.521445916147E9
+4,-5605233993261985758,479635942,1.521445924247E9,1.521445950438E9
+4,8526761030841014695,479635942,1.521445987908E9,1.521445989408E9
+4,7922769253991317306,479635942,1.521445995408E9,1.521446158398E9
+4,1791538812528344202,479635942,1.521446186778E9,1.521446188519E9
+4,7848372095398405158,479635942,1.521446220529E9,1.521446222088E9
+4,133923890735449951,479635942,1.5214462589290001E9,1.521446261629E9
+4,6600398029814266248,479635942,1.521446267029E9,1.5214463070210001E9
+4,2206602076773924495,479635942,1.5214463439490001E9,1.521446346649E9
+4,3101094139201458471,479635942,1.521446347549E9,1.5214463601790001E9
+4,4818368575650506504,479635942,1.521446402599E9,1.52144650466E9
+4,-4248081932763410101,479635942,1.52144653958E9,1.52144654333E9
+4,-7612100732780712818,479635942,1.52144655197E9,1.521446733322E9
+4,978441040342089694,479635942,1.521446741032E9,1.5214467987220001E9
+4,1517119277454205417,479635942,1.5214468174720001E9,1.521446903002E9
+4,-2396236914939302776,479635942,1.521446908432E9,1.521446956552E9
+4,-4087847698739617760,479635942,1.521446975632E9,1.521447029603E9
+4,-5473467692081629597,479635942,1.521447074124E9,1.521447089153E9
+4,-2294853631030743933,479635942,1.521447170845E9,1.521447173183E9
+4,-6907537373787705721,479635942,1.521447178435E9,1.521447179185E9
+4,-1894127036481014615,479635942,1.521447189624E9,1.521447190524E9
+4,6519738396995980928,479635942,1.5214472721560001E9,1.521447272935E9
+4,-5145387251576462634,479635942,1.521447350005E9,1.521447350875E9
+4,5146993993359125762,479635942,1.521447414926E9,1.521447415826E9
+4,4610312666950954573,479635942,1.521447418526E9,1.521447420326E9
+4,-7498215498881490442,479635942,1.521447500426E9,1.521447501176E9
+4,-3600444927354891029,479635942,1.521447580256E9,1.521447581006E9
+4,4354068086212018794,479635942,1.521447641157E9,1.521447642086E9
+4,8693965426244736035,479635942,1.5214476448170002E9,1.521447646617E9
+4,-971911796417160282,479635942,1.521447650218E9,1.521447652917E9
+4,-918411083375874244,479635942,1.521447679678E9,1.5214476805770001E9
+4,-5525241125451737234,479635942,1.521447683188E9,1.521447687447E9
+4,1790624727480502608,479635942,1.521447699629E9,1.521447701878E9
+4,-4049256949871871827,479635942,1.521447749517E9,1.521447750417E9
+4,3529510055304631237,479635942,1.521447752217E9,1.521447753118E9
+4,-1172687350779436820,479635942,1.521447837478E9,1.521447839278E9
+4,-6268652303377976641,479635942,1.521447893368E9,1.521447894119E9
+4,7076946242247979971,479635942,1.52144797353E9,1.52144797443E9
+4,-7715230822356643021,479635942,1.52144799642E9,1.5214479981890001E9
+4,7680507504214796419,479635942,1.52144800248E9,1.521448004219E9
+4,-223108706738756500,479635942,1.52144804517E9,1.521448046969E9
+4,-2881526089818103868,479635942,1.521448047869E9,1.5214480524E9
+4,-8585300386736512915,479635942,1.52144812248E9,1.5214481241000001E9
+4,1499968694765296755,479635942,1.52144812581E9,1.521448128181E9
+8,5879659693373493356,479635942,1.519629388147E9,1.519629389048E9
+8,7000572295194635527,479635942,1.5196294062680001E9,1.519629407168E9
+8,-730751835115696982,479635942,1.519629432278E9,1.519629433177E9
+8,5981485234707577675,479635942,1.51962943492E9,1.519629435788E9
+8,2985217633962242548,479635942,1.519629449107E9,1.519629449858E9
+8,-6221364235205256441,479635942,1.519629453159E9,1.519629455857E9
+8,6500849691958272716,479635942,1.519629469448E9,1.519629470259E9
+8,2514854307262065587,479635942,1.519629471848E9,1.519629473348E9
+8,-8673274891412489569,479635942,1.519629725106E9,1.519629726936E9
+8,-2638264967837450423,479635942,1.519629737826E9,1.519629739625E9
+8,-7220312266624374900,479635942,1.519629816725E9,1.519629819786E9
+8,-7276728189848032775,479635942,1.519630080244E9,1.519630081204E9
+8,6023019915936845420,479635942,1.519630374101E9,1.519630374846E9
+8,-8965577381358949965,479635942,1.519630401935E9,1.519630403436E9
+8,-3508578100474321113,479635942,1.519630460373E9,1.519630462172E9
+8,-3857349432749872481,479635942,1.519630468293E9,1.519630470003E9
+8,1228434321453580016,479635942,1.519630502705E9,1.519630504234E9
+8,-3871080537778103703,479635942,1.519630520282E9,1.519630522772E9
+8,-8620146291320601879,479635942,1.519630527302E9,1.519630530062E9
+8,4760977236215949022,479635942,1.519630622792E9,1.519630638871E9
+8,4045861814072622109,479635942,1.519630711471E9,1.519630714891E9
+8,915524887984917716,479635942,1.519630719573E9,1.519630722572E9
+8,-7611305195042552879,479635942,1.519630744862E9,1.51963076418E9
+8,-7157067388187488120,479635942,1.5196311680970001E9,1.519631324819E9
+8,5762462466114178963,479635942,1.519631360099E9,1.519631434409E9
+8,-529344309654443661,479635942,1.519631470558E9,1.519631482408E9
+8,4933631662716116035,479635942,1.519631506499E9,1.519631511903E9
+8,-7908011693332036987,479635942,1.519631514418E9,1.51963153107E9
+8,7839886249009574762,479635942,1.519631532627E9,1.519631533377E9
+8,-649290018360883606,479635942,1.519631535627E9,1.519631552277E9
+8,293834598050901991,479635942,1.519631554977E9,1.519631746106E9
+8,1256100537099493447,479635942,1.519631769386E9,1.519631819365E9
+8,6644786439117645530,479635942,1.519631910987E9,1.519631913717E9
+8,-7497080258802514379,479635942,1.5196319263470001E9,1.519631929137E9
+8,4622769510674872004,479635942,1.519631972369E9,1.519631999817E9
+8,-1141953560934671341,479635942,1.519632031407E9,1.519632051386E9
+8,5349087180229490448,479635942,1.519632065395E9,1.519632072085E9
+8,-4747120930623285308,479635942,1.51963209054E9,1.519632132746E9
+8,-4377259361624472221,479635942,1.519632133647E9,1.519632136346E9
+8,2292475001329359162,479635942,1.519632137276E9,1.519632153566E9
+8,5431245649784024517,479635942,1.519632190795E9,1.519632209335E9
+8,42979432707295922,479635942,1.519632229255E9,1.519632722964E9
+8,3984345857400187262,479635942,1.519632907794E9,1.519632908725E9
+8,3830666979235437205,479635942,1.519632996714E9,1.519632997464E9
+8,-2342396138591788338,479635942,1.519633009105E9,1.519633010604E9
+8,5897253987423056632,479635942,1.519633087855E9,1.519633088633E9
+8,-7952629300780611977,479635942,1.519633091096E9,1.519633091933E9
+8,1405574861634135866,479635942,1.5196331022220001E9,1.519633103034E9
+8,5451425371067267955,479635942,1.519633184813E9,1.519633185682E9
+8,7083620308083773677,479635942,1.5196332462220001E9,1.519633248025E9
+8,1210433639115223226,479635942,1.519633351311E9,1.519633352211E9
+8,3002616498141307069,479635942,1.519633528432E9,1.519633529362E9
+8,-7229575619417935795,479635942,1.519633599201E9,1.519633600131E9
+8,6291220743044631006,479635942,1.519633675611E9,1.519633676511E9
+8,3527089241349133991,479635942,1.51963375181E9,1.51963375271E9
+8,-6554336874573040199,479635942,1.519634057631E9,1.519634058531E9
+4,-8813298524014584704,627460548,1.521443122766E9,1.521443341766E9
+4,1531767722795445557,627460548,1.521444889766E9,1.521444892766E9
+4,-7110809316548994353,627460548,1.521445068766E9,1.521445089766E9
+4,655350747368713556,627460548,1.521445092766E9,1.521445101766E9
+4,5930162479930420653,627460548,1.521445114766E9,1.521445459766E9
+4,5206709799691970442,627460548,1.521445469766E9,1.521445666766E9
+4,-959897090936021781,627460548,1.521445671766E9,1.521445697766E9
+4,-7680723325071274924,627460548,1.521445698766E9,1.521445700766E9
+4,204882228613436029,627460548,1.521445703766E9,1.521445707766E9
+4,6407896818791341170,627460548,1.521445711766E9,1.521445721766E9
+4,428153403250121348,627460548,1.521445727766E9,1.521446182766E9
+4,4288176115098687383,627460548,1.521446355766E9,1.521446512766E9
+4,-7547315903682837562,627460548,1.521446521766E9,1.521446526766E9
+4,-7399436758347559255,627460548,1.521446529766E9,1.521446804766E9
+4,2046858044437825919,627460548,1.521446808766E9,1.521447102766E9
+4,-5936044964195480951,627460548,1.521447110766E9,1.521447121766E9
+4,-6954135686990999696,627460548,1.521447127766E9,1.521447169766E9
+8,-9137750955228826260,627460548,1.519629824513E9,1.519629848513E9
+8,-6908759077158596339,627460548,1.519629914513E9,1.519629941513E9
+8,-7530471852639074558,627460548,1.519630624513E9,1.519630637513E9
+8,-954170402881087712,627460548,1.519630720513E9,1.519630864513E9
+8,-7274891943979374522,627460548,1.519630867513E9,1.519631834513E9
+8,-743390810149580574,627460548,1.519632036513E9,1.519632048513E9
+8,-9020241438604905728,627460548,1.519632065513E9,1.519632136513E9
+8,-7073068529258976682,627460548,1.519632144513E9,1.519632156513E9
+8,428814765587638922,627460548,1.519632164513E9,1.519632215513E9
+8,-7434917374319927985,627460548,1.519632222513E9,1.519632789513E9
+8,-1350785415966073806,627460548,1.519632797513E9,1.519632823513E9
+20,-5382481647183198032,479635942,1.4998442261620002E9,1.4998442269420002E9
+20,1398528250319112894,479635942,1.499844250904E9,1.499844252492E9
+20,1693662272651064915,479635942,1.4998442956920002E9,1.499844296592E9
+20,-7325575978669824068,479635942,1.499844331752E9,1.4998443335240002E9
+20,7860348933928939920,479635942,1.499844357582E9,1.499844358423E9
+20,-3864232224800149754,479635942,1.499844360102E9,1.499844361003E9
+20,-985755583265118537,479635942,1.499844485713E9,1.499844487364E9
+20,-393482977650394470,479635942,1.499844502963E9,1.499844504463E9
+20,6392595503183202572,479635942,1.499844505964E9,1.499844506714E9
+20,-408288388429413362,479635942,1.4998446221850002E9,1.499844622937E9
+20,-4538912419618731587,479635942,1.4998446251850002E9,1.4998446259380002E9
+20,-842343570071425163,479635942,1.499844662025E9,1.4998446648760002E9
+20,-6265993917731982606,479635942,1.4998449551260002E9,1.499844963227E9
+20,-1509884689013456963,479635942,1.4998449641260002E9,1.4998450392480001E9
+20,-1744524171501944267,479635942,1.499845059288E9,1.4998450884780002E9
+20,142168371532837201,479635942,1.4998450930080001E9,1.499845156038E9
+20,-655704192365399504,479635942,1.499845159068E9,1.499845235109E9
+20,4058817743057105228,479635942,1.49984523766E9,1.49984529817E9
+20,4502167199387201203,479635942,1.499845381872E9,1.499845382622E9
+20,-1644303620433773352,479635942,1.499845550112E9,1.499845551012E9
+20,-6559419888634467752,479635942,1.499845664023E9,1.499845664923E9
+20,-4547895194101985037,479635942,1.4998456893730001E9,1.499845690273E9
+20,-5472266330908296277,479635942,1.4998457175440001E9,1.499845718503E9
+20,926430593630520281,479635942,1.499845743943E9,1.499845745533E9
+20,3904926934982446797,479635942,1.499845756934E9,1.499845759364E9
+20,-2392288144699125293,479635942,1.499845760927E9,1.4998457633860002E9
+20,-1830800002427358914,479635942,1.499845906838E9,1.499845909598E9
+20,-8873520486869837015,479635942,1.499845953279E9,1.4998459578990002E9
+20,-4368514347912812486,479635942,1.4998460836660001E9,1.499846086359E9
+20,-1428421493148291899,479635942,1.4998465025800002E9,1.499846503418E9
+20,-4678123555691334308,479635942,1.499846668749E9,1.4998466695310001E9
+20,-2476468622354613631,479635942,1.4998469973690002E9,1.499847001898E9
+20,-2375692227549851517,479635942,1.4998470637610002E9,1.499847066519E9
+20,-6019814139971918226,479635942,1.4998470701490002E9,1.499847074709E9
+20,-1640692660151311931,479635942,1.49984729926E9,1.49984730151E9
+20,923067085950549949,479635942,1.49984730913E9,1.4998473114120002E9
+20,44731912975200737,479635942,1.4998474519610002E9,1.499847464832E9
+20,-7378325069341625500,479635942,1.499847491982E9,1.499847503742E9
+20,379793201703886583,479635942,1.499847521023E9,1.499847523693E9
+20,5436189141174789783,479635942,1.499847538782E9,1.499847539622E9
+20,1233072008490193348,479635942,1.499847592503E9,1.4998475933130002E9
+20,5206319406890702596,479635942,1.499847596043E9,1.499847596943E9
+20,-5352596564898023182,479635942,1.4998476005730002E9,1.499847601473E9
+20,1907283039276208634,479635942,1.499847673444E9,1.499847675184E9
+20,7751745042807262957,479635942,1.499847693035E9,1.4998476946230001E9
+20,7109650448824691457,479635942,1.499847729756E9,1.499847730655E9
+20,4322196049344734775,479635942,1.499847755164E9,1.499847756065E9
+20,5577902752255699992,627460548,1.49984495666E9,1.49984531266E9
+20,-3117896488428308513,627460548,1.49984584366E9,1.49984584666E9
+20,-3089668326737931626,627460548,1.49984684266E9,1.49984690166E9
+20,1285556521498637281,627460548,1.49984723466E9,1.49984724866E9
+20,3972572812869066761,627460548,1.49984744366E9,1.49984750066E9
+44,-4637271998962938849,479635942,1.499282411138E9,1.499282412669E9
+44,-2092229975352822834,479635942,1.499282577219E9,1.49928257902E9
+44,1049913845051400955,479635942,1.499282580849E9,1.49928258175E9
+44,-211215896574089824,479635942,1.4992826098E9,1.4992826107E9
+44,7370248448609067639,479635942,1.49928264151E9,1.49928264244E9
+44,-1778957851715528286,479635942,1.49928264424E9,1.49928264592E9
+44,1389191646178811019,479635942,1.49928264769E9,1.49928264853E9
+44,8168243402445675110,479635942,1.499282683781E9,1.49928268762E9
+44,-8876888668219959148,479635942,1.49928268912E9,1.499282692121E9
+44,7475017446856161645,479635942,1.499282697521E9,1.49928269902E9
+44,-3723394765263766759,479635942,1.499282976308E9,1.499283028569E9
+44,-7429615039432726871,479635942,1.499283060339E9,1.499283088119E9
+44,1347874962452191384,479635942,1.499283101649E9,1.499283169359E9
+44,-9217296491176434890,479635942,1.499283201401E9,1.4992832181399999E9
+44,-314125919632192747,479635942,1.49928322816E9,1.49928326851E9
+44,-6140603891082981173,479635942,1.499283281681E9,1.499283520268E9
+44,-7857330425916769274,479635942,1.499283713798E9,1.4992838484129999E9
+44,4295255570355324601,479635942,1.499283897665E9,1.499283901267E9
+44,-3305953162844076328,479635942,1.499284063793E9,1.499284140106E9
+44,2210778841204328361,479635942,1.499284150089E9,1.499284233081E9
+44,2406270417491863861,479635942,1.499284236466E9,1.499284271556E9
+44,1394152169360239198,479635942,1.499284289762E9,1.499284302164E9
+44,-4566322393517920709,479635942,1.49928431619E9,1.4992843618669999E9
+44,-1010041142446435004,479635942,1.499284527249E9,1.499284528954E9
+44,-3666842480733855395,479635942,1.499284550624E9,1.499284553099E9
+44,10930907405632125,479635942,1.499284671772E9,1.499284673835E9
+44,-9110182607008228626,479635942,1.499284760212E9,1.49928476211E9
+44,7798407804141648331,479635942,1.499284764007E9,1.4992847659589999E9
+44,8115514891653413031,479635942,1.499284954391E9,1.499284956619E9
+44,7560449628883860911,479635942,1.499284967315E9,1.499284970066E9
+44,-5320048537236409798,479635942,1.499284985824E9,1.499284988628E9
+44,5940334651139579011,479635942,1.499285026386E9,1.499285028889E9
+44,6387616634846144065,479635942,1.499285121702E9,1.499285125662E9
+44,6686525378666461145,479635942,1.499285161298E9,1.499285163526E9
+44,7483199910247397278,479635942,1.4992851863799999E9,1.49928518836E9
+44,1815115588707164796,479635942,1.499285238437E9,1.49928524248E9
+44,-6508976704422882408,627460548,1.499282707418E9,1.499282735418E9
+44,2561808373245313648,627460548,1.499282741418E9,1.499282797418E9
+44,8196876697210806276,627460548,1.499282968418E9,1.499283035418E9
+44,-7862908639673745989,627460548,1.499283036418E9,1.499283038418E9
+44,-7552396558642230191,627460548,1.499283044418E9,1.499283174418E9
+44,-5547151162994732173,627460548,1.499283195418E9,1.499283553418E9
+44,3989077605622453340,627460548,1.499283629418E9,1.499283640418E9
+44,7678424484398778148,627460548,1.499283646418E9,1.499283653418E9
+44,5042189237814686589,627460548,1.499283696418E9,1.499283871418E9
+44,-1014649183845097465,627460548,1.499283914418E9,1.499284420418E9
+44,-7074608251705529132,627460548,1.499284422418E9,1.499284470418E9
+44,1926196190632142284,627460548,1.499284919418E9,1.499284944418E9
+30,1381765217366987910,479635942,1.521702918745E9,1.521702919645E9
+30,-3294655792050400975,479635942,1.5217029213539999E9,1.521702922974E9
+30,-5718600010759177330,479635942,1.5217029245349998E9,1.521702926124E9
+30,3844435200978857876,479635942,1.521702975893E9,1.521702976793E9
+30,-1162217087515206821,479635942,1.521703091513E9,1.5217030924129999E9
+30,-6122269681650267389,479635942,1.521703095113E9,1.521703096013E9
+30,7536986203953647695,479635942,1.521703148689E9,1.5217031507289999E9
+30,2325169921450009372,479635942,1.521703263049E9,1.521703264309E9
+30,-996106118149702428,479635942,1.521703364507E9,1.5217033673869998E9
+30,-5746591137419452351,479635942,1.521703435787E9,1.521703669516E9
+30,-6273515633008229200,479635942,1.521703670266E9,1.521703671105E9
+30,-4479825348419641412,479635942,1.521703757295E9,1.521703758165E9
+30,-9082238007086586498,479635942,1.521703822694E9,1.521703823595E9
+30,3594338235991473819,479635942,1.521703849725E9,1.521703850626E9
+30,4242300907256383083,479635942,1.521703878554E9,1.521703879485E9
+30,-7665876455377814136,479635942,1.5217038977849998E9,1.521703900874E9
+30,7917895577705523388,479635942,1.5217039247549999E9,1.5217039398439999E9
+30,959787402597842355,479635942,1.5217039480049999E9,1.521703952564E9
+30,8055958848017718547,479635942,1.521704363023E9,1.521704369863E9
+30,-5472820722516914569,479635942,1.521704411353E9,1.521704412253E9
+30,2623397767340082296,479635942,1.521704521303E9,1.521704522203E9
+30,-2839886472099990582,479635942,1.521704630113E9,1.5217046319129999E9
+30,-6801935567505257201,479635942,1.521704644573E9,1.521704646373E9
+30,6368471110432347330,479635942,1.521704708953E9,1.521704709703E9
+30,4800062430288851269,479635942,1.5217047346629999E9,1.521704735563E9
+30,4010981228850596464,479635942,1.521705706694E9,1.521705708944E9
+30,-1469401792899758712,479635942,1.521705786853E9,1.5217057876929998E9
+30,6914175599468825885,479635942,1.521705789314E9,1.521705791564E9
+30,-7960810170750439193,479635942,1.521705795464E9,1.521705799964E9
+30,-7593077001334727309,479635942,1.521705843343E9,1.521705844243E9
+30,8541418143942666736,479635942,1.521705887624E9,1.521705888434E9
+30,-8999900358435167988,479635942,1.521705890024E9,1.521705890773E9
+30,-7449604252464815325,479635942,1.521705915674E9,1.521705918014E9
+30,6640854498431894253,479635942,1.521705918763E9,1.521705926353E9
+30,7731870956717998413,479635942,1.521705929053E9,1.521705931753E9
+30,7389418419162051529,479635942,1.5217059461829998E9,1.521705947984E9
+30,7289728920100530745,479635942,1.521705956083E9,1.521705956983E9
+30,-7120581446131048506,479635942,1.521705962383E9,1.521705964213E9
+30,-1219176403905642378,479635942,1.521706014194E9,1.521706014944E9
+30,5189264411278701918,479635942,1.521706017194E9,1.521706020944E9
+50,8533858820337304398,479635942,1.521744183215E9,1.5217441841139998E9
+50,3560356231578910845,479635942,1.5217442881839998E9,1.5217442890839999E9
+50,1748575149657710250,479635942,1.521744290885E9,1.5217442917849998E9
+50,-8799249892086896195,479635942,1.5217443249699998E9,1.521744326949E9
+50,-8642126730840857825,479635942,1.521744328839E9,1.5217443307589998E9
+50,-6169911094748600160,479635942,1.521744390847E9,1.521744392136E9
+50,8907508074867811719,479635942,1.521744393487E9,1.521744394896E9
+50,3895291104380527435,479635942,1.521744396245E9,1.521744397596E9
+50,168185476870227533,479635942,1.521744435636E9,1.5217444369269998E9
+50,8636852723142439205,479635942,1.5217444623669999E9,1.5217444648569999E9
+50,-5847807258660539548,479635942,1.521744471156E9,1.521744473406E9
+50,-9189687574318445053,479635942,1.521744588066E9,1.521744589326E9
+50,-5100840284981432430,479635942,1.521744616866E9,1.5217446195649998E9
+50,4497487509453454711,479635942,1.521744643656E9,1.521744645096E9
+50,4004871161195546717,479635942,1.5217446637849998E9,1.5217446652559998E9
+50,-8389117530910320304,479635942,1.5217446711049998E9,1.5217446765049999E9
+50,-3401963401424953099,479635942,1.521744681635E9,1.5217446866439998E9
+50,-5251821711440645252,479635942,1.521744687816E9,1.521744688985E9
+50,-1529408046372439742,479635942,1.5217447545049999E9,1.5217447556769998E9
+50,4472694011216418964,479635942,1.5217448977549999E9,1.521744898896E9
+50,-1876002449511129254,479635942,1.5217450684529998E9,1.5217450696239998E9
+50,-4703935011461701790,479635942,1.521745121073E9,1.521745122304E9
+50,4193874263778369621,479635942,1.521745144323E9,1.5217451456139998E9
+50,-6670978770975637758,479635942,1.521745148883E9,1.5217451546439998E9
+50,2026699222657945396,479635942,1.5217451569829998E9,1.521745159415E9
+50,-8737691554683567301,479635942,1.5217451874039998E9,1.5217451886039999E9
+50,-1593609965153129060,479635942,1.521745301373E9,1.5217453025129998E9
+50,4768164188694678881,479635942,1.5217453108839998E9,1.521745312054E9
+50,8956058034066115685,479635942,1.521745344155E9,1.521745345204E9
+50,4568560792602626850,479635942,1.5217454373339999E9,1.5217454384139998E9
+50,-6966616625484529104,479635942,1.521745474264E9,1.5217454753739998E9
+50,1678778636347263160,479635942,1.5217454881839998E9,1.521745489324E9
+50,2128464950379447278,479635942,1.521745519293E9,1.5217455446739998E9
+50,8230071782802070799,479635942,1.521745762324E9,1.5217457659529998E9
+50,-9163939346262825256,479635942,1.5217463259029999E9,1.5217463394329998E9
+50,-810766040702141218,479635942,1.5217464237329998E9,1.5217465564529998E9
+50,-4497038220427467687,479635942,1.521746732074E9,1.521746733574E9
+50,4117200318478077002,479635942,1.521746735074E9,1.521746736574E9
+50,-8431845267733393171,479635942,1.521746793273E9,1.521746797774E9
+50,-7472050666034561536,479635942,1.521746832724E9,1.521746833533E9
+50,3747544507295065148,479635942,1.5217468984529998E9,1.521746899353E9
+50,-2387977556999851591,479635942,1.5217469234129999E9,1.5217469251829998E9
+50,-7271451754365306737,479635942,1.521746951554E9,1.521746955304E9
+80,2659083652218610721,479635942,1.519154662783E9,1.519154663681E9
+80,-1129925315249971514,479635942,1.5191547773530002E9,1.519154778252E9
+80,-4128199781180746250,479635942,1.5191549621520002E9,1.5191549637410002E9
+80,-5165781753239550393,479635942,1.519154965302E9,1.5191549668030002E9
+80,2499630658728324810,479635942,1.519154990322E9,1.5191549912210002E9
+80,1360364372284828260,479635942,1.519155001181E9,1.519155002081E9
+80,8401671525057267390,479635942,1.519155003941E9,1.5191550057410002E9
+80,-7238246847319510538,479635942,1.5191550075410001E9,1.5191550084720001E9
+80,2509485127681747059,479635942,1.5191550480110002E9,1.519155048911E9
+80,90187419845953120,479635942,1.5191550515210001E9,1.5191550532010002E9
+80,6589972329856057218,479635942,1.5191550548820002E9,1.5191550557220001E9
+80,6186168014992745881,479635942,1.51915507126E9,1.519155072763E9
+80,3737636894400622299,479635942,1.5191551134650002E9,1.519155115857E9
+80,6803764460634835263,479635942,1.519155239441E9,1.51915524167E9
+80,6250314760774739554,479635942,1.519155267407E9,1.519155268865E9
+80,-1477778064799997236,479635942,1.5191552701850002E9,1.5191552733200002E9
+80,8485855056433952408,479635942,1.5191552753820002E9,1.519155279342E9
+80,5078557318103571687,479635942,1.519155281239E9,1.519155284872E9
+80,5822866718800208402,479635942,1.519155304092E9,1.519155306237E9
+80,-1837489621806747089,479635942,1.5191553166890001E9,1.519155320676E9
+80,2837691774257628004,479635942,1.5191553870860002E9,1.5191553892310002E9
+80,-940835999679064243,479635942,1.519155448355E9,1.5191554504450002E9
+80,-9000932918796050575,479635942,1.519155458696E9,1.5191554690900002E9
+80,627956856981620656,479635942,1.5191554964E9,1.519155500772E9
+80,-555598077542216994,479635942,1.519155519252E9,1.519155525608E9
+80,-5205561935037158310,479635942,1.5191556829320002E9,1.519155685242E9
+30,-3107531139122921790,627460548,1.521703411744E9,1.521703751744E9
+30,-7253452917277046534,627460548,1.521704814744E9,1.521704827744E9
+30,6492374719586161949,627460548,1.521704963744E9,1.521704977744E9
+30,-790365411172299920,627460548,1.521704991744E9,1.521705030744E9
+30,-9047979532457185798,627460548,1.521705036744E9,1.521705093744E9
+30,977880050342378422,627460548,1.521705105744E9,1.521705355744E9
+30,5712233931133130309,627460548,1.521705370744E9,1.521705392744E9
+30,309689767334852799,627460548,1.521705595744E9,1.521705649744E9
+50,-694096982042669143,627460548,1.521744487314E9,1.521744510314E9
+50,-4090817667259290754,627460548,1.521745489314E9,1.521745640314E9
+50,2094500064346394083,627460548,1.521745659314E9,1.521745698314E9
+50,-4635247095524136080,627460548,1.521745779314E9,1.521745794314E9
+50,4443598965928952453,627460548,1.521745966314E9,1.521746018314E9
+50,7015174958425370913,627460548,1.521746021314E9,1.521746071314E9
+50,1115101793338262647,627460548,1.521746091314E9,1.521746106314E9
+50,5083786052484355976,627460548,1.521746117314E9,1.521746128314E9
+50,-4055757595066983206,627460548,1.521746133314E9,1.521746163314E9
+50,5091998011161160946,627460548,1.521746170314E9,1.521746182314E9
+50,-4820716226830569406,627460548,1.521746220314E9,1.521746236314E9
+50,998315113627117151,627460548,1.521746266314E9,1.521746267314E9
+50,4546258366444818094,627460548,1.521746297314E9,1.521746563314E9
+33,3858430809643087710,479635942,1.501924896766E9,1.501924897815E9
+33,3896987748868385804,479635942,1.501924935825E9,1.501924936964E9
+33,-7446762863357396447,479635942,1.5019249417640002E9,1.501924946657E9
+33,-4496013176797839263,479635942,1.501925029635E9,1.5019250307450001E9
+33,5369056297001197612,479635942,1.501925051657E9,1.501925055227E9
+33,2767786118610359050,479635942,1.501925123357E9,1.501925134007E9
+33,9150604624774486415,479635942,1.501925192599E9,1.5019251958070002E9
+33,6737505784756924791,479635942,1.501925330354E9,1.501925331584E9
+33,948508249011544707,479635942,1.501925719637E9,1.501925990655E9
+33,2268916892944896373,479635942,1.5019259918860002E9,1.5019260012440002E9
+33,-3545040679921350089,479635942,1.5019260167540002E9,1.5019261027350001E9
+33,-8469051943704654215,479635942,1.501926137145E9,1.5019261395170002E9
+33,4080093433785993674,479635942,1.501926171798E9,1.501926235907E9
+33,-5586518668884266751,479635942,1.501926261801E9,1.5019264441330001E9
+33,-8587186543032205764,479635942,1.501926634125E9,1.501926637724E9
+33,-3012571462123890843,479635942,1.501926657283E9,1.5019266597150002E9
+33,-1188695500818149594,479635942,1.501926668026E9,1.501926704174E9
+33,4693559876971234790,479635942,1.501926711256E9,1.501926722805E9
+33,4695195228484392284,479635942,1.501926792132E9,1.5019269320830002E9
+33,3212023215827207735,479635942,1.5019269651430001E9,1.5019269744750001E9
+33,-1203206319294200628,479635942,1.501927005736E9,1.501927006903E9
+33,-7628795270406202699,479635942,1.5019270360040002E9,1.5019270387050002E9
+33,-7465130733264694093,479635942,1.501927042184E9,1.501927053553E9
+33,6382529754415590698,479635942,1.501927085565E9,1.501927087604E9
+33,-646205837486144512,479635942,1.501927088864E9,1.501927091204E9
+33,-4164187262230234980,479635942,1.501927221793E9,1.501927223029E9
+33,3887495905483113511,479635942,1.501927246332E9,1.5019272566520002E9
+33,-3433391353912368987,479635942,1.5019273005730002E9,1.501927301822E9
+33,377956745368926078,479635942,1.501927310263E9,1.501927313832E9
+33,1846498266370811436,479635942,1.501927522428E9,1.501927523622E9
+33,-4887754648942702068,479635942,1.5019278928040001E9,1.501927895233E9
+33,-4023362769939516434,479635942,1.501928304342E9,1.5019283054880002E9
+33,-6555220640315993103,479635942,1.5019285644120002E9,1.501928565553E9
+33,2850759686925681774,479635942,1.5019285857740002E9,1.501928587002E9
+33,5658773340969934577,479635942,1.501928826586E9,1.501928828233E9
+33,-4486385315453128463,479635942,1.50192883819E9,1.501928839303E9
+33,7374836464985291244,479635942,1.5019288833140001E9,1.501928884302E9
+33,5297731012355832898,479635942,1.5019289267510002E9,1.5019289279810002E9
+33,-6853049530670363515,479635942,1.501928935931E9,1.501928939173E9
+33,4077286640672801070,479635942,1.5019289602310002E9,1.501928961581E9
+33,-7936874687764443097,479635942,1.501928963951E9,1.501928968753E9
+33,8129726187503205449,479635942,1.501928969801E9,1.501928982371E9
+33,-4798691110118906969,479635942,1.501928984591E9,1.501928995181E9
+33,8307401047263836530,479635942,1.501929013783E9,1.501929014802E9
+33,-8911702973600911726,479635942,1.5019290178600001E9,1.50192902101E9
+33,2925354630311382579,479635942,1.5019290287210002E9,1.501929033311E9
+33,352973918942053231,479635942,1.501929054161E9,1.501929059681E9
+33,-8510895984176683422,479635942,1.5019290642410002E9,1.5019290694320002E9
+33,4980285528909552251,627460548,1.501925064614E9,1.501925073614E9
+33,-1379172195619850954,627460548,1.501925074614E9,1.501925153614E9
+33,5391808398998002444,627460548,1.501925159614E9,1.501925257614E9
+33,8874624227850671002,627460548,1.501925721614E9,1.501925978614E9
+33,2594020836664985310,627460548,1.501925982614E9,1.501925983614E9
+33,-7079084592751185452,627460548,1.501925993614E9,1.501925999614E9
+33,-6105182787576561544,627460548,1.501926018614E9,1.501926102614E9
+33,-6627363941945925880,627460548,1.501926137614E9,1.501926240614E9
+33,3544332513814905914,627460548,1.501926248614E9,1.501926464614E9
+33,-2255062831619284382,627460548,1.501926716614E9,1.501926728614E9
+33,-1779238583978771108,627460548,1.501926741614E9,1.501926980614E9
+10,-694329848772155884,479635942,1.519925040411E9,1.5199250413409998E9
+10,-3848674829203599271,479635942,1.5199250516309998E9,1.51992505253E9
+10,-6466927667216822707,479635942,1.51992511607E9,1.519925116971E9
+10,-2046327642399210634,479635942,1.5199251188009999E9,1.519925119702E9
+10,-221387692078910765,479635942,1.519925180451E9,1.519925181201E9
+10,2370011032020554035,479635942,1.519925202291E9,1.51992521078E9
+10,8567537360142041036,479635942,1.519925315689E9,1.519925317188E9
+10,6894961929101351522,479635942,1.5199256889469998E9,1.519925689849E9
+10,-5228549865492255678,479635942,1.5199258006409998E9,1.5199258015089998E9
+10,-1080641528184924837,479635942,1.5199258042099998E9,1.519925805109E9
+10,253334587267668212,479635942,1.5199258187289999E9,1.519925819628E9
+10,2294338982017915622,479635942,1.519925828689E9,1.5199258295879998E9
+10,5411491795584465178,479635942,1.5199259913799999E9,1.519925992217E9
+10,-2994815581710315378,479635942,1.519926061007E9,1.5199260619069998E9
+10,-2036881050902187208,479635942,1.5199262204259999E9,1.519926221297E9
+10,1948899031500132796,479635942,1.5199262246869998E9,1.5199262272649999E9
+10,3587645585241922140,479635942,1.519926335355E9,1.5199263371239998E9
+10,2324899470920976319,479635942,1.5199264975059998E9,1.519926498254E9
+10,616367067398025496,479635942,1.51992680651E9,1.5199268087589998E9
+10,4184109330493839929,479635942,1.5199268110089998E9,1.5199268132879999E9
+10,-4055601633496161725,479635942,1.5199268725679998E9,1.5199268743679998E9
+10,6223347576632231661,479635942,1.519926923507E9,1.519926924257E9
+10,-702527363376483355,479635942,1.519926926569E9,1.519926928067E9
+10,-1745195706842959513,479635942,1.5199269798479998E9,1.519926980777E9
+10,-4932122414423095672,479635942,1.5199270305749998E9,1.519927032405E9
+10,-3725820379386398582,479635942,1.5199270350159998E9,1.5199270359169998E9
+10,5592294175963738021,479635942,1.519927446105E9,1.519927447007E9
+10,-5287526182807978540,479635942,1.5199274931139998E9,1.519927500254E9
+10,-3855756482143877845,479635942,1.519927513245E9,1.519927679323E9
+10,951970005387399458,479635942,1.519927723033E9,1.5199277239329998E9
+10,-8849369739873446858,479635942,1.5199277725619998E9,1.519927785612E9
+10,-8797672936030168315,479635942,1.519928057951E9,1.5199280602849998E9
+10,3920538051563461167,479635942,1.5199281422089999E9,1.5199281442719998E9
+10,5767703135856322273,479635942,1.519928446632E9,1.5199287098869998E9
+10,9209642235166957271,479635942,1.5199289819459999E9,1.5199289827719998E9
+10,-2885841343153770496,479635942,1.519928988573E9,1.519928991103E9
+10,8500861711369598034,479635942,1.519929011921E9,1.5199290135709999E9
+10,7981669223981208322,479635942,1.519929091919E9,1.5199290927429998E9
+10,-9089444996569473499,479635942,1.5199291193899999E9,1.51992912107E9
+10,7545106266455823383,479635942,1.519929224439E9,1.519929225264E9
+10,-6124408801329058095,479635942,1.5199292278209999E9,1.519929228646E9
+10,1613508512581420285,479635942,1.519929343155E9,1.51992934398E9
+10,9002152297424685439,479635942,1.5199293854499998E9,1.5199293863019998E9
+10,664422771803581537,627460548,1.519927417475E9,1.519927422475E9
+10,7555262360710436896,627460548,1.519927494475E9,1.519927499475E9
+10,-4236829700110294690,627460548,1.519927514475E9,1.519927521475E9
+10,-3885485739390708138,627460548,1.519927522475E9,1.519927679475E9
+10,9141327326280738106,627460548,1.519927774475E9,1.519927830475E9
+10,455065934460074031,627460548,1.519927889475E9,1.519927896475E9
+10,-6031221678959024738,627460548,1.519928233475E9,1.519928252475E9
+10,-6029566416850502386,627460548,1.519928276475E9,1.519928287475E9
+10,297369570454899712,627460548,1.519928363475E9,1.519928726475E9
+40,-2128667663786149119,479635942,1.499886201476E9,1.499886202405E9
+40,-4499492632572217994,479635942,1.499886231506E9,1.499886232406E9
+40,-1403285438922050221,479635942,1.499886256496E9,1.499886258266E9
+40,1759348061655052581,479635942,1.499886292019E9,1.49988629352E9
+40,4359680840207732098,479635942,1.499886297418E9,1.499886299006E9
+40,2229694917955397248,479635942,1.499886302546E9,1.499886304346E9
+40,6083415027352742946,479635942,1.499886318926E9,1.499886324358E9
+40,-4351048654858054263,479635942,1.499886363387E9,1.499886364287E9
+40,-5166894204459964314,479635942,1.499886366987E9,1.499886367857E9
+40,2311082647342722875,479635942,1.499886402957E9,1.499886403679E9
+40,-8157414216892400851,479635942,1.499886407517E9,1.499886409017E9
+40,6368382748637709167,479635942,1.499886426658E9,1.499886428968E9
+40,3850793345159264120,479635942,1.499886432869E9,1.499886437638E9
+40,6848189831090944004,479635942,1.49988644344E9,1.49988644494E9
+40,-1336082787424414530,479635942,1.499887350218E9,1.499887350999E9
+40,5318976844826443487,479635942,1.499887750627E9,1.499887786432E9
+40,2482749292040363445,479635942,1.499887791301E9,1.499887795892E9
+40,-2415561457730032252,479635942,1.499887799467E9,1.4998879042350001E9
+40,-3705638154481773507,479635942,1.49988796402E9,1.499888003263E9
+40,177432733303531730,479635942,1.499888027243E9,1.499888031396E9
+40,-5406904514118003571,479635942,1.4998880330740001E9,1.499888036455E9
+40,5118318927660914446,479635942,1.499888321083E9,1.499888321908E9
+40,3506822557916821954,479635942,1.499888360353E9,1.499888362003E9
+40,-2542096204815812361,479635942,1.499888398491E9,1.499888401819E9
+40,-8787317948707902132,479635942,1.49988846199E9,1.499888463695E9
+40,-4399798985995798920,479635942,1.49988856627E9,1.49988856792E9
+40,811789926456241423,479635942,1.4998885687450001E9,1.499888570395E9
+40,-6031606375200563222,479635942,1.499888650476E9,1.499888660431E9
+40,-3682565786674385527,479635942,1.499888714331E9,1.499888715156E9
+40,-8512495387199779420,479635942,1.499888719308E9,1.499888720986E9
+40,5293617543245991074,479635942,1.499888722636E9,1.499888723489E9
+40,-1431035558974961442,479635942,1.499888764161E9,1.499888764986E9
+40,-6284082264885696337,479635942,1.499888785832E9,1.499888786657E9
+40,8189941698387242275,479635942,1.499888789187E9,1.499888790012E9
+40,8670087417793525524,479635942,1.499888826582E9,1.499888827407E9
+40,-3886100699827300385,479635942,1.499888885653E9,1.49988888821E9
+40,-8741274859527454375,479635942,1.499889001428E9,1.499889002253E9
+46,-3386680420861006908,479635942,1.5188674600440001E9,1.518867461755E9
+46,5301728692581838984,479635942,1.518867476839E9,1.518867477588E9
+46,-2763392567196711517,479635942,1.5188676260240002E9,1.5188676278240001E9
+46,8331190510886899861,479635942,1.518867700515E9,1.5188677024020002E9
+46,-713334474755320980,479635942,1.518867733002E9,1.518867734503E9
+46,-9174905736468618724,479635942,1.518867812022E9,1.518867813523E9
+46,-8113014737180214521,479635942,1.5188678151420002E9,1.518867816673E9
+46,-7465942728863565363,479635942,1.518867824778E9,1.518867825581E9
+46,-1101342095754698007,479635942,1.5188678552220001E9,1.518867857022E9
+46,5205541789616543816,479635942,1.5188678634120002E9,1.518867864341E9
+46,-8343736430450837871,479635942,1.518868089913E9,1.5188683851100001E9
+46,6485147502421143464,479635942,1.5188686780600002E9,1.518868684421E9
+46,-1346652621999171051,479635942,1.51886869177E9,1.518868693601E9
+46,7554914135213063768,479635942,1.518868852299E9,1.518868854999E9
+46,-6398888877273128820,479635942,1.5188688686200001E9,1.5188688732080002E9
+46,-1554822601177516152,479635942,1.518869094247E9,1.5188691033370001E9
+46,1962520126706657240,479635942,1.518869118368E9,1.518869121368E9
+46,-6782345448233934725,479635942,1.518869152299E9,1.518869154999E9
+46,5680745123160974426,479635942,1.518869167719E9,1.518869169519E9
+46,4243868082702061061,479635942,1.5188693279480002E9,1.5188693305270002E9
+46,-1566829234809142319,479635942,1.518869331367E9,1.5188693339480002E9
+46,-1847964570088236835,479635942,1.518869484397E9,1.5188694851460001E9
+46,-8209653442060560603,479635942,1.5188694935470002E9,1.518869494447E9
+46,-3564531886365618265,479635942,1.5188697855640001E9,1.518869787154E9
+46,8338501812493172092,479635942,1.5188698863330002E9,1.518869887233E9
+40,5734455086234358513,627460548,1.499886444457E9,1.499886452457E9
+40,4151690063604635080,627460548,1.499886459457E9,1.499886494457E9
+40,7869494181094017743,627460548,1.499887022457E9,1.499887164457E9
+40,-341589078194487227,627460548,1.499887226457E9,1.499887256457E9
+40,-2922879837909943204,627460548,1.499887495457E9,1.499887497457E9
+40,7217347143671948905,627460548,1.499887499457E9,1.499887506457E9
+40,5512026596709036316,627460548,1.499887510457E9,1.499887520457E9
+40,-8773312499084945466,627460548,1.499887549457E9,1.499887676457E9
+40,3430716370405572856,627460548,1.499887709457E9,1.499887921457E9
+40,1201920478073766500,627460548,1.499887930457E9,1.499887956457E9
+40,1698794885912782453,627460548,1.499887958457E9,1.499888018457E9
+46,-5719055632173539064,627460548,1.518868091239E9,1.518868428239E9
+46,1912938464721822750,627460548,1.518868467239E9,1.518868653239E9
+46,-5619135015759137843,627460548,1.518868693239E9,1.518868694239E9
+46,-7130261418066497890,627460548,1.518868704239E9,1.518868719239E9
+46,5764861178217039580,627460548,1.518868721239E9,1.518868737239E9
+46,-4311952123983752254,627460548,1.518868739239E9,1.518868758239E9
+46,2008732475398282871,627460548,1.518868888239E9,1.518868894239E9
+46,-2598591406595950374,627460548,1.518868989239E9,1.518868999239E9
+46,616464715567148650,627460548,1.518869002239E9,1.518869012239E9
+46,-3936229051375488234,627460548,1.518869025239E9,1.518869032239E9
+46,8089312538434247534,627460548,1.518869037239E9,1.518869065239E9
+53,4719537997627477294,479635942,1.518629576522E9,1.5186295774520001E9
+53,-3961541965162208929,479635942,1.518629581981E9,1.518629583812E9
+53,8503022251468215435,479635942,1.518629605801E9,1.518629607572E9
+53,713199775218361505,479635942,1.518629736256E9,1.518629737186E9
+53,2425805708253376019,479635942,1.518629783058E9,1.518629783809E9
+53,-7515043356625034258,479635942,1.518629792206E9,1.518629793709E9
+53,-4792884246056359027,479635942,1.518629985436E9,1.5186299863040001E9
+53,3707434878573148820,479635942,1.518630076849E9,1.518630079609E9
+53,8129160923945830370,479635942,1.51863008144E9,1.5186300841690001E9
+53,-8014330601047704350,479635942,1.518630097459E9,1.518630100219E9
+53,-3121402578790030156,479635942,1.51863013064E9,1.518630132289E9
+53,1603606642234752669,479635942,1.51863014699E9,1.518630148579E9
+53,2858617537404216434,479635942,1.518630169398E9,1.518630170298E9
+53,-5169750675118715776,479635942,1.518630172098E9,1.518630173058E9
+53,-2304206432183455319,479635942,1.518630199339E9,1.5186302002380002E9
+53,-1807222241646641934,479635942,1.518631485453E9,1.518631626181E9
+53,2091908338149281847,479635942,1.518631649941E9,1.51863165804E9
+53,8789285373404760892,479635942,1.5186316717810001E9,1.51863179043E9
+53,4931398278855665069,479635942,1.518631915292E9,1.518631916494E9
+53,-7759446220816372652,479635942,1.518631918683E9,1.518631920842E9
+53,-1548779265395687539,479635942,1.5186319218930001E9,1.5186319242E9
+53,-5510390377098441463,479635942,1.518631925851E9,1.518631928161E9
+53,3464079960435575554,479635942,1.51863214929E9,1.51863215004E9
+53,-173803762134672206,479635942,1.51863219288E9,1.518632193809E9
+53,-4535314705741276360,479635942,1.518632196569E9,1.518632197469E9
+53,8704017807762211453,479635942,1.5186322448990002E9,1.5186322475440001E9
+53,1610135799173198474,479635942,1.518632372701E9,1.51863237345E9
+53,-6377371896905616609,479635942,1.51863242181E9,1.518632423609E9
+53,-5540642517750075570,479635942,1.518632462759E9,1.518632463629E9
+53,-8425565646658279085,479635942,1.51863251271E9,1.518632514568E9
+53,-2642410273673643222,479635942,1.518632545469E9,1.518632546369E9
+53,2538228681333689978,627460548,1.518630555223E9,1.518630808223E9
+53,562979490760547513,627460548,1.518630826223E9,1.518630835223E9
+53,-4688564986980665542,627460548,1.518630854223E9,1.518630893223E9
+53,-1989663854288220342,627460548,1.518630953223E9,1.518630983223E9
+53,8201289497627416439,627460548,1.518631354223E9,1.518631399223E9
+53,-1429215875582936632,627460548,1.518631414223E9,1.518631811223E9
+34,-6000858646915987928,479635942,1.5204479944160001E9,1.520447995346E9
+34,-2314123508697726837,479635942,1.520448010706E9,1.520448011606E9
+34,5581043896274190172,479635942,1.520448109606E9,1.5204481107480001E9
+34,-6837627839302235162,479635942,1.5204485367910001E9,1.520448537691E9
+34,3770754727273036423,479635942,1.520448540421E9,1.520448541321E9
+34,-4523084511348774216,479635942,1.520448563911E9,1.52044856571E9
+34,-5497367115604635664,479635942,1.52044868823E9,1.52044868913E9
+34,8814609216675781130,479635942,1.5204487329E9,1.520448733681E9
+34,-3490289058455024266,479635942,1.52044873893E9,1.520448739681E9
+34,-8174951488481971175,479635942,1.52044880055E9,1.5204488023500001E9
+34,-8317944317676117748,479635942,1.520448870239E9,1.520448871141E9
+34,-12744667098081097,479635942,1.520448892799E9,1.5204488936990001E9
+34,293713869701688346,479635942,1.5204489244490001E9,1.520448926279E9
+34,6950106888937749318,479635942,1.52044895097E9,1.520448951809E9
+34,2237797513140888629,479635942,1.520448954209E9,1.520448956729E9
+34,3037870119588580492,479635942,1.5204489584090002E9,1.520448960149E9
+34,-4391813490183835592,479635942,1.520448978749E9,1.520448981899E9
+34,-8887674756581127149,479635942,1.520449025039E9,1.520449026839E9
+34,-555061566564083561,479635942,1.520449028638E9,1.520449029539E9
+34,-8463407994976925256,479635942,1.520449068059E9,1.52044906893E9
+34,-8428209022374972541,479635942,1.52044908732E9,1.520449090589E9
+34,-4171994716942111520,479635942,1.520449093859E9,1.520449110448E9
+34,753323244456129368,479635942,1.5204491112E9,1.5204491126990001E9
+34,3780275552049392469,479635942,1.52044911726E9,1.520449141289E9
+34,-954460041138789062,479635942,1.52044938096E9,1.52044938276E9
+34,-5364370288568675355,479635942,1.52044957476E9,1.52044959375E9
+34,-3181746273712470859,479635942,1.520449600079E9,1.520449734539E9
+34,5615705212364081547,479635942,1.520449836779E9,1.52044984218E9
+34,7396459936587200903,479635942,1.520450132369E9,1.520450523297E9
+34,-81958421013514564,479635942,1.520450786277E9,1.520450787418E9
+34,-649421775118629863,479635942,1.520450792308E9,1.520450793297E9
+34,-4296196419647938515,479635942,1.520450824678E9,1.520450827496E9
+34,-2130448844552829855,479635942,1.5204508287580001E9,1.520450829868E9
+34,827706226946107518,479635942,1.520450955418E9,1.520450956407E9
+34,-3635211437937948347,479635942,1.520451044038E9,1.520451046198E9
+34,-2816549764914966441,479635942,1.520451047278E9,1.520451055618E9
+34,-3333312543193866264,479635942,1.520451061559E9,1.5204510627280002E9
+34,849630890640015477,627460548,1.520449102177E9,1.520449174177E9
+34,3596018359152660547,627460548,1.520449223177E9,1.520449292177E9
+34,1448719135241197326,627460548,1.520449295177E9,1.520449339177E9
+34,8819969892212945172,627460548,1.520449347177E9,1.520449431177E9
+34,-3951893586210102027,627460548,1.520449441177E9,1.520449443177E9
+34,-8681924579718721836,627460548,1.520449473177E9,1.520449534177E9
+34,-1781805203761157607,627460548,1.520449538177E9,1.520449748177E9
+34,8153404959014326632,627460548,1.520449915177E9,1.520449951177E9
+34,5556185552738641632,627460548,1.520449974177E9,1.520449998177E9
+34,-5924911185796044082,627460548,1.520450023177E9,1.520450540177E9
+17,1174641044501369031,479635942,1.499239215381E9,1.499239216131E9
+17,-113513919750267864,479635942,1.499239238721E9,1.499239240551E9
+17,1914961879829331968,479635942,1.499239247871E9,1.499239249671E9
+17,6168836024175779195,479635942,1.499239267792E9,1.499239268721E9
+17,-7968231014627295188,479635942,1.499239326863E9,1.499239329115E9
+17,-8499415123635551694,479635942,1.499239333703E9,1.499239335969E9
+17,6627994625602525233,479635942,1.499239448812E9,1.499239449562E9
+17,-5039958006018807985,479635942,1.499239481153E9,1.499239482053E9
+17,4741967673194050437,479635942,1.499239497383E9,1.499239499183E9
+17,-85070342197273786,479635942,1.49923962548E9,1.49923962638E9
+17,438770993668007878,479635942,1.49923963718E9,1.499239638979E9
+17,-6203021544952714408,479635942,1.499239758171E9,1.49923977623E9
+17,-7159997191543915936,479635942,1.499239832242E9,1.499239955992E9
+17,-5775443037996156590,479635942,1.499239979182E9,1.499239979932E9
+17,-893708132266951796,479635942,1.499239988602E9,1.499240019352E9
+17,-6912547387988450452,479635942,1.499240039122E9,1.499240069183E9
+17,5541402448862509122,479635942,1.499240191014E9,1.499240191764E9
+17,4853777467545763960,479635942,1.499240374789E9,1.499240375688E9
+17,-1966508044092881506,479635942,1.499240485039E9,1.49924048684E9
+17,-1643031902044395359,479635942,1.499240721594E9,1.499240722341E9
+17,1619773578788551390,479635942,1.499240745051E9,1.499240745951E9
+17,-1733489000928163983,479635942,1.499240748651E9,1.499240749581E9
+17,1705536870240618908,479635942,1.499240947547E9,1.499240952348E9
+17,2830335267853659225,479635942,1.499240953187E9,1.499240954059E9
+17,6470982455355185698,479635942,1.499241435978E9,1.499241437658E9
+17,-4607800810413217230,479635942,1.499241488328E9,1.499241490038E9
+17,-1826568725686642718,479635942,1.499241493698E9,1.499241495529E9
+17,-4619194132686886559,479635942,1.499241898035E9,1.499241898935E9
+17,-2212123172000043182,479635942,1.499241907155E9,1.499241908054E9
+17,-7071242323630529159,479635942,1.499242135247E9,1.499242136838E9
+17,-8115366638117663932,479635942,1.49924223908E9,1.499242243277E9
+17,6349028877385368540,479635942,1.499242247207E9,1.499242247959E9
+17,-6891175381736931759,479635942,1.499242573066E9,1.499242573963E9
+17,-1524786648176917025,479635942,1.499242587673E9,1.499242588572E9
+17,8049304979851678142,479635942,1.499242616532E9,1.499242618093E9
+17,-797518848902721648,479635942,1.499242619593E9,1.499242621152E9
+17,378123319668265763,479635942,1.499242637533E9,1.499242638283E9
+17,-8370198626871080547,479635942,1.499242702393E9,1.499242703293E9
+17,-5850100425849207414,479635942,1.499242705127E9,1.499242706024E9
+17,6208343689566820418,479635942,1.499242773705E9,1.499242775418E9
+17,-1550605339149442068,479635942,1.499242776973E9,1.499242777785E9
+17,3176154601877577621,479635942,1.499242802264E9,1.499242804094E9
+17,-1853380393051858652,627460548,1.499239751444E9,1.499239783444E9
+17,-3207614366998862337,627460548,1.499239824444E9,1.499240119444E9
+17,7553345385419259252,627460548,1.499240124444E9,1.499240160444E9
+17,2972559417706573068,627460548,1.499240952444E9,1.499241001444E9
+17,-3099714540054183611,627460548,1.499242275444E9,1.499242296444E9
+17,6479166978187231620,627460548,1.499242311444E9,1.499242315444E9
+17,-7869978596500489005,627460548,1.499242490444E9,1.499242508444E9
+17,-5139822931339225334,627460548,1.499242509444E9,1.499242541444E9
+17,366823539272850670,627460548,1.499242550444E9,1.499242559444E9
+11,-5543695754868643088,479635942,1.500055767817E9,1.500055768748E9
+11,-2480867838113463367,479635942,1.500055954738E9,1.5000559556629999E9
+11,-5484082981511469405,479635942,1.5000560360019999E9,1.500056037112E9
+11,4034839475437669677,479635942,1.500056054154E9,1.5000560559819999E9
+11,4488805995615899206,479635942,1.500056057872E9,1.500056059464E9
+11,7883624875693060474,479635942,1.500056157503E9,1.5000561584029999E9
+11,4618971283163984574,479635942,1.5000561899029999E9,1.500056191465E9
+11,6188937660737349562,479635942,1.500056192965E9,1.500056194553E9
+11,8903899750443294539,479635942,1.500056229144E9,1.500056230944E9
+11,-3394480610283052964,479635942,1.500056266374E9,1.500056267244E9
+11,-3469125817796304842,479635942,1.500056269074E9,1.500056271744E9
+11,2795423957250455775,479635942,1.5000562725839999E9,1.5000562760939999E9
+11,7089143145452215105,479635942,1.5000565436399999E9,1.5000565447779999E9
+11,5794082623021317137,479635942,1.5000567601499999E9,1.500056761049E9
+11,-5984407172659360821,479635942,1.500056858848E9,1.500056860139E9
+11,4592988333463663302,479635942,1.500056864158E9,1.500056865389E9
+11,40389292891179278,479635942,1.5000568711499999E9,1.500056873189E9
+11,-4569347279358008062,479635942,1.500057010651E9,1.50005709591E9
+11,4664456424607955005,479635942,1.500057098431E9,1.500057103021E9
+11,-8308622012746565941,479635942,1.50005710683E9,1.500057297632E9
+11,4298366364783496558,479635942,1.5000573356729999E9,1.500057495568E9
+11,905207645040959628,479635942,1.500057503698E9,1.500057504598E9
+11,8321563119332864354,479635942,1.500057506398E9,1.500057510066E9
+11,3829336336412831454,479635942,1.500057921434E9,1.5000579280049999E9
+11,-5292375675026104456,479635942,1.500057931244E9,1.500057937695E9
+11,-6585130645265929790,479635942,1.500058373474E9,1.500058379323E9
+11,-1179295113201761636,479635942,1.500058603427E9,1.500058607144E9
+11,-5204755513226210711,479635942,1.500058612876E9,1.500058614016E9
+11,-9054883140929113592,479635942,1.500058656735E9,1.500058657996E9
+11,-631040589778758465,479635942,1.500058702245E9,1.500058704405E9
+11,5578892479226305874,479635942,1.500058732968E9,1.500058734225E9
+11,2603520482047056700,479635942,1.500058753726E9,1.5000588374559999E9
+11,-8154586923657674779,479635942,1.500058846336E9,1.500058866736E9
+11,-7306787708996232856,479635942,1.500058905885E9,1.500058907056E9
+11,7636997053628670900,479635942,1.500058922356E9,1.500058944496E9
+11,1478375614263718545,479635942,1.500059025766E9,1.500059144477E9
+11,-171987320273188646,479635942,1.500059191218E9,1.500059236189E9
+11,-9157845083007472577,479635942,1.5000592530509999E9,1.500059349203E9
+11,-5754058400308241748,479635942,1.5000595350509999E9,1.500059536372E9
+11,857173072131487462,479635942,1.5000598545839999E9,1.500059871053E9
+11,-1502173484578183518,479635942,1.500059872225E9,1.500059881495E9
+11,-4181829232066143726,479635942,1.500060277259E9,1.500060372057E9
+11,-9084904019957069745,479635942,1.500060380128E9,1.500060424047E9
+11,-1828512306184840082,479635942,1.50006045951E9,1.500060741143E9
+11,8680232449804734557,479635942,1.500060767542E9,1.500060799416E9
+11,-6807911939256734373,479635942,1.500060859475E9,1.5000608613439999E9
+11,1909083224071200532,479635942,1.500060892339E9,1.500060893383E9
+11,-4119442477063225613,479635942,1.500060928666E9,1.500060930096E9
+11,3791025594053356146,479635942,1.500060931141E9,1.500060942691E9
+11,-7044025460470057660,479635942,1.500061121966E9,1.5000611246629999E9
+11,-1057809992410855944,479635942,1.50006115436E9,1.5000611556529999E9
+11,7675744265113592931,479635942,1.500061255893E9,1.500061258448E9
+11,-2635098009724116695,479635942,1.500061282486E9,1.500061283803E9
+11,6326765872777977471,479635942,1.500061364736E9,1.5000613701E9
+11,-3497590175997740077,479635942,1.500061372794E9,1.500061375297E9
+11,-3918505463904649565,479635942,1.500061380246E9,1.500061383849E9
+11,-5233742646009138621,479635942,1.500061399883E9,1.500061400983E9
+11,8533749416559801035,479635942,1.500061482987E9,1.500061484007E9
+11,6578800892235992313,479635942,1.500061609763E9,1.500061611276E9
+11,2665557495979508305,627460548,1.500056905984E9,1.500056942984E9
+11,-3613450615585479724,627460548,1.500056967984E9,1.500056990984E9
+11,8589792464854971908,627460548,1.500057010984E9,1.500057499984E9
+11,863602497122901259,627460548,1.500058735984E9,1.500058742984E9
+11,2591426395849682796,627460548,1.500058744984E9,1.500058875984E9
+11,4129276314654245211,627460548,1.500058881984E9,1.500058884984E9
+11,-5560997421686112404,627460548,1.500058906984E9,1.500058910984E9
+11,7675034701163374306,627460548,1.500058917984E9,1.500058958984E9
+11,-4542691943903189271,627460548,1.500058976984E9,1.500058981984E9
+11,9183554001340422098,627460548,1.500058992984E9,1.500059001984E9
+11,-4456396871196047336,627460548,1.500059009984E9,1.500059150984E9
+11,-5212416744950382231,627460548,1.500059161984E9,1.500059169984E9
+11,-4429629996414924494,627460548,1.500059177984E9,1.500059357984E9
+11,-5355993751012974249,627460548,1.500059854984E9,1.500060039984E9
+11,-7263636355199048301,627460548,1.500060103984E9,1.500060105984E9
+11,-7318378108656503778,627460548,1.500060110984E9,1.500060118984E9
+11,7532861815097238860,627460548,1.500060270984E9,1.500060808984E9
+12,4283593092442975370,479635942,1.5189377686100001E9,1.5189377701100001E9
+12,-6730773103290777465,479635942,1.5189378472080002E9,1.518937848078E9
+12,-2235796387731170381,479635942,1.5189379015080001E9,1.518937902408E9
+12,6605800188608690183,479635942,1.518937929678E9,1.518937930578E9
+12,7561622485425587688,479635942,1.518937979477E9,1.518937980257E9
+12,-6252053639086100173,479635942,1.5189379829880002E9,1.5189379853270001E9
+12,-5957596880538714093,479635942,1.518938028256E9,1.518938029186E9
+12,6481997373775911886,479635942,1.5189381085370002E9,1.518938109437E9
+12,5364980274529319432,479635942,1.518938139617E9,1.518938140516E9
+12,-2054936555839058013,479635942,1.518938147806E9,1.5189381505370002E9
+12,-870844027063078872,479635942,1.518938152367E9,1.518938154196E9
+12,-2421935695111372078,479635942,1.5189383302050002E9,1.518938335005E9
+12,-2077205704846135523,479635942,1.518938401484E9,1.5189384042450001E9
+12,2569312354927215010,479635942,1.5189384115040002E9,1.518938619525E9
+12,3593046407837387529,479635942,1.5189386377050002E9,1.518938691046E9
+12,815443863766839883,479635942,1.518939280572E9,1.518939282884E9
+12,-3012067486874307021,479635942,1.518939416171E9,1.518939419772E9
+12,5433916974079917102,479635942,1.518939538811E9,1.51893954514E9
+12,-236863565709865616,479635942,1.5189399854190001E9,1.51893998614E9
+12,8350609525672883483,479635942,1.518940155638E9,1.5189401646980002E9
+12,-3859450603864411763,479635942,1.5189402635170002E9,1.518940265318E9
+12,-3812269890092998697,479635942,1.518940287997E9,1.518940321716E9
+12,-7535625842753015943,479635942,1.518940801115E9,1.518940911545E9
+12,7872024605165270739,479635942,1.5189409244150002E9,1.518940936658E9
+12,6247176614357948326,479635942,1.518940938996E9,1.5189410276790001E9
+12,851026044269946819,479635942,1.5189410909780002E9,1.5189413166950002E9
+12,-3208158205689746372,479635942,1.518941329414E9,1.518941383805E9
+12,-8491322549521376889,479635942,1.518941478365E9,1.5189414830740001E9
+12,-182841290945481409,479635942,1.518941530265E9,1.518941534765E9
+12,-5301279482316321186,479635942,1.5189415464950001E9,1.5189415682150002E9
+12,3394733332275496354,479635942,1.518941579885E9,1.518941580785E9
+12,360798525811066928,479635942,1.5189416025040002E9,1.5189416453140001E9
+12,-7528480465664847909,479635942,1.518941646214E9,1.518941736035E9
+12,-5462127132215690834,479635942,1.5189418230040002E9,1.518941873882E9
+12,-8520394149655783988,479635942,1.518941878382E9,1.5189421005340002E9
+12,8979004929858640027,479635942,1.518942105036E9,1.518942144753E9
+12,-3956961855162800075,479635942,1.518942146523E9,1.518942200674E9
+12,-1321483133108390696,479635942,1.518942202924E9,1.518942293282E9
+12,6238810720709420671,479635942,1.518942524492E9,1.5189425261420002E9
+12,2218382725502986503,479635942,1.518942540905E9,1.5189425416520002E9
+12,-5629500424456825869,479635942,1.5189425774120002E9,1.518942578342E9
+12,-4627176174521996455,479635942,1.5189426204620001E9,1.518942623012E9
+12,5187513082717779153,479635942,1.5189426428170002E9,1.518942643563E9
+12,-7507594365009239360,479635942,1.5189427098000002E9,1.5189427107020001E9
+12,493910791917772289,479635942,1.5189427390800002E9,1.518942742321E9
+12,-8086246983915300563,479635942,1.518942765061E9,1.518942765811E9
+12,-3337236280372941465,479635942,1.518942770311E9,1.518942771809E9
+12,8685259439690032410,627460548,1.518938402489E9,1.518939222489E9
+12,-6868661479380126344,627460548,1.518940264489E9,1.518940269489E9
+12,-8069229001313049753,627460548,1.518940277489E9,1.518940465489E9
+12,5821047234509037071,627460548,1.518940473489E9,1.518940546489E9
+12,-6145076890289439677,627460548,1.518940551489E9,1.518940638489E9
+12,-6700879027725373579,627460548,1.518940764489E9,1.518940777489E9
+12,-1665937948720462442,627460548,1.518940787489E9,1.518941388489E9
+12,2307096764922462312,627460548,1.518941603489E9,1.518941762489E9
+12,6591131193758243285,627460548,1.518941784489E9,1.518941790489E9
+12,6359970645227808880,627460548,1.518941808489E9,1.518942325489E9
+81,192518120778095635,479635942,1.500020543701E9,1.500020544598E9
+81,-7227601353525798436,479635942,1.500020548107E9,1.5000205489490001E9
+81,-64712697685216447,479635942,1.5000205583370001E9,1.500020559088E9
+81,-5132480708142171343,479635942,1.500020583238E9,1.50002058999E9
+81,7607560735302201258,479635942,1.500020592238E9,1.500020599077E9
+81,-3075084665810469277,479635942,1.500020693609E9,1.500020694359E9
+81,616389932782925533,479635942,1.500020727089E9,1.500020727989E9
+81,1747310175578242858,479635942,1.500020762339E9,1.500020767769E9
+81,6488563634091348225,479635942,1.500020773109E9,1.500020775869E9
+81,232486055601429644,479635942,1.5000207776100001E9,1.500020780219E9
+81,949868499096912208,479635942,1.500020815765E9,1.500020818195E9
+81,6604962992337975618,479635942,1.500020819696E9,1.5000208319950001E9
+81,4588742148984955312,479635942,1.5000208349950001E9,1.500020839435E9
+81,2353503775928897875,479635942,1.500020848526E9,1.500020849426E9
+25,-1918576272533331593,479635942,1.501659060553E9,1.501659061454E9
+25,475574475082999312,479635942,1.501659162974E9,1.501659163874E9
+25,1296706644873663684,479635942,1.501659165645E9,1.501659167414E9
+25,-3451216801643177610,479635942,1.501659207375E9,1.5016592088760002E9
+25,2632304374512274923,479635942,1.501659335176E9,1.5016593367680001E9
+25,-6146646490068665335,479635942,1.5016593526660001E9,1.5016593549160001E9
+25,1798347674854031652,479635942,1.50165945374E9,1.501659454576E9
+25,582209014612805769,479635942,1.5016594568860002E9,1.501659458476E9
+25,2412189380582982752,479635942,1.501659462437E9,1.501659467838E9
+25,7046244486747443510,479635942,1.5016594885670002E9,1.501659489466E9
+25,-3211193533496446790,479635942,1.501659494868E9,1.501659496697E9
+25,-5291335758908400797,479635942,1.501659499397E9,1.501659502157E9
+25,-5458151725296583238,479635942,1.501659769604E9,1.501659774164E9
+25,2694961801384400888,479635942,1.5016598106750002E9,1.501659812384E9
+25,6578363047576436691,479635942,1.501659819404E9,1.5016598202150002E9
+25,-1050316684683158896,479635942,1.501659821056E9,1.501659821894E9
+25,-3572827924039450409,479635942,1.5016598509060001E9,1.5016600926720002E9
+25,4999474947403218734,479635942,1.501660098013E9,1.501660176372E9
+25,-4657384137351831521,479635942,1.501660239163E9,1.501660240663E9
+25,-4221317794909143287,479635942,1.501660618571E9,1.501660623911E9
+25,813603138422814918,479635942,1.501660626431E9,1.501660627331E9
+25,7403618885556415160,479635942,1.5016607085410001E9,1.501660710311E9
+25,3078057363207950509,479635942,1.5016607139710002E9,1.5016607149020002E9
+25,1389898489787163430,479635942,1.501660795302E9,1.501660796232E9
+25,-488294058476917788,479635942,1.5016608796920002E9,1.501660884193E9
+25,-2501611817757284153,479635942,1.501661054739E9,1.5016610556390002E9
+25,5745648008296236700,479635942,1.5016611721000001E9,1.501661173E9
+25,5536885424141391184,479635942,1.5016612506100001E9,1.50166125226E9
+25,6967703414404161427,479635942,1.501661289401E9,1.501661291201E9
+25,6865718704262931946,479635942,1.5016616524320002E9,1.501661654677E9
+25,838757312249711460,479635942,1.5016618080960002E9,1.501661834377E9
+25,-3250595588765349460,479635942,1.501661881359E9,1.5016618821980002E9
+25,603372751702739863,479635942,1.5016618956990001E9,1.5016618979480002E9
+25,4879259615023668465,479635942,1.5016619159780002E9,1.501661918706E9
+25,-1357716852533659053,479635942,1.501661926898E9,1.5016619277970002E9
+25,-5919127137214935923,479635942,1.5016619295970001E9,1.501661931398E9
+25,2530284298257046273,479635942,1.501661933227E9,1.501661934127E9
+25,-5057144647355815593,479635942,1.501661995509E9,1.501661996257E9
+25,-1804655490542361307,479635942,1.501661998658E9,1.501662000278E9
+25,309374400425423835,479635942,1.5016620224480002E9,1.5016620231980002E9
+25,1085217367503410273,479635942,1.501662174941E9,1.5016621758400002E9
+25,6744335931331136197,627460548,1.50165955942E9,1.50165956442E9
+25,-4950803005028863969,627460548,1.50165977142E9,1.50166021942E9
+25,7603458606368951735,627460548,1.50166031242E9,1.50166031542E9
+25,-549765673095479241,627460548,1.50166042242E9,1.50166042542E9
+25,1679327534199456962,627460548,1.50166052342E9,1.50166052942E9
+25,-2723924659348105878,627460548,1.50166053242E9,1.50166055542E9
+25,4585404511370006495,627460548,1.50166056242E9,1.50166060942E9
+25,4821797035227592088,627460548,1.50166180242E9,1.50166184042E9
+9,6090515753093118009,479635942,1.5008809643E9,1.50088096517E9
+9,8607518278720194933,479635942,1.500880995471E9,1.500880996341E9
+9,-6328344669637137655,479635942,1.500881024362E9,1.500881025261E9
+9,5846071625939405160,479635942,1.500881131732E9,1.500881132482E9
+9,-8413814638006670958,479635942,1.500881136982E9,1.500881137732E9
+9,-2511537132679287528,479635942,1.500881180212E9,1.500881182913E9
+9,2513652891765636627,479635942,1.500881184712E9,1.500881185612E9
+9,2671628838296329268,479635942,1.500881396754E9,1.50088140402E9
+9,4965812998068513038,479635942,1.500881410404E9,1.500881422133E9
+9,2551276844589223272,479635942,1.500881423033E9,1.500881426693E9
+9,1711634055538610619,479635942,1.500881440283E9,1.500881443793E9
+9,-755894269467376115,479635942,1.500881458104E9,1.500881502355E9
+9,1595802499149206750,479635942,1.500881518585E9,1.500881557646E9
+9,1663139309445998059,479635942,1.500881567575E9,1.500881665496E9
+9,-3932532742074429241,479635942,1.500881678996E9,1.500881688086E9
+9,-4537658734341497416,479635942,1.500881707406E9,1.500881712837E9
+9,-5068144471233551489,479635942,1.500881736836E9,1.500881753067E9
+9,2656315172392767202,479635942,1.500881896402E9,1.500881901863E9
+9,-2195040381408539092,479635942,1.500881914522E9,1.500881918153E9
+9,-7284052012417037083,479635942,1.500881957453E9,1.500881965082E9
+9,-1186588711760806569,479635942,1.500881980974E9,1.500881986854E9
+9,-5290452604235714733,479635942,1.500882097613E9,1.500882099292E9
+9,-6035739451328999118,479635942,1.500882113872E9,1.500882114622E9
+9,4091804748993463023,479635942,1.500882208404E9,1.500882210655E9
+9,-4575871135502892229,479635942,1.500882336205E9,1.500882340406E9
+9,4949924325371426599,479635942,1.500882382555E9,1.500882387054E9
+9,-872903691188889680,479635942,1.500882495925E9,1.500882496825E9
+9,5913911087254124493,479635942,1.500882588773E9,1.500882589526E9
+9,5780265201893861110,479635942,1.500882819384E9,1.500882820283E9
+9,-5060064149019610810,479635942,1.500882859554E9,1.500882860454E9
+9,6187173133999865237,479635942,1.500882903057E9,1.500882903954E9
+9,5151355861737342949,479635942,1.500883368917E9,1.500883369817E9
+9,-4659309597230372100,479635942,1.500883385297E9,1.500883386197E9
+9,8856506679410435688,479635942,1.500883389887E9,1.500883401707E9
+9,-5502722262409781109,479635942,1.500883441789E9,1.500883449917E9
+9,-4226422481924672357,479635942,1.500883453128E9,1.500883454628E9
+9,-320154619360412133,479635942,1.50088368665E9,1.50088368926E9
+9,-8171623311157724159,479635942,1.500883701802E9,1.50088370339E9
+9,-3915895445743928837,479635942,1.500883708703E9,1.500883709451E9
+9,-3754416137751822639,479635942,1.50088371173E9,1.5008837300740001E9
+9,8881009627207109386,479635942,1.500883779742E9,1.500883781541E9
+9,-2641563171317723985,479635942,1.500883782442E9,1.500883783342E9
+9,-7942470475451468418,479635942,1.500883787901E9,1.500883832603E9
+9,-3362705630856244793,479635942,1.500883887682E9,1.500883928512E9
+9,-305311769432346044,479635942,1.500883931931E9,1.500883989412E9
+9,-4711922360961168655,479635942,1.50088402028E9,1.500884360872E9
+9,-6776836309038358794,479635942,1.500884530099E9,1.500884604832E9
+9,6951188376775391253,479635942,1.500884639031E9,1.500884678572E9
+9,5217572947880372369,479635942,1.50088471052E9,1.50088471682E9
+9,-5789678890717688600,479635942,1.500884768241E9,1.50088484507E9
+9,-6918581243470671782,479635942,1.500884864148E9,1.50088493792E9
+9,2870663491214235785,479635942,1.500884980069E9,1.500885024649E9
+9,-4663032202397766868,479635942,1.500885030559E9,1.500885032239E9
+9,-4474943182138209540,479635942,1.500885045289E9,1.50088509131E9
+9,-577076490395417539,479635942,1.500885113072E9,1.50088513214E9
+9,-3874067388838806318,627460548,1.500881247319E9,1.500881249319E9
+9,749036971346092554,627460548,1.500881301319E9,1.500881328319E9
+9,1808500096443673397,627460548,1.500881424319E9,1.500881790319E9
+9,-7352971782388487486,627460548,1.500883702319E9,1.500883736319E9
+9,5359625835878371672,627460548,1.500883745319E9,1.500883752319E9
+9,8936881148278854430,627460548,1.500883766319E9,1.500883857319E9
+9,-2182114211114290636,627460548,1.500883876319E9,1.500884001319E9
+9,5829145618465743460,627460548,1.500884014319E9,1.500884367319E9
+9,-7460876090151743721,627460548,1.500884470319E9,1.500884612319E9
+9,4670152704022440228,627460548,1.500884630319E9,1.500884689319E9
+9,8720622542923076327,627460548,1.500884698319E9,1.500884758319E9
+9,6876792168881524510,627460548,1.500884761319E9,1.500884958319E9
+9,671801985722518798,627460548,1.500884963319E9,1.500885156319E9
+9,302524817626752602,627460548,1.500885161319E9,1.500885185319E9
+9,-3190508053959966824,627460548,1.500885192319E9,1.500885204319E9
+38,8131974661146223925,479635942,1.522048687981E9,1.522048689752E9
+38,-7138217712064283202,479635942,1.522048759862E9,1.5220487607610002E9
+38,2238269102845267436,479635942,1.5220487974520001E9,1.522048799161E9
+38,-4462245165621976760,479635942,1.522048848871E9,1.5220488497710001E9
+38,8101481607241039878,479635942,1.522048851572E9,1.522048852471E9
+38,3812991432502658978,479635942,1.522048855171E9,1.522048856071E9
+38,-8892851273280719223,479635942,1.522048922821E9,1.522048924501E9
+38,-7495215679650315887,479635942,1.5220489425310001E9,1.522048943252E9
+38,-4760747130502563132,479635942,1.522048944751E9,1.522048945502E9
+38,-958203103780352875,479635942,1.5220490396100001E9,1.5220490405410001E9
+38,7182840473596955637,479635942,1.522049042941E9,1.522049043721E9
+38,-4177663431188146798,479635942,1.52204908602E9,1.52204908782E9
+38,2431421281061135486,479635942,1.522049330099E9,1.5220493336990001E9
+38,3848923199715530916,479635942,1.522049340959E9,1.522049598539E9
+38,-5960738625727644228,479635942,1.522049612068E9,1.522049664539E9
+38,6773885161319474223,479635942,1.522049702757E9,1.522049709868E9
+38,-2489970910499104008,479635942,1.5220497107380002E9,1.522049763147E9
+38,-8281944706982810090,479635942,1.52205008301E9,1.52205008598E9
+38,-4344074848256580206,479635942,1.522050309539E9,1.522050311039E9
+38,3144395660400653551,479635942,1.522051297529E9,1.52205129828E9
+38,1937342201065397105,479635942,1.522051303019E9,1.52205130383E9
+38,-244970285071672010,479635942,1.522051318319E9,1.52205131907E9
+38,4212050593528162588,479635942,1.5220513229090002E9,1.5220513244090002E9
+38,7211650041960773880,479635942,1.522051343579E9,1.522051344479E9
+38,491403866198003510,479635942,1.52205139764E9,1.522051398389E9
+38,294204943368387387,627460548,1.522049331131E9,1.522049772131E9
+38,-6223246366435122896,627460548,1.522050404131E9,1.522050423131E9
+38,-299093952694109024,627460548,1.522050429131E9,1.522050471131E9
+38,588203095281303882,627460548,1.522050559131E9,1.522050680131E9
+38,746210364012139525,627460548,1.522050746131E9,1.522050817131E9
+38,6506854525593490785,627460548,1.522050838131E9,1.522050880131E9
+38,-4876674528238848355,627460548,1.522051037131E9,1.522051042131E9
+38,6151580069918737466,627460548,1.522051043131E9,1.522051046131E9
+38,961136104895701124,627460548,1.522051058131E9,1.522051063131E9
+38,6378313643022324144,627460548,1.522051087131E9,1.522051097131E9
+14,-1021702737721583270,479635942,1.5016119639129999E9,1.501611964814E9
+14,4415002113845876129,479635942,1.501611975734E9,1.501611976635E9
+14,-7943467882940527550,479635942,1.501611994753E9,1.501611995654E9
+14,3323967674802340929,479635942,1.5016122335249999E9,1.501612234425E9
+14,6693875695754850733,479635942,1.501612445477E9,1.501612446437E9
+14,-6481226270892374582,479635942,1.5016124548409998E9,1.501612456608E9
+14,-2112136325040388893,479635942,1.5016124936269999E9,1.5016124946859999E9
+14,-7814402393733373894,479635942,1.501612601777E9,1.5016126036669998E9
+14,1204787480036344335,479635942,1.501612607178E9,1.501612611888E9
+14,-2563243885709823695,479635942,1.501612678098E9,1.501612679148E9
+14,8318719176490939498,479635942,1.501612959678E9,1.501612969066E9
+14,-3474770349561694616,479635942,1.5016129789659998E9,1.501613202258E9
+14,1013020036325701798,479635942,1.501613205258E9,1.501613409915E9
+14,-7838687942193028895,479635942,1.501613432895E9,1.501613550256E9
+14,3551549597481415472,479635942,1.501613662904E9,1.501613666563E9
+14,1866801837750067161,479635942,1.5016136686629999E9,1.5016136696529999E9
+14,-2059668158275998437,479635942,1.501613704725E9,1.501613876475E9
+14,-2891958191777042440,479635942,1.501613908546E9,1.5016140376929998E9
+14,-2851017660901732904,479635942,1.501614059383E9,1.501614085633E9
+14,-3780078887654384401,479635942,1.501614093793E9,1.501614142873E9
+14,-2203157316035126569,479635942,1.501614153613E9,1.501614172066E9
+14,2111736377436960368,479635942,1.501614205454E9,1.501614212684E9
+14,-4939796187505064756,479635942,1.501614262454E9,1.501614265484E9
+14,-5258032461743755707,479635942,1.50161438705E9,1.501614397335E9
+14,-4332372976802204713,479635942,1.501614404296E9,1.501614414135E9
+14,-2008726048910922592,479635942,1.501614476442E9,1.501614480942E9
+14,7020519559909589505,479635942,1.5016146195149999E9,1.5016146466039999E9
+14,-3665578959722742849,479635942,1.501614650353E9,1.501614651853E9
+14,-7119982566538871663,479635942,1.5016153092779999E9,1.501615311077E9
+14,-5146814387774832162,479635942,1.501615341619E9,1.501615342458E9
+14,-1968475871047378773,479635942,1.501615351158E9,1.5016153535E9
+14,-8872554050103052086,479635942,1.5016153987389998E9,1.501615399638E9
+14,-5808111080258819721,479635942,1.501615482194E9,1.501615482945E9
+14,281959769992484241,479635942,1.5016154875349998E9,1.501615489095E9
+14,2069680721360166668,479635942,1.501615491347E9,1.501615493685E9
+14,-2203194315871195098,479635942,1.5016155132749999E9,1.501615515075E9
+14,-5644959211383613757,479635942,1.501615525905E9,1.501615535955E9
+14,-4184214561838531671,479635942,1.501615537756E9,1.501615540457E9
+14,1705370279262435115,479635942,1.5016155772059999E9,1.501615589655E9
+14,7473027341214179601,479635942,1.501615641797E9,1.501615657996E9
+14,-3876124526351470397,479635942,1.501615714817E9,1.501615864817E9
+14,2106191494671183615,479635942,1.5016158731829998E9,1.5016161044859998E9
+14,-68653067807653614,479635942,1.5016163370119998E9,1.501616338812E9
+14,-4913037207397076415,479635942,1.501616459922E9,1.501616460823E9
+14,8407941901201923190,479635942,1.501616475283E9,1.501616477082E9
+14,-9161338843413088220,479635942,1.501616571103E9,1.5016165819329998E9
+14,161073715723794521,479635942,1.50161669183E9,1.501616692656E9
+29,6373613954626632990,479635942,1.519801585983E9,1.5198015877819998E9
+29,889862951668856209,479635942,1.5198016228209999E9,1.519801623692E9
+29,-7578821178615434161,479635942,1.519801682011E9,1.519801682912E9
+29,8625747728379018858,479635942,1.519801685671E9,1.5198016865709999E9
+29,-6158644541272785541,479635942,1.519801709191E9,1.51980171099E9
+29,-7833842548579193423,479635942,1.5198018046069999E9,1.519801806835E9
+29,-1116885831784161835,479635942,1.519801971494E9,1.519801973493E9
+29,906889542286998317,479635942,1.519802235997E9,1.5198025133309999E9
+29,874698431778755260,479635942,1.5198025441859999E9,1.519802570751E9
+29,-2618047012476716658,479635942,1.5198029208539999E9,1.519802921679E9
+29,3949533197419373451,479635942,1.519802926655E9,1.5198029308379998E9
+29,2943292331990812807,479635942,1.5198029508009999E9,1.5198029566569998E9
+29,-2011700701636547229,479635942,1.5198031067519999E9,1.519803108434E9
+29,8841704935251570427,479635942,1.519803153666E9,1.51980315452E9
+29,8772210033948648407,479635942,1.519803303075E9,1.519803308052E9
+29,708620121875807316,479635942,1.519803345616E9,1.519803387168E9
+29,-4852919504576283196,479635942,1.5198034422779999E9,1.519803443102E9
+29,3604698577670393771,479635942,1.519803531102E9,1.519803532754E9
+29,9026976508836738865,479635942,1.51980440054E9,1.519804401366E9
+29,6873272688709542036,479635942,1.5198044247949998E9,1.519804427353E9
+29,8276039023708252039,479635942,1.519804456475E9,1.5198044573E9
+29,7833009565387084629,479635942,1.5198044589759998E9,1.519804460655E9
+29,-4122928436402255343,479635942,1.519804463184E9,1.51980446401E9
+29,-3881433529630694608,479635942,1.519804545105E9,1.5198045459589999E9
+29,-3791273973291884494,479635942,1.5198046671499999E9,1.519804675483E9
+29,-7262801969275249263,479635942,1.5198047145319998E9,1.519804716182E9
+29,-2799763091563717935,479635942,1.519804719483E9,1.519804720307E9
+29,2530627489001561912,479635942,1.519804737799E9,1.519804740302E9
+29,-3933413081449918832,479635942,1.519804792937E9,1.519804793817E9
+14,-307883905288284849,627460548,1.501612827218E9,1.501612837218E9
+14,-8649994580952985792,627460548,1.501612923218E9,1.501612933218E9
+14,7058878073385005352,627460548,1.501612942218E9,1.501613569218E9
+14,5695109220663831475,627460548,1.501613649218E9,1.501614225218E9
+14,4000464388460685497,627460548,1.501614240218E9,1.501614319218E9
+14,-2217101916127203187,627460548,1.501614335218E9,1.501614366218E9
+14,1936956361818924209,627460548,1.501614614218E9,1.501614661218E9
+14,5853897049447232754,627460548,1.501614665218E9,1.501614670218E9
+14,-3869527058619321867,627460548,1.501614686218E9,1.501614799218E9
+14,5695189144668947542,627460548,1.501614840218E9,1.501614883218E9
+14,-7929147682195086959,627460548,1.501615571218E9,1.501616115218E9
+14,-5151618354262112056,627460548,1.501616251218E9,1.501616311218E9
+29,1929241547214098437,627460548,1.519802225524E9,1.519802519524E9
+29,-5614142304865639570,627460548,1.519802536524E9,1.519802615524E9
+29,5346892523983513214,627460548,1.519803347524E9,1.519803386524E9
+29,-2419547652203965706,627460548,1.519803670524E9,1.519803686524E9
+29,6053722807640517315,627460548,1.519803691524E9,1.519803767524E9
+29,-7674888954688925827,627460548,1.519803769524E9,1.519803944524E9
+29,6596488148397808413,627460548,1.519804056524E9,1.519804106524E9
+29,-2570503273283267027,627460548,1.519804108524E9,1.519804127524E9
+29,5739566477168771813,627460548,1.519804322524E9,1.519804348524E9
+61,3942676439566451263,479635942,1.5023918717930002E9,1.5023918731120002E9
+61,7558798174767674546,479635942,1.502391875694E9,1.5023918767470002E9
+61,5936843667444631105,479635942,1.5023919138260002E9,1.502391916194E9
+61,-7792208692954458371,479635942,1.502391933776E9,1.502391935184E9
+61,-1919721854392181822,479635942,1.5023925374640002E9,1.5023926866260002E9
+61,-5556648844250083073,479635942,1.5023929283860002E9,1.502392954052E9
+61,8909244349072401079,479635942,1.5023929630830002E9,1.502393016394E9
+61,-8406301509623552706,479635942,1.502393022153E9,1.502393028453E9
+61,1305685328096165151,479635942,1.502393157996E9,1.5023932556160002E9
+61,542108248419044515,479635942,1.502393264015E9,1.5023934108390002E9
+61,-8451436607884143589,479635942,1.502393420466E9,1.5023934662470002E9
+61,1849653628501251096,479635942,1.502393626203E9,1.502393628486E9
+61,-1458985599779847027,479635942,1.5023937323430002E9,1.5023937358240001E9
+61,-1842935852170325383,479635942,1.5023937580240002E9,1.502393759163E9
+61,-4571047198721082734,479635942,1.5023937725180001E9,1.5023937738330002E9
+61,4286645336572824125,479635942,1.502393923026E9,1.502393925515E9
+61,-313403767917633089,479635942,1.502393991367E9,1.502393992625E9
+61,2987429939682193652,479635942,1.50239405874E9,1.502394059792E9
+61,1257792799527490371,479635942,1.5023941311910002E9,1.5023941323930001E9
+68,-659846217501791887,479635942,1.519201958528E9,1.519201959339E9
+68,2060401380080743560,479635942,1.519202004969E9,1.519202005869E9
+68,6753288535632267848,479635942,1.519202019579E9,1.519202020479E9
+68,-5462639336689000255,479635942,1.519202090441E9,1.519202091278E9
+68,3257060850950043763,479635942,1.51920209353E9,1.519202094282E9
+68,6120795976430160732,479635942,1.51920209659E9,1.519202097341E9
+68,-8615025112689578399,479635942,1.5192021290770001E9,1.519202130878E9
+68,-5245101556940209298,479635942,1.5192021408700001E9,1.51920214267E9
+68,6529954211446053002,479635942,1.519202145369E9,1.519202147168E9
+68,-654200351740303506,479635942,1.519202184009E9,1.519202185718E9
+68,7200653626390166797,479635942,1.51920218731E9,1.519202189648E9
+68,5487976080443953836,479635942,1.519202208849E9,1.519202211307E9
+68,-305378061517564704,479635942,1.519202214007E9,1.519202224028E9
+68,-5436351900679562256,479635942,1.519202577787E9,1.519202578747E9
+68,7315291334904581500,479635942,1.519202636737E9,1.519202638446E9
+68,-7500040465621554565,479635942,1.519202645946E9,1.5192026483470001E9
+68,3315393717469316193,479635942,1.519202650596E9,1.519202653658E9
+68,-5263300673648927295,479635942,1.519202691306E9,1.5192026922350001E9
+68,4728834677068284191,479635942,1.519202694287E9,1.519202696075E9
+68,8524917417667562170,479635942,1.519203007144E9,1.519203007925E9
+68,-4666987856606545871,479635942,1.5192030254160001E9,1.519203027728E9
+61,2739059226238388352,627460548,1.502392531903E9,1.502392766903E9
+61,2010524747431456206,627460548,1.502392923903E9,1.502393086903E9
+61,5475534554314568792,627460548,1.502393101903E9,1.502393113903E9
+61,-4693778579235448871,627460548,1.502393132903E9,1.502393478903E9
+68,7087003071191319852,627460548,1.519202162118E9,1.519202167118E9
+51,543750207684301555,479635942,1.502132828904E9,1.502132830073E9
+51,4497356937130965253,479635942,1.502132880925E9,1.502132882154E9
+51,3286615959954598788,479635942,1.502133030925E9,1.502133032066E9
+51,3070137280515368601,479635942,1.502133054057E9,1.5021330551069999E9
+51,-6783953758166186495,479635942,1.502133192955E9,1.502133194276E9
+51,3762461902215959332,479635942,1.5021332219069998E9,1.50213322644E9
+51,-7782111808452972427,479635942,1.50213346645E9,1.5021336166299999E9
+51,-3322364525940459703,479635942,1.502133618529E9,1.502133798428E9
+51,-4460637422399144253,479635942,1.502133817073E9,1.502134039825E9
+51,-5465175562490924841,479635942,1.502134162089E9,1.502134172237E9
+51,-439721881505226500,479635942,1.502134184612E9,1.502134497945E9
+51,-8567499036176092659,479635942,1.502134515847E9,1.502134672654E9
+51,-433683495461338756,479635942,1.5021346890709999E9,1.502134739865E9
+51,-3864134445828993716,479635942,1.502135040165E9,1.5021351941399999E9
+51,-4292814945283134318,479635942,1.502135434951E9,1.502135916947E9
+51,-4736875507479866621,479635942,1.50213616711E9,1.502136170026E9
+51,4011101075862701804,479635942,1.50213618977E9,1.502136190899E9
+51,2586511385772063378,479635942,1.5021362763969998E9,1.5021362778E9
+51,6731204817223828629,479635942,1.50213630021E9,1.5021363027159998E9
+51,-6764618376058154485,479635942,1.5021363507849998E9,1.502136352158E9
+51,6217368938625732377,479635942,1.502136403856E9,1.5021364049029999E9
+51,315892509708435408,479635942,1.502136407322E9,1.502136408723E9
+51,6044768987979669608,479635942,1.5021364124359999E9,1.502136417496E9
+51,4106314998867256567,479635942,1.502136425225E9,1.5021364300349998E9
+51,8674784803480153578,479635942,1.502136434768E9,1.5021364359229999E9
+51,3304363438203012574,479635942,1.502136480912E9,1.502136482177E9
+51,6254839126541836790,627460548,1.502133329389E9,1.502133380389E9
+51,-8716395367076826556,627460548,1.502133401389E9,1.502133414389E9
+51,7067720531491531927,627460548,1.502133435389E9,1.502133801389E9
+51,8753268841033951747,627460548,1.502133810389E9,1.502134059389E9
+51,-2460379312560727788,627460548,1.502134148389E9,1.502134744389E9
+51,-7532067162463005517,627460548,1.502134755389E9,1.502134785389E9
+51,-3674515065337974130,627460548,1.502135033389E9,1.502135210389E9
+51,-7856585878364129353,627460548,1.502135232389E9,1.502135280389E9
+51,-9065149239914724017,627460548,1.502135414389E9,1.502135925389E9
+23,8743841902486875519,479635942,1.502091441667E9,1.502091442926E9
+23,-1815580840427260185,479635942,1.502091564843E9,1.502091566072E9
+23,1831040082004246906,479635942,1.502091606362E9,1.5020916078E9
+23,5270146368629148846,479635942,1.502091648813E9,1.502091650013E9
+23,-1727642305565292716,479635942,1.5020916523830001E9,1.5020916536460001E9
+23,3774907707132856675,479635942,1.502091780935E9,1.502091782253E9
+23,7821256584854543491,479635942,1.502091785522E9,1.502091789062E9
+23,-8742197624678799052,479635942,1.502091791706E9,1.502091792842E9
+23,7762330275797652110,479635942,1.502091795302E9,1.502091797762E9
+23,-8398575317146482131,479635942,1.5020918618140001E9,1.5020918639720001E9
+23,7280248696194713716,479635942,1.502091941496E9,1.502091942513E9
+23,3126541511176481818,479635942,1.502091948607E9,1.502091951996E9
+23,7520215219730619596,479635942,1.5020919532250001E9,1.502091955684E9
+23,-2517561300612258391,479635942,1.50209206482E9,1.502092067311E9
+23,-5314944328307161117,479635942,1.502092135592E9,1.5020921373630002E9
+23,3198581219875969683,479635942,1.50209213925E9,1.5020921404520001E9
+23,6771874141471754178,479635942,1.502092142614E9,1.502092144775E9
+23,7913393774671415057,479635942,1.502092208253E9,1.502092213171E9
+23,8360142497055685968,479635942,1.502092239272E9,1.502092477951E9
+23,1321633043861626785,479635942,1.502092610971E9,1.50209261832E9
+23,9155526153704761756,479635942,1.502092949399E9,1.5020929551590002E9
+23,3169582306503763089,479635942,1.502092956089E9,1.502092959628E9
+23,-7897431255897957192,479635942,1.5020929650310001E9,1.502092969649E9
+23,1776219904924426574,479635942,1.502092971809E9,1.502092974089E9
+23,3019312906767128780,479635942,1.50209299953E9,1.50209300346E9
+23,5534923394134588498,479635942,1.502093332647E9,1.5020933339390001E9
+23,7755700028751275147,479635942,1.5020934563400002E9,1.502093481694E9
+23,-6661520972218287233,479635942,1.502093656773E9,1.50209366028E9
+23,-3316585860975761736,479635942,1.50209367927E9,1.502093724059E9
+23,8840941970603122908,479635942,1.502093758468E9,1.502093762069E9
+23,-2082341475622691172,479635942,1.502093788888E9,1.502093813879E9
+23,-1443976185612522745,479635942,1.502093861009E9,1.502093875197E9
+23,-4793781874195099802,479635942,1.50209388795E9,1.502093912368E9
+23,-2143264385123146658,479635942,1.502093939968E9,1.502094254727E9
+23,-311803229808367634,479635942,1.502094420718E9,1.502094799828E9
+23,6686205899795050510,479635942,1.5020948166920002E9,1.502094819989E9
+23,-5057128576255093012,479635942,1.502094842487E9,1.502094845637E9
+23,-7181504577915952360,479635942,1.502094864061E9,1.502095028336E9
+23,-2607794686996352512,479635942,1.502095232395E9,1.502095237918E9
+23,-2396571768388247142,479635942,1.5020953859650002E9,1.5020953871330001E9
+23,-2672466432812372448,479635942,1.502095407444E9,1.502095408677E9
+23,7133652549603479553,479635942,1.502095470714E9,1.502095471884E9
+23,595821520593960475,479635942,1.502095521204E9,1.502095522526E9
+23,-3551747066318806915,479635942,1.502095541336E9,1.5020955426560001E9
+23,1740025291928650981,479635942,1.502095672645E9,1.502095673726E9
+23,-3948943269101169837,479635942,1.5020956760080001E9,1.502095677115E9
+23,8400426520138412748,479635942,1.502095723255E9,1.502095724399E9
+23,2134700134595476609,479635942,1.502095781302E9,1.5020957825340002E9
+23,-167616118949794257,479635942,1.502095784783E9,1.502095786081E9
+23,-1193902352413803975,627460548,1.502092210075E9,1.502092482075E9
+23,3462681701974189718,627460548,1.502092535075E9,1.502092554075E9
+23,7743903351294224373,627460548,1.502092563075E9,1.502092629075E9
+23,3809305830801805325,627460548,1.502092640075E9,1.502092662075E9
+23,-7076994491943853816,627460548,1.502092670075E9,1.502092714075E9
+23,-2101983596415915612,627460548,1.502092719075E9,1.502092793075E9
+23,-491556812470713919,627460548,1.502092955075E9,1.502092956075E9
+23,7789936837502469369,627460548,1.502092958075E9,1.502092960075E9
+23,8644528257382294923,627460548,1.502092966075E9,1.502093148075E9
+23,-4636040524457572399,627460548,1.502093620075E9,1.502093668075E9
+23,-3018339305190296126,627460548,1.502093674075E9,1.502093734075E9
+23,-1099296226433255731,627460548,1.502093753075E9,1.502093819075E9
+23,-27370822391443730,627460548,1.502093826075E9,1.502093839075E9
+23,-1999810213917561252,627460548,1.502093847075E9,1.502094320075E9
+23,-4855933800844096675,627460548,1.502094361075E9,1.502094413075E9
+23,3478282783141948789,627460548,1.502094415075E9,1.502094802075E9
+23,-5624830077630378336,627460548,1.502094843075E9,1.502095077075E9
+36,-6055753206250783153,479635942,1.5198367671680002E9,1.5198367679180002E9
+36,4386541687749300082,479635942,1.519836864278E9,1.519836865028E9
+36,3797686429492012788,479635942,1.5198368868390002E9,1.5198368877670002E9
+36,2808234819795757153,479635942,1.5198369140470002E9,1.5198369149480002E9
+36,-424181240516748671,479635942,1.5198369555080001E9,1.5198369564060001E9
+36,-4747319851952021269,479635942,1.5198369650470002E9,1.5198369665470002E9
+36,2531665241675691570,479635942,1.5198370275660002E9,1.5198370284970002E9
+36,8110920152329178923,479635942,1.519837031196E9,1.5198370320670002E9
+36,-3722257502341928075,479635942,1.519837073706E9,1.5198370746060002E9
+36,-2185984822815736965,479635942,1.5198371478660002E9,1.519837149696E9
+36,5822896447778858343,479635942,1.5198371551850002E9,1.5198371560860002E9
+36,7146284416221669138,479635942,1.5198372425740001E9,1.5198372434750001E9
+36,-5976672557143701873,479635942,1.5198376230070002E9,1.519837623877E9
+36,-8190225499557280914,479635942,1.5198380225440001E9,1.519838024857E9
+36,-6057182085704737882,479635942,1.519838199397E9,1.519838202966E9
+36,3653485765639364848,479635942,1.5198382057250001E9,1.5198382093250003E9
+36,2435223857680124326,479635942,1.5198383039470003E9,1.519838309344E9
+36,-2365257864108798165,479635942,1.519838385423E9,1.5198383863830001E9
+36,-1587802966150975571,479635942,1.5198385538140001E9,1.5198387595210001E9
+36,6166197683570272587,479635942,1.5198387879600003E9,1.519838795821E9
+36,4429707809253824246,479635942,1.5198388153200002E9,1.5198388730390003E9
+36,4668575776155057916,479635942,1.5198391563000002E9,1.5198391585500002E9
+36,-5450798862295898118,479635942,1.5198391828810003E9,1.5198391837810001E9
+36,-6406885925288789127,479635942,1.5198392664610002E9,1.5198392696100001E9
+36,2248648754414924262,479635942,1.5198392781000001E9,1.5198392804120002E9
+36,-9149770372330624644,479635942,1.51983928341E9,1.5198392850000002E9
+36,4529843190981844135,479635942,1.5198393328190002E9,1.519839333719E9
+36,2442318781916160208,479635942,1.5198394556390002E9,1.519839456538E9
+36,-5712772855668453246,627460548,1.5198376376390002E9,1.5198376826390002E9
+36,-936071058274127192,627460548,1.5198376846390002E9,1.5198379896390002E9
+36,6182466801921697880,627460548,1.5198384056390002E9,1.5198388956390002E9
+36,2764937084838059256,627460548,1.5198390616390002E9,1.5198390676390002E9
+60,2934912818293942492,479635942,1.519406854441E9,1.51940685534E9
+60,-1260990045655202805,479635942,1.519406856241E9,1.519406858064E9
+60,7545715224929917771,479635942,1.519406859872E9,1.519406861641E9
+60,-8083103261115791360,479635942,1.51940689023E9,1.5194068924819999E9
+60,58595489647474327,479635942,1.51940698566E9,1.5194069865E9
+60,3092960690980761370,479635942,1.51940713125E9,1.519407132E9
+60,2018476724893868400,479635942,1.519407153329E9,1.519407155578E9
+60,380086147145237127,479635942,1.519407318989E9,1.51940732076E9
+60,2739552313725409850,479635942,1.5194075147089999E9,1.519407515457E9
+60,3758754905959647568,479635942,1.519407674907E9,1.519407676707E9
+60,5984495362242184591,479635942,1.519407701577E9,1.519407708689E9
+60,-2296604483576504222,479635942,1.5194077104589999E9,1.519407711296E9
+60,-2003248384817158649,479635942,1.519407740486E9,1.519407742075E9
+60,-5957105046511196668,479635942,1.519407743576E9,1.519407747326E9
+60,-1265979175120690788,479635942,1.519407748136E9,1.519407749637E9
+60,-8566514391736445,479635942,1.519407852449E9,1.519407854008E9
+60,-5069380025109030493,479635942,1.519407870147E9,1.519407871047E9
+60,-6055740349077778112,479635942,1.519407872877E9,1.519407879237E9
+60,1473546834077392745,479635942,1.519407915597E9,1.519407918296E9
+60,-7284278355992301035,479635942,1.519407941606E9,1.519407945117E9
+60,-5340812375235639293,479635942,1.519407989246E9,1.519407990748E9
+60,740834529638730249,479635942,1.519408219764E9,1.519408221264E9
+60,-7071204841825428040,479635942,1.519408224385E9,1.519408227624E9
+60,7795594737268911898,479635942,1.519408253994E9,1.519408254924E9
+60,-8266519952786158444,479635942,1.519408259455E9,1.519408262185E9
+60,533367514623804258,479635942,1.519408405675E9,1.5194084075049999E9
+60,1427775550834252078,479635942,1.519408411856E9,1.519408416744E9
+60,6861773023251980982,479635942,1.519408542383E9,1.519408544843E9
+60,1972420266941434551,627460548,1.51940743701E9,1.51940744301E9
+60,8502387868844848767,627460548,1.51940752801E9,1.51940754301E9
+60,-2973429542682111206,627460548,1.51940762401E9,1.51940766701E9
+18,-3067329546929472089,479635942,1.5203612223750002E9,1.5203612232740002E9
+18,855667287532086690,479635942,1.5203612764930003E9,1.5203612772430003E9
+18,-865732098691420796,479635942,1.5203614495310001E9,1.5203614504300003E9
+18,-7573410709887323988,479635942,1.5203615999210002E9,1.5203616006420002E9
+18,7350953419317389592,479635942,1.5203616913610003E9,1.5203616922600002E9
+18,-2699540771637265069,479635942,1.5203616948110003E9,1.5203616956800003E9
+18,-1245435920705750258,479635942,1.5203620215390003E9,1.5203620360890002E9
+18,8524860812756793355,479635942,1.5203620575700002E9,1.5203621710600002E9
+18,6865513907305010252,479635942,1.5203621790390003E9,1.5203623500990002E9
+18,-5648864626730826723,479635942,1.5203623546590002E9,1.5203623708890002E9
+18,2346706427553166791,479635942,1.5203623997200003E9,1.5203625564700003E9
+18,-8469937292856013198,479635942,1.5203625603700004E9,1.5203626063890002E9
+18,-92057532065615200,479635942,1.5203626454190001E9,1.5203626471580002E9
+18,5072294032365793401,479635942,1.5203626687900002E9,1.5203626775190003E9
+18,8666457031223333664,479635942,1.5203626812690003E9,1.5203626827700002E9
+18,8691499208442153154,479635942,1.5203627061680002E9,1.5203627106690001E9
+18,-8745072969325832104,479635942,1.5203627495980003E9,1.5203627533480003E9
+18,7667558454476846407,479635942,1.5203627552680001E9,1.5203627592270002E9
+18,-2537500513366012888,479635942,1.5203628058470004E9,1.5203628121470003E9
+18,-4077576540543430347,479635942,1.5203628145170002E9,1.5203628222280002E9
+18,-1461324079301056527,479635942,1.5203628407070003E9,1.5203628642870002E9
+18,-8169034302972550824,479635942,1.5203628680970004E9,1.5203629028680003E9
+18,-1886677737163335314,479635942,1.5203629287880003E9,1.5203629507170002E9
+18,-7814509901498706674,479635942,1.5203629678770003E9,1.5203630192060003E9
+18,3930024948530012324,479635942,1.5203630330070002E9,1.5203632433370001E9
+18,805222571233747500,479635942,1.5203632608280003E9,1.5203634886460001E9
+18,-9011889575356403221,479635942,1.5203635424070003E9,1.5203635567460003E9
+18,-8346629542750559307,479635942,1.5203635594170003E9,1.5203636844270003E9
+18,8356553013206954811,479635942,1.5203636933370001E9,1.5203638430460002E9
+18,3680174806659670485,479635942,1.5203640437170002E9,1.5203640475570002E9
+18,-3393406703534878562,479635942,1.5203641263960001E9,1.5203641288560002E9
+18,-7045113756597439853,479635942,1.5203641415470002E9,1.5203641687880003E9
+18,4728653995467216718,479635942,1.5203642106970003E9,1.5203644748780003E9
+18,-3538298193453544904,479635942,1.5203644771570003E9,1.5203646470780003E9
+18,7092346266920155746,479635942,1.5203647351580002E9,1.5203647361490002E9
+18,-8510285598460868821,479635942,1.5203647400180001E9,1.5203647413980002E9
+18,6446444733357926836,479635942,1.5203649295580003E9,1.5203649313590002E9
+18,-4303032119411501748,479635942,1.5203649715590003E9,1.5203649728190002E9
+18,-659150799576613378,479635942,1.5203650543270001E9,1.5203650551100001E9
+18,-7730642124052350267,479635942,1.5203650878980002E9,1.5203650887980003E9
+18,-5222473174776703482,479635942,1.5203651835080001E9,1.5203651842890003E9
+18,-1785119036470513843,479635942,1.5203651865990002E9,1.5203651881890001E9
+18,-6718587564997163128,479635942,1.5203651896880002E9,1.5203651904380002E9
+18,-5806820180983958879,479635942,1.5203652040580003E9,1.5203652049580002E9
+32,8934657918536855286,479635942,1.5192824531660001E9,1.519282454065E9
+32,6049557701565648016,479635942,1.519282456767E9,1.519282458595E9
+32,-7078122234678541414,479635942,1.5192824992450001E9,1.519282500987E9
+32,-1084137115098581840,479635942,1.519282501856E9,1.519282503657E9
+32,2945048538349196271,479635942,1.5192825053370001E9,1.519282508005E9
+32,-6031593413107805380,479635942,1.519282594345E9,1.519282596175E9
+32,158746424866914199,479635942,1.519282620565E9,1.519282621464E9
+32,149915628509184101,479635942,1.519282763514E9,1.519282765134E9
+32,-1366924908160702712,479635942,1.519282767503E9,1.519282768254E9
+32,6209820256056437869,479635942,1.519283029162E9,1.519283032223E9
+32,-6432555845472254846,479635942,1.519283047194E9,1.519283258241E9
+32,-5506749818469597869,479635942,1.51928353193E9,1.519283537362E9
+32,-3446827409990342929,479635942,1.51928365652E9,1.51928365742E9
+32,-1930570731160562298,479635942,1.5192837516790001E9,1.519283753509E9
+32,7658598484312204229,479635942,1.5192837616990001E9,1.519283763558E9
+32,2734822677523225278,479635942,1.519283765359E9,1.519283767158E9
+32,5997939773893072129,479635942,1.519283852118E9,1.51928385395E9
+32,-8199057249826061271,479635942,1.5192838566790001E9,1.519283857579E9
+32,-5568831989031947570,479635942,1.519283911069E9,1.519283922589E9
+32,-1665332462298658611,479635942,1.519283998968E9,1.519284002568E9
+32,-683060160117814657,479635942,1.519284168677E9,1.519284170177E9
+32,7000561940650477507,479635942,1.5192842626990001E9,1.519284263536E9
+32,-970354370344721562,479635942,1.5192842671660001E9,1.519284269866E9
+32,6011199701168848071,479635942,1.519284312076E9,1.519284314326E9
+32,-1783342807546124916,479635942,1.519284379397E9,1.519284382037E9
+32,-7691360342129082254,479635942,1.5192843928370001E9,1.519284402197E9
+32,-3678550659039911234,479635942,1.5192844139880002E9,1.519284416989E9
+32,4803828546838365438,479635942,1.519284426288E9,1.519284431628E9
+32,-4693860957991396146,479635942,1.519284450287E9,1.519284492589E9
+32,6560172732529654812,479635942,1.519284570647E9,1.519284571546E9
+32,-53803932618865730,479635942,1.519284600496E9,1.519284617265E9
+32,368900461102482337,479635942,1.519284639407E9,1.519284641895E9
+32,-7251387275386436079,479635942,1.519284922124E9,1.5192849257250001E9
+32,4351028392153228315,479635942,1.5192849953860002E9,1.519284997874E9
+32,-5874576065582818741,479635942,1.519285006274E9,1.5192850163240001E9
+32,-9172987873079649263,479635942,1.519285033274E9,1.519285035103E9
+32,6793256314041164540,479635942,1.5192850605440001E9,1.519285061444E9
+32,-4534581833271138381,479635942,1.5192850641430001E9,1.5192850659750001E9
+32,-5219473623828731622,479635942,1.519285148263E9,1.519285149193E9
+32,-6914519818091662060,479635942,1.5192851546230001E9,1.519285155522E9
+32,8485520029320013134,479635942,1.519285201243E9,1.519285202868E9
+32,9000857604708628967,479635942,1.519285223292E9,1.519285224793E9
+32,-3926682035437754824,479635942,1.519285262772E9,1.519285263703E9
+32,-1853907465556525539,479635942,1.519285266491E9,1.519285268322E9
+18,-2859186189766336797,627460548,1.5203618431940002E9,1.5203618601940002E9
+18,-4249004404059317400,627460548,1.5203619141940002E9,1.5203619201940002E9
+18,7371028017153776667,627460548,1.5203619611940002E9,1.5203626311940002E9
+18,181758904111519434,627460548,1.5203628071940002E9,1.5203628111940002E9
+18,5637869652697200842,627460548,1.5203628161940002E9,1.5203628251940002E9
+18,878450484024994845,627460548,1.5203628271940002E9,1.5203629141940002E9
+18,4093885003405775926,627460548,1.5203629241940002E9,1.5203634951940002E9
+18,8007889217450764117,627460548,1.5203635191940002E9,1.5203638731940002E9
+18,-6962095228683673558,627460548,1.5203640451940002E9,1.5203641011940002E9
+18,4346470844899869974,627460548,1.5203641181940002E9,1.5203646591940002E9
+18,5241561255946406098,627460548,1.5203648651940002E9,1.5203648671940002E9
+32,-4256920046965479257,627460548,1.519282803154E9,1.519282826154E9
+32,7483473958230435926,627460548,1.519283030154E9,1.519283036154E9
+32,3005432382863606927,627460548,1.519283037154E9,1.519283392154E9
+32,-4244045975232735173,627460548,1.519283456154E9,1.519283464154E9
+32,1344934605056030447,627460548,1.519283912154E9,1.519283923154E9
+32,-5926231927670243563,627460548,1.519283932154E9,1.519283939154E9
+32,-3166164134456886287,627460548,1.519284022154E9,1.519284024154E9
+32,6174671180011375179,627460548,1.519284033154E9,1.519284048154E9
+32,2585718785484032007,627460548,1.519284395154E9,1.519284401154E9
+32,7877891411236254166,627460548,1.519284427154E9,1.519284702154E9
+32,-3811067830832172452,627460548,1.519284916154E9,1.519284933154E9
+42,-4134113363336087883,479635942,1.519370107867E9,1.519370108798E9
+42,-3388073998530469018,479635942,1.5193701769720001E9,1.519370177783E9
+42,-8463177006792599292,479635942,1.5193702627440002E9,1.5193702643040001E9
+42,-7459853409362475299,479635942,1.5193702682910001E9,1.5193702706920002E9
+42,8845189384494358332,479635942,1.519370346621E9,1.5193703475210001E9
+42,1154537846906642197,479635942,1.5193703897640002E9,1.519370390631E9
+42,835405396048213771,479635942,1.519370469801E9,1.5193704707010002E9
+42,5760182802117772295,479635942,1.519370473341E9,1.519370475141E9
+42,4511498811783002287,479635942,1.519370507811E9,1.519370508561E9
+42,3512965613532280728,479635942,1.519370510963E9,1.5193705127010002E9
+42,3232814488176612701,479635942,1.519370518611E9,1.519370520111E9
+42,-3434573738155782907,479635942,1.5193707191590002E9,1.519370720959E9
+42,-7067900348463904811,479635942,1.5193707218590002E9,1.519370730079E9
+42,4767581667772941877,479635942,1.519370793348E9,1.5193708456390002E9
+42,446790151727806869,479635942,1.519370848338E9,1.5193709949480002E9
+42,6327615009309145792,479635942,1.5193710116880002E9,1.5193710185870001E9
+42,4950633481711560151,479635942,1.5193710200870001E9,1.519371020838E9
+42,-8733481403670855711,479635942,1.519371027887E9,1.519371154487E9
+42,-740349156455077247,479635942,1.519371286516E9,1.5193712883460002E9
+42,3903280845553390002,479635942,1.519371340068E9,1.519371340845E9
+42,-5303107034098492109,479635942,1.519371448305E9,1.5193714492350001E9
+42,-2799496898824949138,479635942,1.519371524895E9,1.5193715330240002E9
+42,6264483971090658439,479635942,1.5193715928760002E9,1.519371594437E9
+42,4625783453460589368,479635942,1.5193716065870001E9,1.519371649848E9
+42,5546299446988002901,479635942,1.519371709094E9,1.5193717167480001E9
+42,-3636876291274977720,479635942,1.519372067022E9,1.519372070023E9
+42,3333784659751097057,479635942,1.5193720870010002E9,1.519372116913E9
+42,7778969535395385039,479635942,1.519372145173E9,1.519372147693E9
+42,3609659744209016781,479635942,1.519372150933E9,1.519372153996E9
+42,-1733236380887484169,479635942,1.5193723239720001E9,1.5193723339610002E9
+42,227672378979233137,479635942,1.5193723891620002E9,1.5193723932710001E9
+42,5212567250034234716,479635942,1.519372420091E9,1.5193724218920002E9
+42,-572955803921532859,479635942,1.5193724553690002E9,1.51937245627E9
+42,-2423576877274826193,479635942,1.5193724635E9,1.5193724653600001E9
+42,3265687558345174295,479635942,1.5193725375700002E9,1.519372538499E9
+42,-5092552551673860752,479635942,1.5193725439E9,1.5193725457E9
+42,-3006883077513335746,479635942,1.519372564809E9,1.51937256571E9
+42,728742179982519379,479635942,1.51937258551E9,1.5193725881790001E9
+42,1132235550827174095,479635942,1.519372595108E9,1.519372596881E9
+42,-841880160778604851,479635942,1.519372602194E9,1.519372603059E9
+42,4086894968779327609,479635942,1.5193726596690001E9,1.519372660569E9
+42,1470318877404890547,479635942,1.519372662368E9,1.5193726642280002E9
+55,-48025441560230825,479635942,1.502177640911E9,1.50217764193E9
+55,-19979916166841293,479635942,1.502177746122E9,1.502177747351E9
+55,-5795292549698599562,479635942,1.50217778275E9,1.502177783922E9
+55,7539125758013350596,479635942,1.502177835613E9,1.502177836663E9
+55,-8156276679153796233,479635942,1.502177838493E9,1.5021778397220001E9
+55,-1414055780388177597,479635942,1.502177866755E9,1.502177867986E9
+55,-2208719609941398075,479635942,1.502177910192E9,1.502177911425E9
+55,-5957308379540630610,479635942,1.502177913612E9,1.502177914932E9
+55,5704106334575510346,479635942,1.502177954202E9,1.502177955312E9
+55,8654364280051868767,479635942,1.502178023565E9,1.502178026803E9
+55,7445159219532497199,479635942,1.502178029503E9,1.502178030761E9
+55,5442119162762560274,479635942,1.502178117134E9,1.502178118243E9
+55,-8184944023954090738,479635942,1.502178315099E9,1.50217842343E9
+55,-2426180511212951508,479635942,1.50217842709E9,1.502178507433E9
+55,-6708510743915431498,479635942,1.5021785145410001E9,1.502178650054E9
+55,-1261129340543141979,479635942,1.5021790253470001E9,1.502179032935E9
+55,-3623059917793340700,479635942,1.502179069329E9,1.502179071876E9
+55,-4135913523759044851,479635942,1.502179401667E9,1.502179402778E9
+55,3434714314680258029,479635942,1.50217949548E9,1.502179499138E9
+55,-2554038418762009953,479635942,1.50217950193E9,1.502179503008E9
+55,-4471715255006922851,479635942,1.50217967518E9,1.50217967659E9
+55,-8210513061077247281,479635942,1.50217968019E9,1.502179682591E9
+55,-965390834935908422,479635942,1.502180339165E9,1.502180340276E9
+55,5281527833886871623,479635942,1.502180344955E9,1.502180347389E9
+55,-3285865106376720227,479635942,1.502180376368E9,1.502180377478E9
+55,5011614125230843231,479635942,1.502180465137E9,1.502180466247E9
+55,4702728297047247994,479635942,1.502180470806E9,1.502180472128E9
+55,-1824006112483855637,479635942,1.502180524117E9,1.502180525258E9
+55,-2683383206332124003,479635942,1.502180559248E9,1.502180561619E9
+42,-7912935343280471697,627460548,1.519370783173E9,1.519370999173E9
+42,-851685137523786953,627460548,1.519371004173E9,1.519371231173E9
+42,-5350538308380134869,627460548,1.519371505173E9,1.519371509173E9
+42,3379928035265915344,627460548,1.519371594173E9,1.519371826173E9
+42,1538379527211864972,627460548,1.519371905173E9,1.519371918173E9
+42,875889519431894876,627460548,1.519371942173E9,1.519372004173E9
+42,6205166047096595600,627460548,1.519372019173E9,1.519372029173E9
+42,-3738117419962829203,627460548,1.519372090173E9,1.519372116173E9
+42,2490413319664980551,627460548,1.519372279173E9,1.519372374173E9
+55,5487529334640880100,627460548,1.502178315786E9,1.502178752786E9
+55,-7307326885282895040,627460548,1.502178755786E9,1.502178836786E9
+55,7009414100077454645,627460548,1.502178842786E9,1.502178853786E9
+55,-920859862867254028,627460548,1.502178885786E9,1.502178898786E9
+55,-8583451963099113910,627460548,1.502179038786E9,1.502179039786E9
+55,8815549264414514294,627460548,1.502179070786E9,1.502179184786E9
+55,2188868774875393159,627460548,1.502179206786E9,1.502179236786E9
+55,-4544591479733560606,627460548,1.502179842786E9,1.502179866786E9
+67,-3815805161973396274,479635942,1.501189930017E9,1.5011899309169998E9
+67,8730435977513892906,479635942,1.5011899533769999E9,1.5011899556269999E9
+67,-133607035920218108,479635942,1.501189971468E9,1.501189972218E9
+67,-7395884725851964126,479635942,1.5011899936699998E9,1.5011899944169998E9
+67,608255642897355567,479635942,1.50119004977E9,1.501190051508E9
+67,5022999938454482429,479635942,1.5011902553E9,1.501190256201E9
+67,-8471217710021983369,479635942,1.501190853291E9,1.5011908570419998E9
+67,8642749783684377471,479635942,1.5011908663109999E9,1.5011908672099998E9
+67,8730754906652675946,479635942,1.501190869971E9,1.501190871771E9
+67,-364163425457667417,479635942,1.50119087357E9,1.50119087627E9
+67,-7021725377944245298,479635942,1.50119087807E9,1.5011908808E9
+67,-8168724823461864768,479635942,1.5011908826009998E9,1.50119089259E9
+67,-5674261482481767898,479635942,1.50119089529E9,1.50119089802E9
+67,7915533170076608903,479635942,1.501190923937E9,1.5011909266069999E9
+67,-5125506919487958647,479635942,1.501190929967E9,1.501190942297E9
+67,8117487510307080933,479635942,1.5011909651859999E9,1.501190965937E9
+67,4159376833233017526,479635942,1.5011909893669999E9,1.501190991227E9
+67,5299496890732364785,479635942,1.50119099303E9,1.5011909948279998E9
+67,395668964739582154,479635942,1.501191004728E9,1.5011910156169999E9
+67,8373950708020039731,479635942,1.501191020118E9,1.501191021948E9
+67,4924485034467824004,479635942,1.501191037368E9,1.501191040007E9
+69,4727516487544984000,479635942,1.499677049372E9,1.4996770503019998E9
+69,6424569148418921661,479635942,1.499677275965E9,1.4996772768639998E9
+69,-2253760923462333686,479635942,1.499677285896E9,1.499677287695E9
+69,6908173154531340168,479635942,1.499677334189E9,1.499677335093E9
+69,3653240058018382574,479635942,1.499677380899E9,1.499677383149E9
+69,6366518650006339483,479635942,1.49967741402E9,1.49967741951E9
+69,186780110437232471,479635942,1.499677516921E9,1.499677518511E9
+69,-7929904322239886738,479635942,1.499677605842E9,1.499677608393E9
+69,-3261042088359356438,479635942,1.499677618832E9,1.499677619672E9
+69,7032152434587761662,479635942,1.4996776810519998E9,1.4996776820119998E9
+69,-5680440152726278459,479635942,1.499677687472E9,1.4996776883709998E9
+69,3187804350783910137,479635942,1.4996777459129999E9,1.4996777475939999E9
+69,-8896716654490465770,479635942,1.49967783177E9,1.499677832669E9
+69,7006643973680009199,479635942,1.499677973434E9,1.499677974303E9
+69,-6693344335721958609,479635942,1.4996781064859998E9,1.499678108313E9
+59,3350481714876024846,479635942,1.501095007345E9,1.501095008095E9
+59,-263242049190437257,479635942,1.501095026755E9,1.501095027655E9
+59,7200825810760123433,479635942,1.501095068366E9,1.501095070165E9
+59,4748604066403151000,479635942,1.501095089638E9,1.501095091376E9
+59,8582984287519172746,479635942,1.501095113277E9,1.501095114779E9
+59,8185678414630450983,479635942,1.501095168851E9,1.501095169751E9
+59,8229345466977713381,479635942,1.50109517245E9,1.50109517515E9
+59,-9101456639018303657,479635942,1.50109517599E9,1.501095177731E9
+59,2729289401462033310,479635942,1.501095241601E9,1.501095242531E9
+59,5429075264786362749,479635942,1.501095244331E9,1.501095245231E9
+59,-1080616164905810183,479635942,1.501095268781E9,1.501095270552E9
+59,2564572583354547501,479635942,1.501095272322E9,1.501095278711E9
+59,-3308528293073106383,479635942,1.501095284713E9,1.501095287142E9
+59,4115231572194915299,479635942,1.501095305921E9,1.501095308232E9
+59,5532530606205423292,479635942,1.501095311321E9,1.501095313632E9
+59,-846367447846750194,479635942,1.50109556563E9,1.501095567129E9
+59,-3864831769372273617,479635942,1.501095592119E9,1.50109561204E9
+59,5639042423344374593,479635942,1.5010956138700001E9,1.501095622929E9
+59,2377848345732066714,479635942,1.50109566865E9,1.5010957101100001E9
+59,7974752384273336244,479635942,1.501095746547E9,1.50109574905E9
+59,-2466782466495866825,479635942,1.501095751524E9,1.501095764315E9
+59,6573997764258830319,479635942,1.501095836006E9,1.501095844588E9
+59,4966886326858506422,479635942,1.501095870243E9,1.501095889715E9
+59,-2767740011135421697,479635942,1.501095914051E9,1.5010959302220001E9
+59,-7086667094388192316,479635942,1.501095934182E9,1.501096046846E9
+59,5328559055791554716,479635942,1.501096058312E9,1.501096075887E9
+59,-589989017151786518,479635942,1.501096140153E9,1.501096213248E9
+59,7419762243858291660,479635942,1.501096215476E9,1.501096217292E9
+59,9099795995296812798,479635942,1.501096219438E9,1.501096221499E9
+59,-5550926322996191170,479635942,1.501096269267E9,1.501096277269E9
+59,-284655148077937779,479635942,1.501096364721E9,1.5010963668370001E9
+59,-6893556550553645614,479635942,1.5010965953600001E9,1.501096606333E9
+59,-8389659655529651788,479635942,1.5010966273700001E9,1.501096629763E9
+59,410262070962712932,479635942,1.501096631826E9,1.501096633557E9
+59,-4606593424084476743,479635942,1.501096639498E9,1.501096641561E9
+59,-318946654971018645,479635942,1.50109670921E9,1.501096717544E9
+59,-5015318753063608449,479635942,1.50109672167E9,1.501096731652E9
+59,-4138423948683528377,479635942,1.50109674155E9,1.501096745511E9
+59,7925092161796871972,479635942,1.501096778429E9,1.501096780327E9
+59,2359896765514720549,479635942,1.501096784701E9,1.501096788412E9
+59,8365071322529314144,479635942,1.501096807057E9,1.501096811567E9
+59,480457813073038484,479635942,1.501096815721E9,1.501096823805E9
+59,3072485737982897898,479635942,1.501096825838E9,1.5010968319720001E9
+59,8810057974074351431,479635942,1.501096927892E9,1.501096951982E9
+59,-7531354630400008639,479635942,1.501096957261E9,1.501097003022E9
+59,4815116084446622697,479635942,1.501097021696E9,1.5010970273600001E9
+59,6980557923375114361,479635942,1.501097044635E9,1.501097174652E9
+59,-6131887073244088573,479635942,1.501097203034E9,1.501097211037E9
+59,-6701476241207502825,479635942,1.50109721747E9,1.501097288308E9
+59,3741774792690559203,479635942,1.501097305055E9,1.501097307366E9
+59,5307304353214543868,479635942,1.501097344491E9,1.501097351993E9
+59,-8625391522545896299,479635942,1.5010973781990001E9,1.501097385818E9
+59,1574440280973894149,479635942,1.501097399595E9,1.50109742022E9
+59,3794601120187185550,479635942,1.50109745248E9,1.501097491419E9
+59,-151541347759527609,479635942,1.501097510558E9,1.501097512704E9
+59,3521607041605394326,479635942,1.501097518645E9,1.5010975272220001E9
+59,-7187053144326125550,479635942,1.501097748379E9,1.501097751486E9
+59,-815688429997384126,479635942,1.501097775494E9,1.501097778492E9
+59,-7108337468662161626,479635942,1.501097822131E9,1.50109782609E9
+59,8520246161409867533,479635942,1.501097857496E9,1.501097861897E9
+59,-376454326251101423,479635942,1.501097863795E9,1.50109786792E9
+59,6510323870441083165,479635942,1.501097871964E9,1.501097873943E9
+59,8257877450335423678,479635942,1.501097897045E9,1.501097901073E9
+59,5135009814020022804,479635942,1.501097931693E9,1.501097937636E9
+59,7583242018559711280,479635942,1.501097943461E9,1.501097945305E9
+59,3223441157440427288,479635942,1.501097978716E9,1.50109798309E9
+59,6462731995811607988,479635942,1.501097999342E9,1.501098001652E9
+59,-2080898615186177042,479635942,1.501098047881E9,1.501098050634E9
+59,1912011379066757021,479635942,1.501098067875E9,1.501098070764E9
+59,-8159078852503161638,479635942,1.501098104067E9,1.501098106484E9
+59,-1951913650752808896,479635942,1.501098122323E9,1.501098124719E9
+59,3110665973802534117,479635942,1.501098137752E9,1.50109814325E9
+59,-8245497611270561643,479635942,1.501098269391E9,1.501098270214E9
+59,-2488168393221053635,627460548,1.501095322184E9,1.501095386184E9
+59,-1236381197693815490,627460548,1.501095596184E9,1.501095597184E9
+59,1075521169233739938,627460548,1.501095608184E9,1.501095633184E9
+59,7257814405411876514,627460548,1.501095637184E9,1.501095713184E9
+59,-2580682828761725792,627460548,1.501095729184E9,1.501095777184E9
+59,-8605515118457787891,627460548,1.501095790184E9,1.501095804184E9
+59,8337734177692190825,627460548,1.501095820184E9,1.501096102184E9
+59,3962756607804556038,627460548,1.501096117184E9,1.501096214184E9
+59,-6746492169296944402,627460548,1.501096742184E9,1.501096744184E9
+59,1271352433827133997,627460548,1.501096816184E9,1.501096822184E9
+59,-7207200223368718107,627460548,1.501096826184E9,1.501096854184E9
+59,6228546177887084689,627460548,1.501096888184E9,1.501096914184E9
+59,3000546082855196111,627460548,1.501096920184E9,1.501097012184E9
+59,3897751311146723253,627460548,1.501097014184E9,1.501097183184E9
+59,-1867956002051251955,627460548,1.501097199184E9,1.501097316184E9
+59,5895677467021490133,627460548,1.501097321184E9,1.501097366184E9
+59,-2329144350355433791,627460548,1.501097368184E9,1.501097496184E9
+59,4471501886336186196,627460548,1.501097511184E9,1.501097561184E9
+59,-1958495126941261161,627460548,1.501097586184E9,1.501097592184E9
+59,-3053390146284596499,627460548,1.501097638184E9,1.501097657184E9
+26,-7951946553818325760,479635942,1.521825959717E9,1.521825960887E9
+26,3357184544023295870,479635942,1.521826054367E9,1.521826055658E9
+26,-4399631736425259600,479635942,1.521826128736E9,1.521826129997E9
+26,6022110397615505219,479635942,1.521826136657E9,1.521826138007E9
+26,8305759309897957289,479635942,1.521826158016E9,1.521826162007E9
+26,7706842616310080497,479635942,1.521826163417E9,1.5218261647680001E9
+26,-5162050327556312502,479635942,1.5218262007970002E9,1.5218262020570002E9
+26,-919501539067090173,479635942,1.5218262033470001E9,1.5218262058960001E9
+26,5486337863640098508,479635942,1.5218262071560001E9,1.521826208446E9
+26,5060304467944278326,479635942,1.521826227767E9,1.521826230437E9
+26,506129992933519210,479635942,1.521826246187E9,1.521826247566E9
+26,8817547663100190470,479635942,1.521826250147E9,1.521826252846E9
+26,-5872473440852740808,479635942,1.521826255486E9,1.521826263467E9
+26,1732642658931322388,479635942,1.521826272496E9,1.521826278887E9
+26,-9185125183389841228,479635942,1.5218263447350001E9,1.5218263461160002E9
+26,6999236563657057971,479635942,1.5218263913860002E9,1.521826394117E9
+26,3750575424071875728,479635942,1.5218264003560002E9,1.521826401676E9
+26,-5824662355972113106,479635942,1.521826486845E9,1.5218264881660001E9
+26,5466232292523319184,479635942,1.5218265031660001E9,1.521826506346E9
+26,-4476733568267304747,479635942,1.521826508625E9,1.521826511206E9
+26,2565221287677766301,479635942,1.5218266852350001E9,1.5218266873660002E9
+26,-5670014974571691424,479635942,1.521826704765E9,1.5218267060540001E9
+26,-2165418173409783981,479635942,1.5218267272640002E9,1.5218267295440001E9
+26,-8457613487091296301,479635942,1.5218270235140002E9,1.521827026815E9
+26,3106702097391505352,479635942,1.521827035844E9,1.521827037045E9
+26,7585905348253573254,479635942,1.521827203035E9,1.521827207713E9
+26,6776830795303022736,479635942,1.5218272152740002E9,1.521827217525E9
+26,4953735811770734320,479635942,1.521827513744E9,1.521827518244E9
+26,-8328271299637690925,479635942,1.5218275930340002E9,1.5218275937840002E9
+26,35614618283757807,479635942,1.521827731605E9,1.521827737095E9
+26,-2979046840457590769,479635942,1.5218280616330001E9,1.521828063194E9
+26,-2728367000091771848,479635942,1.521828074023E9,1.5218280747740002E9
+26,-7514454981204438683,479635942,1.521828079423E9,1.521828081734E9
+26,3626426901206878019,479635942,1.521828084973E9,1.5218280880340002E9
+26,-3221552733544669704,479635942,1.5218281942940001E9,1.5218281966330001E9
+26,5357573905117106417,479635942,1.521828264673E9,1.5218282683630002E9
+26,-7678168463197119856,479635942,1.5218284523830001E9,1.521828492163E9
+26,-5868569999655719067,479635942,1.521828673634E9,1.521828676334E9
+26,7783873759049040400,479635942,1.521829133505E9,1.5218291407050002E9
+26,-1164031311247914982,479635942,1.5218291497640002E9,1.5218291542640002E9
+26,7592441470054466012,479635942,1.521829221316E9,1.5218292349650002E9
+26,-5297745363352181993,479635942,1.5218292861460001E9,1.521829287885E9
+26,2723091489567818587,479635942,1.521829289565E9,1.521829296046E9
+26,4868516268449791634,479635942,1.521829360065E9,1.5218293609650002E9
+26,527393528765485641,479635942,1.521829362765E9,1.521829364565E9
+26,6339666651420998039,479635942,1.5218295007050002E9,1.521829501605E9
+26,-4425295826530714774,627460548,1.521826451374E9,1.521826456374E9
+26,-8430066309921190473,627460548,1.521826468374E9,1.521826473374E9
+26,-6669008984552126746,627460548,1.521826803374E9,1.521826807374E9
+26,106157166059108654,627460548,1.521826814374E9,1.521826855374E9
+26,2840894071843762229,627460548,1.521826866374E9,1.521826937374E9
+26,-5342290114834841782,627460548,1.521826952374E9,1.521826956374E9
+26,6657864152520354542,627460548,1.521828113374E9,1.521828125374E9
+26,3323477762541739233,627460548,1.521828195374E9,1.521828228374E9
+26,6623122784089319800,627460548,1.521828277374E9,1.521828279374E9
+26,-1777794211665605813,627460548,1.521828354374E9,1.521828370374E9
+26,-5616297213604956334,627460548,1.521828377374E9,1.521828383374E9
+26,2638863292813237142,627460548,1.521828387374E9,1.521828388374E9
+26,-4802923805251574890,627460548,1.521828406374E9,1.521828408374E9
+26,-3162815773302240229,627460548,1.521828423374E9,1.521828533374E9
+26,833812741686708854,627460548,1.521828786374E9,1.521828792374E9
+26,-7651870039932161040,627460548,1.521828811374E9,1.521828832374E9
+47,-7394957341913254038,479635942,1.4997981351660001E9,1.4997981360970001E9
+47,-8551242594078874640,479635942,1.4997981584450002E9,1.4997981593760002E9
+47,-6377163397148163621,479635942,1.4997981837970002E9,1.49979818548E9
+47,-6295132995662633695,479635942,1.499798267588E9,1.4997982684880002E9
+47,-5318597204528146860,479635942,1.499798270318E9,1.4997982730170002E9
+47,-6254466032864658640,479635942,1.499798273917E9,1.499798276647E9
+47,7677123060856263821,479635942,1.4997982977380002E9,1.499798298638E9
+47,4454235247555596930,479635942,1.49979830044E9,1.499798301338E9
+47,1614145214393745704,479635942,1.49979833968E9,1.49979834193E9
+47,5907756074770712058,479635942,1.4997983426790001E9,1.499798345079E9
+47,6381969761403398165,479635942,1.499798345829E9,1.499798347329E9
+47,-6299726893066242148,479635942,1.4997983649090002E9,1.499798367608E9
+47,4251153438197704520,479635942,1.499798369408E9,1.4997983730080001E9
+47,-1938336866210303686,479635942,1.499798381168E9,1.49979838297E9
+47,-7460474352123181197,479635942,1.499798401028E9,1.499798405588E9
+47,-9032752971001076911,479635942,1.4997984064880002E9,1.4997984119490001E9
+47,4491545254968270050,479635942,1.499798421818E9,1.499798426348E9
+47,5096849667214515801,479635942,1.499798432469E9,1.499798450512E9
+47,-7786563792363918080,479635942,1.499798953296E9,1.499798982118E9
+47,3577437384975642652,479635942,1.4997990938240001E9,1.499799104384E9
+47,8136309372218302977,479635942,1.4997991540530002E9,1.499799170549E9
+47,2698621795661466618,479635942,1.4997994034150002E9,1.499799431052E9
+47,-5291859461102665736,479635942,1.4997994932580001E9,1.499799495404E9
+47,-1367132018171691623,479635942,1.499799526259E9,1.4997995475440001E9
+47,8250323024989648373,479635942,1.4997995494420002E9,1.499799567427E9
+47,628723615451037838,479635942,1.499799575482E9,1.499799583844E9
+47,-206585888389049898,479635942,1.499799600647E9,1.4997996318300002E9
+47,-1048239980980874445,479635942,1.499799674568E9,1.4997997416660001E9
+47,1778960464130521539,479635942,1.4997997483760002E9,1.499799750026E9
+47,1050206711866613923,479635942,1.4997997592110002E9,1.499799780799E9
+47,8475581809093468859,479635942,1.499799818831E9,1.4997998897540002E9
+47,-5620283494443865738,479635942,1.499800003187E9,1.4998000081100001E9
+47,-1677214701488082796,479635942,1.499800053898E9,1.4998000564280002E9
+47,7214425382359845503,479635942,1.4998002808030002E9,1.4998002824520001E9
+47,-162083872625507495,479635942,1.499800321585E9,1.4998003232350001E9
+47,-7523120664886112291,479635942,1.499800415522E9,1.4998004180240002E9
+47,6552979296264424107,479635942,1.4998004296590002E9,1.4998004304810002E9
+47,8192841680952521862,479635942,1.499800476489E9,1.4998004831720002E9
+47,-7680457163052518179,479635942,1.499800483997E9,1.499800484822E9
+74,-7100940967587284080,479635942,1.5214874243070002E9,1.521487425207E9
+74,1277248096502282592,479635942,1.521487464147E9,1.5214874650770001E9
+74,830598226874001399,479635942,1.521487499907E9,1.521487500657E9
+74,-8374182940471393581,479635942,1.5214875163890002E9,1.521487518249E9
+74,-2751237974560907456,479635942,1.521487650095E9,1.5214876514750001E9
+74,-7683165260674316323,479635942,1.5214877409950001E9,1.5214877424050002E9
+74,-3695300300739008445,479635942,1.521487755305E9,1.521487756624E9
+74,3211629094050434423,479635942,1.5214878762340002E9,1.5214878776130002E9
+74,1888656962774903147,479635942,1.5214879817140002E9,1.521487982884E9
+74,-9015144092354152096,479635942,1.5214879932930002E9,1.521487994523E9
+74,-1323593585012316836,479635942,1.521488068084E9,1.521488069344E9
+74,1636015664682159993,479635942,1.5214881690930002E9,1.521488170203E9
+74,-4445037065289837715,479635942,1.521488262394E9,1.5214882635930002E9
+74,2635207561236243114,479635942,1.5214882730730002E9,1.5214882743330002E9
+74,-1635881411443778117,479635942,1.521488290503E9,1.5214882942240002E9
+74,3404172992977330892,479635942,1.521488297523E9,1.5214882987540002E9
+74,3423727598100793694,479635942,1.521488319513E9,1.5214883240140002E9
+74,-6479681697317270682,479635942,1.521488327433E9,1.5214883286030002E9
+74,5192896085133715711,479635942,1.521488350834E9,1.521488351914E9
+74,8425091437886945886,479635942,1.5214883851220002E9,1.5214883875530002E9
+74,-3891276535790995220,479635942,1.521488431923E9,1.521488435163E9
+74,681535430259442243,479635942,1.5214884490240002E9,1.5214884515730002E9
+74,-4593482852994321444,479635942,1.5214884538830001E9,1.5214884551430001E9
+74,1917257299477074749,479635942,1.521488481453E9,1.5214884826230001E9
+74,1778026896245754764,479635942,1.5214884999320002E9,1.5214885031720002E9
+74,-9150537040027592262,479635942,1.5214886090430002E9,1.5214886108430002E9
+47,2143952575760754579,627460548,1.499798422275E9,1.499798467275E9
+47,-1102366506098408593,627460548,1.499798477275E9,1.499798478275E9
+47,1070604568328400281,627460548,1.499798949275E9,1.499799041275E9
+47,-5519527282280551264,627460548,1.499799048275E9,1.499799128275E9
+47,156298530050084862,627460548,1.499799154275E9,1.499799182275E9
+47,-6817764927555924154,627460548,1.499799212275E9,1.499799220275E9
+47,-7623388730705804666,627460548,1.499799360275E9,1.499799480275E9
+47,-5707692991057582718,627460548,1.499799482275E9,1.499799910275E9
+62,233465819468974483,479635942,1.522087738648E9,1.5220877395180001E9
+62,-1665235544586458329,479635942,1.522087933588E9,1.522087934519E9
+62,5650126530783012121,479635942,1.522087941838E9,1.5220879427380002E9
+62,-3808777790712200263,479635942,1.5220881100900002E9,1.52208811201E9
+62,-6387801822708595721,479635942,1.5220881861090002E9,1.522088259371E9
+62,3567274854459386327,479635942,1.5220882674090002E9,1.52208850618E9
+62,-3217050604814709529,479635942,1.5220885375900002E9,1.5220885628200002E9
+62,-4286501893762232022,479635942,1.522088864921E9,1.5220888672310002E9
+62,-7104670778578012518,479635942,1.522088957351E9,1.5220889596620002E9
+62,7165201073217535671,479635942,1.522088960742E9,1.522088961822E9
+62,8400627279522988925,479635942,1.5220889809910002E9,1.522088982042E9
+62,-8511373289042049777,479635942,1.5220890079320002E9,1.5220890102110002E9
+62,3102605401485095728,479635942,1.5220890232020001E9,1.5220890254520001E9
+62,8081285124269112968,479635942,1.522089134381E9,1.522089135312E9
+62,-7434812294372272992,479635942,1.5220891373820002E9,1.522089142032E9
+62,8695563931371837955,479635942,1.522089159433E9,1.522089164683E9
+62,-4923828897717141335,479635942,1.5220892875630002E9,1.522089288673E9
+62,-4992727507577582518,627460548,1.52208814715E9,1.52208857515E9
+31,7921209197648174965,479635942,1.5204927300280004E9,1.5204927309280002E9
+31,2192460630149830418,479635942,1.5204928264170003E9,1.5204928274970002E9
+31,4718873921805288997,479635942,1.5204928786470003E9,1.5204928796970003E9
+31,-2122507024878303514,479635942,1.5204928807170002E9,1.5204928825780003E9
+31,212670736209963259,479635942,1.5204928844080002E9,1.5204928862970002E9
+31,6374557663447655357,479635942,1.5204929134470003E9,1.5204929143480003E9
+31,-9098709584164636914,479635942,1.5204929162380002E9,1.5204929189970002E9
+31,7641140155503107961,479635942,1.5204929997580001E9,1.5204930007180002E9
+31,4117373660847629830,479635942,1.5204930026360002E9,1.5204930036270003E9
+31,-514622113686124186,479635942,1.5204930206970003E9,1.5204930215680003E9
+31,383719709637353600,479635942,1.5204930234580002E9,1.5204930243580003E9
+31,-4158489170791971768,479635942,1.5204930261580002E9,1.5204930271480002E9
+31,864024801799181361,479635942,1.5204931281870003E9,1.5204931300180001E9
+31,8844950406650908608,479635942,1.5204931316970003E9,1.5204931334670002E9
+31,-2269205392455893346,479635942,1.5204931585170002E9,1.5204931595370002E9
+31,-2114760810447394921,479635942,1.5204931941270003E9,1.5204931989570003E9
+31,-4430806205437969470,479635942,1.5204933497960002E9,1.5204933507560003E9
+31,-8089879190856624777,479635942,1.5204933535160003E9,1.5204933554660003E9
+31,-3466292861467391247,479635942,1.5204933581960003E9,1.5204933590960002E9
+31,643253390856272901,479635942,1.5204934448960001E9,1.5204934468470001E9
+31,8389055379227883246,479635942,1.5204934578560002E9,1.5204936887360003E9
+31,3821752786606544586,479635942,1.5204936986370003E9,1.5204936995670002E9
+31,5502783586907073770,479635942,1.5204939322150002E9,1.5204939331760004E9
+31,92039323423905987,479635942,1.5204939370150003E9,1.5204939417260003E9
+31,4240750638010388468,479635942,1.5204946767280002E9,1.5204946776870003E9
+31,4931898673170617537,479635942,1.5204947732380002E9,1.5204947742570002E9
+31,6863282184143612984,479635942,1.5204947873080003E9,1.5204947892880003E9
+31,-1653625742806097825,479635942,1.5204948617980003E9,1.5204948626390002E9
+31,5483340186273732609,479635942,1.5204948782980003E9,1.5204948813880002E9
+31,-2937538317926627262,479635942,1.5204950760900002E9,1.5204950872790003E9
+31,-8217686576126044503,479635942,1.5204953445590003E9,1.5204954473700001E9
+31,9091122659934996368,479635942,1.5204954581700003E9,1.5204954955210001E9
+31,-8052039232102359049,479635942,1.5204956271610003E9,1.5204956290210001E9
+31,3202452113092810722,479635942,1.5204958540820003E9,1.5204958631420002E9
+31,6784409005471784859,479635942,1.5204958816230001E9,1.5204958846510003E9
+31,-8152664018392036595,479635942,1.5204959239520001E9,1.5204959249120002E9
+31,3929441459210376731,479635942,1.5204959267430003E9,1.5204959287220001E9
+31,7618703669974760907,479635942,1.5204960112230003E9,1.5204960122430003E9
+31,3121793074855035377,479635942,1.5204960266420002E9,1.5204960285630002E9
+31,-7889253677765381533,479635942,1.5204960314420002E9,1.5204960381020002E9
+31,5593788379240306940,479635942,1.5204960589230003E9,1.5204960616240003E9
+31,-4869409006441440319,479635942,1.5204960670230002E9,1.5204960696330001E9
+31,2584397914297307428,479635942,1.5204960792620003E9,1.5204960808510003E9
+31,6808007830433701211,479635942,1.5204961608890002E9,1.5204961618480003E9
+31,-6374212182751913759,627460548,1.5204934468980002E9,1.5204937308980002E9
+31,-4197511859593274763,627460548,1.5204937378980002E9,1.5204937448980002E9
+31,6209070167463257299,627460548,1.5204937608980002E9,1.5204937888980002E9
+31,-4875834047199230818,627460548,1.5204950858980002E9,1.5204950998980002E9
+31,-4320597367016870533,627460548,1.5204951638980002E9,1.5204952078980002E9
+31,-8017881740808565020,627460548,1.5204952088980002E9,1.5204952608980002E9
+31,3299433999620492931,627460548,1.5204952678980002E9,1.5204952738980002E9
+31,-276408725174261182,627460548,1.5204952828980002E9,1.5204955178980002E9
+31,4100004042559332621,627460548,1.5204955498980002E9,1.5204955688980002E9
+31,1953503504700232897,627460548,1.5204957348980002E9,1.5204958228980002E9
+56,7286031052324599941,479635942,1.502217249289E9,1.50221725079E9
+56,-7995058155203102065,479635942,1.502217262217E9,1.502217264017E9
+56,125212586940396863,479635942,1.502217841065E9,1.502217844065E9
+56,6141018012366946690,479635942,1.502217857926E9,1.502217859486E9
+56,-4108158772573806331,479635942,1.502217871395E9,1.5022178758660002E9
+56,-4875838995510048156,479635942,1.5022182645970001E9,1.502218272367E9
+56,-8776940733478037134,479635942,1.502218273175E9,1.5022183511160002E9
+56,4598698361605545852,479635942,1.502218397436E9,1.502218401247E9
+56,919305116641171376,479635942,1.502218431457E9,1.5022185647740002E9
+56,2209496077421409303,479635942,1.502218566604E9,1.502218612114E9
+56,256068826457431573,479635942,1.502218615504E9,1.502218706345E9
+56,-8979504765109427979,479635942,1.5022187180740001E9,1.5022187480140002E9
+56,-5062807074700727668,479635942,1.5022187723760002E9,1.502218784915E9
+56,-7516225137999424141,479635942,1.502219112993E9,1.502219133333E9
+56,3378231168590052764,479635942,1.502219144194E9,1.5022191449450002E9
+56,955956651639658129,479635942,1.502219169036E9,1.502219175333E9
+56,-8542388884598226679,479635942,1.502219186194E9,1.5022191879950001E9
+56,-2883769545959296089,479635942,1.502219201614E9,1.502219202513E9
+56,-6208194100392552112,479635942,1.502219293504E9,1.502219294434E9
+56,-2674432477455902243,479635942,1.5022192962640002E9,1.502219298994E9
+56,-6383873380722153309,479635942,1.502219299894E9,1.5022193007940001E9
+56,-273869646409472698,479635942,1.5022193170240002E9,1.502219318885E9
+56,5250843682173965751,479635942,1.502219321644E9,1.502219323444E9
+56,763449791328314297,479635942,1.5022193660870001E9,1.502219366917E9
+56,-2570583434181382529,627460548,1.502217835236E9,1.502217849236E9
+56,-4244945049449255719,627460548,1.502217851236E9,1.502218015236E9
+56,760471664002184084,627460548,1.502218051236E9,1.502218059236E9
+56,-6097291144490623940,627460548,1.502218068236E9,1.502218105236E9
+56,-2122505610086791721,627460548,1.502218244236E9,1.502218767236E9
+73,4698746056602067206,479635942,1.519628148179E9,1.51962814908E9
+73,-6528280314230347635,479635942,1.519628279257E9,1.5196282801569998E9
+73,-810164440262703953,479635942,1.51962832648E9,1.519628327228E9
+73,7974722580389124481,479635942,1.519628457366E9,1.519628458115E9
+73,2499672124474546301,479635942,1.519628558047E9,1.519628558885E9
+73,5762312710716643448,479635942,1.519628560385E9,1.519628561945E9
+73,-4800123670424522558,479635942,1.519628564195E9,1.5196285664459999E9
+73,-7103174223550522392,479635942,1.5196285695349998E9,1.519628571036E9
+73,6913005204637502732,479635942,1.519628577185E9,1.5196285787749999E9
+73,-3453847138190940414,479635942,1.5196285920049999E9,1.5196286653539999E9
+73,5664278828011553720,479635942,1.519628667964E9,1.5196286965839999E9
+73,759890121094263352,479635942,1.519629005796E9,1.519629006665E9
+73,2184777283105237031,479635942,1.519629009847E9,1.519629010597E9
+73,5406120560287454292,479635942,1.519629027184E9,1.5196290309359999E9
+73,5909391876979633603,479635942,1.519629036274E9,1.519629037836E9
+73,-872640667733504566,479635942,1.5196290461139998E9,1.5196290468639998E9
+73,2229309029957584289,627460548,1.519628578501E9,1.519628579501E9
+73,413457750693962833,627460548,1.519628580501E9,1.519628702501E9
+6,8141306054365381644,479635942,1.501486198958E9,1.501486199859E9
+6,9065018149763811076,479635942,1.501486329099E9,1.501486329999E9
+6,-1190819671160564535,479635942,1.5014863327E9,1.501486333599E9
+6,5675679548293145415,479635942,1.501486367741E9,1.501486368491E9
+6,-392481383116102237,479635942,1.501486402631E9,1.5014864033799999E9
+6,6466481676484687062,479635942,1.501486405632E9,1.50148640734E9
+6,7314023513402070129,479635942,1.5014864091399999E9,1.50148641007E9
+6,3172890637915615512,479635942,1.50148643281E9,1.50148643371E9
+6,2489143921017646798,479635942,1.501486534991E9,1.501486535891E9
+6,-5965547874463461661,479635942,1.501486656612E9,1.501486657513E9
+6,-5677556238851400321,479635942,1.501486689162E9,1.501486690062E9
+6,-5488803820677031148,479635942,1.501486699122E9,1.501486700023E9
+6,-2253785796264139997,479635942,1.501486708182E9,1.501486709052E9
+6,2429417179846311158,479635942,1.501486946234E9,1.5014869633339999E9
+6,-7394894551209760989,479635942,1.501486969393E9,1.501487004343E9
+6,-5422988830067458338,479635942,1.5014870127749999E9,1.501487180625E9
+6,7264317951308118050,479635942,1.501487185155E9,1.501487397527E9
+6,5252442164430851011,479635942,1.501487662389E9,1.501487664188E9
+6,3571910543164118603,479635942,1.501487768064E9,1.501487769595E9
+6,-2838242289859826192,479635942,1.501487774214E9,1.501487777184E9
+6,-355372327093400807,479635942,1.501487810305E9,1.501487811985E9
+6,-4547088266076729406,479635942,1.501487865444E9,1.501487866945E9
+6,-4229849076597703889,479635942,1.501488067912E9,1.5014880695019999E9
+6,3874307978585448372,479635942,1.501488072532E9,1.501488073942E9
+6,-3053811312933356931,479635942,1.501488082822E9,1.5014880845609999E9
+6,-3488278371421040274,479635942,1.501488313523E9,1.5014883144819999E9
+6,-666309752389973768,479635942,1.501488842991E9,1.501488843891E9
+6,-2112707463042219799,479635942,1.501488845692E9,1.5014888474919999E9
+6,6498580997500453772,479635942,1.501488857391E9,1.5014888609919999E9
+6,-2962833679997753002,479635942,1.501488885682E9,1.501488888082E9
+6,3053897756119763326,479635942,1.501488890632E9,1.501488892432E9
+6,357680524675104482,479635942,1.501488902632E9,1.501488905872E9
+6,-4633237063126712372,479635942,1.501488909684E9,1.501488910434E9
+6,6858043685481218482,479635942,1.5014889171829998E9,1.501488940824E9
+6,8661377745411371706,479635942,1.501488951503E9,1.501488974122E9
+6,692562988186632292,479635942,1.501488995842E9,1.501489009523E9
+6,8454345605335880828,479635942,1.501489016573E9,1.5014890209229999E9
+6,-1116070765837062941,479635942,1.501489024943E9,1.5014890333439999E9
+6,-9013787633869098893,479635942,1.501489075374E9,1.501489083533E9
+6,1575198716093482053,479635942,1.501489103417E9,1.5014891266959999E9
+6,4103956260681959250,479635942,1.5014891339259999E9,1.501489146136E9
+6,5320552298985772173,479635942,1.501489152136E9,1.50148916573E9
+6,3117657923915150890,479635942,1.501489198097E9,1.501489215257E9
+6,-2681847254649723544,479635942,1.501489227917E9,1.501489524438E9
+6,-7772234340436546031,479635942,1.50148970186E9,1.50148970369E9
+6,6984113525933631377,479635942,1.501489709119E9,1.5014899605809999E9
+6,4861397373548277002,479635942,1.501490031291E9,1.501490059822E9
+6,-8127007269034585581,479635942,1.501490077942E9,1.501490098822E9
+6,-6758332066512509935,479635942,1.501490139863E9,1.501490171149E9
+6,285230501814966796,479635942,1.5014901772389998E9,1.501490213899E9
+6,-166111266919970594,479635942,1.501490221128E9,1.5014903246599998E9
+6,-4100706704446654045,479635942,1.501490620699E9,1.501490621598E9
+6,-4605411364910003765,479635942,1.501490648628E9,1.50149064944E9
+6,-3866689808461687774,479635942,1.5014907025379999E9,1.501490703439E9
+6,-8903984103475669235,479635942,1.501490737879E9,1.501490738779E9
+6,3475379203004552842,479635942,1.50149077775E9,1.501490778501E9
+6,6771927619391553641,479635942,1.50149084699E9,1.501490848819E9
+6,3555111570529820649,479635942,1.501490857972E9,1.5014908598309999E9
+6,-1511442427265712942,479635942,1.501490867779E9,1.50149086961E9
+6,-7695862010491739525,479635942,1.501490948272E9,1.5014909501E9
+6,-1279058410837997392,479635942,1.5014909708899999E9,1.501490971792E9
+78,-9011361941223280730,479635942,1.5194087128470001E9,1.5194087135960002E9
+78,597828038336683425,479635942,1.5194087225660002E9,1.519408723466E9
+78,3564213200788206699,479635942,1.5194087315660002E9,1.519408732466E9
+78,-3251410450130275703,479635942,1.519408734296E9,1.519408736095E9
+78,-4827707693981813972,479635942,1.519408736996E9,1.519408743296E9
+78,6842396962829650299,479635942,1.519408780227E9,1.519408781124E9
+78,-1691601457848570275,479635942,1.5194088643140001E9,1.5194088670140002E9
+78,6742106736870529029,479635942,1.519408867914E9,1.519408868844E9
+78,667682855295149288,479635942,1.5194088805740001E9,1.5194088912540002E9
+78,1136228721632838783,479635942,1.5194090755730002E9,1.5194090773730001E9
+78,4702241742953304014,479635942,1.5194090810630002E9,1.519409081963E9
+78,6331248500942309874,479635942,1.5194090901520002E9,1.5194090919520001E9
+78,1687352770564940314,479635942,1.5194091073720002E9,1.519409110101E9
+78,-8175958674706710905,479635942,1.519409111002E9,1.519409112832E9
+78,35316266755353470,479635942,1.519409141934E9,1.519409142805E9
+78,5003653884098335467,479635942,1.5194092052640002E9,1.5194092070930002E9
+78,-4370452520119706380,479635942,1.519409240604E9,1.5194092415040002E9
+78,-5935907186492763735,479635942,1.519409313414E9,1.519409322473E9
+78,-1008782424325960707,479635942,1.519409337923E9,1.5194093388230002E9
+78,-9001833240160249965,479635942,1.5194093550830002E9,1.5194093559840002E9
+78,-103088198835953130,479635942,1.519409375513E9,1.5194093773130002E9
+78,-8729031188867460064,479635942,1.519409382325E9,1.519409383163E9
+6,9210188441730540451,627460548,1.501486947017E9,1.501487401017E9
+6,-343490424433488126,627460548,1.501487564017E9,1.501487587017E9
+6,7501229949551273215,627460548,1.501487664017E9,1.501487698017E9
+6,7023726278771727989,627460548,1.501488904017E9,1.501489043017E9
+6,1013831176693935851,627460548,1.501489046017E9,1.501489090017E9
+6,3459515849223643364,627460548,1.501489096017E9,1.501489537017E9
+6,-8281019678260577843,627460548,1.501489600017E9,1.501489630017E9
+6,3997983914223212962,627460548,1.501489680017E9,1.501490019017E9
+6,1724053087047461813,627460548,1.501490020017E9,1.501490064017E9
+6,-2746570370054937424,627460548,1.501490069017E9,1.501490350017E9
+6,5989748000031786107,627460548,1.501490367017E9,1.501490433017E9
+78,-3644405701563227601,627460548,1.519408888288E9,1.519408957288E9
+78,-4617602559320993340,627460548,1.519409049288E9,1.519409074288E9
+78,5088943403831986973,627460548,1.519409320288E9,1.519409325288E9
+19,6028848355768087671,479635942,1.519237302516E9,1.519237303324E9
+19,-5051221762931044720,479635942,1.519237314781E9,1.519237315591E9
+19,-5799211519740820623,479635942,1.519237333201E9,1.519237333952E9
+19,1146966086063006305,479635942,1.51923734022E9,1.51923734115E9
+19,6213942601054942341,479635942,1.51923737301E9,1.51923737391E9
+19,-2832685776793357555,479635942,1.51923737577E9,1.51923737667E9
+19,-351100257979666537,479635942,1.519237419721E9,1.51923742056E9
+19,-3285374475791400302,479635942,1.51923742281E9,1.51923742362E9
+19,-5518538856266410847,479635942,1.519237640337E9,1.5192376411529999E9
+19,8946134929553692674,479635942,1.519237647298E9,1.519237648798E9
+19,-4780427220932175267,479635942,1.519237680388E9,1.519237681889E9
+19,2566508364047548458,479635942,1.519237737717E9,1.519237738616E9
+19,295652401805827072,479635942,1.519237767508E9,1.519237768347E9
+19,-7096441995564781476,479635942,1.519237769846E9,1.519237771346E9
+19,-1165846100694611862,479635942,1.519237772846E9,1.519237774347E9
+19,-5162730470194357797,479635942,1.519237880075E9,1.519237882473E9
+19,1444482675075310923,479635942,1.519237901015E9,1.519237901765E9
+19,-3383998895735685034,479635942,1.519237909473E9,1.519237911753E9
+19,1402333567164908550,479635942,1.519237921833E9,1.519237925433E9
+19,6903295436849698658,479635942,1.519238084792E9,1.519238085692E9
+19,-5261750990350492111,479635942,1.519238269442E9,1.519238270192E9
+19,-2128280113966390728,479635942,1.51923836685E9,1.519238367661E9
+19,4627887667438372412,479635942,1.519238449018E9,1.519238449891E9
+19,-2173480526031768290,479635942,1.519238454509E9,1.519238457149E9
+19,5239742934098383464,479635942,1.519238477369E9,1.51923848052E9
+19,-8379047387576512746,479635942,1.519238519098E9,1.519238519938E9
+19,-8722982905601966219,479635942,1.519238547088E9,1.519238548018E9
+19,1318424142803243899,479635942,1.519238592418E9,1.519238594908E9
+19,-5438412136153030075,479635942,1.519238618369E9,1.519238622871E9
+19,2224270384256314041,479635942,1.519238633611E9,1.5192386391E9
+19,7723718644442095952,479635942,1.519238640001E9,1.519238961178E9
+19,2253797242199800066,479635942,1.519238992829E9,1.519238993728E9
+19,2049656652565732174,479635942,1.519239006448E9,1.519239201535E9
+19,2471398529509504796,479635942,1.519239468568E9,1.519239469468E9
+19,-8854767250839283109,479635942,1.519239495718E9,1.519239507447E9
+19,-6026046154867841437,479635942,1.519239536157E9,1.519239544917E9
+19,625738784370450155,479635942,1.519239549836E9,1.519239550587E9
+19,4203402333149566388,479635942,1.519239574888E9,1.519239582476E9
+19,-6499400431325262116,479635942,1.519239619556E9,1.519239629577E9
+19,-6132583495483120942,479635942,1.519239717594E9,1.519239719395E9
+19,-1490272467932344596,479635942,1.519239733825E9,1.519239765445E9
+19,-1656999970416748654,479635942,1.519239768864E9,1.519239792204E9
+19,-8520777268265145208,479635942,1.519239822084E9,1.519239890604E9
+19,1301007286925811684,479635942,1.519239913763E9,1.519239923603E9
+19,9199763139909967687,479635942,1.519239938393E9,1.519239982072E9
+19,6111448393157487946,479635942,1.519240000494E9,1.519240090043E9
+19,7290323716025233615,479635942,1.519240103662E9,1.519240221806E9
+19,5033702741749148784,479635942,1.519240227117E9,1.519240249256E9
+19,6308521703074585928,479635942,1.519240279526E9,1.519240329865E9
+19,5194732260996399601,479635942,1.519240464231E9,1.5192404777E9
+19,-7585094175769183574,479635942,1.519240618159E9,1.519240619062E9
+19,-8066897750035161654,479635942,1.519240648939E9,1.519240651939E9
+19,-4674137383446323737,479635942,1.519240816998E9,1.519240817898E9
+19,7875866929159281894,479635942,1.51924089701E9,1.51924089776E9
+48,4790704554089194909,479635942,1.502305287918E9,1.502305288729E9
+48,6212635237664191872,479635942,1.502305398986E9,1.502305400789E9
+48,8399905263604661135,479635942,1.5023054033890002E9,1.502305405069E9
+48,-3112090855975957995,479635942,1.502305406838E9,1.502305408519E9
+48,-2318641406649556887,479635942,1.502305429519E9,1.5023054318890002E9
+48,-9190940846721587034,479635942,1.502305471459E9,1.502305472359E9
+48,-3930128046966389082,479635942,1.502305475119E9,1.502305476019E9
+48,-3941886940628148436,479635942,1.5023055156790001E9,1.50230551748E9
+48,-4107281696769170658,479635942,1.502305534638E9,1.502305536286E9
+48,-6043645318129225075,479635942,1.502306144763E9,1.502306147265E9
+48,3244416218092439856,479635942,1.502306157165E9,1.502306163875E9
+48,6735411992212048803,479635942,1.502306180458E9,1.502306239474E9
+48,559154123080819694,479635942,1.5023062420310001E9,1.502306288754E9
+48,-8066955475557413138,479635942,1.50230653504E9,1.502306576786E9
+48,-5885406572919342504,479635942,1.5023065850900002E9,1.502306627756E9
+48,-162065876543376615,479635942,1.5023066372410002E9,1.502307029142E9
+48,8933180889007863511,479635942,1.502307325484E9,1.502307327134E9
+48,-1626320486275157895,479635942,1.502307363546E9,1.502307365194E9
+48,7208890045493190891,479635942,1.502307366872E9,1.50230736855E9
+48,-1797910056301520170,479635942,1.5023073868660002E9,1.50230739352E9
+48,4483959113167145503,479635942,1.502307401879E9,1.502307404382E9
+48,-2505442633121216769,479635942,1.502307471259E9,1.502307472964E9
+48,-5102107702007524451,479635942,1.502307484598E9,1.502307485421E9
+48,6450116291038204849,479635942,1.5023074995010002E9,1.502307500326E9
+48,7131347818050049092,479635942,1.5023075842020001E9,1.502307585027E9
+48,-9000065078716718490,479635942,1.502307587558E9,1.502307588382E9
+48,3271519467239371525,479635942,1.5023076101620002E9,1.502307610987E9
+48,2524730172130113058,479635942,1.50230761349E9,1.50230761514E9
+48,-7526758433824278794,479635942,1.502307620997E9,1.5023076218500001E9
+48,8487159644906055738,479635942,1.502307658453E9,1.502307659277E9
+79,4313940813830172671,479635942,1.5245133684290001E9,1.524513369269E9
+79,-1574674127851301219,479635942,1.5245133851390002E9,1.524513386069E9
+79,3885380142608347575,479635942,1.524513413118E9,1.524513415068E9
+79,6082956534531698008,479635942,1.524513420767E9,1.524513422657E9
+79,-529538213786321798,479635942,1.524513554598E9,1.5245135557970002E9
+79,-5233143905061212353,479635942,1.524513690227E9,1.524513691397E9
+79,-6749936408276765452,479635942,1.524513698506E9,1.524513699588E9
+79,4195997416838376465,479635942,1.524513702948E9,1.524513710957E9
+79,1395027177858466145,479635942,1.524513906708E9,1.524513907877E9
+79,4329072157274374587,479635942,1.524513961278E9,1.524513963708E9
+79,9104944897137045253,479635942,1.5245139715970001E9,1.524513974958E9
+79,-7937042204976751146,479635942,1.524514008017E9,1.524514010358E9
+79,-5359913606374734079,479635942,1.524514011767E9,1.524514025928E9
+79,1470764958350755913,479635942,1.5245140493270001E9,1.5245140507680001E9
+79,-8816464584560565024,479635942,1.524514061027E9,1.524514062437E9
+79,7031661784234920102,479635942,1.524514065046E9,1.524514066457E9
+79,7630475052421829992,479635942,1.5245141082480001E9,1.524514111458E9
+79,4459805701445750788,479635942,1.524514146947E9,1.5245141482970002E9
+79,1018930148492736803,479635942,1.524514150937E9,1.524514153157E9
+79,-3012724119970807671,479635942,1.5245141591890001E9,1.524514161618E9
+79,-6091802513688418518,479635942,1.524514185857E9,1.524514188198E9
+79,391085927997469060,479635942,1.524514201398E9,1.524514202537E9
+79,4026496079609271954,479635942,1.524514270997E9,1.524514271987E9
+19,-4547642813019348592,627460548,1.519238634431E9,1.519238968431E9
+19,5325245226839771994,627460548,1.519238983431E9,1.519239214431E9
+19,973218997822966150,627460548,1.519239620431E9,1.519239633431E9
+19,628300341692610971,627460548,1.519239667431E9,1.519239802431E9
+19,3014920702555012859,627460548,1.519239811431E9,1.519239896431E9
+19,-2621136301447011161,627460548,1.519239899431E9,1.519240352431E9
+19,7975499753437858593,627460548,1.519240363431E9,1.519240365431E9
+19,-3189134808378560287,627460548,1.519240424431E9,1.519240452431E9
+19,8600203470179719851,627460548,1.519240472431E9,1.519240602431E9
+19,3770848338282036760,627460548,1.519240676431E9,1.519240682431E9
+48,-3834210982116192238,627460548,1.502306145236E9,1.502306308236E9
+48,-6350312161582800364,627460548,1.502306351236E9,1.502306387236E9
+48,-8840098135087323120,627460548,1.502306388236E9,1.502306390236E9
+48,3427549274498046150,627460548,1.502306529236E9,1.502307069236E9
+28,1040427654212385429,1201859611,1.521789223098E9,1.521792477381E9
+29,6556782270497265025,1201859611,1.519801439524E9,1.519804840842E9
+33,-6056317502954263346,1201859611,1.501924860614E9,1.501929162822E9
+30,-4509912318961261997,1201859611,1.521702888744E9,1.5217061028439999E9
+59,4526313175717176312,1201859611,1.501094993184E9,1.501098434005E9
+69,5297223516241122749,1201859611,1.499677025521E9,1.4996782686339998E9
+76,1604686477683752455,1201859611,1.519679166069E9,1.519680393604E9
+11,-5926799641916171529,1201859611,1.500055626984E9,1.500061931105E9
+49,-8089365359392949673,1201859611,1.518804886421E9,1.518807260224E9
+65,7663622432945194351,1201859611,1.499761469866E9,1.499763686776E9
+9,7816983797231647252,1201859611,1.500880944319E9,1.500885608209E9
+32,-5227152884695581212,1201859611,1.519282265154E9,1.519285345753E9
+14,2702796889550818087,1201859611,1.501611902218E9,1.50161679806E9
+17,1920899555557485333,1201859611,1.499238990444E9,1.499242935794E9
+63,-1533397159073443657,1201859611,1.521534483703E9,1.521536109483E9
+35,-5091309530965523951,1201859611,1.519975368235E9,1.519978307823E9
+3,3286614479096879038,1201859611,1.5012633016019998E9,1.5012697935319998E9
+78,-6903129361326617772,1201859611,1.519408682288E9,1.519409544651E9
+45,4031524100617037594,1201859611,1.502352322421E9,1.502355083889E9
+81,-3039017337926577879,1201859611,1.500020387605E9,1.500021118887E9
+4,3413059497672580978,1201859611,1.521442341766E9,1.5214483285210001E9
+19,107421361993047462,1201859611,1.519237249431E9,1.519241038728E9
+23,1667169207552354980,1201859611,1.502091434075E9,1.502096130444E9
+61,-6074962741691029584,1201859611,1.502391863903E9,1.5023941796430001E9
+6,5601914189163822671,1201859611,1.501486121017E9,1.501491202042E9
+80,-8519964041684473957,1201859611,1.519154607775E9,1.51915581276E9
+24,7437298780708400991,1201859611,1.502263499684E9,1.502266791831E9
+46,-6047084444995931845,1201859611,1.518867417239E9,1.518869908982E9
+38,8006099507947146869,1201859611,1.522048625131E9,1.522051483289E9
+13,-1944483825207215539,1201859611,1.521659356202E9,1.521664064724E9
+20,3772502504273691038,1201859611,1.49984414566E9,1.499847846184E9
+39,1077981989772730875,1201859611,1.501872789635E9,1.501875581284E9
+58,1432421368744511818,1201859611,1.519404898117E9,1.51940681295E9
+70,382192189422054743,1201859611,1.501576972043E9,1.50157821128E9
+2,7921822167645060769,1201859611,1.524466721245E9,1.5244732439699998E9
+12,621520344043114077,1201859611,1.518937687489E9,1.5189428261990001E9
+42,-4744849287861989057,1201859611,1.519370038173E9,1.519372747634E9
+48,2089968467991060156,1201859611,1.502305247236E9,1.5023078405010002E9
+26,8588031391057209555,1201859611,1.521825843374E9,1.5218295814350002E9
+67,-2034556668057200826,1201859611,1.501189878386E9,1.501191326419E9
+73,4038311380879008479,1201859611,1.519628083501E9,1.5196290640549998E9
+22,1702604805711203895,1201859611,1.499780494611E9,1.499784016244E9
+36,-4482700421175446431,1201859611,1.5198367056390002E9,1.5198396267280002E9
+43,-5059295208201598568,1201859611,1.518591995006E9,1.5185946256460001E9
+66,6831185462073925864,1201859611,1.519719456367E9,1.5197209466030002E9
+40,8695525273439978119,1201859611,1.499886014457E9,1.4998891463E9
+75,5110935768283636594,1201859611,1.499974132752E9,1.4999754008790002E9
+27,-3534261932286880816,1201859611,1.4994121920089998E9,1.4994154300719998E9
+55,-7312033798960504347,1201859611,1.502177604786E9,1.502180609284E9
+79,2185843916018887178,1201859611,1.524513306088E9,1.524514433208E9
+50,4815143990666075450,1201859611,1.521744131314E9,1.5217470064229999E9
+7,-7434725489921472031,1201859611,1.499367268538E9,1.4993722404810002E9
+47,3287404548624903503,1201859611,1.499798046275E9,1.499800963408E9
+71,5850834331784891980,1201859611,1.5012309768E9,1.501232161654E9
+51,-8939688255858788198,1201859611,1.502132725389E9,1.502136639393E9
+56,-1655462257548164667,1201859611,1.502217242236E9,1.5022195563930001E9
+53,-8118175245509467954,1201859611,1.518629438223E9,1.518632591188E9
+74,-2523754915779770005,1201859611,1.521487415459E9,1.521488712033E9
+77,8292766365109962602,1201859611,1.501530768092E9,1.501532001474E9
+8,-6047692687641618500,1201859611,1.519629355513E9,1.519634174599E9
+34,-5864392879735263686,1201859611,1.520447895177E9,1.520451442709E9
+25,-5797038905919203719,1201859611,1.50165899742E9,1.501662280055E9
+31,27763026888340976,1201859611,1.5204926438980002E9,1.5204962007880003E9
+62,2045378005814092654,1201859611,1.52208759315E9,1.5220894282940001E9
+15,8620646447947530699,1201859611,1.50105479387E9,1.5010588258569999E9
+54,3329842588530743954,1201859611,1.501789556646E9,1.5017919091299999E9
+5,3570466563200151918,1201859611,1.499929241381E9,1.499934640063E9
+1,-928762887014768240,1201859611,1.5223419101579998E9,1.5223499941319997E9
+10,799052670452099784,1201859611,1.519924977475E9,1.519929815329E9
+37,-3015666225396535697,1201859611,1.520406325296E9,1.520409425879E9
+68,-4052763906460934747,1201859611,1.519201828118E9,1.519203136445E9
+57,8939056937015267775,1201859611,1.5192422426949997E9,1.5192442915019996E9
+64,1978039981849347272,1201859611,1.499330278298E9,1.4993318242410002E9
+16,-5204063793680029428,1201859611,1.519755823446E9,1.519760198065E9
+18,7002524279394511806,1201859611,1.5203611051940002E9,1.5203654402490003E9
+60,1782504894750799124,1201859611,1.51940682201E9,1.519408675632E9
+72,-4361505612007328068,1201859611,1.501145006228E9,1.5011461628769999E9
+44,819277571598892579,1201859611,1.499282239418E9,1.499285561123E9
+21,2586283319979737404,1201859611,1.518876947209E9,1.5188804750140002E9
+41,8437372657831499562,1201859611,1.519887529198E9,1.5198902487549999E9
+52,1261300119102051947,1201859611,1.499455466949E9,1.4994579101969998E9
diff --git a/tests/e2e/expected/pushdown/histogram_fact.csv b/tests/e2e/expected/pushdown/histogram_fact.csv
new file mode 100644
index 00000000..7b2aa217
--- /dev/null
+++ b/tests/e2e/expected/pushdown/histogram_fact.csv
@@ -0,0 +1,811 @@
+container_id,visual_id,event_id,bin_id,hist_value,lower_bound,upper_bound,bin_name
+58,1650601208,479635942,0,0.23099994659423828,0.0,20.0,0.0-20.0
+58,1650601208,479635942,1,17.36300015449524,20.0,40.0,20.0-40.0
+58,1650601208,479635942,2,7.54699969291687,40.0,60.0,40.0-60.0
+58,1650601208,479635942,3,0.31100010871887207,60.0,80.0,60.0-80.0
+58,1650601208,479635942,4,2.447999954223633,80.0,100.0,80.0-100.0
+58,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+58,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+58,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+58,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+58,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+21,1650601208,479635942,0,7.312999486923218,0.0,20.0,0.0-20.0
+21,1650601208,479635942,1,39.688000202178955,20.0,40.0,20.0-40.0
+21,1650601208,479635942,2,198.11000037193298,40.0,60.0,40.0-60.0
+21,1650601208,479635942,3,70.43300008773804,60.0,80.0,60.0-80.0
+21,1650601208,479635942,4,8.516999959945679,80.0,100.0,80.0-100.0
+21,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+21,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+21,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+21,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+21,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+71,1650601208,479635942,0,6.758000135421753,0.0,20.0,0.0-20.0
+71,1650601208,479635942,1,19.253000259399414,20.0,40.0,20.0-40.0
+71,1650601208,479635942,2,9.220000267028809,40.0,60.0,40.0-60.0
+71,1650601208,479635942,3,11.587000131607056,60.0,80.0,60.0-80.0
+71,1650601208,479635942,4,3.30299973487854,80.0,100.0,80.0-100.0
+71,1650601208,479635942,5,0.7760000228881836,100.0,120.0,100.0-120.0
+71,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+71,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+71,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+71,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+2,1650601208,479635942,0,5.552999973297119,0.0,20.0,0.0-20.0
+2,1650601208,479635942,1,39.76800012588501,20.0,40.0,20.0-40.0
+2,1650601208,479635942,2,15.734000205993652,40.0,60.0,40.0-60.0
+2,1650601208,479635942,3,18.09100031852722,60.0,80.0,60.0-80.0
+2,1650601208,479635942,4,53.58200001716614,80.0,100.0,80.0-100.0
+2,1650601208,479635942,5,102.90800023078918,100.0,120.0,100.0-120.0
+2,1650601208,479635942,6,850.6170001029968,120.0,140.0,120.0-140.0
+2,1650601208,479635942,7,143.0,140.0,160.0,140.0-160.0
+2,1650601208,479635942,8,34.0,160.0,180.0,160.0-180.0
+2,1650601208,479635942,9,15.0,180.0,200.0,180.0-200.0
+75,1650601208,479635942,0,11.133000135421753,0.0,20.0,0.0-20.0
+75,1650601208,479635942,1,15.557999849319458,20.0,40.0,20.0-40.0
+75,1650601208,479635942,2,16.977999925613403,40.0,60.0,40.0-60.0
+75,1650601208,479635942,3,0.9600000381469727,60.0,80.0,60.0-80.0
+75,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+75,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+75,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+75,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+75,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+75,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+13,1650601208,479635942,0,3.309000253677368,0.0,20.0,0.0-20.0
+13,1650601208,479635942,1,30.49999976158142,20.0,40.0,20.0-40.0
+13,1650601208,479635942,2,15.688999891281128,40.0,60.0,40.0-60.0
+13,1650601208,479635942,3,10.263000011444092,60.0,80.0,60.0-80.0
+13,1650601208,479635942,4,0.26399993896484375,80.0,100.0,80.0-100.0
+13,1650601208,479635942,5,68.17199993133545,100.0,120.0,100.0-120.0
+13,1650601208,479635942,6,1340.5370004177094,120.0,140.0,120.0-140.0
+13,1650601208,479635942,7,195.0,140.0,160.0,140.0-160.0
+13,1650601208,479635942,8,88.0,160.0,180.0,160.0-180.0
+13,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+65,1650601208,479635942,0,5.704999685287476,0.0,20.0,0.0-20.0
+65,1650601208,479635942,1,20.86199951171875,20.0,40.0,20.0-40.0
+65,1650601208,479635942,2,16.904999494552612,40.0,60.0,40.0-60.0
+65,1650601208,479635942,3,5.159999847412109,60.0,80.0,60.0-80.0
+65,1650601208,479635942,4,5.632999897003174,80.0,100.0,80.0-100.0
+65,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+65,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+65,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+65,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+65,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+5,1650601208,479635942,0,0.0,0.0,20.0,0.0-20.0
+5,1650601208,479635942,1,58.30999946594238,20.0,40.0,20.0-40.0
+5,1650601208,479635942,2,17.987999200820923,40.0,60.0,40.0-60.0
+5,1650601208,479635942,3,12.355000257492065,60.0,80.0,60.0-80.0
+5,1650601208,479635942,4,12.01200008392334,80.0,100.0,80.0-100.0
+5,1650601208,479635942,5,68.18899941444397,100.0,120.0,100.0-120.0
+5,1650601208,479635942,6,836.606999874115,120.0,140.0,120.0-140.0
+5,1650601208,479635942,7,138.0,140.0,160.0,140.0-160.0
+5,1650601208,479635942,8,115.0,160.0,180.0,160.0-180.0
+5,1650601208,479635942,9,85.0,180.0,200.0,180.0-200.0
+45,1650601208,479635942,0,0.75,0.0,20.0,0.0-20.0
+45,1650601208,479635942,1,48.87399911880493,20.0,40.0,20.0-40.0
+45,1650601208,479635942,2,48.92199969291687,40.0,60.0,40.0-60.0
+45,1650601208,479635942,3,1.7999999523162842,60.0,80.0,60.0-80.0
+45,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+45,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+45,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+45,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+45,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+45,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+7,1650601208,479635942,0,1.5900001525878906,0.0,20.0,0.0-20.0
+7,1650601208,479635942,1,4.076000213623047,20.0,40.0,20.0-40.0
+7,1650601208,479635942,2,17.454000234603882,40.0,60.0,40.0-60.0
+7,1650601208,479635942,3,3.0820000171661377,60.0,80.0,60.0-80.0
+7,1650601208,479635942,4,4.050999879837036,80.0,100.0,80.0-100.0
+7,1650601208,479635942,5,72.08199954032898,100.0,120.0,100.0-120.0
+7,1650601208,479635942,6,1025.4879999160767,120.0,140.0,120.0-140.0
+7,1650601208,479635942,7,256.0,140.0,160.0,140.0-160.0
+7,1650601208,479635942,8,23.0,160.0,180.0,160.0-180.0
+7,1650601208,479635942,9,7.0,180.0,200.0,180.0-200.0
+41,1650601208,479635942,0,6.815000057220459,0.0,20.0,0.0-20.0
+41,1650601208,479635942,1,22.197999954223633,20.0,40.0,20.0-40.0
+41,1650601208,479635942,2,23.501999616622925,40.0,60.0,40.0-60.0
+41,1650601208,479635942,3,7.4629998207092285,60.0,80.0,60.0-80.0
+41,1650601208,479635942,4,3.1670000553131104,80.0,100.0,80.0-100.0
+41,1650601208,479635942,5,17.20400047302246,100.0,120.0,100.0-120.0
+41,1650601208,479635942,6,254.64599990844727,120.0,140.0,120.0-140.0
+41,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+41,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+41,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+72,1650601208,479635942,0,3.739000082015991,0.0,20.0,0.0-20.0
+72,1650601208,479635942,1,15.936999559402466,20.0,40.0,20.0-40.0
+72,1650601208,479635942,2,29.878000020980835,40.0,60.0,40.0-60.0
+72,1650601208,479635942,3,7.961999893188477,60.0,80.0,60.0-80.0
+72,1650601208,479635942,4,3.7730000019073486,80.0,100.0,80.0-100.0
+72,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+72,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+72,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+72,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+72,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+63,1650601208,479635942,0,2.9769999980926514,0.0,20.0,0.0-20.0
+63,1650601208,479635942,1,5.459000110626221,20.0,40.0,20.0-40.0
+63,1650601208,479635942,2,17.04200005531311,40.0,60.0,40.0-60.0
+63,1650601208,479635942,3,12.180999994277954,60.0,80.0,60.0-80.0
+63,1650601208,479635942,4,0.8999998569488525,80.0,100.0,80.0-100.0
+63,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+63,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+63,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+63,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+63,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+64,1650601208,479635942,0,2.2839996814727783,0.0,20.0,0.0-20.0
+64,1650601208,479635942,1,8.54800009727478,20.0,40.0,20.0-40.0
+64,1650601208,479635942,2,10.621000289916992,40.0,60.0,40.0-60.0
+64,1650601208,479635942,3,5.288000106811523,60.0,80.0,60.0-80.0
+64,1650601208,479635942,4,4.488000154495239,80.0,100.0,80.0-100.0
+64,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+64,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+64,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+64,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+64,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+66,1650601208,479635942,0,4.3500001430511475,0.0,20.0,0.0-20.0
+66,1650601208,479635942,1,12.407999753952026,20.0,40.0,20.0-40.0
+66,1650601208,479635942,2,10.442999839782715,40.0,60.0,40.0-60.0
+66,1650601208,479635942,3,6.661999940872192,60.0,80.0,60.0-80.0
+66,1650601208,479635942,4,4.061000108718872,80.0,100.0,80.0-100.0
+66,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+66,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+66,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+66,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+66,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+1,1650601208,479635942,0,17.065999746322632,0.0,20.0,0.0-20.0
+1,1650601208,479635942,1,27.56600069999695,20.0,40.0,20.0-40.0
+1,1650601208,479635942,2,35.28600001335144,40.0,60.0,40.0-60.0
+1,1650601208,479635942,3,20.41099977493286,60.0,80.0,60.0-80.0
+1,1650601208,479635942,4,33.45799970626831,80.0,100.0,80.0-100.0
+1,1650601208,479635942,5,49.44500017166138,100.0,120.0,100.0-120.0
+1,1650601208,479635942,6,732.9229998588562,120.0,140.0,120.0-140.0
+1,1650601208,479635942,7,88.0,140.0,160.0,140.0-160.0
+1,1650601208,479635942,8,64.0,160.0,180.0,160.0-180.0
+1,1650601208,479635942,9,50.0,180.0,200.0,180.0-200.0
+15,1650601208,479635942,0,15.610000133514404,0.0,20.0,0.0-20.0
+15,1650601208,479635942,1,38.424999475479126,20.0,40.0,20.0-40.0
+15,1650601208,479635942,2,30.39199995994568,40.0,60.0,40.0-60.0
+15,1650601208,479635942,3,12.213000059127808,60.0,80.0,60.0-80.0
+15,1650601208,479635942,4,9.17799997329712,80.0,100.0,80.0-100.0
+15,1650601208,479635942,5,14.23799991607666,100.0,120.0,100.0-120.0
+15,1650601208,479635942,6,149.02300000190735,120.0,140.0,120.0-140.0
+15,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+15,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+15,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+77,1650601208,479635942,0,3.374000072479248,0.0,20.0,0.0-20.0
+77,1650601208,479635942,1,10.215000629425049,20.0,40.0,20.0-40.0
+77,1650601208,479635942,2,10.065999984741211,40.0,60.0,40.0-60.0
+77,1650601208,479635942,3,5.822000026702881,60.0,80.0,60.0-80.0
+77,1650601208,479635942,4,6.808000087738037,80.0,100.0,80.0-100.0
+77,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+77,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+77,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+77,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+77,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+3,1650601208,479635942,0,12.914000034332275,0.0,20.0,0.0-20.0
+3,1650601208,479635942,1,23.630000591278076,20.0,40.0,20.0-40.0
+3,1650601208,479635942,2,25.780999660491943,40.0,60.0,40.0-60.0
+3,1650601208,479635942,3,9.639999866485596,60.0,80.0,60.0-80.0
+3,1650601208,479635942,4,10.754999876022339,80.0,100.0,80.0-100.0
+3,1650601208,479635942,5,47.10400056838989,100.0,120.0,100.0-120.0
+3,1650601208,479635942,6,770.1849999427795,120.0,140.0,120.0-140.0
+3,1650601208,479635942,7,133.0,140.0,160.0,140.0-160.0
+3,1650601208,479635942,8,64.0,160.0,180.0,160.0-180.0
+3,1650601208,479635942,9,9.0,180.0,200.0,180.0-200.0
+37,1650601208,479635942,0,8.831999778747559,0.0,20.0,0.0-20.0
+37,1650601208,479635942,1,31.012999773025513,20.0,40.0,20.0-40.0
+37,1650601208,479635942,2,26.063000202178955,40.0,60.0,40.0-60.0
+37,1650601208,479635942,3,12.044999837875366,60.0,80.0,60.0-80.0
+37,1650601208,479635942,4,6.138999938964844,80.0,100.0,80.0-100.0
+37,1650601208,479635942,5,17.294999837875366,100.0,120.0,100.0-120.0
+37,1650601208,479635942,6,476.46999979019165,120.0,140.0,120.0-140.0
+37,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+37,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+37,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+27,1650601208,479635942,0,6.152000188827515,0.0,20.0,0.0-20.0
+27,1650601208,479635942,1,5.763999938964844,20.0,40.0,20.0-40.0
+27,1650601208,479635942,2,14.504999876022339,40.0,60.0,40.0-60.0
+27,1650601208,479635942,3,4.658000230789185,60.0,80.0,60.0-80.0
+27,1650601208,479635942,4,2.986999988555908,80.0,100.0,80.0-100.0
+27,1650601208,479635942,5,28.151000261306763,100.0,120.0,100.0-120.0
+27,1650601208,479635942,6,291.1559998989105,120.0,140.0,120.0-140.0
+27,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+27,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+27,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+57,1650601208,479635942,0,0.748999834060669,0.0,20.0,0.0-20.0
+57,1650601208,479635942,1,7.174000024795532,20.0,40.0,20.0-40.0
+57,1650601208,479635942,2,9.626000165939331,40.0,60.0,40.0-60.0
+57,1650601208,479635942,3,8.250999927520752,60.0,80.0,60.0-80.0
+57,1650601208,479635942,4,16.186000108718872,80.0,100.0,80.0-100.0
+57,1650601208,479635942,5,20.353999614715576,100.0,120.0,100.0-120.0
+57,1650601208,479635942,6,494.6379997730255,120.0,140.0,120.0-140.0
+57,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+57,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+57,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+24,1650601208,479635942,0,7.280000448226929,0.0,20.0,0.0-20.0
+24,1650601208,479635942,1,27.253000020980835,20.0,40.0,20.0-40.0
+24,1650601208,479635942,2,30.388999938964844,40.0,60.0,40.0-60.0
+24,1650601208,479635942,3,6.756000280380249,60.0,80.0,60.0-80.0
+24,1650601208,479635942,4,5.3409998416900635,80.0,100.0,80.0-100.0
+24,1650601208,479635942,5,9.391999959945679,100.0,120.0,100.0-120.0
+24,1650601208,479635942,6,296.80799984931946,120.0,140.0,120.0-140.0
+24,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+24,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+24,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+52,1650601208,479635942,0,1.6480000019073486,0.0,20.0,0.0-20.0
+52,1650601208,479635942,1,13.079999923706055,20.0,40.0,20.0-40.0
+52,1650601208,479635942,2,17.981000185012817,40.0,60.0,40.0-60.0
+52,1650601208,479635942,3,5.105000019073486,60.0,80.0,60.0-80.0
+52,1650601208,479635942,4,5.0,80.0,100.0,80.0-100.0
+52,1650601208,479635942,5,14.773999691009521,100.0,120.0,100.0-120.0
+52,1650601208,479635942,6,269.58200001716614,120.0,140.0,120.0-140.0
+52,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+52,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+52,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+28,1650601208,479635942,0,10.282000303268433,0.0,20.0,0.0-20.0
+28,1650601208,479635942,1,35.23700022697449,20.0,40.0,20.0-40.0
+28,1650601208,479635942,2,31.35200023651123,40.0,60.0,40.0-60.0
+28,1650601208,479635942,3,6.669000148773193,60.0,80.0,60.0-80.0
+28,1650601208,479635942,4,5.691999912261963,80.0,100.0,80.0-100.0
+28,1650601208,479635942,5,24.655999660491943,100.0,120.0,100.0-120.0
+28,1650601208,479635942,6,262.2939999103546,120.0,140.0,120.0-140.0
+28,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+28,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+28,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+54,1650601208,479635942,0,3.561000108718872,0.0,20.0,0.0-20.0
+54,1650601208,479635942,1,9.060999870300293,20.0,40.0,20.0-40.0
+54,1650601208,479635942,2,9.157999992370605,40.0,60.0,40.0-60.0
+54,1650601208,479635942,3,0.9579999446868896,60.0,80.0,60.0-80.0
+54,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+54,1650601208,479635942,5,24.14199995994568,100.0,120.0,100.0-120.0
+54,1650601208,479635942,6,491.4690001010895,120.0,140.0,120.0-140.0
+54,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+54,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+54,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+39,1650601208,479635942,0,8.365000247955322,0.0,20.0,0.0-20.0
+39,1650601208,479635942,1,17.322999954223633,20.0,40.0,20.0-40.0
+39,1650601208,479635942,2,16.073999881744385,40.0,60.0,40.0-60.0
+39,1650601208,479635942,3,8.408999681472778,60.0,80.0,60.0-80.0
+39,1650601208,479635942,4,8.45300006866455,80.0,100.0,80.0-100.0
+39,1650601208,479635942,5,13.613999843597412,100.0,120.0,100.0-120.0
+39,1650601208,479635942,6,497.2000000476837,120.0,140.0,120.0-140.0
+39,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+39,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+39,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+35,1650601208,479635942,0,4.420000076293945,0.0,20.0,0.0-20.0
+35,1650601208,479635942,1,22.999000072479248,20.0,40.0,20.0-40.0
+35,1650601208,479635942,2,42.980000019073486,40.0,60.0,40.0-60.0
+35,1650601208,479635942,3,10.475000381469727,60.0,80.0,60.0-80.0
+35,1650601208,479635942,4,20.78100037574768,80.0,100.0,80.0-100.0
+35,1650601208,479635942,5,9.062000036239624,100.0,120.0,100.0-120.0
+35,1650601208,479635942,6,349.4390001296997,120.0,140.0,120.0-140.0
+35,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+35,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+35,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+43,1650601208,479635942,0,5.408000469207764,0.0,20.0,0.0-20.0
+43,1650601208,479635942,1,17.420000553131104,20.0,40.0,20.0-40.0
+43,1650601208,479635942,2,12.46899962425232,40.0,60.0,40.0-60.0
+43,1650601208,479635942,3,2.246000051498413,60.0,80.0,60.0-80.0
+43,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+43,1650601208,479635942,5,21.176999807357788,100.0,120.0,100.0-120.0
+43,1650601208,479635942,6,327.8110001087189,120.0,140.0,120.0-140.0
+43,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+43,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+43,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+49,1650601208,479635942,0,1.2899999618530273,0.0,20.0,0.0-20.0
+49,1650601208,479635942,1,5.510999917984009,20.0,40.0,20.0-40.0
+49,1650601208,479635942,2,8.889999866485596,40.0,60.0,40.0-60.0
+49,1650601208,479635942,3,10.263000011444092,60.0,80.0,60.0-80.0
+49,1650601208,479635942,4,8.491999864578247,80.0,100.0,80.0-100.0
+49,1650601208,479635942,5,8.203999757766724,100.0,120.0,100.0-120.0
+49,1650601208,479635942,6,92.0,120.0,140.0,120.0-140.0
+49,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+49,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+49,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+22,1650601208,479635942,0,5.926999807357788,0.0,20.0,0.0-20.0
+22,1650601208,479635942,1,21.940999746322632,20.0,40.0,20.0-40.0
+22,1650601208,479635942,2,46.76200008392334,40.0,60.0,40.0-60.0
+22,1650601208,479635942,3,46.16399955749512,60.0,80.0,60.0-80.0
+22,1650601208,479635942,4,37.27300024032593,80.0,100.0,80.0-100.0
+22,1650601208,479635942,5,33.235000133514404,100.0,120.0,100.0-120.0
+22,1650601208,479635942,6,356.1919996738434,120.0,140.0,120.0-140.0
+22,1650601208,479635942,7,85.0,140.0,160.0,140.0-160.0
+22,1650601208,479635942,8,32.0,160.0,180.0,160.0-180.0
+22,1650601208,479635942,9,22.0,180.0,200.0,180.0-200.0
+76,1650601208,479635942,0,1.9129998683929443,0.0,20.0,0.0-20.0
+76,1650601208,479635942,1,10.884999752044678,20.0,40.0,20.0-40.0
+76,1650601208,479635942,2,15.609000444412231,40.0,60.0,40.0-60.0
+76,1650601208,479635942,3,10.48099970817566,60.0,80.0,60.0-80.0
+76,1650601208,479635942,4,2.2669999599456787,80.0,100.0,80.0-100.0
+76,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+76,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+76,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+76,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+76,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+16,1650601208,479635942,0,3.705000162124634,0.0,20.0,0.0-20.0
+16,1650601208,479635942,1,18.13099980354309,20.0,40.0,20.0-40.0
+16,1650601208,479635942,2,51.05400037765503,40.0,60.0,40.0-60.0
+16,1650601208,479635942,3,11.666000127792358,60.0,80.0,60.0-80.0
+16,1650601208,479635942,4,10.354000091552734,80.0,100.0,80.0-100.0
+16,1650601208,479635942,5,34.42799949645996,100.0,120.0,100.0-120.0
+16,1650601208,479635942,6,1141.1679999828339,120.0,140.0,120.0-140.0
+16,1650601208,479635942,7,234.0,140.0,160.0,140.0-160.0
+16,1650601208,479635942,8,90.0,160.0,180.0,160.0-180.0
+16,1650601208,479635942,9,25.0,180.0,200.0,180.0-200.0
+70,1650601208,479635942,0,3.2849998474121094,0.0,20.0,0.0-20.0
+70,1650601208,479635942,1,13.48900032043457,20.0,40.0,20.0-40.0
+70,1650601208,479635942,2,4.489999771118164,40.0,60.0,40.0-60.0
+70,1650601208,479635942,3,0.0989999771118164,60.0,80.0,60.0-80.0
+70,1650601208,479635942,4,1.7320001125335693,80.0,100.0,80.0-100.0
+70,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+70,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+70,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+70,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+70,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+4,1650601208,479635942,0,18.390999794006348,0.0,20.0,0.0-20.0
+4,1650601208,479635942,1,69.01699995994568,20.0,40.0,20.0-40.0
+4,1650601208,479635942,2,42.043999671936035,40.0,60.0,40.0-60.0
+4,1650601208,479635942,3,13.719000339508057,60.0,80.0,60.0-80.0
+4,1650601208,479635942,4,62.8050000667572,80.0,100.0,80.0-100.0
+4,1650601208,479635942,5,52.789000034332275,100.0,120.0,100.0-120.0
+4,1650601208,479635942,6,860.7999999523163,120.0,140.0,120.0-140.0
+4,1650601208,479635942,7,213.0,140.0,160.0,140.0-160.0
+4,1650601208,479635942,8,87.0,160.0,180.0,160.0-180.0
+4,1650601208,479635942,9,51.0,180.0,200.0,180.0-200.0
+8,1650601208,479635942,0,7.2659995555877686,0.0,20.0,0.0-20.0
+8,1650601208,479635942,1,21.407999992370605,20.0,40.0,20.0-40.0
+8,1650601208,479635942,2,11.783000230789185,40.0,60.0,40.0-60.0
+8,1650601208,479635942,3,15.637000322341919,60.0,80.0,60.0-80.0
+8,1650601208,479635942,4,38.78800010681152,80.0,100.0,80.0-100.0
+8,1650601208,479635942,5,71.13999962806702,100.0,120.0,100.0-120.0
+8,1650601208,479635942,6,842.5719997882843,120.0,140.0,120.0-140.0
+8,1650601208,479635942,7,138.0,140.0,160.0,140.0-160.0
+8,1650601208,479635942,8,58.0,160.0,180.0,160.0-180.0
+8,1650601208,479635942,9,36.0,180.0,200.0,180.0-200.0
+20,1650601208,479635942,0,8.585999965667725,0.0,20.0,0.0-20.0
+20,1650601208,479635942,1,19.19599986076355,20.0,40.0,20.0-40.0
+20,1650601208,479635942,2,18.27999973297119,40.0,60.0,40.0-60.0
+20,1650601208,479635942,3,9.07699990272522,60.0,80.0,60.0-80.0
+20,1650601208,479635942,4,10.724000215530396,80.0,100.0,80.0-100.0
+20,1650601208,479635942,5,29.31999969482422,100.0,120.0,100.0-120.0
+20,1650601208,479635942,6,306.6889998912811,120.0,140.0,120.0-140.0
+20,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+20,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+20,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+44,1650601208,479635942,0,2.114000082015991,0.0,20.0,0.0-20.0
+44,1650601208,479635942,1,12.960999727249146,20.0,40.0,20.0-40.0
+44,1650601208,479635942,2,29.532000064849854,40.0,60.0,40.0-60.0
+44,1650601208,479635942,3,5.055999994277954,60.0,80.0,60.0-80.0
+44,1650601208,479635942,4,3.4509999752044678,80.0,100.0,80.0-100.0
+44,1650601208,479635942,5,22.24899935722351,100.0,120.0,100.0-120.0
+44,1650601208,479635942,6,475.2669997215271,120.0,140.0,120.0-140.0
+44,1650601208,479635942,7,130.0,140.0,160.0,140.0-160.0
+44,1650601208,479635942,8,150.0,160.0,180.0,160.0-180.0
+44,1650601208,479635942,9,53.0,180.0,200.0,180.0-200.0
+30,1650601208,479635942,0,6.010999917984009,0.0,20.0,0.0-20.0
+30,1650601208,479635942,1,37.361000776290894,20.0,40.0,20.0-40.0
+30,1650601208,479635942,2,37.32800030708313,40.0,60.0,40.0-60.0
+30,1650601208,479635942,3,2.7860000133514404,60.0,80.0,60.0-80.0
+30,1650601208,479635942,4,3.692999839782715,80.0,100.0,80.0-100.0
+30,1650601208,479635942,5,1.7960000038146973,100.0,120.0,100.0-120.0
+30,1650601208,479635942,6,232.7720000743866,120.0,140.0,120.0-140.0
+30,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+30,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+30,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+50,1650601208,479635942,0,10.335999965667725,0.0,20.0,0.0-20.0
+50,1650601208,479635942,1,28.96899962425232,20.0,40.0,20.0-40.0
+50,1650601208,479635942,2,24.430999755859375,40.0,60.0,40.0-60.0
+50,1650601208,479635942,3,5.9659998416900635,60.0,80.0,60.0-80.0
+50,1650601208,479635942,4,5.861000061035156,80.0,100.0,80.0-100.0
+50,1650601208,479635942,5,5.640999794006348,100.0,120.0,100.0-120.0
+50,1650601208,479635942,6,166.0,120.0,140.0,120.0-140.0
+50,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+50,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+50,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+80,1650601208,479635942,0,4.133999824523926,0.0,20.0,0.0-20.0
+80,1650601208,479635942,1,24.025999784469604,20.0,40.0,20.0-40.0
+80,1650601208,479635942,2,25.842000484466553,40.0,60.0,40.0-60.0
+80,1650601208,479635942,3,4.501000165939331,60.0,80.0,60.0-80.0
+80,1650601208,479635942,4,6.442999839782715,80.0,100.0,80.0-100.0
+80,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+80,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+80,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+80,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+80,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+33,1650601208,479635942,0,10.035000085830688,0.0,20.0,0.0-20.0
+33,1650601208,479635942,1,41.073999881744385,20.0,40.0,20.0-40.0
+33,1650601208,479635942,2,41.460999727249146,40.0,60.0,40.0-60.0
+33,1650601208,479635942,3,25.85599994659424,60.0,80.0,60.0-80.0
+33,1650601208,479635942,4,52.42799973487854,80.0,100.0,80.0-100.0
+33,1650601208,479635942,5,374.3180003166199,100.0,120.0,100.0-120.0
+33,1650601208,479635942,6,399.98800015449524,120.0,140.0,120.0-140.0
+33,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+33,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+33,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+10,1650601208,479635942,0,9.16100025177002,0.0,20.0,0.0-20.0
+10,1650601208,479635942,1,30.361000061035156,20.0,40.0,20.0-40.0
+10,1650601208,479635942,2,11.082000255584717,40.0,60.0,40.0-60.0
+10,1650601208,479635942,3,4.306999921798706,60.0,80.0,60.0-80.0
+10,1650601208,479635942,4,2.4040000438690186,80.0,100.0,80.0-100.0
+10,1650601208,479635942,5,186.60700011253357,100.0,120.0,100.0-120.0
+10,1650601208,479635942,6,261.4119999408722,120.0,140.0,120.0-140.0
+10,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+10,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+10,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+40,1650601208,479635942,0,6.553999662399292,0.0,20.0,0.0-20.0
+40,1650601208,479635942,1,21.205999851226807,20.0,40.0,20.0-40.0
+40,1650601208,479635942,2,19.22100019454956,40.0,60.0,40.0-60.0
+40,1650601208,479635942,3,12.73099970817566,60.0,80.0,60.0-80.0
+40,1650601208,479635942,4,5.411999940872192,80.0,100.0,80.0-100.0
+40,1650601208,479635942,5,18.870999813079834,100.0,120.0,100.0-120.0
+40,1650601208,479635942,6,166.01900005340576,120.0,140.0,120.0-140.0
+40,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+40,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+40,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+46,1650601208,479635942,0,6.091999769210815,0.0,20.0,0.0-20.0
+46,1650601208,479635942,1,5.6419997215271,20.0,40.0,20.0-40.0
+46,1650601208,479635942,2,15.571000099182129,40.0,60.0,40.0-60.0
+46,1650601208,479635942,3,8.923999786376953,60.0,80.0,60.0-80.0
+46,1650601208,479635942,4,18.31600022315979,80.0,100.0,80.0-100.0
+46,1650601208,479635942,5,9.361999988555908,100.0,120.0,100.0-120.0
+46,1650601208,479635942,6,286.8710000514984,120.0,140.0,120.0-140.0
+46,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+46,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+46,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+53,1650601208,479635942,0,9.881999731063843,0.0,20.0,0.0-20.0
+53,1650601208,479635942,1,14.146000385284424,20.0,40.0,20.0-40.0
+53,1650601208,479635942,2,15.546000003814697,40.0,60.0,40.0-60.0
+53,1650601208,479635942,3,0.6959998607635498,60.0,80.0,60.0-80.0
+53,1650601208,479635942,4,1.937999963760376,80.0,100.0,80.0-100.0
+53,1650601208,479635942,5,13.47599983215332,100.0,120.0,100.0-120.0
+53,1650601208,479635942,6,254.0,120.0,140.0,120.0-140.0
+53,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+53,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+53,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+34,1650601208,479635942,0,5.322999715805054,0.0,20.0,0.0-20.0
+34,1650601208,479635942,1,21.000999689102173,20.0,40.0,20.0-40.0
+34,1650601208,479635942,2,17.105000257492065,40.0,60.0,40.0-60.0
+34,1650601208,479635942,3,7.132999897003174,60.0,80.0,60.0-80.0
+34,1650601208,479635942,4,11.398000001907349,80.0,100.0,80.0-100.0
+34,1650601208,479635942,5,30.367000102996826,100.0,120.0,100.0-120.0
+34,1650601208,479635942,6,549.6099998950958,120.0,140.0,120.0-140.0
+34,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+34,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+34,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+17,1650601208,479635942,0,9.160999536514282,0.0,20.0,0.0-20.0
+17,1650601208,479635942,1,16.122000217437744,20.0,40.0,20.0-40.0
+17,1650601208,479635942,2,17.057000160217285,40.0,60.0,40.0-60.0
+17,1650601208,479635942,3,5.045000314712524,60.0,80.0,60.0-80.0
+17,1650601208,479635942,4,4.797000169754028,80.0,100.0,80.0-100.0
+17,1650601208,479635942,5,9.59500002861023,100.0,120.0,100.0-120.0
+17,1650601208,479635942,6,194.6470000743866,120.0,140.0,120.0-140.0
+17,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+17,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+17,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+11,1650601208,479635942,0,7.879000186920166,0.0,20.0,0.0-20.0
+11,1650601208,479635942,1,37.40599989891052,20.0,40.0,20.0-40.0
+11,1650601208,479635942,2,25.96399974822998,40.0,60.0,40.0-60.0
+11,1650601208,479635942,3,13.407999753952026,60.0,80.0,60.0-80.0
+11,1650601208,479635942,4,16.212000131607056,80.0,100.0,80.0-100.0
+11,1650601208,479635942,5,52.43899965286255,100.0,120.0,100.0-120.0
+11,1650601208,479635942,6,1057.5130002498627,120.0,140.0,120.0-140.0
+11,1650601208,479635942,7,149.0,140.0,160.0,140.0-160.0
+11,1650601208,479635942,8,40.0,160.0,180.0,160.0-180.0
+11,1650601208,479635942,9,5.0,180.0,200.0,180.0-200.0
+12,1650601208,479635942,0,10.884999752044678,0.0,20.0,0.0-20.0
+12,1650601208,479635942,1,25.68499994277954,20.0,40.0,20.0-40.0
+12,1650601208,479635942,2,8.270999908447266,40.0,60.0,40.0-60.0
+12,1650601208,479635942,3,11.29800009727478,60.0,80.0,60.0-80.0
+12,1650601208,479635942,4,33.06700015068054,80.0,100.0,80.0-100.0
+12,1650601208,479635942,5,65.29999995231628,100.0,120.0,100.0-120.0
+12,1650601208,479635942,6,974.7320001125336,120.0,140.0,120.0-140.0
+12,1650601208,479635942,7,151.0,140.0,160.0,140.0-160.0
+12,1650601208,479635942,8,134.0,160.0,180.0,160.0-180.0
+12,1650601208,479635942,9,53.0,180.0,200.0,180.0-200.0
+81,1650601208,479635942,0,0.0,0.0,20.0,0.0-20.0
+81,1650601208,479635942,1,5.164000034332275,20.0,40.0,20.0-40.0
+81,1650601208,479635942,2,20.323000192642212,40.0,60.0,40.0-60.0
+81,1650601208,479635942,3,20.067999839782715,60.0,80.0,60.0-80.0
+81,1650601208,479635942,4,3.0439999103546143,80.0,100.0,80.0-100.0
+81,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+81,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+81,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+81,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+81,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+25,1650601208,479635942,0,6.118000030517578,0.0,20.0,0.0-20.0
+25,1650601208,479635942,1,29.918999910354614,20.0,40.0,20.0-40.0
+25,1650601208,479635942,2,9.591000318527222,40.0,60.0,40.0-60.0
+25,1650601208,479635942,3,6.742000102996826,60.0,80.0,60.0-80.0
+25,1650601208,479635942,4,7.473999977111816,80.0,100.0,80.0-100.0
+25,1650601208,479635942,5,14.598999977111816,100.0,120.0,100.0-120.0
+25,1650601208,479635942,6,338.90899991989136,120.0,140.0,120.0-140.0
+25,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+25,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+25,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+9,1650601208,479635942,0,0.75,0.0,20.0,0.0-20.0
+9,1650601208,479635942,1,18.699000358581543,20.0,40.0,20.0-40.0
+9,1650601208,479635942,2,28.75600028038025,40.0,60.0,40.0-60.0
+9,1650601208,479635942,3,19.00499987602234,60.0,80.0,60.0-80.0
+9,1650601208,479635942,4,26.515999794006348,80.0,100.0,80.0-100.0
+9,1650601208,479635942,5,43.538999795913696,100.0,120.0,100.0-120.0
+9,1650601208,479635942,6,782.3210000991821,120.0,140.0,120.0-140.0
+9,1650601208,479635942,7,173.0,140.0,160.0,140.0-160.0
+9,1650601208,479635942,8,85.0,160.0,180.0,160.0-180.0
+9,1650601208,479635942,9,24.0,180.0,200.0,180.0-200.0
+38,1650601208,479635942,0,5.002999782562256,0.0,20.0,0.0-20.0
+38,1650601208,479635942,1,14.337000370025635,20.0,40.0,20.0-40.0
+38,1650601208,479635942,2,4.3329997062683105,40.0,60.0,40.0-60.0
+38,1650601208,479635942,3,0.0,60.0,80.0,60.0-80.0
+38,1650601208,479635942,4,1.0320000648498535,80.0,100.0,80.0-100.0
+38,1650601208,479635942,5,28.73099994659424,100.0,120.0,100.0-120.0
+38,1650601208,479635942,6,343.4079999923706,120.0,140.0,120.0-140.0
+38,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+38,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+38,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+14,1650601208,479635942,0,5.801000118255615,0.0,20.0,0.0-20.0
+14,1650601208,479635942,1,22.826000690460205,20.0,40.0,20.0-40.0
+14,1650601208,479635942,2,22.742000102996826,40.0,60.0,40.0-60.0
+14,1650601208,479635942,3,6.872999906539917,60.0,80.0,60.0-80.0
+14,1650601208,479635942,4,19.932000160217285,80.0,100.0,80.0-100.0
+14,1650601208,479635942,5,45.425999879837036,100.0,120.0,100.0-120.0
+14,1650601208,479635942,6,1009.1250002384186,120.0,140.0,120.0-140.0
+14,1650601208,479635942,7,196.0,140.0,160.0,140.0-160.0
+14,1650601208,479635942,8,78.0,160.0,180.0,160.0-180.0
+14,1650601208,479635942,9,75.0,180.0,200.0,180.0-200.0
+29,1650601208,479635942,0,5.468000173568726,0.0,20.0,0.0-20.0
+29,1650601208,479635942,1,28.407000064849854,20.0,40.0,20.0-40.0
+29,1650601208,479635942,2,17.767000198364258,40.0,60.0,40.0-60.0
+29,1650601208,479635942,3,1.4659998416900635,60.0,80.0,60.0-80.0
+29,1650601208,479635942,4,0.9079999923706055,80.0,100.0,80.0-100.0
+29,1650601208,479635942,5,44.509000062942505,100.0,120.0,100.0-120.0
+29,1650601208,479635942,6,300.03399991989136,120.0,140.0,120.0-140.0
+29,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+29,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+29,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+61,1650601208,479635942,0,1.1099998950958252,0.0,20.0,0.0-20.0
+61,1650601208,479635942,1,9.28600001335144,20.0,40.0,20.0-40.0
+61,1650601208,479635942,2,5.766999959945679,40.0,60.0,40.0-60.0
+61,1650601208,479635942,3,3.621000051498413,60.0,80.0,60.0-80.0
+61,1650601208,479635942,4,0.5829999446868896,80.0,100.0,80.0-100.0
+61,1650601208,479635942,5,19.401000022888184,100.0,120.0,100.0-120.0
+61,1650601208,479635942,6,505.2630000114441,120.0,140.0,120.0-140.0
+61,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+61,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+61,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+68,1650601208,479635942,0,4.630000114440918,0.0,20.0,0.0-20.0
+68,1650601208,479635942,1,11.36900019645691,20.0,40.0,20.0-40.0
+68,1650601208,479635942,2,6.5929999351501465,40.0,60.0,40.0-60.0
+68,1650601208,479635942,3,6.558000087738037,60.0,80.0,60.0-80.0
+68,1650601208,479635942,4,11.668999910354614,80.0,100.0,80.0-100.0
+68,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+68,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+68,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+68,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+68,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+51,1650601208,479635942,0,3.254999876022339,0.0,20.0,0.0-20.0
+51,1650601208,479635942,1,12.04800009727478,20.0,40.0,20.0-40.0
+51,1650601208,479635942,2,15.393000364303589,40.0,60.0,40.0-60.0
+51,1650601208,479635942,3,3.812000274658203,60.0,80.0,60.0-80.0
+51,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+51,1650601208,479635942,5,32.21999979019165,100.0,120.0,100.0-120.0
+51,1650601208,479635942,6,1226.6640002727509,120.0,140.0,120.0-140.0
+51,1650601208,479635942,7,170.0,140.0,160.0,140.0-160.0
+51,1650601208,479635942,8,136.0,160.0,180.0,160.0-180.0
+51,1650601208,479635942,9,121.0,180.0,200.0,180.0-200.0
+23,1650601208,479635942,0,11.47700047492981,0.0,20.0,0.0-20.0
+23,1650601208,479635942,1,19.835000038146973,20.0,40.0,20.0-40.0
+23,1650601208,479635942,2,11.426000118255615,40.0,60.0,40.0-60.0
+23,1650601208,479635942,3,9.200999975204468,60.0,80.0,60.0-80.0
+23,1650601208,479635942,4,36.223000049591064,80.0,100.0,80.0-100.0
+23,1650601208,479635942,5,43.32000017166138,100.0,120.0,100.0-120.0
+23,1650601208,479635942,6,804.4109997749329,120.0,140.0,120.0-140.0
+23,1650601208,479635942,7,231.0,140.0,160.0,140.0-160.0
+23,1650601208,479635942,8,100.0,160.0,180.0,160.0-180.0
+23,1650601208,479635942,9,38.0,180.0,200.0,180.0-200.0
+36,1650601208,479635942,0,7.728999853134155,0.0,20.0,0.0-20.0
+36,1650601208,479635942,1,13.174999952316284,20.0,40.0,20.0-40.0
+36,1650601208,479635942,2,10.326000213623047,40.0,60.0,40.0-60.0
+36,1650601208,479635942,3,3.003000020980835,60.0,80.0,60.0-80.0
+36,1650601208,479635942,4,6.536999702453613,80.0,100.0,80.0-100.0
+36,1650601208,479635942,5,6.004999876022339,100.0,120.0,100.0-120.0
+36,1650601208,479635942,6,265.28200006484985,120.0,140.0,120.0-140.0
+36,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+36,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+36,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+60,1650601208,479635942,0,2.7950000762939453,0.0,20.0,0.0-20.0
+60,1650601208,479635942,1,10.498999834060669,20.0,40.0,20.0-40.0
+60,1650601208,479635942,2,29.247999668121338,40.0,60.0,40.0-60.0
+60,1650601208,479635942,3,15.557000398635864,60.0,80.0,60.0-80.0
+60,1650601208,479635942,4,5.699000120162964,80.0,100.0,80.0-100.0
+60,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+60,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+60,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+60,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+60,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+18,1650601208,479635942,0,4.121000051498413,0.0,20.0,0.0-20.0
+18,1650601208,479635942,1,5.961999893188477,20.0,40.0,20.0-40.0
+18,1650601208,479635942,2,24.38099980354309,40.0,60.0,40.0-60.0
+18,1650601208,479635942,3,4.855000257492065,60.0,80.0,60.0-80.0
+18,1650601208,479635942,4,2.5339999198913574,80.0,100.0,80.0-100.0
+18,1650601208,479635942,5,64.44199967384338,100.0,120.0,100.0-120.0
+18,1650601208,479635942,6,1509.6249997615814,120.0,140.0,120.0-140.0
+18,1650601208,479635942,7,198.0,140.0,160.0,140.0-160.0
+18,1650601208,479635942,8,60.0,160.0,180.0,160.0-180.0
+18,1650601208,479635942,9,25.0,180.0,200.0,180.0-200.0
+32,1650601208,479635942,0,5.072999715805054,0.0,20.0,0.0-20.0
+32,1650601208,479635942,1,29.634999752044678,20.0,40.0,20.0-40.0
+32,1650601208,479635942,2,19.76199984550476,40.0,60.0,40.0-60.0
+32,1650601208,479635942,3,6.398000240325928,60.0,80.0,60.0-80.0
+32,1650601208,479635942,4,12.368999719619751,80.0,100.0,80.0-100.0
+32,1650601208,479635942,5,40.537999868392944,100.0,120.0,100.0-120.0
+32,1650601208,479635942,6,262.5460000038147,120.0,140.0,120.0-140.0
+32,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+32,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+32,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+42,1650601208,479635942,0,4.313999891281128,0.0,20.0,0.0-20.0
+42,1650601208,479635942,1,23.26799988746643,20.0,40.0,20.0-40.0
+42,1650601208,479635942,2,16.91800022125244,40.0,60.0,40.0-60.0
+42,1650601208,479635942,3,8.259999990463257,60.0,80.0,60.0-80.0
+42,1650601208,479635942,4,14.244999885559082,80.0,100.0,80.0-100.0
+42,1650601208,479635942,5,47.08099985122681,100.0,120.0,100.0-120.0
+42,1650601208,479635942,6,376.23800015449524,120.0,140.0,120.0-140.0
+42,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+42,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+42,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+55,1650601208,479635942,0,4.744000196456909,0.0,20.0,0.0-20.0
+55,1650601208,479635942,1,21.726999521255493,20.0,40.0,20.0-40.0
+55,1650601208,479635942,2,6.817000150680542,40.0,60.0,40.0-60.0
+55,1650601208,479635942,3,3.1489996910095215,60.0,80.0,60.0-80.0
+55,1650601208,479635942,4,9.75,80.0,100.0,80.0-100.0
+55,1650601208,479635942,5,11.67799997329712,100.0,120.0,100.0-120.0
+55,1650601208,479635942,6,312.9119999408722,120.0,140.0,120.0-140.0
+55,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+55,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+55,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+67,1650601208,479635942,0,6.002000093460083,0.0,20.0,0.0-20.0
+67,1650601208,479635942,1,7.947999715805054,20.0,40.0,20.0-40.0
+67,1650601208,479635942,2,33.82499957084656,40.0,60.0,40.0-60.0
+67,1650601208,479635942,3,16.243000030517578,60.0,80.0,60.0-80.0
+67,1650601208,479635942,4,2.634000062942505,80.0,100.0,80.0-100.0
+67,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+67,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+67,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+67,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+67,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+69,1650601208,479635942,0,8.27899980545044,0.0,20.0,0.0-20.0
+69,1650601208,479635942,1,8.6419997215271,20.0,40.0,20.0-40.0
+69,1650601208,479635942,2,7.4670000076293945,40.0,60.0,40.0-60.0
+69,1650601208,479635942,3,0.0,60.0,80.0,60.0-80.0
+69,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+69,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+69,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+69,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+69,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+69,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+59,1650601208,479635942,0,4.7699995040893555,0.0,20.0,0.0-20.0
+59,1650601208,479635942,1,48.91099977493286,20.0,40.0,20.0-40.0
+59,1650601208,479635942,2,36.63100028038025,40.0,60.0,40.0-60.0
+59,1650601208,479635942,3,32.36099982261658,60.0,80.0,60.0-80.0
+59,1650601208,479635942,4,38.46499967575073,80.0,100.0,80.0-100.0
+59,1650601208,479635942,5,101.41899991035461,100.0,120.0,100.0-120.0
+59,1650601208,479635942,6,562.0110001564026,120.0,140.0,120.0-140.0
+59,1650601208,479635942,7,26.0,140.0,160.0,140.0-160.0
+59,1650601208,479635942,8,19.0,160.0,180.0,160.0-180.0
+59,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+26,1650601208,479635942,0,15.731999635696411,0.0,20.0,0.0-20.0
+26,1650601208,479635942,1,19.463000059127808,20.0,40.0,20.0-40.0
+26,1650601208,479635942,2,49.53899931907654,40.0,60.0,40.0-60.0
+26,1650601208,479635942,3,17.043999910354614,60.0,80.0,60.0-80.0
+26,1650601208,479635942,4,27.563000440597534,80.0,100.0,80.0-100.0
+26,1650601208,479635942,5,2.25,100.0,120.0,100.0-120.0
+26,1650601208,479635942,6,38.789000034332275,120.0,140.0,120.0-140.0
+26,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+26,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+26,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+47,1650601208,479635942,0,2.9720001220703125,0.0,20.0,0.0-20.0
+47,1650601208,479635942,1,22.3510000705719,20.0,40.0,20.0-40.0
+47,1650601208,479635942,2,21.10200047492981,40.0,60.0,40.0-60.0
+47,1650601208,479635942,3,7.428999662399292,60.0,80.0,60.0-80.0
+47,1650601208,479635942,4,4.352999925613403,80.0,100.0,80.0-100.0
+47,1650601208,479635942,5,61.835999488830566,100.0,120.0,100.0-120.0
+47,1650601208,479635942,6,285.79299998283386,120.0,140.0,120.0-140.0
+47,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+47,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+47,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+74,1650601208,479635942,0,8.037999629974365,0.0,20.0,0.0-20.0
+74,1650601208,479635942,1,13.637999773025513,20.0,40.0,20.0-40.0
+74,1650601208,479635942,2,14.28600025177002,40.0,60.0,40.0-60.0
+74,1650601208,479635942,3,6.2930004596710205,60.0,80.0,60.0-80.0
+74,1650601208,479635942,4,2.2950000762939453,80.0,100.0,80.0-100.0
+74,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+74,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+74,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+74,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+74,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+62,1650601208,479635942,0,2.2869999408721924,0.0,20.0,0.0-20.0
+62,1650601208,479635942,1,9.260000228881836,20.0,40.0,20.0-40.0
+62,1650601208,479635942,2,13.741999864578247,40.0,60.0,40.0-60.0
+62,1650601208,479635942,3,2.553999900817871,60.0,80.0,60.0-80.0
+62,1650601208,479635942,4,0.0,80.0,100.0,80.0-100.0
+62,1650601208,479635942,5,4.011999845504761,100.0,120.0,100.0-120.0
+62,1650601208,479635942,6,333.25099992752075,120.0,140.0,120.0-140.0
+62,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+62,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+62,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+31,1650601208,479635942,0,8.723000288009644,0.0,20.0,0.0-20.0
+31,1650601208,479635942,1,26.126999616622925,20.0,40.0,20.0-40.0
+31,1650601208,479635942,2,28.31600022315979,40.0,60.0,40.0-60.0
+31,1650601208,479635942,3,10.780999898910522,60.0,80.0,60.0-80.0
+31,1650601208,479635942,4,13.184000015258789,80.0,100.0,80.0-100.0
+31,1650601208,479635942,5,7.829999685287476,100.0,120.0,100.0-120.0
+31,1650601208,479635942,6,366.47199988365173,120.0,140.0,120.0-140.0
+31,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+31,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+31,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+56,1650601208,479635942,0,0.8810000419616699,0.0,20.0,0.0-20.0
+56,1650601208,479635942,1,9.948999881744385,20.0,40.0,20.0-40.0
+56,1650601208,479635942,2,29.597999811172485,40.0,60.0,40.0-60.0
+56,1650601208,479635942,3,4.230999946594238,60.0,80.0,60.0-80.0
+56,1650601208,479635942,4,10.319999694824219,80.0,100.0,80.0-100.0
+56,1650601208,479635942,5,32.77300024032593,100.0,120.0,100.0-120.0
+56,1650601208,479635942,6,365.38800024986267,120.0,140.0,120.0-140.0
+56,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+56,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+56,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+73,1650601208,479635942,0,1.3550000190734863,0.0,20.0,0.0-20.0
+73,1650601208,479635942,1,10.442999601364136,20.0,40.0,20.0-40.0
+73,1650601208,479635942,2,2.88700008392334,40.0,60.0,40.0-60.0
+73,1650601208,479635942,3,1.9110000133514404,60.0,80.0,60.0-80.0
+73,1650601208,479635942,4,1.8510000705718994,80.0,100.0,80.0-100.0
+73,1650601208,479635942,5,3.1599998474121094,100.0,120.0,100.0-120.0
+73,1650601208,479635942,6,99.08299994468689,120.0,140.0,120.0-140.0
+73,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+73,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+73,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+6,1650601208,479635942,0,10.128000259399414,0.0,20.0,0.0-20.0
+6,1650601208,479635942,1,21.66099977493286,20.0,40.0,20.0-40.0
+6,1650601208,479635942,2,13.740999698638916,40.0,60.0,40.0-60.0
+6,1650601208,479635942,3,1.6570000648498535,60.0,80.0,60.0-80.0
+6,1650601208,479635942,4,4.210999965667725,80.0,100.0,80.0-100.0
+6,1650601208,479635942,5,56.05999994277954,100.0,120.0,100.0-120.0
+6,1650601208,479635942,6,891.0009999275208,120.0,140.0,120.0-140.0
+6,1650601208,479635942,7,197.0,140.0,160.0,140.0-160.0
+6,1650601208,479635942,8,164.0,160.0,180.0,160.0-180.0
+6,1650601208,479635942,9,45.0,180.0,200.0,180.0-200.0
+78,1650601208,479635942,0,3.1459999084472656,0.0,20.0,0.0-20.0
+78,1650601208,479635942,1,9.855000019073486,20.0,40.0,20.0-40.0
+78,1650601208,479635942,2,11.586000204086304,40.0,60.0,40.0-60.0
+78,1650601208,479635942,3,11.25,60.0,80.0,60.0-80.0
+78,1650601208,479635942,4,11.0239999294281,80.0,100.0,80.0-100.0
+78,1650601208,479635942,5,5.151000022888184,100.0,120.0,100.0-120.0
+78,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+78,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+78,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+78,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+19,1650601208,479635942,0,8.42300009727478,0.0,20.0,0.0-20.0
+19,1650601208,479635942,1,37.29699969291687,20.0,40.0,20.0-40.0
+19,1650601208,479635942,2,15.55299997329712,40.0,60.0,40.0-60.0
+19,1650601208,479635942,3,11.168000221252441,60.0,80.0,60.0-80.0
+19,1650601208,479635942,4,14.953999996185303,80.0,100.0,80.0-100.0
+19,1650601208,479635942,5,46.43299961090088,100.0,120.0,100.0-120.0
+19,1650601208,479635942,6,709.7850000858307,120.0,140.0,120.0-140.0
+19,1650601208,479635942,7,212.0,140.0,160.0,140.0-160.0
+19,1650601208,479635942,8,27.0,160.0,180.0,160.0-180.0
+19,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+48,1650601208,479635942,0,5.446999549865723,0.0,20.0,0.0-20.0
+48,1650601208,479635942,1,16.72699999809265,20.0,40.0,20.0-40.0
+48,1650601208,479635942,2,13.656999588012695,40.0,60.0,40.0-60.0
+48,1650601208,479635942,3,1.0499999523162842,60.0,80.0,60.0-80.0
+48,1650601208,479635942,4,0.47300004959106445,80.0,100.0,80.0-100.0
+48,1650601208,479635942,5,19.126999855041504,100.0,120.0,100.0-120.0
+48,1650601208,479635942,6,571.6639997959137,120.0,140.0,120.0-140.0
+48,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+48,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+48,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
+79,1650601208,479635942,0,9.5600004196167,0.0,20.0,0.0-20.0
+79,1650601208,479635942,1,13.09499979019165,20.0,40.0,20.0-40.0
+79,1650601208,479635942,2,31.70799946784973,40.0,60.0,40.0-60.0
+79,1650601208,479635942,3,3.321000099182129,60.0,80.0,60.0-80.0
+79,1650601208,479635942,4,0.7890000343322754,80.0,100.0,80.0-100.0
+79,1650601208,479635942,5,0.0,100.0,120.0,100.0-120.0
+79,1650601208,479635942,6,0.0,120.0,140.0,120.0-140.0
+79,1650601208,479635942,7,0.0,140.0,160.0,140.0-160.0
+79,1650601208,479635942,8,0.0,160.0,180.0,160.0-180.0
+79,1650601208,479635942,9,0.0,180.0,200.0,180.0-200.0
diff --git a/tests/e2e/expected/pushdown/stats_aggregator_fact.csv b/tests/e2e/expected/pushdown/stats_aggregator_fact.csv
new file mode 100644
index 00000000..c5651fd7
--- /dev/null
+++ b/tests/e2e/expected/pushdown/stats_aggregator_fact.csv
@@ -0,0 +1,973 @@
+container_id,visual_id,channel_name,event_id,event_instance_id,aggregation_label,statistic_value
+58,430930200,coolant_1s,1201859611,1432421368744511818,min,11.0
+58,430930200,coolant_1s,1201859611,1432421368744511818,mean,71.05970651180458
+58,430930200,coolant_1s,1201859611,1432421368744511818,max,95.0
+58,430930200,speed_1s_diff,1201859611,1432421368744511818,min,-13.0
+58,430930200,speed_1s_diff,1201859611,1432421368744511818,mean,0.0
+58,430930200,speed_1s_diff,1201859611,1432421368744511818,max,13.0
+58,1374207915,Engine RPM,1201859611,1432421368744511818,min,0.0
+58,1374207915,Engine RPM,1201859611,1432421368744511818,mean,1211.9540284966436
+58,1374207915,Engine RPM,1201859611,1432421368744511818,max,2441.0
+58,1374207915,rpm_1s,1201859611,1432421368744511818,min,0.0
+58,1374207915,rpm_1s,1201859611,1432421368744511818,mean,1211.838531101375
+58,1374207915,rpm_1s,1201859611,1432421368744511818,max,2441.0
+21,430930200,coolant_1s,1201859611,2586283319979737404,min,41.0
+21,430930200,coolant_1s,1201859611,2586283319979737404,mean,81.94178674855078
+21,430930200,coolant_1s,1201859611,2586283319979737404,max,93.0
+21,430930200,speed_1s_diff,1201859611,2586283319979737404,min,-24.0
+21,430930200,speed_1s_diff,1201859611,2586283319979737404,mean,0.0
+21,430930200,speed_1s_diff,1201859611,2586283319979737404,max,17.0
+71,430930200,coolant_1s,1201859611,5850834331784891980,min,24.0
+71,430930200,coolant_1s,1201859611,5850834331784891980,mean,73.02575676121619
+71,430930200,coolant_1s,1201859611,5850834331784891980,max,95.0
+71,430930200,speed_1s_diff,1201859611,5850834331784891980,min,-14.0
+71,430930200,speed_1s_diff,1201859611,5850834331784891980,mean,0.0
+71,430930200,speed_1s_diff,1201859611,5850834331784891980,max,15.0
+21,1374207915,Engine RPM,1201859611,2586283319979737404,min,0.0
+21,1374207915,Engine RPM,1201859611,2586283319979737404,mean,1484.8370648465818
+21,1374207915,Engine RPM,1201859611,2586283319979737404,max,4315.0
+21,1374207915,rpm_1s,1201859611,2586283319979737404,min,0.0
+21,1374207915,rpm_1s,1201859611,2586283319979737404,mean,1485.8446271713678
+21,1374207915,rpm_1s,1201859611,2586283319979737404,max,4315.0
+71,1374207915,Engine RPM,1201859611,5850834331784891980,min,0.0
+71,1374207915,Engine RPM,1201859611,5850834331784891980,mean,1210.6967274379303
+71,1374207915,Engine RPM,1201859611,5850834331784891980,max,2766.0
+71,1374207915,rpm_1s,1201859611,5850834331784891980,min,0.0
+71,1374207915,rpm_1s,1201859611,5850834331784891980,mean,1212.7424171754622
+71,1374207915,rpm_1s,1201859611,5850834331784891980,max,2766.0
+2,430930200,coolant_1s,1201859611,7921822167645060769,min,17.0
+2,430930200,coolant_1s,1201859611,7921822167645060769,mean,86.48472072621517
+2,430930200,coolant_1s,1201859611,7921822167645060769,max,98.0
+2,430930200,speed_1s_diff,1201859611,7921822167645060769,min,-14.0
+2,430930200,speed_1s_diff,1201859611,7921822167645060769,mean,0.0
+2,430930200,speed_1s_diff,1201859611,7921822167645060769,max,16.0
+75,430930200,coolant_1s,1201859611,5110935768283636594,min,30.0
+75,430930200,coolant_1s,1201859611,5110935768283636594,mean,72.03738821251518
+75,430930200,coolant_1s,1201859611,5110935768283636594,max,94.0
+75,430930200,speed_1s_diff,1201859611,5110935768283636594,min,-13.0
+75,430930200,speed_1s_diff,1201859611,5110935768283636594,mean,0.0
+75,430930200,speed_1s_diff,1201859611,5110935768283636594,max,14.0
+2,1374207915,Engine RPM,1201859611,7921822167645060769,min,0.0
+2,1374207915,Engine RPM,1201859611,7921822167645060769,mean,1429.4126314544558
+2,1374207915,Engine RPM,1201859611,7921822167645060769,max,3255.0
+2,1374207915,rpm_1s,1201859611,7921822167645060769,min,0.0
+2,1374207915,rpm_1s,1201859611,7921822167645060769,mean,1429.1606230628358
+2,1374207915,rpm_1s,1201859611,7921822167645060769,max,3254.0
+75,1374207915,Engine RPM,1201859611,5110935768283636594,min,0.0
+75,1374207915,Engine RPM,1201859611,5110935768283636594,mean,1186.5843996568703
+75,1374207915,Engine RPM,1201859611,5110935768283636594,max,2634.0
+75,1374207915,rpm_1s,1201859611,5110935768283636594,min,0.0
+75,1374207915,rpm_1s,1201859611,5110935768283636594,mean,1183.672643902938
+75,1374207915,rpm_1s,1201859611,5110935768283636594,max,2634.0
+13,430930200,coolant_1s,1201859611,-1944483825207215539,min,17.0
+13,430930200,coolant_1s,1201859611,-1944483825207215539,mean,80.0719329761365
+13,430930200,coolant_1s,1201859611,-1944483825207215539,max,94.0
+13,430930200,speed_1s_diff,1201859611,-1944483825207215539,min,-18.0
+13,430930200,speed_1s_diff,1201859611,-1944483825207215539,mean,0.0
+13,430930200,speed_1s_diff,1201859611,-1944483825207215539,max,14.0
+65,430930200,coolant_1s,1201859611,7663622432945194351,min,24.0
+65,430930200,coolant_1s,1201859611,7663622432945194351,mean,76.26011430381257
+65,430930200,coolant_1s,1201859611,7663622432945194351,max,94.0
+65,430930200,speed_1s_diff,1201859611,7663622432945194351,min,-25.0
+65,430930200,speed_1s_diff,1201859611,7663622432945194351,mean,0.005864017934646281
+65,430930200,speed_1s_diff,1201859611,7663622432945194351,max,14.0
+13,1374207915,Engine RPM,1201859611,-1944483825207215539,min,0.0
+13,1374207915,Engine RPM,1201859611,-1944483825207215539,mean,1658.5566166308397
+13,1374207915,Engine RPM,1201859611,-1944483825207215539,max,3008.0
+13,1374207915,rpm_1s,1201859611,-1944483825207215539,min,0.0
+13,1374207915,rpm_1s,1201859611,-1944483825207215539,mean,1658.472083582725
+13,1374207915,rpm_1s,1201859611,-1944483825207215539,max,3008.0
+65,1374207915,Engine RPM,1201859611,7663622432945194351,min,0.0
+65,1374207915,Engine RPM,1201859611,7663622432945194351,mean,1033.478161341088
+65,1374207915,Engine RPM,1201859611,7663622432945194351,max,2707.0
+65,1374207915,rpm_1s,1201859611,7663622432945194351,min,0.0
+65,1374207915,rpm_1s,1201859611,7663622432945194351,mean,1032.6033623438395
+65,1374207915,rpm_1s,1201859611,7663622432945194351,max,2631.0
+5,430930200,coolant_1s,1201859611,3570466563200151918,min,30.0
+5,430930200,coolant_1s,1201859611,3570466563200151918,mean,86.50359106161959
+5,430930200,coolant_1s,1201859611,3570466563200151918,max,93.0
+5,430930200,speed_1s_diff,1201859611,3570466563200151918,min,-49.0
+5,430930200,speed_1s_diff,1201859611,3570466563200151918,mean,0.0
+5,430930200,speed_1s_diff,1201859611,3570466563200151918,max,39.0
+45,430930200,coolant_1s,1201859611,4031524100617037594,min,89.0
+45,430930200,coolant_1s,1201859611,4031524100617037594,mean,90.3648305900915
+45,430930200,coolant_1s,1201859611,4031524100617037594,max,93.0
+45,430930200,speed_1s_diff,1201859611,4031524100617037594,min,-10.0
+45,430930200,speed_1s_diff,1201859611,4031524100617037594,mean,-0.024986709967894028
+45,430930200,speed_1s_diff,1201859611,4031524100617037594,max,11.0
+5,1374207915,Engine RPM,1201859611,3570466563200151918,min,0.0
+5,1374207915,Engine RPM,1201859611,3570466563200151918,mean,1490.0271707300046
+5,1374207915,Engine RPM,1201859611,3570466563200151918,max,3658.0
+5,1374207915,rpm_1s,1201859611,3570466563200151918,min,0.0
+5,1374207915,rpm_1s,1201859611,3570466563200151918,mean,1490.4425669184475
+5,1374207915,rpm_1s,1201859611,3570466563200151918,max,3646.0
+45,1374207915,Engine RPM,1201859611,4031524100617037594,min,0.0
+45,1374207915,Engine RPM,1201859611,4031524100617037594,mean,1373.587975935938
+45,1374207915,Engine RPM,1201859611,4031524100617037594,max,2373.0
+45,1374207915,rpm_1s,1201859611,4031524100617037594,min,0.0
+45,1374207915,rpm_1s,1201859611,4031524100617037594,mean,1373.0976292449066
+45,1374207915,rpm_1s,1201859611,4031524100617037594,max,2373.0
+7,430930200,coolant_1s,1201859611,-7434725489921472031,min,30.0
+7,430930200,coolant_1s,1201859611,-7434725489921472031,mean,86.36500257550907
+7,430930200,coolant_1s,1201859611,-7434725489921472031,max,96.0
+7,430930200,speed_1s_diff,1201859611,-7434725489921472031,min,-13.0
+7,430930200,speed_1s_diff,1201859611,-7434725489921472031,mean,0.0
+7,430930200,speed_1s_diff,1201859611,-7434725489921472031,max,13.0
+7,1374207915,Engine RPM,1201859611,-7434725489921472031,min,0.0
+7,1374207915,Engine RPM,1201859611,-7434725489921472031,mean,1589.8300959432972
+7,1374207915,Engine RPM,1201859611,-7434725489921472031,max,3072.0
+7,1374207915,rpm_1s,1201859611,-7434725489921472031,min,0.0
+7,1374207915,rpm_1s,1201859611,-7434725489921472031,mean,1589.9044574597033
+7,1374207915,rpm_1s,1201859611,-7434725489921472031,max,3072.0
+41,430930200,coolant_1s,1201859611,8437372657831499562,min,7.0
+41,430930200,coolant_1s,1201859611,8437372657831499562,mean,75.75952443698638
+41,430930200,coolant_1s,1201859611,8437372657831499562,max,95.0
+41,430930200,speed_1s_diff,1201859611,8437372657831499562,min,-14.0
+41,430930200,speed_1s_diff,1201859611,8437372657831499562,mean,0.0
+41,430930200,speed_1s_diff,1201859611,8437372657831499562,max,17.0
+41,1374207915,Engine RPM,1201859611,8437372657831499562,min,0.0
+41,1374207915,Engine RPM,1201859611,8437372657831499562,mean,1577.3415777230712
+41,1374207915,Engine RPM,1201859611,8437372657831499562,max,2764.0
+41,1374207915,rpm_1s,1201859611,8437372657831499562,min,0.0
+41,1374207915,rpm_1s,1201859611,8437372657831499562,mean,1577.8877736555137
+41,1374207915,rpm_1s,1201859611,8437372657831499562,max,2764.0
+72,430930200,coolant_1s,1201859611,-4361505612007328068,min,22.0
+72,430930200,coolant_1s,1201859611,-4361505612007328068,mean,71.81384326508437
+72,430930200,coolant_1s,1201859611,-4361505612007328068,max,94.0
+72,430930200,speed_1s_diff,1201859611,-4361505612007328068,min,-13.0
+72,430930200,speed_1s_diff,1201859611,-4361505612007328068,mean,0.0
+72,430930200,speed_1s_diff,1201859611,-4361505612007328068,max,17.0
+72,1374207915,Engine RPM,1201859611,-4361505612007328068,min,0.0
+72,1374207915,Engine RPM,1201859611,-4361505612007328068,mean,1238.9722056881737
+72,1374207915,Engine RPM,1201859611,-4361505612007328068,max,2747.0
+72,1374207915,rpm_1s,1201859611,-4361505612007328068,min,0.0
+72,1374207915,rpm_1s,1201859611,-4361505612007328068,mean,1237.592792664496
+72,1374207915,rpm_1s,1201859611,-4361505612007328068,max,2747.0
+63,430930200,coolant_1s,1201859611,-1533397159073443657,min,2.0
+63,430930200,coolant_1s,1201859611,-1533397159073443657,mean,51.822878863979106
+63,430930200,coolant_1s,1201859611,-1533397159073443657,max,70.0
+63,430930200,speed_1s_diff,1201859611,-1533397159073443657,min,-13.0
+63,430930200,speed_1s_diff,1201859611,-1533397159073443657,mean,0.0
+63,430930200,speed_1s_diff,1201859611,-1533397159073443657,max,15.0
+64,430930200,coolant_1s,1201859611,1978039981849347272,min,28.0
+64,430930200,coolant_1s,1201859611,1978039981849347272,mean,75.63724535841438
+64,430930200,coolant_1s,1201859611,1978039981849347272,max,94.0
+64,430930200,speed_1s_diff,1201859611,1978039981849347272,min,-18.0
+64,430930200,speed_1s_diff,1201859611,1978039981849347272,mean,0.0
+64,430930200,speed_1s_diff,1201859611,1978039981849347272,max,16.0
+63,1374207915,Engine RPM,1201859611,-1533397159073443657,min,0.0
+63,1374207915,Engine RPM,1201859611,-1533397159073443657,mean,1174.1847038505005
+63,1374207915,Engine RPM,1201859611,-1533397159073443657,max,2580.0
+63,1374207915,rpm_1s,1201859611,-1533397159073443657,min,0.0
+63,1374207915,rpm_1s,1201859611,-1533397159073443657,mean,1174.0833446165725
+63,1374207915,rpm_1s,1201859611,-1533397159073443657,max,2396.0
+64,1374207915,Engine RPM,1201859611,1978039981849347272,min,0.0
+64,1374207915,Engine RPM,1201859611,1978039981849347272,mean,1053.9283989931587
+64,1374207915,Engine RPM,1201859611,1978039981849347272,max,2979.0
+64,1374207915,rpm_1s,1201859611,1978039981849347272,min,0.0
+64,1374207915,rpm_1s,1201859611,1978039981849347272,mean,1054.365356277456
+64,1374207915,rpm_1s,1201859611,1978039981849347272,max,2964.0
+66,430930200,coolant_1s,1201859611,6831185462073925864,min,-1.0
+66,430930200,coolant_1s,1201859611,6831185462073925864,mean,45.183517242335085
+66,430930200,coolant_1s,1201859611,6831185462073925864,max,64.0
+66,430930200,speed_1s_diff,1201859611,6831185462073925864,min,-13.0
+66,430930200,speed_1s_diff,1201859611,6831185462073925864,mean,0.0
+66,430930200,speed_1s_diff,1201859611,6831185462073925864,max,15.0
+66,1374207915,Engine RPM,1201859611,6831185462073925864,min,0.0
+66,1374207915,Engine RPM,1201859611,6831185462073925864,mean,1308.5081461022414
+66,1374207915,Engine RPM,1201859611,6831185462073925864,max,2577.0
+66,1374207915,rpm_1s,1201859611,6831185462073925864,min,0.0
+66,1374207915,rpm_1s,1201859611,6831185462073925864,mean,1308.4926628904345
+66,1374207915,rpm_1s,1201859611,6831185462073925864,max,2577.0
+1,430930200,coolant_1s,1201859611,-928762887014768240,min,20.0
+1,430930200,coolant_1s,1201859611,-928762887014768240,mean,81.47659307165077
+1,430930200,coolant_1s,1201859611,-928762887014768240,max,93.0
+1,430930200,speed_1s_diff,1201859611,-928762887014768240,min,-20.0
+1,430930200,speed_1s_diff,1201859611,-928762887014768240,mean,1.237015359033603E-4
+1,430930200,speed_1s_diff,1201859611,-928762887014768240,max,16.0
+15,430930200,coolant_1s,1201859611,8620646447947530699,min,29.0
+15,430930200,coolant_1s,1201859611,8620646447947530699,mean,86.09125401444594
+15,430930200,coolant_1s,1201859611,8620646447947530699,max,100.0
+15,430930200,speed_1s_diff,1201859611,8620646447947530699,min,-15.0
+15,430930200,speed_1s_diff,1201859611,8620646447947530699,mean,0.0
+15,430930200,speed_1s_diff,1201859611,8620646447947530699,max,18.0
+77,430930200,coolant_1s,1201859611,8292766365109962602,min,30.0
+77,430930200,coolant_1s,1201859611,8292766365109962602,mean,71.54822269117298
+77,430930200,coolant_1s,1201859611,8292766365109962602,max,95.0
+77,430930200,speed_1s_diff,1201859611,8292766365109962602,min,-16.0
+77,430930200,speed_1s_diff,1201859611,8292766365109962602,mean,0.0
+77,430930200,speed_1s_diff,1201859611,8292766365109962602,max,12.0
+1,1374207915,Engine RPM,1201859611,-928762887014768240,min,0.0
+1,1374207915,Engine RPM,1201859611,-928762887014768240,mean,1303.5914744669135
+1,1374207915,Engine RPM,1201859611,-928762887014768240,max,3430.0
+1,1374207915,rpm_1s,1201859611,-928762887014768240,min,0.0
+1,1374207915,rpm_1s,1201859611,-928762887014768240,mean,1304.1977853971118
+1,1374207915,rpm_1s,1201859611,-928762887014768240,max,3430.0
+15,1374207915,Engine RPM,1201859611,8620646447947530699,min,0.0
+15,1374207915,Engine RPM,1201859611,8620646447947530699,mean,1402.273463882204
+15,1374207915,Engine RPM,1201859611,8620646447947530699,max,2882.0
+15,1374207915,rpm_1s,1201859611,8620646447947530699,min,0.0
+15,1374207915,rpm_1s,1201859611,8620646447947530699,mean,1402.6216503190205
+15,1374207915,rpm_1s,1201859611,8620646447947530699,max,2882.0
+77,1374207915,Engine RPM,1201859611,8292766365109962602,min,0.0
+77,1374207915,Engine RPM,1201859611,8292766365109962602,mean,1277.184755053257
+77,1374207915,Engine RPM,1201859611,8292766365109962602,max,2312.0
+77,1374207915,rpm_1s,1201859611,8292766365109962602,min,0.0
+77,1374207915,rpm_1s,1201859611,8292766365109962602,mean,1278.857758598333
+77,1374207915,rpm_1s,1201859611,8292766365109962602,max,2312.0
+3,430930200,coolant_1s,1201859611,3286614479096879038,min,31.0
+3,430930200,coolant_1s,1201859611,3286614479096879038,mean,86.7964519026567
+3,430930200,coolant_1s,1201859611,3286614479096879038,max,101.0
+3,430930200,speed_1s_diff,1201859611,3286614479096879038,min,-16.0
+3,430930200,speed_1s_diff,1201859611,3286614479096879038,mean,0.0
+3,430930200,speed_1s_diff,1201859611,3286614479096879038,max,16.0
+37,430930200,coolant_1s,1201859611,-3015666225396535697,min,15.0
+37,430930200,coolant_1s,1201859611,-3015666225396535697,mean,78.68194046069054
+37,430930200,coolant_1s,1201859611,-3015666225396535697,max,94.0
+37,430930200,speed_1s_diff,1201859611,-3015666225396535697,min,-20.0
+37,430930200,speed_1s_diff,1201859611,-3015666225396535697,mean,0.0
+37,430930200,speed_1s_diff,1201859611,-3015666225396535697,max,15.0
+3,1374207915,Engine RPM,1201859611,3286614479096879038,min,0.0
+3,1374207915,Engine RPM,1201859611,3286614479096879038,mean,1433.0548539779463
+3,1374207915,Engine RPM,1201859611,3286614479096879038,max,3111.0
+3,1374207915,rpm_1s,1201859611,3286614479096879038,min,0.0
+3,1374207915,rpm_1s,1201859611,3286614479096879038,mean,1432.434283788608
+3,1374207915,rpm_1s,1201859611,3286614479096879038,max,3111.0
+37,1374207915,Engine RPM,1201859611,-3015666225396535697,min,0.0
+37,1374207915,Engine RPM,1201859611,-3015666225396535697,mean,1537.2965390817742
+37,1374207915,Engine RPM,1201859611,-3015666225396535697,max,2991.0
+37,1374207915,rpm_1s,1201859611,-3015666225396535697,min,0.0
+37,1374207915,rpm_1s,1201859611,-3015666225396535697,mean,1537.6658722062787
+37,1374207915,rpm_1s,1201859611,-3015666225396535697,max,2991.0
+27,430930200,coolant_1s,1201859611,-3534261932286880816,min,31.0
+27,430930200,coolant_1s,1201859611,-3534261932286880816,mean,84.65791740302966
+27,430930200,coolant_1s,1201859611,-3534261932286880816,max,94.0
+27,430930200,speed_1s_diff,1201859611,-3534261932286880816,min,-14.0
+27,430930200,speed_1s_diff,1201859611,-3534261932286880816,mean,0.0
+27,430930200,speed_1s_diff,1201859611,-3534261932286880816,max,12.0
+57,430930200,coolant_1s,1201859611,8939056937015267775,min,73.0
+57,430930200,coolant_1s,1201859611,8939056937015267775,mean,88.20734359065447
+57,430930200,coolant_1s,1201859611,8939056937015267775,max,92.0
+57,430930200,speed_1s_diff,1201859611,8939056937015267775,min,-13.0
+57,430930200,speed_1s_diff,1201859611,8939056937015267775,mean,0.0
+57,430930200,speed_1s_diff,1201859611,8939056937015267775,max,12.0
+27,1374207915,Engine RPM,1201859611,-3534261932286880816,min,0.0
+27,1374207915,Engine RPM,1201859611,-3534261932286880816,mean,1441.383705808705
+27,1374207915,Engine RPM,1201859611,-3534261932286880816,max,2501.0
+27,1374207915,rpm_1s,1201859611,-3534261932286880816,min,0.0
+27,1374207915,rpm_1s,1201859611,-3534261932286880816,mean,1441.9025204368031
+27,1374207915,rpm_1s,1201859611,-3534261932286880816,max,2501.0
+57,1374207915,Engine RPM,1201859611,8939056937015267775,min,0.0
+57,1374207915,Engine RPM,1201859611,8939056937015267775,mean,1606.2501516080329
+57,1374207915,Engine RPM,1201859611,8939056937015267775,max,2830.0
+57,1374207915,rpm_1s,1201859611,8939056937015267775,min,0.0
+57,1374207915,rpm_1s,1201859611,8939056937015267775,mean,1605.4117230493096
+57,1374207915,rpm_1s,1201859611,8939056937015267775,max,2830.0
+24,430930200,coolant_1s,1201859611,7437298780708400991,min,29.0
+24,430930200,coolant_1s,1201859611,7437298780708400991,mean,84.96984794432757
+24,430930200,coolant_1s,1201859611,7437298780708400991,max,94.0
+24,430930200,speed_1s_diff,1201859611,7437298780708400991,min,-19.0
+24,430930200,speed_1s_diff,1201859611,7437298780708400991,mean,0.0
+24,430930200,speed_1s_diff,1201859611,7437298780708400991,max,16.0
+24,1374207915,Engine RPM,1201859611,7437298780708400991,min,0.0
+24,1374207915,Engine RPM,1201859611,7437298780708400991,mean,1468.2751083580695
+24,1374207915,Engine RPM,1201859611,7437298780708400991,max,2606.0
+24,1374207915,rpm_1s,1201859611,7437298780708400991,min,0.0
+24,1374207915,rpm_1s,1201859611,7437298780708400991,mean,1468.2773554513326
+24,1374207915,rpm_1s,1201859611,7437298780708400991,max,2529.0
+52,430930200,coolant_1s,1201859611,1261300119102051947,min,64.0
+52,430930200,coolant_1s,1201859611,1261300119102051947,mean,89.34073331886282
+52,430930200,coolant_1s,1201859611,1261300119102051947,max,93.0
+52,430930200,speed_1s_diff,1201859611,1261300119102051947,min,-19.0
+52,430930200,speed_1s_diff,1201859611,1261300119102051947,mean,0.0
+52,430930200,speed_1s_diff,1201859611,1261300119102051947,max,13.0
+52,1374207915,Engine RPM,1201859611,1261300119102051947,min,0.0
+52,1374207915,Engine RPM,1201859611,1261300119102051947,mean,1549.1329341290623
+52,1374207915,Engine RPM,1201859611,1261300119102051947,max,2645.0
+52,1374207915,rpm_1s,1201859611,1261300119102051947,min,0.0
+52,1374207915,rpm_1s,1201859611,1261300119102051947,mean,1548.1622332550762
+52,1374207915,rpm_1s,1201859611,1261300119102051947,max,2644.0
+28,430930200,coolant_1s,1201859611,1040427654212385429,min,16.0
+28,430930200,coolant_1s,1201859611,1040427654212385429,mean,80.05025592424866
+28,430930200,coolant_1s,1201859611,1040427654212385429,max,94.0
+28,430930200,speed_1s_diff,1201859611,1040427654212385429,min,-33.0
+28,430930200,speed_1s_diff,1201859611,1040427654212385429,mean,0.0
+28,430930200,speed_1s_diff,1201859611,1040427654212385429,max,15.0
+28,1374207915,Engine RPM,1201859611,1040427654212385429,min,0.0
+28,1374207915,Engine RPM,1201859611,1040427654212385429,mean,1501.7751038300937
+28,1374207915,Engine RPM,1201859611,1040427654212385429,max,2592.0
+28,1374207915,rpm_1s,1201859611,1040427654212385429,min,0.0
+28,1374207915,rpm_1s,1201859611,1040427654212385429,mean,1502.2266075213695
+28,1374207915,rpm_1s,1201859611,1040427654212385429,max,2592.0
+54,430930200,coolant_1s,1201859611,3329842588530743954,min,67.0
+54,430930200,coolant_1s,1201859611,3329842588530743954,mean,89.93323482747094
+54,430930200,coolant_1s,1201859611,3329842588530743954,max,94.0
+54,430930200,speed_1s_diff,1201859611,3329842588530743954,min,-12.0
+54,430930200,speed_1s_diff,1201859611,3329842588530743954,mean,0.0
+54,430930200,speed_1s_diff,1201859611,3329842588530743954,max,14.0
+54,1374207915,Engine RPM,1201859611,3329842588530743954,min,0.0
+54,1374207915,Engine RPM,1201859611,3329842588530743954,mean,1568.8600029955448
+54,1374207915,Engine RPM,1201859611,3329842588530743954,max,2892.0
+54,1374207915,rpm_1s,1201859611,3329842588530743954,min,0.0
+54,1374207915,rpm_1s,1201859611,3329842588530743954,mean,1568.6361157110307
+54,1374207915,rpm_1s,1201859611,3329842588530743954,max,2892.0
+39,430930200,coolant_1s,1201859611,1077981989772730875,min,55.0
+39,430930200,coolant_1s,1201859611,1077981989772730875,mean,88.40019071146253
+39,430930200,coolant_1s,1201859611,1077981989772730875,max,96.0
+39,430930200,speed_1s_diff,1201859611,1077981989772730875,min,-16.0
+39,430930200,speed_1s_diff,1201859611,1077981989772730875,mean,0.0
+39,430930200,speed_1s_diff,1201859611,1077981989772730875,max,16.0
+39,1374207915,Engine RPM,1201859611,1077981989772730875,min,0.0
+39,1374207915,Engine RPM,1201859611,1077981989772730875,mean,1546.1728841483266
+39,1374207915,Engine RPM,1201859611,1077981989772730875,max,3788.0
+39,1374207915,rpm_1s,1201859611,1077981989772730875,min,0.0
+39,1374207915,rpm_1s,1201859611,1077981989772730875,mean,1545.3243398661664
+39,1374207915,rpm_1s,1201859611,1077981989772730875,max,3511.0
+35,430930200,coolant_1s,1201859611,-5091309530965523951,min,9.0
+35,430930200,coolant_1s,1201859611,-5091309530965523951,mean,77.25033440079919
+35,430930200,coolant_1s,1201859611,-5091309530965523951,max,95.0
+35,430930200,speed_1s_diff,1201859611,-5091309530965523951,min,-25.0
+35,430930200,speed_1s_diff,1201859611,-5091309530965523951,mean,0.0
+35,430930200,speed_1s_diff,1201859611,-5091309530965523951,max,14.0
+35,1374207915,Engine RPM,1201859611,-5091309530965523951,min,0.0
+35,1374207915,Engine RPM,1201859611,-5091309530965523951,mean,1562.233448947786
+35,1374207915,Engine RPM,1201859611,-5091309530965523951,max,2759.0
+35,1374207915,rpm_1s,1201859611,-5091309530965523951,min,0.0
+35,1374207915,rpm_1s,1201859611,-5091309530965523951,mean,1563.170036056927
+35,1374207915,rpm_1s,1201859611,-5091309530965523951,max,2759.0
+43,430930200,coolant_1s,1201859611,-5059295208201598568,min,12.0
+43,430930200,coolant_1s,1201859611,-5059295208201598568,mean,75.0400358853205
+43,430930200,coolant_1s,1201859611,-5059295208201598568,max,94.0
+43,430930200,speed_1s_diff,1201859611,-5059295208201598568,min,-18.0
+43,430930200,speed_1s_diff,1201859611,-5059295208201598568,mean,0.0
+43,430930200,speed_1s_diff,1201859611,-5059295208201598568,max,19.0
+43,1374207915,Engine RPM,1201859611,-5059295208201598568,min,0.0
+43,1374207915,Engine RPM,1201859611,-5059295208201598568,mean,1613.6854438659657
+43,1374207915,Engine RPM,1201859611,-5059295208201598568,max,2792.0
+43,1374207915,rpm_1s,1201859611,-5059295208201598568,min,0.0
+43,1374207915,rpm_1s,1201859611,-5059295208201598568,mean,1614.2016543166471
+43,1374207915,rpm_1s,1201859611,-5059295208201598568,max,2792.0
+49,430930200,coolant_1s,1201859611,-8089365359392949673,min,17.0
+49,430930200,coolant_1s,1201859611,-8089365359392949673,mean,79.0921866724866
+49,430930200,coolant_1s,1201859611,-8089365359392949673,max,94.0
+49,430930200,speed_1s_diff,1201859611,-8089365359392949673,min,-16.0
+49,430930200,speed_1s_diff,1201859611,-8089365359392949673,mean,0.0
+49,430930200,speed_1s_diff,1201859611,-8089365359392949673,max,18.0
+49,1374207915,Engine RPM,1201859611,-8089365359392949673,min,262.0
+49,1374207915,Engine RPM,1201859611,-8089365359392949673,mean,1587.8939076350948
+49,1374207915,Engine RPM,1201859611,-8089365359392949673,max,3136.0
+49,1374207915,rpm_1s,1201859611,-8089365359392949673,min,767.0
+49,1374207915,rpm_1s,1201859611,-8089365359392949673,mean,1588.1983050075885
+49,1374207915,rpm_1s,1201859611,-8089365359392949673,max,3136.0
+22,430930200,coolant_1s,1201859611,1702604805711203895,min,40.0
+22,430930200,coolant_1s,1201859611,1702604805711203895,mean,86.71665559683699
+22,430930200,coolant_1s,1201859611,1702604805711203895,max,93.0
+22,430930200,speed_1s_diff,1201859611,1702604805711203895,min,-14.0
+22,430930200,speed_1s_diff,1201859611,1702604805711203895,mean,0.0
+22,430930200,speed_1s_diff,1201859611,1702604805711203895,max,18.0
+76,430930200,coolant_1s,1201859611,1604686477683752455,min,11.0
+76,430930200,coolant_1s,1201859611,1604686477683752455,mean,57.1490059364652
+76,430930200,coolant_1s,1201859611,1604686477683752455,max,84.0
+76,430930200,speed_1s_diff,1201859611,1604686477683752455,min,-11.0
+76,430930200,speed_1s_diff,1201859611,1604686477683752455,mean,0.0
+76,430930200,speed_1s_diff,1201859611,1604686477683752455,max,12.0
+22,1374207915,Engine RPM,1201859611,1702604805711203895,min,0.0
+22,1374207915,Engine RPM,1201859611,1702604805711203895,mean,1585.0838331225893
+22,1374207915,Engine RPM,1201859611,1702604805711203895,max,3563.0
+22,1374207915,rpm_1s,1201859611,1702604805711203895,min,0.0
+22,1374207915,rpm_1s,1201859611,1702604805711203895,mean,1584.5281453411126
+22,1374207915,rpm_1s,1201859611,1702604805711203895,max,3563.0
+76,1374207915,Engine RPM,1201859611,1604686477683752455,min,0.0
+76,1374207915,Engine RPM,1201859611,1604686477683752455,mean,1273.3780449260523
+76,1374207915,Engine RPM,1201859611,1604686477683752455,max,2590.0
+76,1374207915,rpm_1s,1201859611,1604686477683752455,min,0.0
+76,1374207915,rpm_1s,1201859611,1604686477683752455,mean,1273.3005575325442
+76,1374207915,rpm_1s,1201859611,1604686477683752455,max,2590.0
+16,430930200,coolant_1s,1201859611,-5204063793680029428,min,12.0
+16,430930200,coolant_1s,1201859611,-5204063793680029428,mean,79.50146401315618
+16,430930200,coolant_1s,1201859611,-5204063793680029428,max,94.0
+16,430930200,speed_1s_diff,1201859611,-5204063793680029428,min,-17.0
+16,430930200,speed_1s_diff,1201859611,-5204063793680029428,mean,0.0
+16,430930200,speed_1s_diff,1201859611,-5204063793680029428,max,14.0
+70,430930200,coolant_1s,1201859611,382192189422054743,min,25.0
+70,430930200,coolant_1s,1201859611,382192189422054743,mean,72.15030700325742
+70,430930200,coolant_1s,1201859611,382192189422054743,max,94.0
+70,430930200,speed_1s_diff,1201859611,382192189422054743,min,-14.0
+70,430930200,speed_1s_diff,1201859611,382192189422054743,mean,0.0
+70,430930200,speed_1s_diff,1201859611,382192189422054743,max,13.0
+16,1374207915,Engine RPM,1201859611,-5204063793680029428,min,0.0
+16,1374207915,Engine RPM,1201859611,-5204063793680029428,mean,1755.3705995771268
+16,1374207915,Engine RPM,1201859611,-5204063793680029428,max,3177.0
+16,1374207915,rpm_1s,1201859611,-5204063793680029428,min,0.0
+16,1374207915,rpm_1s,1201859611,-5204063793680029428,mean,1754.9468463514531
+16,1374207915,rpm_1s,1201859611,-5204063793680029428,max,3177.0
+70,1374207915,Engine RPM,1201859611,382192189422054743,min,0.0
+70,1374207915,Engine RPM,1201859611,382192189422054743,mean,1159.3155368583996
+70,1374207915,Engine RPM,1201859611,382192189422054743,max,2534.0
+70,1374207915,rpm_1s,1201859611,382192189422054743,min,0.0
+70,1374207915,rpm_1s,1201859611,382192189422054743,mean,1156.745666899119
+70,1374207915,rpm_1s,1201859611,382192189422054743,max,2534.0
+4,430930200,coolant_1s,1201859611,3413059497672580978,min,12.0
+4,430930200,coolant_1s,1201859611,3413059497672580978,mean,84.76381528899793
+4,430930200,coolant_1s,1201859611,3413059497672580978,max,95.0
+4,430930200,speed_1s_diff,1201859611,3413059497672580978,min,-15.0
+4,430930200,speed_1s_diff,1201859611,3413059497672580978,mean,0.0
+4,430930200,speed_1s_diff,1201859611,3413059497672580978,max,16.0
+8,430930200,coolant_1s,1201859611,-6047692687641618500,min,65.0
+8,430930200,coolant_1s,1201859611,-6047692687641618500,mean,86.09475987773212
+8,430930200,coolant_1s,1201859611,-6047692687641618500,max,93.0
+8,430930200,speed_1s_diff,1201859611,-6047692687641618500,min,-15.0
+8,430930200,speed_1s_diff,1201859611,-6047692687641618500,mean,0.0
+8,430930200,speed_1s_diff,1201859611,-6047692687641618500,max,13.0
+4,1374207915,Engine RPM,1201859611,3413059497672580978,min,0.0
+4,1374207915,Engine RPM,1201859611,3413059497672580978,mean,1526.1111291013062
+4,1374207915,Engine RPM,1201859611,3413059497672580978,max,3347.0
+4,1374207915,rpm_1s,1201859611,3413059497672580978,min,0.0
+4,1374207915,rpm_1s,1201859611,3413059497672580978,mean,1526.3905646579901
+4,1374207915,rpm_1s,1201859611,3413059497672580978,max,3347.0
+8,1374207915,Engine RPM,1201859611,-6047692687641618500,min,0.0
+8,1374207915,Engine RPM,1201859611,-6047692687641618500,mean,1569.5588696551204
+8,1374207915,Engine RPM,1201859611,-6047692687641618500,max,3385.0
+8,1374207915,rpm_1s,1201859611,-6047692687641618500,min,0.0
+8,1374207915,rpm_1s,1201859611,-6047692687641618500,mean,1570.0512537118273
+8,1374207915,rpm_1s,1201859611,-6047692687641618500,max,3385.0
+20,430930200,coolant_1s,1201859611,3772502504273691038,min,30.0
+20,430930200,coolant_1s,1201859611,3772502504273691038,mean,84.58550302593561
+20,430930200,coolant_1s,1201859611,3772502504273691038,max,94.0
+20,430930200,speed_1s_diff,1201859611,3772502504273691038,min,-17.0
+20,430930200,speed_1s_diff,1201859611,3772502504273691038,mean,0.0
+20,430930200,speed_1s_diff,1201859611,3772502504273691038,max,14.0
+20,1374207915,Engine RPM,1201859611,3772502504273691038,min,0.0
+20,1374207915,Engine RPM,1201859611,3772502504273691038,mean,1415.7485190357797
+20,1374207915,Engine RPM,1201859611,3772502504273691038,max,2556.0
+20,1374207915,rpm_1s,1201859611,3772502504273691038,min,0.0
+20,1374207915,rpm_1s,1201859611,3772502504273691038,mean,1414.2253118880474
+20,1374207915,rpm_1s,1201859611,3772502504273691038,max,2556.0
+44,430930200,coolant_1s,1201859611,819277571598892579,min,31.0
+44,430930200,coolant_1s,1201859611,819277571598892579,mean,85.31638420615018
+44,430930200,coolant_1s,1201859611,819277571598892579,max,97.0
+44,430930200,speed_1s_diff,1201859611,819277571598892579,min,-19.0
+44,430930200,speed_1s_diff,1201859611,819277571598892579,mean,0.0
+44,430930200,speed_1s_diff,1201859611,819277571598892579,max,17.0
+44,1374207915,Engine RPM,1201859611,819277571598892579,min,0.0
+44,1374207915,Engine RPM,1201859611,819277571598892579,mean,1664.4286443199032
+44,1374207915,Engine RPM,1201859611,819277571598892579,max,3328.0
+44,1374207915,rpm_1s,1201859611,819277571598892579,min,0.0
+44,1374207915,rpm_1s,1201859611,819277571598892579,mean,1665.364066064984
+44,1374207915,rpm_1s,1201859611,819277571598892579,max,3328.0
+30,430930200,coolant_1s,1201859611,-4509912318961261997,min,34.0
+30,430930200,coolant_1s,1201859611,-4509912318961261997,mean,83.29358140675642
+30,430930200,coolant_1s,1201859611,-4509912318961261997,max,94.0
+30,430930200,speed_1s_diff,1201859611,-4509912318961261997,min,-12.0
+30,430930200,speed_1s_diff,1201859611,-4509912318961261997,mean,0.0
+30,430930200,speed_1s_diff,1201859611,-4509912318961261997,max,17.0
+50,430930200,coolant_1s,1201859611,4815143990666075450,min,12.0
+50,430930200,coolant_1s,1201859611,4815143990666075450,mean,79.43062367363801
+50,430930200,coolant_1s,1201859611,4815143990666075450,max,94.0
+50,430930200,speed_1s_diff,1201859611,4815143990666075450,min,-17.0
+50,430930200,speed_1s_diff,1201859611,4815143990666075450,mean,0.0
+50,430930200,speed_1s_diff,1201859611,4815143990666075450,max,22.0
+80,430930200,coolant_1s,1201859611,-8519964041684473957,min,16.0
+80,430930200,coolant_1s,1201859611,-8519964041684473957,mean,62.131615743914985
+80,430930200,coolant_1s,1201859611,-8519964041684473957,max,91.0
+80,430930200,speed_1s_diff,1201859611,-8519964041684473957,min,-19.0
+80,430930200,speed_1s_diff,1201859611,-8519964041684473957,mean,0.0
+80,430930200,speed_1s_diff,1201859611,-8519964041684473957,max,16.0
+30,1374207915,Engine RPM,1201859611,-4509912318961261997,min,0.0
+30,1374207915,Engine RPM,1201859611,-4509912318961261997,mean,1467.0242777097128
+30,1374207915,Engine RPM,1201859611,-4509912318961261997,max,2775.0
+30,1374207915,rpm_1s,1201859611,-4509912318961261997,min,0.0
+30,1374207915,rpm_1s,1201859611,-4509912318961261997,mean,1466.814784883101
+30,1374207915,rpm_1s,1201859611,-4509912318961261997,max,2775.0
+50,1374207915,Engine RPM,1201859611,4815143990666075450,min,259.0
+50,1374207915,Engine RPM,1201859611,4815143990666075450,mean,1558.7114812813386
+50,1374207915,Engine RPM,1201859611,4815143990666075450,max,2820.0
+50,1374207915,rpm_1s,1201859611,4815143990666075450,min,259.0
+50,1374207915,rpm_1s,1201859611,4815143990666075450,mean,1558.8581966952029
+50,1374207915,rpm_1s,1201859611,4815143990666075450,max,2820.0
+80,1374207915,Engine RPM,1201859611,-8519964041684473957,min,761.0
+80,1374207915,Engine RPM,1201859611,-8519964041684473957,mean,1354.3557869387535
+80,1374207915,Engine RPM,1201859611,-8519964041684473957,max,2785.0
+80,1374207915,rpm_1s,1201859611,-8519964041684473957,min,761.0
+80,1374207915,rpm_1s,1201859611,-8519964041684473957,mean,1354.5041639900755
+80,1374207915,rpm_1s,1201859611,-8519964041684473957,max,2785.0
+33,430930200,coolant_1s,1201859611,-6056317502954263346,min,75.0
+33,430930200,coolant_1s,1201859611,-6056317502954263346,mean,90.50106921824994
+33,430930200,coolant_1s,1201859611,-6056317502954263346,max,103.0
+33,430930200,speed_1s_diff,1201859611,-6056317502954263346,min,-12.0
+33,430930200,speed_1s_diff,1201859611,-6056317502954263346,mean,-0.0037190205587934637
+33,430930200,speed_1s_diff,1201859611,-6056317502954263346,max,13.0
+33,1374207915,Engine RPM,1201859611,-6056317502954263346,min,0.0
+33,1374207915,Engine RPM,1201859611,-6056317502954263346,mean,1602.7588354916938
+33,1374207915,Engine RPM,1201859611,-6056317502954263346,max,3200.0
+33,1374207915,rpm_1s,1201859611,-6056317502954263346,min,0.0
+33,1374207915,rpm_1s,1201859611,-6056317502954263346,mean,1602.8919513063895
+33,1374207915,rpm_1s,1201859611,-6056317502954263346,max,3200.0
+10,430930200,coolant_1s,1201859611,799052670452099784,min,8.0
+10,430930200,coolant_1s,1201859611,799052670452099784,mean,74.14057927361539
+10,430930200,coolant_1s,1201859611,799052670452099784,max,93.0
+10,430930200,speed_1s_diff,1201859611,799052670452099784,min,-19.0
+10,430930200,speed_1s_diff,1201859611,799052670452099784,mean,0.0
+10,430930200,speed_1s_diff,1201859611,799052670452099784,max,16.0
+10,1374207915,Engine RPM,1201859611,799052670452099784,min,0.0
+10,1374207915,Engine RPM,1201859611,799052670452099784,mean,1314.130454751977
+10,1374207915,Engine RPM,1201859611,799052670452099784,max,2545.0
+10,1374207915,rpm_1s,1201859611,799052670452099784,min,0.0
+10,1374207915,rpm_1s,1201859611,799052670452099784,mean,1313.5844632662936
+10,1374207915,rpm_1s,1201859611,799052670452099784,max,2545.0
+40,430930200,coolant_1s,1201859611,8695525273439978119,min,29.0
+40,430930200,coolant_1s,1201859611,8695525273439978119,mean,84.2353432147225
+40,430930200,coolant_1s,1201859611,8695525273439978119,max,94.0
+40,430930200,speed_1s_diff,1201859611,8695525273439978119,min,-14.0
+40,430930200,speed_1s_diff,1201859611,8695525273439978119,mean,0.0
+40,430930200,speed_1s_diff,1201859611,8695525273439978119,max,15.0
+46,430930200,coolant_1s,1201859611,-6047084444995931845,min,11.0
+46,430930200,coolant_1s,1201859611,-6047084444995931845,mean,77.08755838797703
+46,430930200,coolant_1s,1201859611,-6047084444995931845,max,94.0
+46,430930200,speed_1s_diff,1201859611,-6047084444995931845,min,-16.0
+46,430930200,speed_1s_diff,1201859611,-6047084444995931845,mean,0.0
+46,430930200,speed_1s_diff,1201859611,-6047084444995931845,max,16.0
+40,1374207915,Engine RPM,1201859611,8695525273439978119,min,0.0
+40,1374207915,Engine RPM,1201859611,8695525273439978119,mean,1467.4526332701093
+40,1374207915,Engine RPM,1201859611,8695525273439978119,max,2880.0
+40,1374207915,rpm_1s,1201859611,8695525273439978119,min,0.0
+40,1374207915,rpm_1s,1201859611,8695525273439978119,mean,1466.7077663342445
+40,1374207915,rpm_1s,1201859611,8695525273439978119,max,2880.0
+46,1374207915,Engine RPM,1201859611,-6047084444995931845,min,0.0
+46,1374207915,Engine RPM,1201859611,-6047084444995931845,mean,1544.2393300147194
+46,1374207915,Engine RPM,1201859611,-6047084444995931845,max,2886.0
+46,1374207915,rpm_1s,1201859611,-6047084444995931845,min,0.0
+46,1374207915,rpm_1s,1201859611,-6047084444995931845,mean,1544.5573050577018
+46,1374207915,rpm_1s,1201859611,-6047084444995931845,max,2886.0
+53,430930200,coolant_1s,1201859611,-8118175245509467954,min,40.0
+53,430930200,coolant_1s,1201859611,-8118175245509467954,mean,80.60631342221673
+53,430930200,coolant_1s,1201859611,-8118175245509467954,max,94.0
+53,430930200,speed_1s_diff,1201859611,-8118175245509467954,min,-33.0
+53,430930200,speed_1s_diff,1201859611,-8118175245509467954,mean,0.0
+53,430930200,speed_1s_diff,1201859611,-8118175245509467954,max,43.0
+53,1374207915,Engine RPM,1201859611,-8118175245509467954,min,0.0
+53,1374207915,Engine RPM,1201859611,-8118175245509467954,mean,1498.354045653113
+53,1374207915,Engine RPM,1201859611,-8118175245509467954,max,2616.0
+53,1374207915,rpm_1s,1201859611,-8118175245509467954,min,0.0
+53,1374207915,rpm_1s,1201859611,-8118175245509467954,mean,1498.9662048460405
+53,1374207915,rpm_1s,1201859611,-8118175245509467954,max,2616.0
+34,430930200,coolant_1s,1201859611,-5864392879735263686,min,14.0
+34,430930200,coolant_1s,1201859611,-5864392879735263686,mean,74.21999745200145
+34,430930200,coolant_1s,1201859611,-5864392879735263686,max,94.0
+34,430930200,speed_1s_diff,1201859611,-5864392879735263686,min,-15.0
+34,430930200,speed_1s_diff,1201859611,-5864392879735263686,mean,0.0
+34,430930200,speed_1s_diff,1201859611,-5864392879735263686,max,17.0
+34,1374207915,Engine RPM,1201859611,-5864392879735263686,min,0.0
+34,1374207915,Engine RPM,1201859611,-5864392879735263686,mean,1384.0831067538634
+34,1374207915,Engine RPM,1201859611,-5864392879735263686,max,2975.0
+34,1374207915,rpm_1s,1201859611,-5864392879735263686,min,0.0
+34,1374207915,rpm_1s,1201859611,-5864392879735263686,mean,1384.2135636709313
+34,1374207915,rpm_1s,1201859611,-5864392879735263686,max,2975.0
+17,430930200,coolant_1s,1201859611,1920899555557485333,min,29.0
+17,430930200,coolant_1s,1201859611,1920899555557485333,mean,84.91004600337592
+17,430930200,coolant_1s,1201859611,1920899555557485333,max,94.0
+17,430930200,speed_1s_diff,1201859611,1920899555557485333,min,-13.0
+17,430930200,speed_1s_diff,1201859611,1920899555557485333,mean,0.0
+17,430930200,speed_1s_diff,1201859611,1920899555557485333,max,16.0
+17,1374207915,Engine RPM,1201859611,1920899555557485333,min,0.0
+17,1374207915,Engine RPM,1201859611,1920899555557485333,mean,1377.4672276716858
+17,1374207915,Engine RPM,1201859611,1920899555557485333,max,2689.0
+17,1374207915,rpm_1s,1201859611,1920899555557485333,min,0.0
+17,1374207915,rpm_1s,1201859611,1920899555557485333,mean,1377.5334634684446
+17,1374207915,rpm_1s,1201859611,1920899555557485333,max,2689.0
+11,430930200,coolant_1s,1201859611,-5926799641916171529,min,30.0
+11,430930200,coolant_1s,1201859611,-5926799641916171529,mean,86.09463730157664
+11,430930200,coolant_1s,1201859611,-5926799641916171529,max,93.0
+11,430930200,speed_1s_diff,1201859611,-5926799641916171529,min,-17.0
+11,430930200,speed_1s_diff,1201859611,-5926799641916171529,mean,0.0
+11,430930200,speed_1s_diff,1201859611,-5926799641916171529,max,24.0
+11,1374207915,Engine RPM,1201859611,-5926799641916171529,min,0.0
+11,1374207915,Engine RPM,1201859611,-5926799641916171529,mean,1487.563622831777
+11,1374207915,Engine RPM,1201859611,-5926799641916171529,max,3096.0
+11,1374207915,rpm_1s,1201859611,-5926799641916171529,min,0.0
+11,1374207915,rpm_1s,1201859611,-5926799641916171529,mean,1488.538408599783
+11,1374207915,rpm_1s,1201859611,-5926799641916171529,max,3096.0
+12,430930200,coolant_1s,1201859611,621520344043114077,min,8.0
+12,430930200,coolant_1s,1201859611,621520344043114077,mean,82.9118961763229
+12,430930200,coolant_1s,1201859611,621520344043114077,max,95.0
+12,430930200,speed_1s_diff,1201859611,621520344043114077,min,-106.0
+12,430930200,speed_1s_diff,1201859611,621520344043114077,mean,0.0
+12,430930200,speed_1s_diff,1201859611,621520344043114077,max,16.0
+12,1374207915,Engine RPM,1201859611,621520344043114077,min,0.0
+12,1374207915,Engine RPM,1201859611,621520344043114077,mean,1735.6059370257688
+12,1374207915,Engine RPM,1201859611,621520344043114077,max,3272.0
+12,1374207915,rpm_1s,1201859611,621520344043114077,min,0.0
+12,1374207915,rpm_1s,1201859611,621520344043114077,mean,1735.2661037425933
+12,1374207915,rpm_1s,1201859611,621520344043114077,max,3272.0
+81,430930200,coolant_1s,1201859611,-3039017337926577879,min,22.0
+81,430930200,coolant_1s,1201859611,-3039017337926577879,mean,53.439409149490345
+81,430930200,coolant_1s,1201859611,-3039017337926577879,max,79.0
+81,430930200,speed_1s_diff,1201859611,-3039017337926577879,min,-26.0
+81,430930200,speed_1s_diff,1201859611,-3039017337926577879,mean,0.04375876884315798
+81,430930200,speed_1s_diff,1201859611,-3039017337926577879,max,11.0
+81,1374207915,Engine RPM,1201859611,-3039017337926577879,min,0.0
+81,1374207915,Engine RPM,1201859611,-3039017337926577879,mean,1418.3154289942524
+81,1374207915,Engine RPM,1201859611,-3039017337926577879,max,2562.0
+81,1374207915,rpm_1s,1201859611,-3039017337926577879,min,0.0
+81,1374207915,rpm_1s,1201859611,-3039017337926577879,mean,1418.4741891692327
+81,1374207915,rpm_1s,1201859611,-3039017337926577879,max,2551.0
+25,430930200,coolant_1s,1201859611,-5797038905919203719,min,33.0
+25,430930200,coolant_1s,1201859611,-5797038905919203719,mean,85.00035794413705
+25,430930200,coolant_1s,1201859611,-5797038905919203719,max,94.0
+25,430930200,speed_1s_diff,1201859611,-5797038905919203719,min,-20.0
+25,430930200,speed_1s_diff,1201859611,-5797038905919203719,mean,0.0
+25,430930200,speed_1s_diff,1201859611,-5797038905919203719,max,12.0
+25,1374207915,Engine RPM,1201859611,-5797038905919203719,min,0.0
+25,1374207915,Engine RPM,1201859611,-5797038905919203719,mean,1410.768188471251
+25,1374207915,Engine RPM,1201859611,-5797038905919203719,max,2472.0
+25,1374207915,rpm_1s,1201859611,-5797038905919203719,min,0.0
+25,1374207915,rpm_1s,1201859611,-5797038905919203719,mean,1409.4206712007287
+25,1374207915,rpm_1s,1201859611,-5797038905919203719,max,2472.0
+9,430930200,coolant_1s,1201859611,7816983797231647252,min,42.0
+9,430930200,coolant_1s,1201859611,7816983797231647252,mean,88.33248425676416
+9,430930200,coolant_1s,1201859611,7816983797231647252,max,96.0
+9,430930200,speed_1s_diff,1201859611,7816983797231647252,min,-12.0
+9,430930200,speed_1s_diff,1201859611,7816983797231647252,mean,0.0
+9,430930200,speed_1s_diff,1201859611,7816983797231647252,max,9.0
+9,1374207915,Engine RPM,1201859611,7816983797231647252,min,0.0
+9,1374207915,Engine RPM,1201859611,7816983797231647252,mean,1632.3920343462057
+9,1374207915,Engine RPM,1201859611,7816983797231647252,max,3220.0
+9,1374207915,rpm_1s,1201859611,7816983797231647252,min,0.0
+9,1374207915,rpm_1s,1201859611,7816983797231647252,mean,1631.8401591437218
+9,1374207915,rpm_1s,1201859611,7816983797231647252,max,3220.0
+38,430930200,coolant_1s,1201859611,8006099507947146869,min,9.0
+38,430930200,coolant_1s,1201859611,8006099507947146869,mean,77.4673975336255
+38,430930200,coolant_1s,1201859611,8006099507947146869,max,95.0
+38,430930200,speed_1s_diff,1201859611,8006099507947146869,min,-13.0
+38,430930200,speed_1s_diff,1201859611,8006099507947146869,mean,0.0
+38,430930200,speed_1s_diff,1201859611,8006099507947146869,max,14.0
+38,1374207915,Engine RPM,1201859611,8006099507947146869,min,0.0
+38,1374207915,Engine RPM,1201859611,8006099507947146869,mean,1514.527505517681
+38,1374207915,Engine RPM,1201859611,8006099507947146869,max,2737.0
+38,1374207915,rpm_1s,1201859611,8006099507947146869,min,0.0
+38,1374207915,rpm_1s,1201859611,8006099507947146869,mean,1514.4074666291426
+38,1374207915,rpm_1s,1201859611,8006099507947146869,max,2737.0
+14,430930200,coolant_1s,1201859611,2702796889550818087,min,31.0
+14,430930200,coolant_1s,1201859611,2702796889550818087,mean,85.51840970358967
+14,430930200,coolant_1s,1201859611,2702796889550818087,max,93.0
+14,430930200,speed_1s_diff,1201859611,2702796889550818087,min,-17.0
+14,430930200,speed_1s_diff,1201859611,2702796889550818087,mean,0.0
+14,430930200,speed_1s_diff,1201859611,2702796889550818087,max,22.0
+29,430930200,coolant_1s,1201859611,6556782270497265025,min,9.0
+29,430930200,coolant_1s,1201859611,6556782270497265025,mean,75.96046944173662
+29,430930200,coolant_1s,1201859611,6556782270497265025,max,95.0
+29,430930200,speed_1s_diff,1201859611,6556782270497265025,min,-16.0
+29,430930200,speed_1s_diff,1201859611,6556782270497265025,mean,0.0
+29,430930200,speed_1s_diff,1201859611,6556782270497265025,max,17.0
+14,1374207915,Engine RPM,1201859611,2702796889550818087,min,0.0
+14,1374207915,Engine RPM,1201859611,2702796889550818087,mean,1605.0366029386216
+14,1374207915,Engine RPM,1201859611,2702796889550818087,max,3406.0
+14,1374207915,rpm_1s,1201859611,2702796889550818087,min,0.0
+14,1374207915,rpm_1s,1201859611,2702796889550818087,mean,1605.6637174961218
+14,1374207915,rpm_1s,1201859611,2702796889550818087,max,3406.0
+29,1374207915,Engine RPM,1201859611,6556782270497265025,min,0.0
+29,1374207915,Engine RPM,1201859611,6556782270497265025,mean,1476.2099141015954
+29,1374207915,Engine RPM,1201859611,6556782270497265025,max,2787.0
+29,1374207915,rpm_1s,1201859611,6556782270497265025,min,0.0
+29,1374207915,rpm_1s,1201859611,6556782270497265025,mean,1476.2314320344917
+29,1374207915,rpm_1s,1201859611,6556782270497265025,max,2787.0
+61,430930200,coolant_1s,1201859611,-6074962741691029584,min,51.0
+61,430930200,coolant_1s,1201859611,-6074962741691029584,mean,87.06961921460437
+61,430930200,coolant_1s,1201859611,-6074962741691029584,max,95.0
+61,430930200,speed_1s_diff,1201859611,-6074962741691029584,min,-11.0
+61,430930200,speed_1s_diff,1201859611,-6074962741691029584,mean,8.636548144402064E-4
+61,430930200,speed_1s_diff,1201859611,-6074962741691029584,max,12.0
+68,430930200,coolant_1s,1201859611,-4052763906460934747,min,10.0
+68,430930200,coolant_1s,1201859611,-4052763906460934747,mean,52.49620163606516
+68,430930200,coolant_1s,1201859611,-4052763906460934747,max,74.0
+68,430930200,speed_1s_diff,1201859611,-4052763906460934747,min,-16.0
+68,430930200,speed_1s_diff,1201859611,-4052763906460934747,mean,0.0
+68,430930200,speed_1s_diff,1201859611,-4052763906460934747,max,13.0
+61,1374207915,Engine RPM,1201859611,-6074962741691029584,min,0.0
+61,1374207915,Engine RPM,1201859611,-6074962741691029584,mean,1618.4896166180724
+61,1374207915,Engine RPM,1201859611,-6074962741691029584,max,2544.0
+61,1374207915,rpm_1s,1201859611,-6074962741691029584,min,0.0
+61,1374207915,rpm_1s,1201859611,-6074962741691029584,mean,1618.8330209751616
+61,1374207915,rpm_1s,1201859611,-6074962741691029584,max,2544.0
+68,1374207915,Engine RPM,1201859611,-4052763906460934747,min,0.0
+68,1374207915,Engine RPM,1201859611,-4052763906460934747,mean,1295.0187685856379
+68,1374207915,Engine RPM,1201859611,-4052763906460934747,max,2818.0
+68,1374207915,rpm_1s,1201859611,-4052763906460934747,min,0.0
+68,1374207915,rpm_1s,1201859611,-4052763906460934747,mean,1292.8835138849922
+68,1374207915,rpm_1s,1201859611,-4052763906460934747,max,2594.0
+51,430930200,coolant_1s,1201859611,-8939688255858788198,min,31.0
+51,430930200,coolant_1s,1201859611,-8939688255858788198,mean,85.44686515384528
+51,430930200,coolant_1s,1201859611,-8939688255858788198,max,94.0
+51,430930200,speed_1s_diff,1201859611,-8939688255858788198,min,-20.0
+51,430930200,speed_1s_diff,1201859611,-8939688255858788198,mean,0.0
+51,430930200,speed_1s_diff,1201859611,-8939688255858788198,max,17.0
+51,1374207915,Engine RPM,1201859611,-8939688255858788198,min,0.0
+51,1374207915,Engine RPM,1201859611,-8939688255858788198,mean,1804.0610418580354
+51,1374207915,Engine RPM,1201859611,-8939688255858788198,max,3682.0
+51,1374207915,rpm_1s,1201859611,-8939688255858788198,min,0.0
+51,1374207915,rpm_1s,1201859611,-8939688255858788198,mean,1803.5733463865897
+51,1374207915,rpm_1s,1201859611,-8939688255858788198,max,3682.0
+23,430930200,coolant_1s,1201859611,1667169207552354980,min,26.0
+23,430930200,coolant_1s,1201859611,1667169207552354980,mean,85.88982616138162
+23,430930200,coolant_1s,1201859611,1667169207552354980,max,94.0
+23,430930200,speed_1s_diff,1201859611,1667169207552354980,min,-27.0
+23,430930200,speed_1s_diff,1201859611,1667169207552354980,mean,0.0
+23,430930200,speed_1s_diff,1201859611,1667169207552354980,max,16.0
+23,1374207915,Engine RPM,1201859611,1667169207552354980,min,0.0
+23,1374207915,Engine RPM,1201859611,1667169207552354980,mean,1646.6120441591759
+23,1374207915,Engine RPM,1201859611,1667169207552354980,max,3497.0
+23,1374207915,rpm_1s,1201859611,1667169207552354980,min,0.0
+23,1374207915,rpm_1s,1201859611,1667169207552354980,mean,1646.1574882712498
+23,1374207915,rpm_1s,1201859611,1667169207552354980,max,3497.0
+36,430930200,coolant_1s,1201859611,-4482700421175446431,min,5.0
+36,430930200,coolant_1s,1201859611,-4482700421175446431,mean,76.04038870429954
+36,430930200,coolant_1s,1201859611,-4482700421175446431,max,95.0
+36,430930200,speed_1s_diff,1201859611,-4482700421175446431,min,-18.0
+36,430930200,speed_1s_diff,1201859611,-4482700421175446431,mean,0.0
+36,430930200,speed_1s_diff,1201859611,-4482700421175446431,max,13.0
+36,1374207915,Engine RPM,1201859611,-4482700421175446431,min,0.0
+36,1374207915,Engine RPM,1201859611,-4482700421175446431,mean,1543.0496063472635
+36,1374207915,Engine RPM,1201859611,-4482700421175446431,max,2557.0
+36,1374207915,rpm_1s,1201859611,-4482700421175446431,min,0.0
+36,1374207915,rpm_1s,1201859611,-4482700421175446431,mean,1542.1204170821707
+36,1374207915,rpm_1s,1201859611,-4482700421175446431,max,2557.0
+60,430930200,coolant_1s,1201859611,1782504894750799124,min,87.0
+60,430930200,coolant_1s,1201859611,1782504894750799124,mean,89.50172041549492
+60,430930200,coolant_1s,1201859611,1782504894750799124,max,91.0
+60,430930200,speed_1s_diff,1201859611,1782504894750799124,min,-16.0
+60,430930200,speed_1s_diff,1201859611,1782504894750799124,mean,0.0
+60,430930200,speed_1s_diff,1201859611,1782504894750799124,max,16.0
+60,1374207915,Engine RPM,1201859611,1782504894750799124,min,353.0
+60,1374207915,Engine RPM,1201859611,1782504894750799124,mean,1471.711980551247
+60,1374207915,Engine RPM,1201859611,1782504894750799124,max,2923.0
+60,1374207915,rpm_1s,1201859611,1782504894750799124,min,353.0
+60,1374207915,rpm_1s,1201859611,1782504894750799124,mean,1472.4990132958542
+60,1374207915,rpm_1s,1201859611,1782504894750799124,max,2923.0
+18,430930200,coolant_1s,1201859611,7002524279394511806,min,17.0
+18,430930200,coolant_1s,1201859611,7002524279394511806,mean,80.69768318984516
+18,430930200,coolant_1s,1201859611,7002524279394511806,max,94.0
+18,430930200,speed_1s_diff,1201859611,7002524279394511806,min,-15.0
+18,430930200,speed_1s_diff,1201859611,7002524279394511806,mean,0.0
+18,430930200,speed_1s_diff,1201859611,7002524279394511806,max,16.0
+32,430930200,coolant_1s,1201859611,-5227152884695581212,min,13.0
+32,430930200,coolant_1s,1201859611,-5227152884695581212,mean,78.1569788861685
+32,430930200,coolant_1s,1201859611,-5227152884695581212,max,94.0
+32,430930200,speed_1s_diff,1201859611,-5227152884695581212,min,-16.0
+32,430930200,speed_1s_diff,1201859611,-5227152884695581212,mean,0.0
+32,430930200,speed_1s_diff,1201859611,-5227152884695581212,max,11.0
+18,1374207915,Engine RPM,1201859611,7002524279394511806,min,0.0
+18,1374207915,Engine RPM,1201859611,7002524279394511806,mean,1747.0170084475576
+18,1374207915,Engine RPM,1201859611,7002524279394511806,max,3359.0
+18,1374207915,rpm_1s,1201859611,7002524279394511806,min,0.0
+18,1374207915,rpm_1s,1201859611,7002524279394511806,mean,1746.8726163625854
+18,1374207915,rpm_1s,1201859611,7002524279394511806,max,3359.0
+32,1374207915,Engine RPM,1201859611,-5227152884695581212,min,0.0
+32,1374207915,Engine RPM,1201859611,-5227152884695581212,mean,1547.4348167386645
+32,1374207915,Engine RPM,1201859611,-5227152884695581212,max,2584.0
+32,1374207915,rpm_1s,1201859611,-5227152884695581212,min,0.0
+32,1374207915,rpm_1s,1201859611,-5227152884695581212,mean,1547.6998655206403
+32,1374207915,rpm_1s,1201859611,-5227152884695581212,max,2584.0
+42,430930200,coolant_1s,1201859611,-4744849287861989057,min,12.0
+42,430930200,coolant_1s,1201859611,-4744849287861989057,mean,76.7494228555472
+42,430930200,coolant_1s,1201859611,-4744849287861989057,max,94.0
+42,430930200,speed_1s_diff,1201859611,-4744849287861989057,min,-18.0
+42,430930200,speed_1s_diff,1201859611,-4744849287861989057,mean,0.0
+42,430930200,speed_1s_diff,1201859611,-4744849287861989057,max,18.0
+55,430930200,coolant_1s,1201859611,-7312033798960504347,min,34.0
+55,430930200,coolant_1s,1201859611,-7312033798960504347,mean,84.60425601861019
+55,430930200,coolant_1s,1201859611,-7312033798960504347,max,94.0
+55,430930200,speed_1s_diff,1201859611,-7312033798960504347,min,-16.0
+55,430930200,speed_1s_diff,1201859611,-7312033798960504347,mean,0.0
+55,430930200,speed_1s_diff,1201859611,-7312033798960504347,max,16.0
+42,1374207915,Engine RPM,1201859611,-4744849287861989057,min,0.0
+42,1374207915,Engine RPM,1201859611,-4744849287861989057,mean,1597.5412463324378
+42,1374207915,Engine RPM,1201859611,-4744849287861989057,max,3019.0
+42,1374207915,rpm_1s,1201859611,-4744849287861989057,min,0.0
+42,1374207915,rpm_1s,1201859611,-4744849287861989057,mean,1598.1903238423918
+42,1374207915,rpm_1s,1201859611,-4744849287861989057,max,3019.0
+55,1374207915,Engine RPM,1201859611,-7312033798960504347,min,0.0
+55,1374207915,Engine RPM,1201859611,-7312033798960504347,mean,1492.36312314672
+55,1374207915,Engine RPM,1201859611,-7312033798960504347,max,2472.0
+55,1374207915,rpm_1s,1201859611,-7312033798960504347,min,0.0
+55,1374207915,rpm_1s,1201859611,-7312033798960504347,mean,1491.5621365257193
+55,1374207915,rpm_1s,1201859611,-7312033798960504347,max,2472.0
+67,430930200,coolant_1s,1201859611,-2034556668057200826,min,27.0
+67,430930200,coolant_1s,1201859611,-2034556668057200826,mean,69.42530660544675
+67,430930200,coolant_1s,1201859611,-2034556668057200826,max,95.0
+67,430930200,speed_1s_diff,1201859611,-2034556668057200826,min,-12.0
+67,430930200,speed_1s_diff,1201859611,-2034556668057200826,mean,0.0
+67,430930200,speed_1s_diff,1201859611,-2034556668057200826,max,13.0
+69,430930200,coolant_1s,1201859611,5297223516241122749,min,90.0
+69,430930200,coolant_1s,1201859611,5297223516241122749,mean,90.62423930893844
+69,430930200,coolant_1s,1201859611,5297223516241122749,max,92.0
+69,430930200,speed_1s_diff,1201859611,5297223516241122749,min,-14.0
+69,430930200,speed_1s_diff,1201859611,5297223516241122749,mean,0.0
+69,430930200,speed_1s_diff,1201859611,5297223516241122749,max,13.0
+67,1374207915,Engine RPM,1201859611,-2034556668057200826,min,0.0
+67,1374207915,Engine RPM,1201859611,-2034556668057200826,mean,1114.4268945866636
+67,1374207915,Engine RPM,1201859611,-2034556668057200826,max,2616.0
+67,1374207915,rpm_1s,1201859611,-2034556668057200826,min,0.0
+67,1374207915,rpm_1s,1201859611,-2034556668057200826,mean,1114.4915454302397
+67,1374207915,rpm_1s,1201859611,-2034556668057200826,max,2616.0
+69,1374207915,Engine RPM,1201859611,5297223516241122749,min,0.0
+69,1374207915,Engine RPM,1201859611,5297223516241122749,mean,936.3941858379201
+69,1374207915,Engine RPM,1201859611,5297223516241122749,max,3109.0
+69,1374207915,rpm_1s,1201859611,5297223516241122749,min,0.0
+69,1374207915,rpm_1s,1201859611,5297223516241122749,mean,935.9267717744602
+69,1374207915,rpm_1s,1201859611,5297223516241122749,max,3109.0
+59,430930200,coolant_1s,1201859611,4526313175717176312,min,25.0
+59,430930200,coolant_1s,1201859611,4526313175717176312,mean,86.59994838482683
+59,430930200,coolant_1s,1201859611,4526313175717176312,max,101.0
+59,430930200,speed_1s_diff,1201859611,4526313175717176312,min,-33.0
+59,430930200,speed_1s_diff,1201859611,4526313175717176312,mean,0.0
+59,430930200,speed_1s_diff,1201859611,4526313175717176312,max,22.0
+59,1374207915,Engine RPM,1201859611,4526313175717176312,min,0.0
+59,1374207915,Engine RPM,1201859611,4526313175717176312,mean,1651.2476025409273
+59,1374207915,Engine RPM,1201859611,4526313175717176312,max,2868.0
+59,1374207915,rpm_1s,1201859611,4526313175717176312,min,0.0
+59,1374207915,rpm_1s,1201859611,4526313175717176312,mean,1650.9548130846458
+59,1374207915,rpm_1s,1201859611,4526313175717176312,max,2868.0
+26,430930200,coolant_1s,1201859611,8588031391057209555,min,14.0
+26,430930200,coolant_1s,1201859611,8588031391057209555,mean,80.88859170596483
+26,430930200,coolant_1s,1201859611,8588031391057209555,max,94.0
+26,430930200,speed_1s_diff,1201859611,8588031391057209555,min,-16.0
+26,430930200,speed_1s_diff,1201859611,8588031391057209555,mean,0.0
+26,430930200,speed_1s_diff,1201859611,8588031391057209555,max,17.0
+26,1374207915,Engine RPM,1201859611,8588031391057209555,min,0.0
+26,1374207915,Engine RPM,1201859611,8588031391057209555,mean,1426.7536469896415
+26,1374207915,Engine RPM,1201859611,8588031391057209555,max,3404.0
+26,1374207915,rpm_1s,1201859611,8588031391057209555,min,0.0
+26,1374207915,rpm_1s,1201859611,8588031391057209555,mean,1426.1991845250093
+26,1374207915,rpm_1s,1201859611,8588031391057209555,max,3404.0
+47,430930200,coolant_1s,1201859611,3287404548624903503,min,54.0
+47,430930200,coolant_1s,1201859611,3287404548624903503,mean,88.95273887062815
+47,430930200,coolant_1s,1201859611,3287404548624903503,max,94.0
+47,430930200,speed_1s_diff,1201859611,3287404548624903503,min,-15.0
+47,430930200,speed_1s_diff,1201859611,3287404548624903503,mean,0.0
+47,430930200,speed_1s_diff,1201859611,3287404548624903503,max,15.0
+74,430930200,coolant_1s,1201859611,-2523754915779770005,min,18.0
+74,430930200,coolant_1s,1201859611,-2523754915779770005,mean,59.68428026208551
+74,430930200,coolant_1s,1201859611,-2523754915779770005,max,95.0
+74,430930200,speed_1s_diff,1201859611,-2523754915779770005,min,-15.0
+74,430930200,speed_1s_diff,1201859611,-2523754915779770005,mean,0.0
+74,430930200,speed_1s_diff,1201859611,-2523754915779770005,max,13.0
+47,1374207915,Engine RPM,1201859611,3287404548624903503,min,0.0
+47,1374207915,Engine RPM,1201859611,3287404548624903503,mean,1507.952652274933
+47,1374207915,Engine RPM,1201859611,3287404548624903503,max,2910.0
+47,1374207915,rpm_1s,1201859611,3287404548624903503,min,0.0
+47,1374207915,rpm_1s,1201859611,3287404548624903503,mean,1507.281984084302
+47,1374207915,rpm_1s,1201859611,3287404548624903503,max,2910.0
+74,1374207915,Engine RPM,1201859611,-2523754915779770005,min,270.0
+74,1374207915,Engine RPM,1201859611,-2523754915779770005,mean,1330.4312155224095
+74,1374207915,Engine RPM,1201859611,-2523754915779770005,max,2400.0
+74,1374207915,rpm_1s,1201859611,-2523754915779770005,min,753.0
+74,1374207915,rpm_1s,1201859611,-2523754915779770005,mean,1331.2984095495528
+74,1374207915,rpm_1s,1201859611,-2523754915779770005,max,2400.0
+62,430930200,coolant_1s,1201859611,2045378005814092654,min,88.0
+62,430930200,coolant_1s,1201859611,2045378005814092654,mean,89.70193074767752
+62,430930200,coolant_1s,1201859611,2045378005814092654,max,91.0
+62,430930200,speed_1s_diff,1201859611,2045378005814092654,min,-17.0
+62,430930200,speed_1s_diff,1201859611,2045378005814092654,mean,-0.009263578225744286
+62,430930200,speed_1s_diff,1201859611,2045378005814092654,max,14.0
+62,1374207915,Engine RPM,1201859611,2045378005814092654,min,0.0
+62,1374207915,Engine RPM,1201859611,2045378005814092654,mean,1369.4898685597188
+62,1374207915,Engine RPM,1201859611,2045378005814092654,max,2902.0
+62,1374207915,rpm_1s,1201859611,2045378005814092654,min,0.0
+62,1374207915,rpm_1s,1201859611,2045378005814092654,mean,1369.6446403953091
+62,1374207915,rpm_1s,1201859611,2045378005814092654,max,2902.0
+31,430930200,coolant_1s,1201859611,27763026888340976,min,14.0
+31,430930200,coolant_1s,1201859611,27763026888340976,mean,79.9971604410171
+31,430930200,coolant_1s,1201859611,27763026888340976,max,94.0
+31,430930200,speed_1s_diff,1201859611,27763026888340976,min,-14.0
+31,430930200,speed_1s_diff,1201859611,27763026888340976,mean,0.0
+31,430930200,speed_1s_diff,1201859611,27763026888340976,max,17.0
+31,1374207915,Engine RPM,1201859611,27763026888340976,min,0.0
+31,1374207915,Engine RPM,1201859611,27763026888340976,mean,1450.6490898227182
+31,1374207915,Engine RPM,1201859611,27763026888340976,max,2665.0
+31,1374207915,rpm_1s,1201859611,27763026888340976,min,0.0
+31,1374207915,rpm_1s,1201859611,27763026888340976,mean,1450.1808124311776
+31,1374207915,rpm_1s,1201859611,27763026888340976,max,2665.0
+56,430930200,coolant_1s,1201859611,-1655462257548164667,min,62.0
+56,430930200,coolant_1s,1201859611,-1655462257548164667,mean,89.53733216897544
+56,430930200,coolant_1s,1201859611,-1655462257548164667,max,95.0
+56,430930200,speed_1s_diff,1201859611,-1655462257548164667,min,-13.0
+56,430930200,speed_1s_diff,1201859611,-1655462257548164667,mean,-0.017717034755572354
+56,430930200,speed_1s_diff,1201859611,-1655462257548164667,max,14.0
+56,1374207915,Engine RPM,1201859611,-1655462257548164667,min,0.0
+56,1374207915,Engine RPM,1201859611,-1655462257548164667,mean,1593.7827630223187
+56,1374207915,Engine RPM,1201859611,-1655462257548164667,max,2718.0
+56,1374207915,rpm_1s,1201859611,-1655462257548164667,min,0.0
+56,1374207915,rpm_1s,1201859611,-1655462257548164667,mean,1593.3649536052717
+56,1374207915,rpm_1s,1201859611,-1655462257548164667,max,2718.0
+73,430930200,coolant_1s,1201859611,4038311380879008479,min,4.0
+73,430930200,coolant_1s,1201859611,4038311380879008479,mean,46.54092278189158
+73,430930200,coolant_1s,1201859611,4038311380879008479,max,72.0
+73,430930200,speed_1s_diff,1201859611,4038311380879008479,min,-10.0
+73,430930200,speed_1s_diff,1201859611,4038311380879008479,mean,0.0
+73,430930200,speed_1s_diff,1201859611,4038311380879008479,max,14.0
+73,1374207915,Engine RPM,1201859611,4038311380879008479,min,0.0
+73,1374207915,Engine RPM,1201859611,4038311380879008479,mean,1448.0903827211644
+73,1374207915,Engine RPM,1201859611,4038311380879008479,max,2539.0
+73,1374207915,rpm_1s,1201859611,4038311380879008479,min,0.0
+73,1374207915,rpm_1s,1201859611,4038311380879008479,mean,1450.9755159680149
+73,1374207915,rpm_1s,1201859611,4038311380879008479,max,2539.0
+6,430930200,coolant_1s,1201859611,5601914189163822671,min,25.0
+6,430930200,coolant_1s,1201859611,5601914189163822671,mean,85.90773515201593
+6,430930200,coolant_1s,1201859611,5601914189163822671,max,94.0
+6,430930200,speed_1s_diff,1201859611,5601914189163822671,min,-18.0
+6,430930200,speed_1s_diff,1201859611,5601914189163822671,mean,0.0
+6,430930200,speed_1s_diff,1201859611,5601914189163822671,max,15.0
+78,430930200,coolant_1s,1201859611,-6903129361326617772,min,88.0
+78,430930200,coolant_1s,1201859611,-6903129361326617772,mean,89.44107063957185
+78,430930200,coolant_1s,1201859611,-6903129361326617772,max,91.0
+78,430930200,speed_1s_diff,1201859611,-6903129361326617772,min,-15.0
+78,430930200,speed_1s_diff,1201859611,-6903129361326617772,mean,0.0
+78,430930200,speed_1s_diff,1201859611,-6903129361326617772,max,19.0
+6,1374207915,Engine RPM,1201859611,5601914189163822671,min,0.0
+6,1374207915,Engine RPM,1201859611,5601914189163822671,mean,1605.4123428562618
+6,1374207915,Engine RPM,1201859611,5601914189163822671,max,3333.0
+6,1374207915,rpm_1s,1201859611,5601914189163822671,min,0.0
+6,1374207915,rpm_1s,1201859611,5601914189163822671,mean,1605.9475991382276
+6,1374207915,rpm_1s,1201859611,5601914189163822671,max,3333.0
+78,1374207915,Engine RPM,1201859611,-6903129361326617772,min,0.0
+78,1374207915,Engine RPM,1201859611,-6903129361326617772,mean,1503.6662265415732
+78,1374207915,Engine RPM,1201859611,-6903129361326617772,max,3632.0
+78,1374207915,rpm_1s,1201859611,-6903129361326617772,min,0.0
+78,1374207915,rpm_1s,1201859611,-6903129361326617772,mean,1500.6144988381545
+78,1374207915,rpm_1s,1201859611,-6903129361326617772,max,3632.0
+19,430930200,coolant_1s,1201859611,107421361993047462,min,16.0
+19,430930200,coolant_1s,1201859611,107421361993047462,mean,77.2196362542339
+19,430930200,coolant_1s,1201859611,107421361993047462,max,93.0
+19,430930200,speed_1s_diff,1201859611,107421361993047462,min,-11.0
+19,430930200,speed_1s_diff,1201859611,107421361993047462,mean,0.0
+19,430930200,speed_1s_diff,1201859611,107421361993047462,max,12.0
+48,430930200,coolant_1s,1201859611,2089968467991060156,min,37.0
+48,430930200,coolant_1s,1201859611,2089968467991060156,mean,85.42180610184076
+48,430930200,coolant_1s,1201859611,2089968467991060156,max,94.0
+48,430930200,speed_1s_diff,1201859611,2089968467991060156,min,-16.0
+48,430930200,speed_1s_diff,1201859611,2089968467991060156,mean,0.0
+48,430930200,speed_1s_diff,1201859611,2089968467991060156,max,17.0
+79,430930200,coolant_1s,1201859611,2185843916018887178,min,24.0
+79,430930200,coolant_1s,1201859611,2185843916018887178,mean,67.72829867036053
+79,430930200,coolant_1s,1201859611,2185843916018887178,max,95.0
+79,430930200,speed_1s_diff,1201859611,2185843916018887178,min,-12.0
+79,430930200,speed_1s_diff,1201859611,2185843916018887178,mean,0.0
+79,430930200,speed_1s_diff,1201859611,2185843916018887178,max,13.0
+19,1374207915,Engine RPM,1201859611,107421361993047462,min,0.0
+19,1374207915,Engine RPM,1201859611,107421361993047462,mean,1597.5680194141225
+19,1374207915,Engine RPM,1201859611,107421361993047462,max,2889.0
+19,1374207915,rpm_1s,1201859611,107421361993047462,min,0.0
+19,1374207915,rpm_1s,1201859611,107421361993047462,mean,1597.4993485791213
+19,1374207915,rpm_1s,1201859611,107421361993047462,max,2889.0
+48,1374207915,Engine RPM,1201859611,2089968467991060156,min,0.0
+48,1374207915,Engine RPM,1201859611,2089968467991060156,mean,1603.6036940271517
+48,1374207915,Engine RPM,1201859611,2089968467991060156,max,2721.0
+48,1374207915,rpm_1s,1201859611,2089968467991060156,min,0.0
+48,1374207915,rpm_1s,1201859611,2089968467991060156,mean,1603.127672977594
+48,1374207915,rpm_1s,1201859611,2089968467991060156,max,2721.0
+79,1374207915,Engine RPM,1201859611,2185843916018887178,min,0.0
+79,1374207915,Engine RPM,1201859611,2185843916018887178,mean,1261.5219812119237
+79,1374207915,Engine RPM,1201859611,2185843916018887178,max,2835.0
+79,1374207915,rpm_1s,1201859611,2185843916018887178,min,0.0
+79,1374207915,rpm_1s,1201859611,2185843916018887178,mean,1261.13286969096
+79,1374207915,rpm_1s,1201859611,2185843916018887178,max,2589.0
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/__init__.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_helpers.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_helpers.py
new file mode 100644
index 00000000..79709027
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_helpers.py
@@ -0,0 +1,222 @@
+"""Helpers for pushdown pipeline tests: a runner and test-only rules."""
+
+import contextlib
+from collections.abc import Callable
+from unittest import mock
+
+import numpy as np
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+from pyspark.sql import Column, DataFrame
+
+from impulse_query_engine.analyze.metadata.time_series_expression import (
+ SeriesType,
+ TimeSeriesOp,
+ TimeSeriesSelector,
+)
+from impulse_query_engine.analyze.query.optimizer import (
+ VARIANT_COL,
+ VARIANTS_COL,
+ Pushdown,
+ PushdownPlan,
+ PushdownRule,
+ SeriesContext,
+ apply_pushdowns,
+ pipeline,
+)
+from impulse_query_engine.analyze.query.solvers.solver_config import SolverConfig
+from impulse_query_engine.model.series.sample_series import SampleSeries
+
+CONFIG = SolverConfig()
+
+
+@contextlib.contextmanager
+def chunk_rows(n: int | None):
+ """Run the pipeline with *n* rows per chunk (the default when ``None``)."""
+ if n is None:
+ yield
+ return
+ with mock.patch.object(pipeline, "CHUNK_ROWS", n):
+ yield
+
+
+def channel_rows(spark, series_by_key, time_type=T.LongType()) -> tuple[DataFrame, DataFrame]:
+ """Channel data and the variants of each channel, as ``apply_pushdowns`` takes them.
+
+ *series_by_key* maps ``(cid, ch, variant)`` to ``(tstarts, tends, values)``; all
+ variants of a channel have the same samples, as in a solve.
+ """
+ by_channel: dict[tuple, tuple] = {}
+ for (cid, ch, vid), samples in series_by_key.items():
+ known, vids = by_channel.setdefault((cid, ch), (samples, []))
+ assert known is samples or known == samples, f"variants of {(cid, ch)} differ"
+ vids.append(vid)
+ schema = T.StructType(
+ [
+ T.StructField("container_id", T.LongType()),
+ T.StructField("channel_id", T.LongType()),
+ T.StructField("tstart", time_type),
+ T.StructField("tend", time_type),
+ T.StructField("value", T.DoubleType()),
+ ]
+ )
+ cast = int if isinstance(time_type, T.IntegralType) else float
+
+ def to_time(t):
+ return None if t is None else cast(t)
+
+ rows = [
+ (cid, ch, to_time(ts), to_time(te), None if v is None else float(v))
+ for (cid, ch), ((tstarts, tends, values), _) in by_channel.items()
+ for ts, te, v in zip(tstarts, tends, values, strict=True)
+ ]
+ variants = T.StructType(
+ [
+ T.StructField("container_id", T.LongType()),
+ T.StructField("channel_id", T.LongType()),
+ T.StructField(VARIANTS_COL, T.ArrayType(T.IntegerType())),
+ ]
+ )
+ return spark.createDataFrame(rows, schema=schema), spark.createDataFrame(
+ [(cid, ch, sorted(vids)) for (cid, ch), (_, vids) in by_channel.items()], variants
+ )
+
+
+def plan_for(pushdowns: dict[int, Pushdown]) -> PushdownPlan:
+ """A plan computing *pushdowns* as the given variants."""
+ rules = tuple(dict.fromkeys(p.rule for p in pushdowns.values()))
+ return PushdownPlan(
+ rules=rules,
+ variant_ids={p.variant_key: vid for vid, p in pushdowns.items()},
+ pushdowns_by_variant=dict(pushdowns),
+ )
+
+
+def run(
+ spark,
+ series_by_key,
+ pushdowns: dict[int, Pushdown],
+ time_type=T.LongType(),
+ chunk_size: int | None = None,
+ include_raw: bool = True,
+) -> dict[tuple, SampleSeries]:
+ """Run *pushdowns* on the series; ``{(cid, ch, variant): SampleSeries}`` like the cache."""
+ df, variants = channel_rows(spark, series_by_key, time_type)
+ with chunk_rows(chunk_size):
+ result = apply_pushdowns(
+ df, variants, plan_for(pushdowns), CONFIG, include_raw=include_raw
+ )
+ expected = [*df.schema.fields, T.StructField(VARIANT_COL, T.IntegerType())]
+ assert [(f.name, f.dataType) for f in result.schema] == [
+ (f.name, f.dataType) for f in expected
+ ]
+ pdf = result.toPandas()
+ out = {}
+ for key, group in pdf.groupby(["container_id", "channel_id", VARIANT_COL]):
+ group = group.sort_values(["tstart", "tend"], kind="stable")
+ out[tuple(int(k) for k in key)] = SampleSeries(
+ group["tstart"], group["tend"], group["value"].astype(np.float64)
+ )
+ return out
+
+
+def random_series(rng, n, nan_tend=False):
+ """Samples with gaps, zero-duration samples and NaN values, in series order.
+
+ With *nan_tend*, some samples have a NaN ``tend`` (use a double time column).
+ """
+ tstarts, tends, values = [], [], []
+ t, after_point = 0, False
+ for _ in range(n):
+ if rng.random() < 0.15:
+ t += int(rng.integers(1, 6))
+ duration = 0 if not after_point and rng.random() < 0.1 else int(rng.integers(1, 5))
+ tstarts.append(t)
+ nan = nan_tend and duration > 0 and rng.random() < 0.1
+ tends.append(np.nan if nan else t + duration)
+ values.append(np.nan if rng.random() < 0.1 else float(rng.integers(0, 4)))
+ t, after_point = t + duration, duration == 0
+ return tstarts, tends, values
+
+
+def _selector_op(node, operand, operation: str) -> TimeSeriesSelector | None:
+ """The continuous selector of ``selector.()``, or ``None``."""
+ if (
+ type(node) is not TimeSeriesOp
+ or node.optype != "cls"
+ or node.operation != operation
+ or len(node.args) != 1
+ or node.kwargs
+ ):
+ return None
+ selector = operand(node.args[0])
+ if not isinstance(selector, TimeSeriesSelector):
+ return None
+ return selector if selector.series_type is SeriesType.CONTINUOUS else None
+
+
+class DiffRule(PushdownRule):
+ """Test-only ``channel.diff()``: the value change within a block, 0 at block starts."""
+
+ name = "diff"
+
+ def match(self, node, operand):
+ selector = _selector_op(node, operand, "diff")
+ if selector is None:
+ return None
+ return Pushdown(rule=self, selector=selector, params=(), consumed=(selector,))
+
+ def row_columns(self, ctx: SeriesContext):
+ return {"diff": F.when(ctx.block.starts, F.lit(0.0)).otherwise(ctx.value - ctx.prev.value)}
+
+ def emit(self, ctx: SeriesContext) -> Column:
+ return F.array(ctx.element(value=ctx.col("diff")))
+
+
+class OffsetRule(PushdownRule):
+ """Test-only per-sample rule adding its ``offset`` parameter to every value."""
+
+ name = "offset"
+ ordered = False
+
+ def match(self, node, operand):
+ return None
+
+ def emit(self, ctx: SeriesContext) -> Column:
+ return F.array(ctx.element(value=ctx.value + ctx.param("offset")))
+
+
+class ProbeRule(PushdownRule):
+ """Test-only rule emitting every sample with ``probe(ctx)`` as its value."""
+
+ match = OffsetRule.match
+
+ def __init__(
+ self,
+ name: str,
+ probe: Callable[[SeriesContext], Column],
+ runs: dict | None = None,
+ ):
+ self.name = name
+ self.probe = probe
+ self._runs = runs or {}
+
+ def runs(self):
+ return self._runs
+
+ def emit(self, ctx: SeriesContext) -> Column:
+ return F.array(ctx.element(value=self.probe(ctx)))
+
+ def pushdown(self, selector: TimeSeriesSelector) -> Pushdown:
+ return Pushdown(rule=self, selector=selector, params=(), consumed=(selector,))
+
+
+def top_level_items(text: str) -> list[str]:
+ """The comma-separated items of *text*, not splitting inside parentheses."""
+ items, depth, start = [], 0, 0
+ for i, char in enumerate(text):
+ depth += {"(": 1, ")": -1}.get(char, 0)
+ if char == "," and depth == 0:
+ items.append(text[start:i].strip())
+ start = i + 1
+ return [*items, text[start:].strip()]
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_test.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_test.py
new file mode 100644
index 00000000..2fc7efa3
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/optimizer/pipeline_test.py
@@ -0,0 +1,412 @@
+# pylint: disable=missing-function-docstring
+"""The pushdown pipeline's services match a straightforward per-series reference.
+
+Probe rules emit one service column per sample; the series are cut into chunks of
+a few samples, so every service has to carry information across chunk boundaries.
+"""
+
+import re
+
+import numpy as np
+import numpy.testing as nptest
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+import pytest
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesSelector
+from impulse_query_engine.analyze.query.optimizer import (
+ VARIANT_COL,
+ VARIANTS_COL,
+ Classifier,
+ Pushdown,
+ SeriesPipeline,
+ apply_pushdowns,
+)
+from impulse_query_engine.model.series.sample_series import SampleSeries
+from tests.conftest import spark
+from tests.impulse_query_engine.unit.analyze.query.optimizer.pipeline_helpers import (
+ CONFIG,
+ DiffRule,
+ OffsetRule,
+ ProbeRule,
+ channel_rows,
+ chunk_rows,
+ plan_for,
+ random_series,
+ run,
+ top_level_items,
+)
+
+_X = TimeSeriesSelector(TagSelector("channel_name") == "x")
+
+
+def _same_value(prev, cur) -> bool:
+ # Spark compares NaN as equal to NaN.
+ return prev == cur or (np.isnan(prev) and np.isnan(cur))
+
+
+def _value_change(prev, cur):
+ return prev.value.isNull() | (prev.value != cur.value)
+
+
+def _runs(breaks: list[bool], tstarts: list) -> dict[str, list]:
+ ids = np.cumsum(breaks).tolist()
+ starts, start = [], None
+ for brk, ts in zip(breaks, tstarts, strict=True):
+ start = ts if brk else start
+ starts.append(start)
+ return {
+ "starts": [float(b) for b in breaks],
+ "id": ids,
+ "start": starts,
+ "next_starts": [float(b) for b in breaks[1:]] + [None],
+ "is_last": [float(i == ids[-1]) for i in ids],
+ }
+
+
+def _reference(tstarts, tends, values) -> dict[str, list]:
+ """Every probed service for one series, computed sample by sample."""
+ n = len(tstarts)
+ block_breaks = [i == 0 or tstarts[i] - tends[i - 1] != 0 for i in range(n)]
+ level_breaks = [i == 0 or not _same_value(values[i - 1], values[i]) for i in range(n)]
+ block = _runs(block_breaks, tstarts)
+ block_end = []
+ for i in range(n):
+ members = [tends[j] for j in range(n) if block["id"][j] == block["id"][i]]
+ finite = [t for t in members if not np.isnan(t)]
+ block_end.append(max(finite) if finite else None)
+ return {
+ "prev_tstart": [None, *tstarts[:-1]],
+ "prev_tend": [None, *tends[:-1]],
+ "prev_value": [None, *values[:-1]],
+ "next_tstart": [*tstarts[1:], None],
+ "next_value": [*values[1:], None],
+ "n": [n] * n,
+ "last_tstart": [tstarts[-1]] * n,
+ "last_tend": [tends[-1]] * n,
+ **{f"block_{name}": column for name, column in block.items()},
+ "block_end": block_end,
+ **{f"level_{name}": column for name, column in _runs(level_breaks, tstarts).items()},
+ }
+
+
+_PROBES = {
+ "prev_tstart": lambda ctx: ctx.prev.tstart,
+ "prev_tend": lambda ctx: ctx.prev.tend,
+ "prev_value": lambda ctx: ctx.prev.value,
+ "next_tstart": lambda ctx: ctx.next.tstart,
+ "next_value": lambda ctx: ctx.next.value,
+ "n": lambda ctx: ctx.n,
+ "last_tstart": lambda ctx: ctx.last.tstart,
+ "last_tend": lambda ctx: ctx.last.tend,
+ "block_starts": lambda ctx: ctx.block.starts,
+ "block_id": lambda ctx: ctx.block.id,
+ "block_start": lambda ctx: ctx.block.start,
+ "block_next_starts": lambda ctx: ctx.block.next_starts,
+ "block_is_last": lambda ctx: ctx.block.is_last,
+ "block_end": lambda ctx: ctx.block_end,
+ "level_starts": lambda ctx: ctx.run("level").starts,
+ "level_id": lambda ctx: ctx.run("level").id,
+ "level_start": lambda ctx: ctx.run("level").start,
+ "level_next_starts": lambda ctx: ctx.run("level").next_starts,
+ "level_is_last": lambda ctx: ctx.run("level").is_last,
+}
+
+
+def _probe_pushdowns() -> dict[int, Pushdown]:
+ probes = {}
+ for vid, (name, probe) in enumerate(_PROBES.items(), start=1):
+ runs = {"level": _value_change} if name.startswith("level_") else None
+ probes[vid] = ProbeRule(f"probe_{name}", probe, runs).pushdown(_X)
+ return probes
+
+
+def _as_float(column: list) -> np.ndarray:
+ return np.array([np.nan if v is None else float(v) for v in column])
+
+
+@pytest.mark.parametrize(
+ ("time_type", "nan_tend"), [(T.LongType(), False), (T.DoubleType(), True)]
+)
+@pytest.mark.parametrize("chunk_size", [1, 2, 3, 7, None])
+def test_services_across_chunk_boundaries(spark, chunk_size, time_type, nan_tend):
+ rng = np.random.default_rng(17)
+ pushdowns = _probe_pushdowns()
+ series, expected = {}, {}
+ for cid, n in enumerate([1, 2, 5, 40, 90]):
+ samples = random_series(rng, n, nan_tend=nan_tend)
+ reference = _reference(*samples)
+ for vid, pushdown in pushdowns.items():
+ series[(cid, 1, vid)] = samples
+ expected[(cid, 1, vid)] = (
+ samples,
+ reference[pushdown.rule.name.removeprefix("probe_")],
+ )
+ actual = run(spark, series, pushdowns, time_type=time_type, chunk_size=chunk_size)
+ assert actual.keys() == expected.keys()
+ for key, ((tstarts, tends, _), column) in expected.items():
+ name = pushdowns[key[2]].rule.name
+ nptest.assert_array_equal(actual[key].tstarts, tstarts, err_msg=name)
+ nptest.assert_array_equal(actual[key].tends, tends, err_msg=name)
+ nptest.assert_array_equal(actual[key].values, _as_float(column), err_msg=name)
+
+
+def test_diff_matches_sample_series(spark):
+ rng = np.random.default_rng(23)
+ series, expected = {}, {}
+ for cid, n in enumerate([1, 2, 30, 70]):
+ samples = random_series(rng, n)
+ series[(cid, 1, 1)] = samples
+ expected[(cid, 1, 1)] = SampleSeries(*samples).diff()
+ pushdowns = {1: Classifier([DiffRule()])(_X.diff())}
+ for chunk_size in (1, 3, None):
+ actual = run(spark, series, pushdowns, chunk_size=chunk_size)
+ for key, exp in expected.items():
+ nptest.assert_array_equal(actual[key].tstarts, exp.tstarts)
+ nptest.assert_array_equal(actual[key].tends, exp.tends)
+ nptest.assert_array_equal(actual[key].values, exp.values)
+
+
+_OFFSET = OffsetRule()
+
+
+def _offset(value: float) -> Pushdown:
+ return Pushdown(rule=_OFFSET, selector=_X, params=(("offset", value),), consumed=(_X,))
+
+
+def test_per_sample_rules_share_the_row_branch_with_raw_rows(spark):
+ samples = ([0, 1, 5], [1, 2, 6], [1.0, 2.0, 4.0])
+ series = {(1, 1, 0): samples, (1, 1, 1): samples, (1, 1, 2): samples, (2, 1, 3): samples}
+ pushdowns = {1: _offset(10.0), 2: _offset(0.5), 3: Classifier([DiffRule()])(_X.diff())}
+ actual = run(spark, series, pushdowns, chunk_size=2)
+ nptest.assert_array_equal(actual[(1, 1, 0)].values, [1.0, 2.0, 4.0])
+ nptest.assert_array_equal(actual[(1, 1, 1)].values, [11.0, 12.0, 14.0])
+ nptest.assert_array_equal(actual[(1, 1, 2)].values, [1.5, 2.5, 4.5])
+ nptest.assert_array_equal(actual[(2, 1, 3)].values, [0.0, 1.0, 0.0])
+ nptest.assert_array_equal(actual[(1, 1, 1)].tstarts, [0, 1, 5])
+
+
+def test_per_sample_rules_skip_the_series_scan(spark):
+ df, variants = channel_rows(spark, {(1, 1, 1): ([0, 1], [1, 2], [1.0, 2.0])})
+ result = apply_pushdowns(df, variants, plan_for({1: _offset(1.0)}), CONFIG, include_raw=False)
+ plan = result._jdf.queryExecution().optimizedPlan().toString()
+ assert "_pd_chunk" not in plan
+ assert "Window" not in plan
+ assert "Aggregate" not in plan
+
+
+def test_raw_rows_only_when_asked(spark):
+ samples = ([0, 1], [1, 2], [1.0, 2.0])
+ series = {(1, 1, 0): samples, (1, 1, 1): samples}
+ pushdowns = {1: Classifier([DiffRule()])(_X.diff())}
+ assert set(run(spark, series, pushdowns)) == {(1, 1, 0), (1, 1, 1)}
+ assert set(run(spark, series, pushdowns, include_raw=False)) == {(1, 1, 1)}
+
+
+def test_ordered_services_are_refused_to_per_sample_rules(spark):
+ class _Neighbourly(OffsetRule):
+ name = "neighbourly"
+
+ def emit(self, ctx):
+ return F.array(ctx.element(value=ctx.prev.value))
+
+ df, variants = channel_rows(spark, {(1, 1, 1): ([0], [1], [1.0])})
+ pushdown = Pushdown(rule=_Neighbourly(), selector=_X, params=(), consumed=(_X,))
+ with pytest.raises(TypeError, match="needs an ordered rule"):
+ apply_pushdowns(df, variants, plan_for({1: pushdown}), CONFIG)
+
+
+def test_block_end_is_only_available_to_kept_samples(spark):
+ rule = ProbeRule("early_block_end", lambda ctx: ctx.value)
+ rule.row_columns = lambda ctx: {"end": ctx.block_end}
+ df, variants = channel_rows(spark, {(1, 1, 1): ([0], [1], [1.0])})
+ with pytest.raises(TypeError, match="kept samples"):
+ apply_pushdowns(df, variants, plan_for({1: rule.pushdown(_X)}), CONFIG)
+
+
+def test_one_shuffle_of_the_samples(spark):
+ rng = np.random.default_rng(3)
+ series = {(cid, 1, 1): random_series(rng, 30) for cid in range(3)}
+ df, variants = channel_rows(spark, series)
+ plan = plan_for({1: Classifier([DiffRule()])(_X.diff())})
+ result = apply_pushdowns(df, variants, plan, CONFIG, include_raw=False)
+ result.collect()
+ final = _final_plan(result)
+ exchanges = _shuffles(final)
+ key = "container_id, channel_id"
+ samples = [e for e in exchanges if "_pd_chunk" in e]
+ assert len(samples) == 1 and samples[0].startswith(f"hashpartitioning({key}, _pd_chunk")
+ # The others are the per-channel boundaries and chunk summaries.
+ assert all(e.startswith(f"hashpartitioning({key}") for e in exchanges)
+ # One full read, shared by the chunk pass and its summaries, and the boundaries.
+ assert len(_channel_data_scans(final)) == 2
+
+
+def test_chunks_scale_with_the_variants_of_a_channel(spark):
+ # Both variants of channel (1, 1) fan out per chunk, so it gets twice as many chunks
+ # to keep the rows per chunk; each of its samples enters the chunk shuffle once.
+ samples = (list(range(12)), list(range(1, 13)), [1.0] * 12)
+ df, variants = channel_rows(
+ spark, {(1, 1, 1): samples, (1, 1, 2): samples, (2, 1, 1): samples}
+ )
+ diff, probe = DiffRule(), ProbeRule("probe_value", lambda ctx: ctx.value)
+ rules = [(diff, {1: Classifier([diff])(_X.diff())}), (probe, {2: probe.pushdown(_X)})]
+ with chunk_rows(4):
+ chunks = SeriesPipeline(rules, CONFIG)._chunked(df, variants)
+ per_channel = chunks.groupBy("container_id", "channel_id").agg(
+ F.countDistinct("_pd_chunk").alias("chunks"), F.count(F.lit(1)).alias("samples")
+ )
+ counts = {(r[0], r[1]): (r["chunks"], r["samples"]) for r in per_channel.collect()}
+ assert counts == {(1, 1): (6, 12), (2, 1): (3, 12)}
+
+
+def test_samples_are_shuffled_once_per_channel(spark):
+ samples = random_series(np.random.default_rng(11), 40)
+ # Channel (1, 1) ships two ordered variants, channel (2, 1) its raw rows.
+ series = {(1, 1, 1): samples, (1, 1, 2): samples, (2, 1, 0): samples}
+ df, variants = channel_rows(spark, series)
+ probe = ProbeRule("probe_value", lambda ctx: ctx.value)
+ plan = plan_for({1: Classifier([DiffRule()])(_X.diff()), 2: probe.pushdown(_X)})
+ result = apply_pushdowns(df, variants, plan, CONFIG)
+ result.collect()
+ final = _final_plan(result)
+ # The variants fan out after the chunk shuffle, which no further shuffle follows.
+ assert all(VARIANT_COL not in e for e in _shuffles(final))
+ lines = final.splitlines()
+ shuffle = next(
+ i for i, line in enumerate(lines) if re.search(r"- Exchange \w+\(.*_pd_chunk", line)
+ )
+ assert not any("Generate" in line for line in _subtree(lines, shuffle))
+ # One broadcast of the variants for the boundaries and the fan-out, one of their keys
+ # for the full read, one for the raw rows.
+ assert len([s for s in _scans(final) if VARIANTS_COL in s]) == 3
+
+
+def test_samples_and_summaries_see_the_same_chunk_shuffle(spark):
+ # The samples and their chunk summaries both read the chunk shuffle, which they share
+ # only if their plans below it are equal. Window inputs are computed before the
+ # shuffle and pruned per consumer, so the shuffle carries the raw columns only; the
+ # summaries are joined on guarded keys, so no runtime filter built from them can
+ # reach the samples' scan.
+ samples = random_series(np.random.default_rng(5), 30)
+ df, variants = channel_rows(spark, {(1, 1, 1): samples})
+ plan = plan_for({1: Classifier([DiffRule()])(_X.diff())})
+ result = apply_pushdowns(df, variants, plan, CONFIG, include_raw=False)
+ result.collect()
+ lines = _final_plan(result).splitlines()
+ shuffle = next(
+ i for i, line in enumerate(lines) if re.search(r"- Exchange \w+\(.*_pd_chunk", line)
+ )
+ projection = re.search(r"Project \[(.*)\]$", lines[shuffle + 1]).group(1)
+ columns = [re.sub(r"#\d+L?", "", c.split(" AS ")[-1]) for c in top_level_items(projection)]
+ assert columns == ["container_id", "channel_id", "tstart", "tend", "value", "_pd_chunk"]
+ optimized = re.sub(r"#\d+L?", "", result._jdf.queryExecution().optimizedPlan().toString())
+ for key in ("container_id", "channel_id", "_pd_chunk"):
+ guarded = f"CASE WHEN (SPARK_PARTITION_ID() >= 0) THEN {key} END AS _pd_join_{key}"
+ assert guarded in optimized
+
+
+def test_raw_rows_are_joined_for_raw_channels_only(spark):
+ # The row branch broadcasts only the channels shipping raw rows, so the pushed
+ # channel's samples are dropped at the join instead of being exploded and filtered.
+ samples = random_series(np.random.default_rng(13), 20)
+ series = {(1, 1, 1): samples, (1, 1, 2): samples, (2, 1, 0): samples}
+ df, variants = channel_rows(spark, series)
+ plan = plan_for({1: Classifier([DiffRule()])(_X.diff()), 2: _offset(1.0)})
+ result = apply_pushdowns(df, variants, plan, CONFIG)
+ optimized = re.sub(r"#\d+L?", "", result._jdf.queryExecution().optimizedPlan().toString())
+ shipping = r"size\(filter\(__impulse_variants, lambdafunction\(lambda \w+ IN \(0,2\).*\) > 0"
+ assert re.search(shipping, optimized)
+
+
+def _final_plan(df) -> str:
+ """The final physical plan of *df*, which must have been collected."""
+ return df._jdf.queryExecution().executedPlan().toString().split("== Initial Plan ==")[0]
+
+
+def _shuffles(final: str) -> list[str]:
+ """Partitionings of the shuffles (not broadcasts), each listed once even when reused."""
+ return [re.sub(r"#\d+L?", "", e) for e in re.findall(r"(?<=- )Exchange (\w+\([^)]*\))", final)]
+
+
+def _scans(final: str) -> list[str]:
+ """Output columns of each in-memory scan in the plan."""
+ return re.findall(r"Scan ExistingRDD\[([^\]]*)\]", final)
+
+
+def _channel_data_scans(final: str) -> list[str]:
+ """Scans of the channel data, not of the variants of each channel."""
+ return [s for s in _scans(final) if VARIANTS_COL not in s]
+
+
+def _subtree(lines: list[str], i: int) -> list[str]:
+ """The plan lines below the node on line *i*."""
+
+ def depth(line: str) -> int:
+ return len(line) - len(line.lstrip(" :+-"))
+
+ below = []
+ for line in lines[i + 1 :]:
+ if depth(line) <= depth(lines[i]):
+ break
+ below.append(line)
+ return below
+
+
+def _values_by_tend(df) -> dict:
+ return {row["tend"]: row["value"] for row in df.collect()}
+
+
+@pytest.mark.parametrize("chunk_size", [1, 2, None])
+def test_null_and_nan_tstarts_keep_their_place_in_the_series(spark, chunk_size):
+ # Spark orders a null tstart first and a NaN tstart last; their chunks must too.
+ nan = float("nan")
+ samples = ([None, 0.0, 1.0, 2.0, 3.0, 4.0, nan], [0.5, 1, 2, 3, 4, 5, 5.5], [1.0] * 7)
+ df, variants = channel_rows(
+ spark, {(1, 1, 1): samples, (1, 1, 2): samples}, time_type=T.DoubleType()
+ )
+ prev = ProbeRule("prev_start", lambda ctx: ctx.prev.tstart).pushdown(_X)
+ nxt = ProbeRule("next_start", lambda ctx: ctx.next.tstart).pushdown(_X)
+ with chunk_rows(chunk_size):
+ result = apply_pushdowns(
+ df, variants, plan_for({1: prev, 2: nxt}), CONFIG, include_raw=False
+ )
+ by_variant = {v: _values_by_tend(result.where(F.col(VARIANT_COL) == v)) for v in (1, 2)}
+ expected_prev = {0.5: None, 1.0: None, 2.0: 0.0, 3.0: 1.0, 4.0: 2.0, 5.0: 3.0, 5.5: 4.0}
+ expected_next = {0.5: 0.0, 1.0: 1.0, 2.0: 2.0, 3.0: 3.0, 4.0: 4.0, 5.0: nan, 5.5: None}
+ for actual, expected in ((by_variant[1], expected_prev), (by_variant[2], expected_next)):
+ assert actual.keys() == expected.keys()
+ nptest.assert_array_equal(_as_float(actual.values()), _as_float(expected.values()))
+
+
+def test_series_without_a_finite_tend_is_still_chunked(spark):
+ nan = float("nan")
+ samples = ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], [nan] * 6, [1.0] * 6)
+ df, variants = channel_rows(spark, {(1, 1, 1): samples}, time_type=T.DoubleType())
+ rule = DiffRule()
+ with chunk_rows(2):
+ pipeline = SeriesPipeline([(rule, {1: Classifier([rule])(_X.diff())})], CONFIG)
+ chunks = pipeline._chunked(df, variants)
+ assert chunks.select("_pd_chunk").distinct().count() == 3
+
+
+def test_rule_names_cannot_collide_with_pipeline_columns(spark):
+ # The rule column "ts" of a rule named "prev" once overwrote the pipeline's prev tstart.
+ rule = ProbeRule("prev", lambda ctx: ctx.prev.tstart)
+ rule.row_columns = lambda ctx: {"ts": ctx.value * 100}
+ samples = ([0, 1, 2], [1, 2, 3], [1.0, 2.0, 3.0])
+ actual = run(spark, {(1, 1, 1): samples}, {1: rule.pushdown(_X)}, chunk_size=1)
+ nptest.assert_array_equal(actual[(1, 1, 1)].values, [np.nan, 0.0, 1.0])
+
+
+@pytest.mark.parametrize("hook", ["row_columns", "runs"])
+def test_invalid_rule_column_and_run_names_are_rejected(spark, hook):
+ rule = ProbeRule("bad_names", lambda ctx: ctx.value)
+ if hook == "row_columns":
+ rule.row_columns = lambda ctx: {"Bad__name": ctx.value}
+ else:
+ rule._runs = {"Bad__name": _value_change}
+ df, variants = channel_rows(spark, {(1, 1, 1): ([0], [1], [1.0])})
+ with pytest.raises(ValueError, match="name 'Bad__name'"):
+ apply_pushdowns(df, variants, plan_for({1: rule.pushdown(_X)}), CONFIG)
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/plan_test.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/plan_test.py
new file mode 100644
index 00000000..ad001174
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/optimizer/plan_test.py
@@ -0,0 +1,119 @@
+# pylint: disable=missing-function-docstring
+"""PushdownPlan: channel variant tagging and native calculated-channel targets."""
+
+import pyspark.sql.functions as F
+import pyspark.sql.types as T
+import pytest
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesSelector
+from impulse_query_engine.analyze.query.channels.calculated_channel import CalculatedChannel
+from impulse_query_engine.analyze.query.optimizer import (
+ PushdownContext,
+ PushdownPlan,
+ QueryOptimizer,
+ ResamplePushdownRule,
+)
+from tests.conftest import spark
+
+_LONG = PushdownContext(T.LongType(), T.LongType())
+
+
+def _channel(name, **kwargs):
+ return TimeSeriesSelector(TagSelector("channel_name") == name, **kwargs)
+
+
+def _cc(expr, name):
+ return CalculatedChannel(expr, {"channel_name": name})
+
+
+@pytest.fixture
+def selectors():
+ return {"pushed": _channel("rpm"), "pushed_2": _channel("spd"), "raw": _channel("temp")}
+
+
+def test_channel_variants(spark, selectors):
+ pushed, pushed_2, raw = selectors["pushed"], selectors["pushed_2"], selectors["raw"]
+ plan = QueryOptimizer().optimize(
+ [pushed.resample(1e6), pushed_2.resample(5e5), raw.max()], _LONG
+ )
+ pushed_variant = plan.variants_by_selector[pushed.selector_id]
+ pushed_2_variant = plan.variants_by_selector[pushed_2.selector_id]
+ cases = {
+ "none": ([], [0]),
+ "null": ([None], [0]),
+ "raw": ([raw.selector_id], [0]),
+ "pushed": ([pushed.selector_id], list(pushed_variant)),
+ "raw_and_pushed": ([raw.selector_id, pushed.selector_id], [0]),
+ "pushed_and_null": ([pushed.selector_id, None], list(pushed_variant)),
+ "pushed_twice": ([pushed.selector_id, pushed.selector_id], list(pushed_variant)),
+ "two_pushed": (
+ [pushed.selector_id, pushed_2.selector_id],
+ list(pushed_variant + pushed_2_variant),
+ ),
+ }
+ df = spark.createDataFrame(
+ [(name, ids) for name, (ids, _) in cases.items()],
+ schema="name string, selector_ids array",
+ )
+ actual = {
+ r["name"]: r["variants"]
+ for r in df.select(
+ "name", plan.channel_variants(F.col("selector_ids")).alias("variants")
+ ).collect()
+ }
+ assert actual == {name: expected for name, (_, expected) in cases.items()}
+
+
+def test_channel_selector_ids(spark):
+ df = spark.createDataFrame(
+ [(0, [7, None, 7, 3]), (1, []), (2, None)], schema="i int, selector_ids array"
+ )
+ result = df.select("i", PushdownPlan.channel_selector_ids("selector_ids").alias("ids"))
+ assert result.schema["ids"].dataType.elementType == T.LongType()
+ assert {r["i"]: r["ids"] for r in result.collect()} == {0: [7, 3], 1: [], 2: None}
+
+
+class TestNativeTargets:
+ def test_whole_expression_pushdowns(self, selectors):
+ rpm, spd = selectors["pushed"], selectors["pushed_2"]
+ aliased = _channel("vehicle_speed", uses_alias=True)
+ channels = [
+ _cc(rpm.resample(1e6), "rpm_1s"),
+ _cc(rpm.resample(1e6), "rpm_1s_copy"),
+ _cc(spd.resample(5e5), "spd"),
+ _cc(aliased.resample(1e6), "vehicle_speed"),
+ ]
+ plan = QueryOptimizer().optimize(channels, _LONG)
+ v_1s = plan.variant_ids[("resample", (("sample_rate", 1e6),))]
+ v_half = plan.variant_ids[("resample", (("sample_rate", 5e5),))]
+ assert plan.native_targets(channels) == {
+ v_1s: {
+ rpm.selector_id: [
+ (channels[0].channel_id, False),
+ (channels[1].channel_id, False),
+ ],
+ aliased.selector_id: [(channels[3].channel_id, True)],
+ },
+ v_half: {spd.selector_id: [(channels[2].channel_id, False)]},
+ }
+
+ def test_needs_every_selection_to_be_a_whole_pushdown(self, selectors):
+ rpm, spd = selectors["pushed"], selectors["pushed_2"]
+ native = _cc(rpm.resample(1e6), "rpm_1s")
+ for other in (_cc(spd.resample(1e6) * 2, "spd_x2"), spd.resample(1e6)):
+ selections = [native, other]
+ plan = QueryOptimizer().optimize(selections, _LONG)
+ assert not plan.is_empty
+ assert plan.native_targets(selections) is None
+ assert QueryOptimizer().optimize([native], _LONG).native_targets([]) is None
+
+ def test_rule_with_custom_decode_is_not_native(self, selectors):
+ class _CustomDecode(ResamplePushdownRule):
+ def decode(self, rows, pushdown, cache):
+ return super().decode(rows, pushdown, cache)
+
+ channels = [_cc(selectors["pushed"].resample(1e6), "rpm_1s")]
+ plan = QueryOptimizer([_CustomDecode()]).optimize(channels, _LONG)
+ assert not plan.is_empty
+ assert plan.native_targets(channels) is None
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/query_optimizer_test.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/query_optimizer_test.py
new file mode 100644
index 00000000..79db1458
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/optimizer/query_optimizer_test.py
@@ -0,0 +1,409 @@
+# pylint: disable=missing-function-docstring
+"""Planning tests for the rule-based pushdown optimizer (no data involved)."""
+
+import math
+import pickle
+
+import pyspark.sql.types as T
+import pytest
+from pyspark import cloudpickle
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import (
+ SeriesType,
+ TimeSeriesExpression,
+ TimeSeriesOp,
+ TimeSeriesSelector,
+)
+from impulse_query_engine.analyze.query.aggregations.histogram import HistogramDuration
+from impulse_query_engine.analyze.query.aggregations.stats_aggregator import StatsAggregator
+from impulse_query_engine.analyze.query.channels.calculated_channel import CalculatedChannel
+from impulse_query_engine.analyze.query.events.sequence_of_events_expression import (
+ SequenceOfEventsExpression,
+)
+from impulse_query_engine.analyze.query.optimizer import (
+ Pushdown,
+ PushdownContext,
+ PushdownPlan,
+ QueryOptimizer,
+ ResamplePushdownRule,
+)
+from impulse_query_engine.analyze.query.optimizer.traversal import Classifier, discover
+from impulse_query_engine.model.series.intervals import Intervals
+
+_LONG = PushdownContext(T.LongType(), T.LongType())
+_DOUBLE = PushdownContext(T.DoubleType(), T.DoubleType())
+
+
+def _channel(name: str, **kwargs) -> TimeSeriesSelector:
+ return TimeSeriesSelector(TagSelector("channel_name") == name, **kwargs)
+
+
+def _plan(*selections, context=_LONG, rules=None):
+ return QueryOptimizer(rules).optimize(selections, context)
+
+
+@pytest.fixture
+def rpm():
+ return _channel("rpm")
+
+
+@pytest.fixture
+def spd():
+ return _channel("spd")
+
+
+class TestPolicy:
+ def test_resample_only_selection_is_pushed(self, rpm):
+ plan = _plan(rpm.resample(1e6))
+ assert not plan.is_empty
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+ assert plan.raw_selector_ids == frozenset()
+ assert [p.describe() for p in plan.pushdowns_by_variant.values()] == [
+ "resample(sample_rate=1000000.0)"
+ ]
+
+ def test_raw_use_in_another_selection_blocks_pushdown(self, rpm):
+ assert _plan(rpm.resample(1e6), rpm.max()).is_empty
+
+ def test_raw_use_in_the_same_expression_blocks_pushdown(self, rpm):
+ assert _plan(rpm.resample(1e6) + rpm).is_empty
+
+ def test_other_channels_keep_raw_data(self, rpm, spd):
+ plan = _plan(rpm.resample(1e6), spd.max(), spd.resample(1e6))
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+ assert plan.raw_selector_ids == {spd.selector_id}
+
+ def test_two_rates_give_two_variants(self, rpm):
+ plan = _plan(rpm.resample(1e6), rpm.resample(1e5))
+ assert set(plan.variant_ids.values()) == {1, 2}
+ assert plan.variants_by_selector == {rpm.selector_id: (1, 2)}
+
+ def test_equivalent_calls_share_one_variant(self, rpm):
+ plan = _plan(rpm.resample(1e6), rpm.resample(sample_rate=1_000_000))
+ assert plan.variants_by_selector == {rpm.selector_id: (1,)}
+
+ def test_equal_selectors_built_separately_are_one_selector(self):
+ plan = _plan(_channel("rpm").resample(1e6), _channel("rpm").max())
+ assert plan.is_empty
+
+ def test_node_shared_across_selections_and_branches(self, rpm):
+ resampled = rpm.resample(1e6)
+ plan = _plan(resampled + resampled, resampled.cumtrapz())
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+
+ def test_non_expression_selection_gives_empty_plan(self, rpm):
+ assert _plan(rpm.resample(1e6), object()).is_empty
+
+
+class TestDiscovery:
+ def test_inside_udf_argument(self, rpm):
+ # TimeSeriesUDF stores its first operand in ``optype`` too.
+ plan = _plan(rpm.resample(1e6).apply(lambda s: s))
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+
+ def test_inside_calculated_channel(self, rpm):
+ plan = _plan(CalculatedChannel(rpm.resample(1e6) * 2, {"channel_name": "rpm_1hz"}))
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+
+ def test_inside_stats_aggregator_and_event(self, rpm, spd):
+ event = spd.resample(1e6) > 50
+ stats = StatsAggregator(
+ input_expressions=[rpm.resample(1e6)], statistics=["max"], event_expression=event
+ )
+ plan = _plan(stats, event)
+ assert plan.pushed_selector_ids == {rpm.selector_id, spd.selector_id}
+
+ def test_inside_histogram_and_sequence_of_events(self, rpm, spd):
+ plan = _plan(
+ HistogramDuration(rpm.resample(1e6), bins=[0, 1000, 2000]),
+ SequenceOfEventsExpression([spd.resample(1e6) > 10, spd.resample(1e6) > 20]),
+ )
+ assert plan.pushed_selector_ids == {rpm.selector_id, spd.selector_id}
+
+ def test_aliased_selector(self):
+ alias = _channel("vehicle_speed", uses_alias=True)
+ assert _plan(alias.resample(1e6)).pushed_selector_ids == {alias.selector_id}
+
+ def test_walk_disagreeing_with_get_selectors_keeps_raw_data(self, rpm):
+ # One selection over-counts a pushed subtree, another hides a raw use; the
+ # totals would balance, so only the per-selection guard keeps rpm raw.
+ resampled = rpm.resample(1e6)
+ over_counted = _Opaque({"a": resampled, "b": resampled}, reported=[rpm])
+ hidden_raw_use = _Opaque({"a": rpm.resample(1e6)}, reported=[rpm, rpm])
+ assert _plan(over_counted, hidden_raw_use).is_empty
+
+
+class _MatchesAnyNode(ResamplePushdownRule):
+ """Claims every non-selector node that reads exactly one selector, of any type."""
+
+ name = "any_node"
+
+ def match(self, node, operand):
+ if isinstance(node, TimeSeriesSelector) or not isinstance(node, TimeSeriesExpression):
+ return None
+ selectors = node.get_selectors()
+ if len(selectors) != 1:
+ return None
+ return Pushdown(rule=self, selector=selectors[0], params=(), consumed=tuple(selectors))
+
+ def supports(self, pushdown, context):
+ return True
+
+
+class TestOnlyOperationsArePushedDown:
+ # Only TimeSeriesOp.build serves precomputed results; other node types must
+ # keep their raw rows.
+ def test_aggregation_node_is_not_a_candidate(self, rpm):
+ found, leaves = discover(
+ HistogramDuration(rpm, bins=[0, 1000]), Classifier([_MatchesAnyNode()])
+ )
+ assert found == []
+ assert [s.selector_id for s in leaves] == [rpm.selector_id]
+
+ def test_udf_node_is_not_a_candidate_but_its_arguments_are(self, rpm):
+ found, _ = discover(rpm.resample(1e6).apply(lambda s: s), Classifier([_MatchesAnyNode()]))
+ assert [node.operation for node, _ in found] == ["resample"]
+
+ def test_lookup_ignores_other_node_types(self, rpm):
+ rule = _MatchesAnyNode()
+ plan = QueryOptimizer([rule]).optimize([rpm.resample(1e6)], _LONG)
+ assert plan.lookup(rpm.resample(1e6)) is not None
+ assert plan.lookup(HistogramDuration(rpm, bins=[0, 1000])) is None
+
+
+class _OnPushedOperand(ResamplePushdownRule):
+ """Matches ``x.cumtrapz()`` on a pushed ``x``, recording the operands it sees."""
+
+ def __init__(self, name="on_pushed", chained=False, supported=True):
+ self.name = name
+ self.chained = chained
+ self.supported = supported
+ self.seen = []
+
+ def match(self, node, operand):
+ if type(node) is not TimeSeriesOp or node.operation != "cumtrapz":
+ return None
+ inner = operand(node.args[0])
+ self.seen.append(inner)
+ if not isinstance(inner, Pushdown):
+ return None
+ return Pushdown(
+ rule=self,
+ selector=inner.selector,
+ params=(("inner", inner.variant_key),),
+ consumed=inner.consumed,
+ source=inner if self.chained else None,
+ )
+
+ def supports(self, pushdown, context):
+ return self.supported
+
+
+_RESAMPLE = "resample(sample_rate=1000000.0)"
+
+
+def _described(plan):
+ return [p.describe() for p in plan.pushdowns_by_variant.values()]
+
+
+class TestBottomUpClassification:
+ def test_rules_see_their_operands_classification(self, rpm):
+ rule = _OnPushedOperand()
+ classify = Classifier([rule, ResamplePushdownRule()])
+ for node in (rpm.cumtrapz(), rpm.resample(1e6).cumtrapz(), (rpm * 2).cumtrapz()):
+ classify(node)
+ channel, pushed, evaluated_in_pandas = rule.seen
+ assert channel is rpm
+ assert pushed.describe() == _RESAMPLE
+ assert evaluated_in_pandas is None
+
+ def test_chained_pushdown_is_not_planned_but_its_operand_is(self, rpm):
+ expr = rpm.resample(1e6).cumtrapz()
+ plan = _plan(expr, rules=[_OnPushedOperand(chained=True), ResamplePushdownRule()])
+ assert _described(plan) == [_RESAMPLE]
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+ assert plan.lookup(expr) is None
+ assert plan.lookup(expr.args[0])[1] == 1
+
+ def test_absorbing_rule_is_planned_as_one_variant(self, rpm):
+ expr = rpm.resample(1e6).cumtrapz()
+ plan = _plan(expr, rules=[_OnPushedOperand(), ResamplePushdownRule()])
+ (pushdown,) = plan.pushdowns_by_variant.values()
+ assert pushdown.rule.name == "on_pushed" and pushdown.source is None
+ assert pushdown.consumed == (rpm,)
+ hit, variant = plan.lookup(expr)
+ assert hit.variant_key == pushdown.variant_key and variant == 1
+ assert plan.lookup(expr.args[0]) is None
+
+ def test_unsupported_parent_falls_back_to_its_pushed_operand(self, rpm):
+ expr = rpm.resample(1e6).cumtrapz()
+ plan = _plan(expr, rules=[_OnPushedOperand(supported=False), ResamplePushdownRule()])
+ assert _described(plan) == [_RESAMPLE]
+ assert plan.lookup(expr) is None
+ assert plan.lookup(expr.args[0])[1] == 1
+
+ def test_first_matching_rule_decides(self, rpm):
+ # The UDF classifies without knowing what is supported, so an unsupported
+ # first rule must not hand the node to the next one.
+ expr = rpm.resample(1e6).cumtrapz()
+ rules = [
+ _OnPushedOperand("unsupported", supported=False),
+ _OnPushedOperand("supported"),
+ ResamplePushdownRule(),
+ ]
+ plan = _plan(expr, rules=rules)
+ assert _described(plan) == [_RESAMPLE]
+ assert plan.lookup(expr) is None
+ assert plan.lookup(expr.args[0])[1] == 1
+
+ def test_shared_nodes_are_matched_once(self, rpm):
+ matched = []
+
+ class _Recording(ResamplePushdownRule):
+ def match(self, node, operand):
+ matched.append(node)
+ return super().match(node, operand)
+
+ resampled = rpm.resample(1e6)
+ plan = _plan(resampled + resampled, resampled.cumtrapz(), rules=[_Recording()])
+ assert len(matched) == len({id(node) for node in matched}) == 3
+ assert plan.pushed_selector_ids == {rpm.selector_id}
+
+ def test_chained_pushdowns_are_their_own_variant_and_cannot_be_planned(self, rpm):
+ inner = Classifier([ResamplePushdownRule()])(rpm.resample(1e6))
+ chained = Pushdown(
+ rule=inner.rule, selector=rpm, params=inner.params, consumed=(rpm,), source=inner
+ )
+ assert inner.variant_key == ("resample", (("sample_rate", 1e6),))
+ assert chained.variant_key != inner.variant_key
+ assert chained.describe() == f"{_RESAMPLE} on {_RESAMPLE}"
+ with pytest.raises(ValueError, match="has a source"):
+ PushdownPlan.build([inner.rule], [chained], [rpm.selector_id])
+
+
+class TestRuleNames:
+ @pytest.mark.parametrize("name", ["Bad", "a__b", "_x", "x_", "1x", ""])
+ def test_invalid_rule_names_are_rejected(self, name):
+ rule = type("_Named", (ResamplePushdownRule,), {"name": name})()
+ with pytest.raises(ValueError, match="rule name"):
+ QueryOptimizer([rule])
+
+ def test_rule_names_must_be_unique(self):
+ with pytest.raises(ValueError, match="unique"):
+ QueryOptimizer([ResamplePushdownRule(), ResamplePushdownRule()])
+
+ def test_plan_rejects_two_rules_sharing_a_name(self, rpm, spd):
+ first, second = ResamplePushdownRule(), ResamplePushdownRule()
+ plan = PushdownPlan.build(
+ (first, second),
+ [Classifier([first])(rpm.resample(1e6)), Classifier([second])(spd.resample(2e6))],
+ [rpm.selector_id, spd.selector_id],
+ )
+ with pytest.raises(ValueError, match="two different pushdown rules"):
+ plan.variants_by_rule()
+
+
+class _Opaque(TimeSeriesExpression):
+ """Node whose attributes disagree with the selectors it reports."""
+
+ def __init__(self, attributes, reported):
+ super().__init__()
+ self.__dict__.update(attributes)
+ self._reported = lambda: list(reported) # not visible to the walk
+
+ def build(self, cache):
+ return None
+
+ def get_selectors(self):
+ return self._reported()
+
+ def get_required_tag_exprs(self):
+ return set()
+
+ def required_tags(self):
+ return set()
+
+ def get_selector_expr(self):
+ return None
+
+ def __str__(self):
+ return "_Opaque"
+
+
+class TestResampleRule:
+ @pytest.mark.parametrize(
+ "make",
+ [
+ lambda c: (c * 2).resample(1e6),
+ lambda c: c.resample(True),
+ lambda c: c.resample(0),
+ lambda c: c.resample(-1e6),
+ lambda c: c.resample(math.nan),
+ lambda c: c.resample(math.inf),
+ lambda c: c.resample(c),
+ lambda c: c.resample(1e6, 2),
+ lambda c: c.resample(1e6, sample_rate=1e6),
+ lambda c: c.resample(rate=1e6),
+ lambda c: c.rolling_average(1e6),
+ ],
+ )
+ def test_unsupported_calls_are_not_matched(self, rpm, make):
+ assert Classifier([ResamplePushdownRule()])(make(rpm)) is None
+
+ def test_default_rate(self, rpm):
+ pushdown = Classifier([ResamplePushdownRule()])(rpm.resample())
+ assert pushdown.params == (("sample_rate", 1.0),)
+ assert pushdown.selector is rpm
+
+ def test_points_in_time_selector_is_not_matched(self):
+ poi = _channel("dtc", series_type=SeriesType.POINTS_IN_TIME)
+ assert Classifier([ResamplePushdownRule()])(poi.resample(1e6)) is None
+
+ def test_fractional_rate_needs_double_time_columns(self, rpm):
+ assert _plan(rpm.resample(0.5), context=_LONG).is_empty
+ assert not _plan(rpm.resample(0.5), context=_DOUBLE).is_empty
+
+ @pytest.mark.parametrize(
+ "time_type", [T.FloatType(), T.DecimalType(20, 0), T.TimestampType(), T.StringType()]
+ )
+ def test_unsupported_time_column_types(self, rpm, time_type):
+ assert _plan(rpm.resample(1e6), context=PushdownContext(time_type, time_type)).is_empty
+
+
+class TestResultTypeCheck:
+ def test_misdeclared_result_type_is_dropped(self, rpm):
+ class _IntervalsResample(ResamplePushdownRule):
+ def result_type(self, pushdown):
+ return Intervals
+
+ assert _plan(rpm.resample(1e6), rules=[_IntervalsResample()]).is_empty
+
+ def test_failing_type_inference_drops_candidate(self, rpm):
+ class _Exploding(ResamplePushdownRule):
+ def match(self, node, operand):
+ pushdown = super().match(node, operand)
+ if pushdown is not None:
+ node.evaluation_type = _raise
+ return pushdown
+
+ def _raise():
+ raise RuntimeError("boom")
+
+ assert _plan(rpm.resample(1e6), rules=[_Exploding()]).is_empty
+
+
+class TestPlanInExecutor:
+ @pytest.mark.parametrize("dumps", [pickle.dumps, cloudpickle.dumps])
+ def test_plan_and_selections_survive_pickling(self, rpm, spd, dumps):
+ selections = [rpm.resample(1e6).cumtrapz(), spd.resample(1e6), spd.max()]
+ plan = _plan(*selections)
+ plan2, selections2 = pickle.loads(dumps((plan, selections)))
+
+ pushed = selections2[0].args[0]
+ pushdown, variant = plan2.lookup(pushed)
+ assert variant == 1
+ assert pushdown.selector.selector_id == rpm.selector_id
+ # spd is needed raw by spd.max(), so its resample is evaluated in pandas.
+ assert plan2.lookup(selections2[1]) is None
+ assert plan2.lookup(selections2[2]) is None
diff --git a/tests/impulse_query_engine/unit/analyze/query/optimizer/resample_rule_test.py b/tests/impulse_query_engine/unit/analyze/query/optimizer/resample_rule_test.py
new file mode 100644
index 00000000..f0e2c941
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/optimizer/resample_rule_test.py
@@ -0,0 +1,371 @@
+# pylint: disable=missing-function-docstring
+"""Parity of the Spark ``resample`` pushdown with ``SampleSeries.resample``."""
+
+import re
+
+import numpy as np
+import numpy.testing as nptest
+import pyspark.sql.types as T
+import pytest
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesSelector
+from impulse_query_engine.analyze.query.optimizer import (
+ VARIANT_COL,
+ Classifier,
+ ResamplePushdownRule,
+)
+from impulse_query_engine.model.series.sample_series import SampleSeries
+from tests.conftest import spark
+from tests.impulse_query_engine.unit.analyze.query.optimizer.pipeline_helpers import (
+ CONFIG,
+ DiffRule,
+ channel_rows,
+ plan_for,
+ random_series,
+ run,
+ top_level_items,
+)
+from impulse_query_engine.analyze.query.optimizer import apply_pushdowns
+
+_RULE = ResamplePushdownRule()
+_X = TimeSeriesSelector(TagSelector("channel_name") == "x")
+
+
+def _pushdown(rate):
+ return Classifier([_RULE])(_X.resample(rate))
+
+
+def _resample_in_spark(spark, series_by_key, rates, time_type=T.LongType(), chunk_rows=None):
+ """Resample ``{(cid, ch, variant): (tstarts, tends, values)}`` at ``{variant: rate}``.
+
+ Variant 0 rows are raw rows. Returns ``{(cid, ch, variant): SampleSeries}``
+ built like the pandas cache does.
+ """
+ pushdowns = {vid: _pushdown(rate) for vid, rate in rates.items()}
+ return run(spark, series_by_key, pushdowns, time_type=time_type, chunk_size=chunk_rows)
+
+
+def _expected(series, rate):
+ tstarts, tends, values = series
+ return SampleSeries(tstarts, tends, [np.nan if v is None else v for v in values]).resample(
+ rate
+ )
+
+
+def _assert_same(actual, expected):
+ nptest.assert_array_equal(actual.tstarts, expected.tstarts)
+ nptest.assert_array_equal(actual.tends, expected.tends)
+ nptest.assert_array_equal(actual.values, expected.values)
+
+
+def _where_lt(tstarts, tends, values, threshold):
+ s = SampleSeries(tstarts, tends, values)
+ w = s.where(s < threshold)
+ return list(w.tstarts), list(w.tends), list(w.values)
+
+
+# Inputs of the SampleSeries.resample unit tests plus further edge cases, as
+# (tstarts, tends, values, rate). Integral, so exact for LongType columns.
+_CASES = {
+ "two_samples": ([0, 1], [1, 3], [1, 2], 1),
+ "long_first_sample": ([0, 4, 5], [4, 5, 6], [1, 3, 2], 1),
+ "uneven": ([0, 4, 6], [4, 6, 7], [1, 3, 2], 1),
+ "gap": ([0, 1, 6], [1, 3, 7], [1, 2, 4], 1),
+ "where_gap": (*_where_lt([0, 1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 10], [5, 5, 5, 4, 5, 1], 5), 1),
+ "where_gap_rate_2": (
+ *_where_lt([0, 1, 2, 3, 4, 5, 10], [1, 2, 3, 4, 5, 10, 11], [5, 5, 5, 4, 5, 1, 2], 5),
+ 2,
+ ),
+ "trailing_point": ([0, 4, 5, 6], [4, 5, 6, 6], [1, 3, 2, 3], 1),
+ "where_trailing_point": (
+ *_where_lt([0, 1, 2, 3, 4, 5, 10], [1, 2, 3, 4, 5, 10, 10], [5, 5, 5, 4, 5, 1, 2], 6),
+ 1,
+ ),
+ "interior_point": ([0, 2, 3], [1, 2, 4], [10, 20, 30], 1),
+ "interior_and_trailing_point": ([0, 2, 3], [1, 2, 3], [10, 20, 30], 1),
+ "isolated_trailing_point": ([0, 1, 5], [1, 2, 5], [1, 2, 3], 1),
+ "single_sample": ([5], [7], [1.0], 1),
+ "single_point": ([5], [5], [1.0], 1),
+ "nan_and_null_values": ([0, 1, 2, 3], [1, 2, 3, 4], [1.0, np.nan, None, 4.0], 1),
+ "grid_off_sample_boundaries": ([0, 3, 7, 12], [3, 7, 12, 20], [1, 2, 3, 4], 4),
+ "rate_beyond_block": ([0, 1, 2], [1, 2, 3], [1, 2, 3], 10),
+ "grid_on_sample_starts": (list(range(10)), list(range(1, 11)), list(range(10)), 3),
+}
+
+
+@pytest.mark.parametrize("name", list(_CASES))
+def test_resample_matches_sample_series(spark, name):
+ tstarts, tends, values, rate = _CASES[name]
+ actual = _resample_in_spark(spark, {(1, 1, 1): (tstarts, tends, values)}, {1: rate})
+ _assert_same(actual[(1, 1, 1)], _expected((tstarts, tends, values), rate))
+
+
+# Few samples per chunk put chunk boundaries at gaps, inside buckets and next to
+# zero-duration samples, with empty chunks in between.
+@pytest.mark.parametrize("chunk_rows", [None, 1, 2, 3])
+def test_all_cases_in_one_pass_and_raw_rows_unchanged(spark, chunk_rows):
+ series = {}
+ expected = {}
+ for i, (tstarts, tends, values, _rate) in enumerate(_CASES.values()):
+ # rate 1 -> variant 1, rate 2 -> variant 2; containers keep channels apart.
+ for vid, r in ((1, 1), (2, 2)):
+ series[(i, 7, vid)] = (tstarts, tends, values)
+ expected[(i, 7, vid)] = _expected((tstarts, tends, values), r)
+ # Raw rows (variant 0) pass through untouched.
+ series[(i, 7, 0)] = (tstarts, tends, values)
+ expected[(i, 7, 0)] = SampleSeries(
+ tstarts, tends, [np.nan if v is None else v for v in values]
+ )
+ actual = _resample_in_spark(spark, series, {1: 1, 2: 2}, chunk_rows=chunk_rows)
+ assert actual.keys() == expected.keys()
+ for key, exp in expected.items():
+ _assert_same(actual[key], exp)
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 7])
+def test_ns_epoch_longs_above_2_pow_53(spark, chunk_rows):
+ rng = np.random.default_rng(7)
+ base = 1_700_000_000_123_456_789
+ steps = rng.integers(9_000_000, 11_000_000, size=500) # ~10 ms, jittered
+ times = base + np.concatenate([[0], np.cumsum(steps)])
+ tstarts, tends = times[:-1].tolist(), times[1:].tolist()
+ values = rng.normal(size=500).tolist()
+ rate = 100_000_000 # 100 ms
+ actual = _resample_in_spark(
+ spark, {(1, 1, 1): (tstarts, tends, values)}, {1: rate}, chunk_rows=chunk_rows
+ )
+ expected = _expected((tstarts, tends, values), rate)
+ assert len(expected) < len(tstarts) / 5
+ _assert_same(actual[(1, 1, 1)], expected)
+
+
+def test_only_linear_window_frames(spark):
+ # Spark recomputes a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING frame for every
+ # row (quadratic in the series length), so the rule must not use one.
+ plan = _optimized_plan(spark)
+ assert "specifiedwindowframe(RowFrame, currentrow$(), unboundedfollowing$())" not in plan
+ assert "specifiedwindowframe(RowFrame, unboundedpreceding$(), currentrow$())" in plan
+
+
+def test_keep_filter_does_not_inline_the_rule_columns(spark):
+ # Spark pushes filters through deterministic projections by substituting column
+ # definitions; resample's grid-index columns refer to each other several times, so an
+ # inlined keep condition would grow to about 100k characters.
+ df, variants = channel_rows(
+ spark, {(1, 1, 1): ([0, 1], [1, 2], [1.0, 2.0]), (1, 1, 2): ([0, 1], [1, 2], [1.0, 2.0])}
+ )
+ plan = plan_for({1: _pushdown(1), 2: _pushdown(2)})
+ result = apply_pushdowns(df, variants, plan, CONFIG, include_raw=False)
+ optimized = result._jdf.queryExecution().optimizedPlan().toString()
+ conditions = re.findall(r"Filter (.*)", optimized)
+ assert conditions and not any("raise_error" in c for c in conditions)
+ assert max(len(c) for c in conditions) < 1_000
+
+
+def test_random_series_across_chunk_sizes(spark):
+ rng = np.random.default_rng(3)
+ rates = {1: 1, 2: 3, 3: 7}
+ series, expected = {}, {}
+ for cid, n in enumerate([1, 2, 40, 60, 60, 80]):
+ tstarts, tends, values = random_series(rng, n)
+ for vid, rate in rates.items():
+ series[(cid, 1, vid)] = (tstarts, tends, values)
+ expected[(cid, 1, vid)] = _expected((tstarts, tends, values), rate)
+ series[(cid, 1, 0)] = (tstarts, tends, values)
+ expected[(cid, 1, 0)] = SampleSeries(tstarts, tends, values)
+ assert sum(len(e) for e in expected.values()) > 0
+ for chunk_rows in (1, 2, 3, 5, 8, 13):
+ actual = _resample_in_spark(spark, series, rates, chunk_rows=chunk_rows)
+ assert actual.keys() == expected.keys()
+ for key, exp in expected.items():
+ _assert_same(actual[key], exp)
+
+
+def _optimized_plan(spark) -> str:
+ df, variants = channel_rows(spark, {(1, 1, 1): ([0, 1], [1, 2], [1.0, 2.0])})
+ result = apply_pushdowns(df, variants, plan_for({1: _pushdown(1)}), CONFIG, include_raw=False)
+ return result._jdf.queryExecution().optimizedPlan().toString()
+
+
+def test_windows_over_samples_are_chunked(spark):
+ # Spark buffers a whole window partition, so no window over the samples may be
+ # partitioned by the series alone; only the chunk summaries are.
+ plan = _optimized_plan(spark)
+ specs = re.findall(r"windowspecdefinition\((.*?), specifiedwindowframe", plan)
+ channel_key = ["container_id", "channel_id"]
+ seen = set()
+ for spec in specs:
+ items = top_level_items(re.sub(r"#\d+L?", "", spec))
+ partition = [item for item in items if not item.endswith(("NULLS FIRST", "NULLS LAST"))]
+ order = [item.split(" ")[0] for item in items[len(partition) :]]
+ seen.add((tuple(partition), tuple(order)))
+ if partition == channel_key:
+ assert order == ["_pd_chunk"]
+ else:
+ # Per chunk of a channel; after the fan-out, per chunk of a series.
+ assert partition in (
+ [*channel_key, "_pd_chunk"],
+ [*channel_key, VARIANT_COL, "_pd_chunk"],
+ )
+ assert (tuple([*channel_key, "_pd_chunk"]), ("tstart", "tend", "value")) in seen
+ assert any(partition == tuple(channel_key) for partition, _ in seen)
+ # Photon runs window aggregates over structs row-based.
+ assert not re.search(r"(max|min)\(struct\(", plan)
+
+
+def test_resample_and_diff_in_one_pass(spark):
+ rng = np.random.default_rng(5)
+ series, expected = {}, {}
+ for cid in range(3):
+ samples = random_series(rng, 50)
+ series[(cid, 1, 1)] = series[(cid, 1, 2)] = samples
+ expected[(cid, 1, 1)] = _expected(samples, 3)
+ expected[(cid, 1, 2)] = SampleSeries(*samples).diff()
+ pushdowns = {1: _pushdown(3), 2: Classifier([DiffRule()])(_X.diff())}
+ for chunk_size in (2, 7):
+ actual = run(spark, series, pushdowns, chunk_size=chunk_size)
+ assert actual.keys() == expected.keys()
+ for key, exp in expected.items():
+ _assert_same(actual[key], exp)
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 7])
+def test_ns_epoch_grid_on_sample_starts(spark, chunk_rows):
+ # Every fifth sample starts exactly on a grid point, the case where the bucket
+ # index estimate needs its correction step.
+ base = 1_700_000_000_123_456_789
+ times = [base + i * 10_000_000 for i in range(301)]
+ tstarts, tends = times[:-1], times[1:]
+ values = [float(i) for i in range(300)]
+ rate = 50_000_000
+ actual = _resample_in_spark(
+ spark, {(1, 1, 1): (tstarts, tends, values)}, {1: rate}, chunk_rows=chunk_rows
+ )
+ _assert_same(actual[(1, 1, 1)], _expected((tstarts, tends, values), rate))
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 2])
+def test_integer_time_columns(spark, chunk_rows):
+ tstarts, tends, values, rate = _CASES["where_gap_rate_2"]
+ actual = _resample_in_spark(
+ spark,
+ {(1, 1, 1): (tstarts, tends, values)},
+ {1: rate},
+ time_type=T.IntegerType(),
+ chunk_rows=chunk_rows,
+ )
+ _assert_same(actual[(1, 1, 1)], _expected((tstarts, tends, values), rate))
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 2])
+def test_seconds_as_double_with_binary_exact_rate(spark, chunk_rows):
+ tstarts = [10.0, 10.25, 11.0, 12.5, 14.0]
+ tends = [10.25, 11.0, 12.5, 14.0, 14.0]
+ values = [1.0, 2.0, 3.0, 4.0, 5.0]
+ actual = _resample_in_spark(
+ spark,
+ {(1, 1, 1): (tstarts, tends, values)},
+ {1: 0.75},
+ time_type=T.DoubleType(),
+ chunk_rows=chunk_rows,
+ )
+ _assert_same(actual[(1, 1, 1)], _expected((tstarts, tends, values), 0.75))
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 7])
+def test_seconds_as_double_with_fractional_rate(spark, chunk_rows):
+ # 0.1 is not binary-exact: grid points may differ from numpy by one ulp where
+ # numpy fuses the multiply-add. Sample boundaries stay clear of the grid, so the
+ # values must match exactly.
+ rng = np.random.default_rng(11)
+ times = 1234.5678 + np.concatenate([[0.0], np.cumsum(rng.uniform(0.031, 0.037, 400))])
+ tstarts, tends = times[:-1].tolist(), times[1:].tolist()
+ values = rng.normal(size=400).tolist()
+ actual = _resample_in_spark(
+ spark,
+ {(1, 1, 1): (tstarts, tends, values)},
+ {1: 0.1},
+ time_type=T.DoubleType(),
+ chunk_rows=chunk_rows,
+ )[(1, 1, 1)]
+ expected = _expected((tstarts, tends, values), 0.1)
+ _assert_close(actual, expected)
+
+
+def _assert_close(actual, expected):
+ """Equal values; grid times within one ulp, where numpy may fuse the multiply-add."""
+ assert len(actual) == len(expected)
+ nptest.assert_array_max_ulp(actual.tstarts, expected.tstarts, maxulp=1)
+ nptest.assert_array_max_ulp(actual.tends, expected.tends, maxulp=1)
+ nptest.assert_array_equal(actual.values, expected.values)
+
+
+def _as_seconds(series):
+ """*series* on an irregular seconds-as-double clock: times are not binary-exact."""
+ tstarts, tends, values = series
+ clock = [1234.5678 + t * 0.0371 for t in tstarts], [1234.5678 + t * 0.0371 for t in tends]
+ return (*clock, values)
+
+
+@pytest.mark.parametrize("chunk_rows", [None, 1, 3, 8])
+def test_random_double_time_series(spark, chunk_rows):
+ # Gaps, zero-duration samples and NaN values on a clock that is not binary-exact,
+ # at fractional rates below and above the sample spacing.
+ rng = np.random.default_rng(17)
+ rates = {1: 0.1, 2: 0.25, 3: 0.6}
+ series, expected = {}, {}
+ for cid, n in enumerate([1, 2, 40, 90, 150]):
+ samples = _as_seconds(random_series(rng, n))
+ for vid, rate in rates.items():
+ series[(cid, 1, vid)] = samples
+ expected[(cid, 1, vid)] = _expected(samples, rate)
+ assert sum(len(e) for e in expected.values()) > 300
+ actual = _resample_in_spark(
+ spark, series, rates, time_type=T.DoubleType(), chunk_rows=chunk_rows
+ )
+ assert actual.keys() == expected.keys()
+ for key, exp in expected.items():
+ _assert_close(actual[key], exp)
+
+
+@pytest.mark.filterwarnings("ignore:All-NaN slice encountered:RuntimeWarning")
+@pytest.mark.parametrize("chunk_rows", [None, 2, 7])
+def test_nan_tends(spark, chunk_rows):
+ # A NaN tend ends its block, and the block ends at its largest finite tend, so the
+ # sample with the NaN tend adds no grid points of its own.
+ nan = float("nan")
+ series = {
+ (0, 1, 1): ([0.0, 1.0, 2.0, 3.0], [1.0, nan, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]),
+ (1, 1, 1): ([0.0, 1.0, 2.0], [1.0, 2.0, nan], [1.0, 2.0, 3.0]),
+ }
+ rng = np.random.default_rng(23)
+ while len(series) < 12:
+ tstarts, tends, values = random_series(rng, int(rng.integers(2, 60)), nan_tend=True)
+ samples = ([float(t) for t in tstarts], tends, values)
+ try:
+ _expected(samples, 1)
+ except ValueError:
+ # A block without a finite tend has no end (see the next test).
+ continue
+ if any(np.isnan(tends)):
+ series[(len(series), 1, 1)] = samples
+ for key in list(series):
+ series[(key[0], 1, 2)] = series[key]
+ actual = _resample_in_spark(
+ spark, series, {1: 1, 2: 3}, time_type=T.DoubleType(), chunk_rows=chunk_rows
+ )
+ for (cid, ch, vid), samples in series.items():
+ _assert_same(actual[(cid, ch, vid)], _expected(samples, {1: 1, 2: 3}[vid]))
+
+
+@pytest.mark.filterwarnings("ignore:All-NaN slice encountered:RuntimeWarning")
+def test_block_without_a_finite_tend_fails_like_pandas(spark):
+ # A sample with a NaN tend after a block break forms a block without an end; the
+ # pandas evaluation fails on it, and so must the pushdown instead of guessing one.
+ nan = float("nan")
+ samples = ([0.0, 1.0, 2.0], [nan, 2.0, 3.0], [1.0, 2.0, 3.0])
+ with pytest.raises(ValueError):
+ _expected(samples, 1)
+ with pytest.raises(Exception, match="resample:"):
+ _resample_in_spark(spark, {(1, 1, 1): samples}, {1: 1}, time_type=T.DoubleType())
diff --git a/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_channel_resolution_test.py b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_channel_resolution_test.py
new file mode 100644
index 00000000..f6e8ac22
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_channel_resolution_test.py
@@ -0,0 +1,176 @@
+# pylint: disable=missing-function-docstring
+"""DefaultSolver channel resolution when several channels of a container match a selection.
+
+A direct selection keeps every matching channel. An alias resolves to one channel per
+container: the best alias priority, then the lowest ``channel_id``.
+"""
+
+import os
+
+import numpy as np
+import pandas as pd
+import pyspark.sql.functions as F
+import pytest
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesSelector
+from impulse_query_engine.analyze.query.solvers.default_solver import DefaultSolver
+from impulse_query_engine.analyze.query.solvers.solver_config import (
+ ChannelMappingConfig,
+ SolverConfig,
+ TableConfig,
+)
+from impulse_query_engine.measurement_db import MeasurementDB, MeasurementDBConfig
+from tests.conftest import spark
+
+_ALIAS_SCHEMA = "spark_catalog.silver_key_value_store_alias"
+# unit="C" matches the ambient and intake temperatures: channels 6 and 9 in containers 1
+# and 2, channels 2 and 8 in container 3.
+_CELSIUS_CHANNELS = {1: [6, 9], 2: [6, 9], 3: [2, 8]}
+
+
+def _alias_config() -> SolverConfig:
+ return SolverConfig(
+ project_id="SAMPLE_PROJECT",
+ container_metrics=TableConfig(column_name_mapping={"project": "project_id"}),
+ channel_mapping=ChannelMappingConfig(filters={"toolbox_id": "container_concept"}),
+ )
+
+
+def _resolved(spark, solver, query, selector) -> dict:
+ """``{container_id: [channel_id, ...]}`` the filter pipeline resolves *selector* to."""
+ matches = query.select(selector)._run_filter_pipeline(spark, solver, None)
+ rows = matches.where(
+ F.exists("selector_ids", lambda sid: sid.cast("long") == selector.selector_id)
+ ).collect()
+ resolved: dict = {}
+ for row in rows:
+ resolved.setdefault(row["container_id"], []).append(row["channel_id"])
+ return {cid: sorted(channels) for cid, channels in resolved.items()}
+
+
+def _alias_tables(spark) -> dict:
+ names = (
+ "container_tags",
+ "container_metrics",
+ "channel_metrics",
+ "channels",
+ "channel_mapping",
+ "channel_tags",
+ )
+ return {name: spark.read.table(f"{_ALIAS_SCHEMA}.{name}") for name in names}
+
+
+def _alias_db(tables, ws, with_channel_tags=False) -> MeasurementDB:
+ tables = dict(tables)
+ if not with_channel_tags:
+ tables.pop("channel_tags")
+ cfg = MeasurementDBConfig.for_debug(tables)
+ cfg.channel_mapping_table = "channel_mapping"
+ return MeasurementDB(cfg, ws=ws)
+
+
+def _with_channel(channel_metrics, container_id, channel_id, **columns):
+ """*channel_metrics* plus a copy of container 1's Engine RPM row as a new channel."""
+ template = channel_metrics.where(
+ (F.col("container_id") == 1) & (F.col("channel_name") == "Engine RPM")
+ )
+ row = template.withColumn("container_id", F.lit(container_id).cast("long")).withColumn(
+ "channel_id", F.lit(channel_id).cast(channel_metrics.schema["channel_id"].dataType)
+ )
+ for name, value in columns.items():
+ row = row.withColumn(name, F.lit(value))
+ return channel_metrics.unionByName(row.select(*channel_metrics.columns))
+
+
+class TestDirectSelectors:
+ def test_wide_mode_keeps_every_matching_channel(self, spark, basic_narrow_db):
+ q = basic_narrow_db.query
+ celsius = q.channel(unit="C")
+ assert _resolved(spark, DefaultSolver(spark), q, celsius) == _CELSIUS_CHANNELS
+
+ def test_wide_mode_solves_on_one_of_the_matching_channels(self, spark, basic_narrow_db):
+ q = basic_narrow_db.query
+ result = q.select(q.channel(unit="C").max().alias("t_max")).solve(
+ spark, solver=DefaultSolver(spark)
+ )
+ actual = {r["container_id"]: r["t_max"] for r in result.collect()}
+
+ base_path = os.path.dirname(os.path.abspath(__file__))
+ base_path = base_path[: base_path.find("tests")]
+ data = pd.read_csv(f"{base_path}/tests/unit/data/basic_narrow_csv/channel_data.csv")
+ assert actual.keys() == _CELSIUS_CHANNELS.keys()
+ for cid, channels in _CELSIUS_CHANNELS.items():
+ maxima = [
+ np.nanmax(data[(data.container_id == cid) & (data.channel_id == ch)].value)
+ for ch in channels
+ ]
+ assert np.isfinite(actual[cid])
+ assert any(actual[cid] == pytest.approx(m) for m in maxima)
+
+ def test_eav_mode_keeps_every_matching_channel(
+ self, spark, setup_key_value_store_alias_db, mock_workspace_client
+ ):
+ # Re-tag channel 9 of container 1 and channels 2 and 8 of container 3 as
+ # "Engine RPM", next to channel 5 of containers 1 and 2.
+ tables = _alias_tables(spark)
+ tags = tables["channel_tags"]
+ retagged = [(1, 9), (3, 2), (3, 8)]
+ keep = ~F.lit(False)
+ for cid, ch in retagged:
+ keep = keep & ~((F.col("container_id") == cid) & (F.col("channel_id") == ch))
+ extra = spark.createDataFrame(
+ [(cid, ch, "channel_name", "Engine RPM") for cid, ch in retagged],
+ schema=tags.schema,
+ )
+ tables["channel_tags"] = tags.where(keep).unionByName(extra)
+ db = _alias_db(tables, mock_workspace_client, with_channel_tags=True)
+
+ q = db.query
+ rpm = q.channel(channel_name="Engine RPM")
+ assert _resolved(spark, DefaultSolver(spark, config=_alias_config()), q, rpm) == {
+ 1: [5, 9],
+ 2: [5],
+ 3: [2, 8],
+ }
+
+
+class TestAliasedSelectors:
+ @pytest.mark.parametrize(("extra_channel", "expected"), [(2, 2), (8, 5)])
+ def test_priority_tie_keeps_lowest_channel_id(
+ self, spark, setup_key_value_store_alias_db, mock_workspace_client, extra_channel, expected
+ ):
+ # Container 1 gets a second engine_speed candidate (EngSpd); both candidates
+ # have no priority, so the lower channel_id wins.
+ tables = _alias_tables(spark)
+ tables["channel_metrics"] = _with_channel(
+ tables["channel_metrics"],
+ 1,
+ extra_channel,
+ channel_name="EngSpd",
+ data_key="ProjSpecREC_10Hz",
+ )
+ db = _alias_db(tables, mock_workspace_client)
+ q = db.query
+ engine_speed = q.channel_with_alias(channel_alias="engine_speed")
+ solver = DefaultSolver(spark, config=_alias_config())
+ assert _resolved(spark, solver, q, engine_speed)[1] == [expected]
+
+ def test_selector_matching_several_aliases_resolves_each_alias(
+ self, spark, key_value_store_alias_db
+ ):
+ q = key_value_store_alias_db.query
+ solver = DefaultSolver(spark, config=_alias_config())
+ expected: dict = {}
+ for alias in ("engine_speed", "vehicle_speed"):
+ one = _resolved(spark, solver, q, q.channel_with_alias(channel_alias=alias))
+ for cid, channels in one.items():
+ expected.setdefault(cid, set()).update(channels)
+ both = TimeSeriesSelector(
+ (TagSelector("channel_alias") == "engine_speed")
+ | (TagSelector("channel_alias") == "vehicle_speed"),
+ uses_alias=True,
+ )
+ resolved = _resolved(spark, solver, q, both)
+ assert resolved == {cid: sorted(channels) for cid, channels in expected.items()}
+ assert any(len(channels) == 2 for channels in resolved.values())
diff --git a/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_native_calculated_channels_test.py b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_native_calculated_channels_test.py
new file mode 100644
index 00000000..81e331b4
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_native_calculated_channels_test.py
@@ -0,0 +1,338 @@
+# pylint: disable=missing-function-docstring
+"""Calculated channels that are pushdowns as a whole are solved without the pandas UDF.
+
+Each case solves the same calculated channels with pushdown off (pandas UDF) and on
+(native when every channel qualifies) and requires identical rows, types, nulls and
+NaNs.
+"""
+
+import pandas as pd
+import pyspark.sql.functions as F
+import pytest
+from pyspark.sql import DataFrame
+
+from impulse_query_engine.analyze.query.channels.calculated_channel import CalculatedChannel
+from impulse_query_engine.analyze.query.optimizer import ResamplePushdownRule, query_optimizer
+from impulse_query_engine.analyze.query.solvers.default_solver import DefaultSolver
+from impulse_query_engine.analyze.query.solvers.solver_config import (
+ ChannelMappingConfig,
+ SolverConfig,
+ TableConfig,
+)
+from impulse_query_engine.measurement_db import MeasurementDB, MeasurementDBConfig
+from tests.conftest import spark
+from tests.impulse_query_engine.unit.analyze.query.optimizer.pipeline_helpers import DiffRule
+from tests.impulse_query_engine.unit.analyze.query.solvers.default_solver_pushdown_test import (
+ PUSHED_SCANS,
+ SAMPLE_SHUFFLE,
+ channel_data_scans,
+ container_shuffles_of_channel_data,
+ shuffles,
+ small_chunks,
+)
+
+RATE = 1e7 # 10 s in µs; the fixture channels are sampled roughly every 0.9 s
+_KVS_SCHEMA = "spark_catalog.silver_key_value_store"
+
+
+def _kvs_config() -> SolverConfig:
+ return SolverConfig(
+ project_id="SAMPLE_PROJECT",
+ container_tags=TableConfig(column_name_mapping={"element_id": "key"}),
+ container_metrics=TableConfig(column_name_mapping={"project": "project_id"}),
+ channel_mapping=ChannelMappingConfig(),
+ )
+
+
+def _unit_conversion_config() -> SolverConfig:
+ return SolverConfig(
+ project_id="SAMPLE_PROJECT",
+ container_metrics=TableConfig(column_name_mapping={"project": "project_id"}),
+ channel_mapping=ChannelMappingConfig(filters={"toolbox_id": "container_concept"}),
+ )
+
+
+@pytest.fixture
+def udf_calls(monkeypatch) -> list:
+ """Number of grouped-map UDF runs, per solve."""
+ calls: list[int] = []
+ original = DefaultSolver._apply_grouped_map
+
+ def _counting(self, *args, **kwargs):
+ calls[-1] += 1
+ return original(self, *args, **kwargs)
+
+ monkeypatch.setattr(DefaultSolver, "_apply_grouped_map", _counting)
+ return calls
+
+
+def _solve(spark, db, make_channels, enable_pushdown, udf_calls=None, **solver_kwargs):
+ if udf_calls is not None:
+ udf_calls.append(0)
+ q = db.query
+ solver = DefaultSolver(spark, enable_pushdown=enable_pushdown, **solver_kwargs)
+ return q.select(*make_channels(q)).solve_calculated_channels(spark, solver=solver)
+
+
+def _nan_and_null_counts(df: DataFrame) -> tuple:
+ return tuple(
+ df.select(
+ F.sum(F.isnan("value").cast("int")), F.sum(F.col("value").isNull().cast("int"))
+ ).first()
+ )
+
+
+def _rows(df: DataFrame) -> pd.DataFrame:
+ pdf = df.toPandas()
+ pdf["identity"] = pdf["identity"].map(lambda m: tuple(sorted(m.items())))
+ return pdf.sort_values(["container_id", "channel_id", "tstart", "tend"]).reset_index(drop=True)
+
+
+def _assert_same(native: DataFrame, udf: DataFrame) -> pd.DataFrame:
+ assert [(f.name, f.dataType) for f in native.schema] == [
+ (f.name, f.dataType) for f in udf.schema
+ ]
+ assert _nan_and_null_counts(native) == _nan_and_null_counts(udf)
+ expected = _rows(udf)
+ assert len(expected) > 0
+ pd.testing.assert_frame_equal(_rows(native), expected, check_exact=True)
+ return expected
+
+
+def _solve_both(spark, db, make_channels, udf_calls, **solver_kwargs) -> pd.DataFrame:
+ udf = _solve(spark, db, make_channels, False, udf_calls, **solver_kwargs)
+ native = _solve(spark, db, make_channels, True, udf_calls, **solver_kwargs)
+ return _assert_same(native, udf), native
+
+
+@pytest.mark.usefixtures("small_chunks")
+class TestNativeCalculatedChannels:
+ def test_resampled_channels_skip_the_udf(self, spark, key_value_store_db, udf_calls):
+ def channels(q):
+ return [
+ CalculatedChannel(
+ q.channel(channel_name="Engine RPM").resample(RATE), {"channel_name": "rpm"}
+ ),
+ CalculatedChannel(
+ q.channel(channel_name="Vehicle Speed Sensor").resample(RATE / 2),
+ {"channel_name": "speed", "data_key": "CALC"},
+ ),
+ ]
+
+ rows, native = _solve_both(
+ spark, key_value_store_db, channels, udf_calls, config=_kvs_config()
+ )
+ assert udf_calls == [1, 0]
+ assert rows["value"].gt(0).any()
+ assert rows["channel_id"].nunique() == 2
+ plan = native._jdf.queryExecution().executedPlan().toString()
+ assert "FlatMapGroupsInPandas" not in plan
+ assert channel_data_scans(native) == PUSHED_SCANS
+ # The samples are shuffled by series and chunk, never by container.
+ assert len([e for e in shuffles(native) if e.startswith(SAMPLE_SHUFFLE)]) == 1
+ assert container_shuffles_of_channel_data(native) == 0
+
+ def test_two_rules_share_the_reads(self, spark, key_value_store_db, udf_calls, monkeypatch):
+ monkeypatch.setattr(
+ query_optimizer, "default_rules", lambda: (ResamplePushdownRule(), DiffRule())
+ )
+
+ def channels(q):
+ return [
+ CalculatedChannel(
+ q.channel(channel_name="Engine RPM").resample(RATE), {"channel_name": "rpm"}
+ ),
+ CalculatedChannel(
+ q.channel(channel_name="Vehicle Speed Sensor").diff(),
+ {"channel_name": "speed_diff"},
+ ),
+ ]
+
+ rows, native = _solve_both(
+ spark, key_value_store_db, channels, udf_calls, config=_kvs_config()
+ )
+ assert udf_calls == [1, 0]
+ assert rows["channel_id"].nunique() == 2
+ assert rows["value"].ne(0).any()
+ assert channel_data_scans(native) == PUSHED_SCANS
+ assert len([e for e in shuffles(native) if e.startswith(SAMPLE_SHUFFLE)]) == 1
+
+ def test_same_expression_under_two_identities_and_two_rates(
+ self, spark, key_value_store_db, udf_calls
+ ):
+ def channels(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [
+ CalculatedChannel(rpm.resample(RATE), {"channel_name": "rpm_a"}),
+ CalculatedChannel(rpm.resample(RATE), {"channel_name": "rpm_b"}),
+ CalculatedChannel(rpm.resample(RATE / 4), {"channel_name": "rpm_c"}),
+ ]
+
+ rows, _ = _solve_both(spark, key_value_store_db, channels, udf_calls, config=_kvs_config())
+ assert udf_calls == [1, 0]
+ assert rows["channel_id"].nunique() == 3
+
+ def test_mixed_selection_uses_the_udf(self, spark, key_value_store_db, udf_calls):
+ def channels(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [
+ CalculatedChannel(rpm.resample(RATE), {"channel_name": "rpm"}),
+ CalculatedChannel(rpm.resample(RATE) * 2, {"channel_name": "rpm_x2"}),
+ ]
+
+ _solve_both(spark, key_value_store_db, channels, udf_calls, config=_kvs_config())
+ assert udf_calls == [1, 1]
+
+ def test_aliased_and_direct_selection_of_one_channel_with_unit_conversion(
+ self, spark, key_value_store_unit_conversion_db, udf_calls
+ ):
+ # vehicle_speed resolves to the "Vehicle Speed Sensor" channel; only the
+ # aliased selection converts its values.
+ def channels(q):
+ return [
+ CalculatedChannel(
+ q.channel_with_alias(channel_alias="vehicle_speed").resample(RATE),
+ {"channel_name": "speed_converted"},
+ ),
+ CalculatedChannel(
+ q.channel(channel_name="Vehicle Speed Sensor").resample(RATE),
+ {"channel_name": "speed_raw"},
+ ),
+ ]
+
+ rows, _ = _solve_both(
+ spark,
+ key_value_store_unit_conversion_db,
+ channels,
+ udf_calls,
+ config=_unit_conversion_config(),
+ )
+ assert udf_calls == [1, 0]
+ by_channel = rows.groupby("channel_id")["value"].sum()
+ assert len(by_channel) == 2 and by_channel.iloc[0] != by_channel.iloc[1]
+
+ def test_nan_conversion_factor_leaves_values_unconverted(
+ self, spark, unit_conversion_dataframes, mock_workspace_client, udf_calls
+ ):
+ # vehicle_speed converts km/h to m/s; a NaN km/h factor must be skipped like
+ # the pandas evaluation does, not turn every value into null.
+ tables = dict(unit_conversion_dataframes)
+ tables["unit_conversion"] = tables["unit_conversion"].withColumn(
+ "conversion_factor",
+ F.when(F.col("unit") == "km/h", F.lit(float("nan"))).otherwise(
+ F.col("conversion_factor").cast("double")
+ ),
+ )
+ cfg = MeasurementDBConfig.for_debug(tables)
+ cfg.channel_mapping_table = "channel_mapping"
+ cfg.unit_conversion_table = "unit_conversion"
+ db = MeasurementDB(cfg, ws=mock_workspace_client)
+
+ def channels(q):
+ return [
+ CalculatedChannel(
+ q.channel_with_alias(channel_alias="vehicle_speed").resample(RATE),
+ {"channel_name": "speed_aliased"},
+ ),
+ CalculatedChannel(
+ q.channel(channel_name="Vehicle Speed Sensor").resample(RATE),
+ {"channel_name": "speed_raw"},
+ ),
+ ]
+
+ rows, _ = _solve_both(spark, db, channels, udf_calls, config=_unit_conversion_config())
+ assert udf_calls == [1, 0]
+ aliased, raw = (
+ rows[rows["identity"] == (("channel_name", name),)]
+ for name in ("speed_aliased", "speed_raw")
+ )
+ same_samples = aliased.merge(raw, on=["container_id", "tstart", "tend"])
+ assert len(same_samples) > 0 and same_samples["value_x"].notna().all()
+ pd.testing.assert_series_equal(
+ same_samples["value_x"], same_samples["value_y"], check_names=False
+ )
+
+ def test_raw_data(self, spark, setup_raw_channels_db, mock_workspace_client, udf_calls):
+ tables = {
+ name: spark.read.table(f"spark_catalog.silver_raw.{name}")
+ for name in ("container_metrics", "channel_metrics", "channels")
+ }
+ db = MeasurementDB(MeasurementDBConfig.for_debug(tables), ws=mock_workspace_client)
+
+ def channels(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [CalculatedChannel(rpm.resample(2e6), {"channel_name": "rpm_2s"})]
+
+ _solve_both(spark, db, channels, udf_calls, is_raw_data=True)
+ assert udf_calls == [1, 0]
+
+ def test_ns_timestamps_nan_values_and_a_single_sample(
+ self, spark, setup_key_value_store_db, mock_workspace_client, udf_calls
+ ):
+ # Timestamps beyond 2^53 (not exact in float64), NaN values, and container 3's
+ # Engine RPM reduced to a single sample (passed through by resample).
+ tables = {
+ name: spark.read.table(f"{_KVS_SCHEMA}.{name}")
+ for name in ("container_tags", "container_metrics", "channel_metrics", "channels")
+ }
+ data = tables["channels"]
+ rpm_3 = (F.col("container_id") == 3) & (F.col("channel_id") == 5)
+ first_3 = data.where(rpm_3).agg(F.min("tstart")).first()[0]
+ rpm_1 = (F.col("container_id") == 1) & (F.col("channel_id") == 5)
+ first_1 = data.where(rpm_1).agg(F.min("tstart")).first()[0]
+ tables["channels"] = (
+ data.where(~rpm_3 | (F.col("tstart") == first_3))
+ .withColumn("tstart", F.col("tstart") * 1000 + 1)
+ .withColumn("tend", F.col("tend") * 1000 + 1)
+ .withColumn(
+ "value",
+ # tstart is already scaled here; every sample but the first becomes NaN.
+ F.when(
+ rpm_1 & (F.col("tstart") > first_1 * 1000 + 1), F.lit(float("nan"))
+ ).otherwise(F.col("value").cast("double")),
+ )
+ )
+ db = MeasurementDB(MeasurementDBConfig.for_debug(tables), ws=mock_workspace_client)
+
+ def channels(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [CalculatedChannel(rpm.resample(RATE * 1000), {"channel_name": "rpm_ns"})]
+
+ rows, native = _solve_both(spark, db, channels, udf_calls, config=_kvs_config())
+ assert udf_calls == [1, 0]
+ assert rows["tstart"].max() > 2**53
+ assert len(rows[rows.container_id == 3]) == 1
+ assert _nan_and_null_counts(native)[1] > 0
+
+
+@pytest.mark.parametrize("channel_id_type", ["short", "string"])
+@pytest.mark.parametrize("enable_pushdown", [False, True])
+def test_channel_id_must_be_integer_or_long(
+ spark,
+ setup_key_value_store_db,
+ mock_workspace_client,
+ monkeypatch,
+ channel_id_type,
+ enable_pushdown,
+):
+ def _prelude(*args, **kwargs):
+ raise AssertionError("the channel data was prepared before the channel_id check")
+
+ # The check reads only the schema, before the prelude runs its first Spark job.
+ monkeypatch.setattr(DefaultSolver, "_prepare_channels_join", _prelude)
+ tables = {
+ name: spark.read.table(f"{_KVS_SCHEMA}.{name}")
+ for name in ("container_tags", "container_metrics", "channel_metrics", "channels")
+ }
+ for name in ("channel_metrics", "channels"):
+ tables[name] = tables[name].withColumn(
+ "channel_id", F.col("channel_id").cast(channel_id_type)
+ )
+ db = MeasurementDB(MeasurementDBConfig.for_debug(tables), ws=mock_workspace_client)
+
+ def channels(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [CalculatedChannel(rpm.resample(RATE), {"channel_name": "rpm"})]
+
+ with pytest.raises(ValueError, match="integer or long channel_id"):
+ _solve(spark, db, channels, enable_pushdown, config=_kvs_config())
diff --git a/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_pushdown_test.py b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_pushdown_test.py
new file mode 100644
index 00000000..246e84a6
--- /dev/null
+++ b/tests/impulse_query_engine/unit/analyze/query/solvers/default_solver_pushdown_test.py
@@ -0,0 +1,439 @@
+# pylint: disable=missing-function-docstring
+"""DefaultSolver with ``enable_pushdown``: identical results, fewer rows into the UDF."""
+
+import re
+from collections import Counter
+
+import numpy as np
+import numpy.testing as nptest
+import pandas as pd
+import pyspark.sql.functions as F
+import pytest
+from pyspark.sql import SparkSession
+
+from impulse_query_engine.analyze.metadata.tag_expression import TagSelector
+from impulse_query_engine.analyze.metadata.time_series_expression import TimeSeriesSelector
+from impulse_query_engine.analyze.query.channels.calculated_channel import CalculatedChannel
+from impulse_query_engine.analyze.query.optimizer import (
+ RAW_VARIANT,
+ VARIANT_COL,
+ Classifier,
+ PushdownPlan,
+ QueryOptimizer,
+ ResamplePushdownRule,
+ pipeline,
+)
+from impulse_query_engine.analyze.query.solvers.default_solver import (
+ DefaultSolver,
+ TimeSeriesCache,
+)
+from impulse_query_engine.analyze.query.solvers.solver_config import (
+ ChannelMappingConfig,
+ SolverConfig,
+ TableConfig,
+)
+from impulse_query_engine.measurement_db import MeasurementDB, MeasurementDBConfig
+from impulse_query_engine.model.series.intervals import Intervals
+from impulse_query_engine.model.series.sample_series import SampleSeries
+from tests.conftest import spark
+
+RATE = 1e7 # 10 s; the fixture channels are sampled roughly every 0.9 s
+
+
+def _kvs_config() -> SolverConfig:
+ return SolverConfig(
+ project_id="SAMPLE_PROJECT",
+ container_tags=TableConfig(column_name_mapping={"element_id": "key"}),
+ container_metrics=TableConfig(column_name_mapping={"project": "project_id"}),
+ channel_mapping=ChannelMappingConfig(),
+ )
+
+
+def _unit_conversion_config() -> SolverConfig:
+ return SolverConfig(
+ project_id="SAMPLE_PROJECT",
+ container_metrics=TableConfig(column_name_mapping={"project": "project_id"}),
+ channel_mapping=ChannelMappingConfig(filters={"toolbox_id": "container_concept"}),
+ )
+
+
+@pytest.fixture
+def small_chunks(monkeypatch):
+ """Run the pushdown pipeline in chunks of a few samples, so channels span many chunks."""
+ monkeypatch.setattr(pipeline, "CHUNK_ROWS", 7)
+
+
+def _final_plan(df) -> str:
+ """The final physical plan of *df*, which must have been executed (e.g. collected)."""
+ return df._jdf.queryExecution().executedPlan().toString().split("== Initial Plan ==")[0]
+
+
+def channel_data_scans(df) -> list[str]:
+ """Columns of each channels-table scan in the final plan of the executed *df*."""
+ return sorted(
+ re.sub(r"#\d+L?", "", columns)
+ for columns in re.findall(r"FileScan parquet \S+\.channels\[([^\]]*)\]", _final_plan(df))
+ )
+
+
+def shuffles(df) -> list[str]:
+ """Partitionings of the shuffles in the final plan of the executed *df* (reuses once)."""
+ return [
+ re.sub(r"#\d+L?", "", e)
+ for e in re.findall(r"(?<=- )Exchange (\w+\([^)]*\))", _final_plan(df))
+ ]
+
+
+def container_shuffles_of_channel_data(df) -> int:
+ """Shuffles by container alone that read the channels table, in the executed *df*."""
+ lines = _final_plan(df).splitlines()
+
+ def depth(line: str) -> int:
+ return len(line) - len(line.lstrip(" :+-"))
+
+ count = 0
+ for i, line in enumerate(lines):
+ if re.search(r"- Exchange hashpartitioning\(container_id#\d+L?, \d+\)", line):
+ subtree = []
+ for below in lines[i + 1 :]:
+ if depth(below) <= depth(line):
+ break
+ subtree.append(below)
+ count += any(re.search(r"FileScan parquet \S+\.channels\[", b) for b in subtree)
+ return count
+
+
+#: Pushed channels: one full read, and one narrow read for the series boundaries.
+PUSHED_SCANS = [
+ "container_id,channel_id,tstart,tend",
+ "container_id,channel_id,tstart,tend,value",
+]
+#: Channels also used raw add one read.
+MIXED_SCANS = sorted([*PUSHED_SCANS, "container_id,channel_id,tstart,tend,value"])
+#: The one shuffle of the pushed samples: by channel and chunk, once per sample.
+SAMPLE_SHUFFLE = "hashpartitioning(container_id, channel_id, _pd_chunk"
+
+
+@pytest.fixture
+def udf_rows(monkeypatch):
+ """Rows reaching the grouped-map UDF, per solve: ``{(channel_id, variant): count}``."""
+ recorded: list[Counter] = []
+ original = DefaultSolver._apply_grouped_map
+
+ def _recording(self, joined_df, *args, **kwargs):
+ has_variants = VARIANT_COL in joined_df.columns
+ cols = ["channel_id", VARIANT_COL] if has_variants else ["channel_id"]
+ counts = Counter()
+ for row in joined_df.groupBy(*cols).count().collect():
+ variant = row[VARIANT_COL] if has_variants else None
+ counts[(row["channel_id"], variant)] += row["count"]
+ recorded.append(counts)
+ return original(self, joined_df, *args, **kwargs)
+
+ monkeypatch.setattr(DefaultSolver, "_apply_grouped_map", _recording)
+ return recorded
+
+
+def _solve_both(spark, db, make_selections, **solver_kwargs) -> tuple[pd.DataFrame, pd.DataFrame]:
+ """``toPandas`` results with pushdown off and on, sorted by container."""
+ results = []
+ for enable_pushdown in (False, True):
+ solver = DefaultSolver(spark, enable_pushdown=enable_pushdown, **solver_kwargs)
+ query = db.query
+ pdf = query.select(*make_selections(query)).toPandas(spark, solver=solver)
+ results.append(pdf.sort_values("container_id").reset_index(drop=True))
+ return results[0], results[1]
+
+
+def _assert_same(actual, expected):
+ assert list(actual.columns) == list(expected.columns)
+ assert len(actual) == len(expected) > 0
+ for col in expected.columns:
+ for a, e in zip(actual[col], expected[col], strict=True):
+ if isinstance(e, SampleSeries):
+ assert isinstance(a, SampleSeries)
+ nptest.assert_array_equal(a.tstarts, e.tstarts)
+ nptest.assert_array_equal(a.tends, e.tends)
+ nptest.assert_array_equal(a.values, e.values)
+ else:
+ nptest.assert_equal(a, e)
+
+
+def _derived_rows(pdf: pd.DataFrame, col: str) -> int:
+ return sum(len(s) for s in pdf[col])
+
+
+@pytest.mark.usefixtures("small_chunks")
+class TestIdenticalResults:
+ def test_resample_only(self, spark: SparkSession, key_value_store_db, udf_rows):
+ off, on = _solve_both(
+ spark,
+ key_value_store_db,
+ lambda q: [q.channel(channel_name="Engine RPM").resample(RATE).alias("rpm")],
+ config=_kvs_config(),
+ )
+ _assert_same(on, off)
+ assert all(np.isfinite(s.values).all() and len(s) > 1 for s in on["rpm"])
+
+ rows_off, rows_on = udf_rows
+ assert set(rows_on) == {(5, 1)}
+ assert rows_on[(5, 1)] == _derived_rows(off, "rpm") < rows_off[(5, None)] / 5
+
+ def test_pushed_channels_are_read_twice_and_shuffled_by_chunk(
+ self, spark: SparkSession, key_value_store_db
+ ):
+ q = key_value_store_db.query
+ result = q.select(q.channel(channel_name="Engine RPM").resample(RATE).alias("rpm")).solve(
+ spark, solver=DefaultSolver(spark, config=_kvs_config(), enable_pushdown=True)
+ )
+ assert len(result.collect()) > 0 # executes result's own plan, which the checks read
+ assert channel_data_scans(result) == PUSHED_SCANS
+ samples = [e for e in shuffles(result) if "_pd_chunk" in e]
+ assert len(samples) == 1 and samples[0].startswith(SAMPLE_SHUFFLE)
+ # The grouped-map UDF still groups the shipped rows by container.
+ assert container_shuffles_of_channel_data(result) == 1
+
+ def test_channels_used_raw_add_one_read(self, spark: SparkSession, key_value_store_db):
+ q = key_value_store_db.query
+ result = q.select(
+ q.channel(channel_name="Engine RPM").resample(RATE).alias("rpm"),
+ q.channel(channel_name="Vehicle Speed Sensor").max().alias("speed_max"),
+ ).solve(spark, solver=DefaultSolver(spark, config=_kvs_config(), enable_pushdown=True))
+ rows = result.collect()
+ assert rows and all(row["speed_max"] > 0 for row in rows)
+ assert channel_data_scans(result) == MIXED_SCANS
+
+ def test_raw_use_of_the_same_channel_ships_raw_rows_only(
+ self, spark: SparkSession, key_value_store_db, udf_rows
+ ):
+ def selections(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [rpm.resample(RATE).alias("rpm_10s"), rpm.max().alias("rpm_max")]
+
+ off, on = _solve_both(spark, key_value_store_db, selections, config=_kvs_config())
+ _assert_same(on, off)
+ rows_off, rows_on = udf_rows
+ # Nothing is pushed down, so the frame keeps today's shape.
+ assert rows_on == rows_off
+
+ def test_mixed_channels(self, spark: SparkSession, key_value_store_db, udf_rows):
+ def selections(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ spd = q.channel(channel_name="Vehicle Speed Sensor")
+ return [
+ rpm.resample(RATE).alias("rpm_10s"),
+ spd.max().alias("spd_max"),
+ spd.resample(RATE).alias("spd_10s"),
+ ]
+
+ off, on = _solve_both(spark, key_value_store_db, selections, config=_kvs_config())
+ _assert_same(on, off)
+ rows_off, rows_on = udf_rows
+ assert rows_on == {
+ (7, RAW_VARIANT): rows_off[(7, None)],
+ (5, 1): _derived_rows(off, "rpm_10s"),
+ }
+
+ def test_two_rates_and_nested_ops(self, spark: SparkSession, key_value_store_db, udf_rows):
+ def selections(q):
+ rpm = q.channel(channel_name="Engine RPM")
+ return [
+ rpm.resample(RATE).cumtrapz().alias("rpm_integral"),
+ (rpm.resample(RATE / 2) > 1500).alias("rpm_high"),
+ rpm.resample(RATE / 2).mean().alias("rpm_mean"),
+ ]
+
+ off, on = _solve_both(spark, key_value_store_db, selections, config=_kvs_config())
+ _assert_same(on, off)
+ assert any(len(intervals) > 0 for intervals in on["rpm_high"])
+ _, rows_on = udf_rows
+ assert set(rows_on) == {(5, 1), (5, 2)}
+
+ def test_aliased_selector_with_unit_conversion(
+ self, spark: SparkSession, key_value_store_unit_conversion_db, udf_rows
+ ):
+ def selections(q):
+ speed = q.channel_with_alias(channel_alias="vehicle_speed")
+ return [speed.resample(RATE).alias("speed_10s")]
+
+ off, on = _solve_both(
+ spark, key_value_store_unit_conversion_db, selections, config=_unit_conversion_config()
+ )
+ _assert_same(on, off)
+ _, rows_on = udf_rows
+ assert {variant for _, variant in rows_on} == {1}
+
+ def test_alongside_points_in_time_selection(
+ self, spark: SparkSession, basic_narrow_db, udf_rows
+ ):
+ def selections(q):
+ amb = q.channel(channel_name="Ambient Air Temperature")
+ dtc_count = q.poi_channel(channel_name="DTC_count")
+ rpm = q.channel(channel_name="Engine RPM")
+ return [
+ amb.where(dtc_count.to_points_in_time()).alias("sampled"),
+ rpm.resample(RATE).alias("rpm_10s"),
+ ]
+
+ off, on = _solve_both(spark, basic_narrow_db, selections)
+ _assert_same(on, off)
+ _, rows_on = udf_rows
+ assert rows_on[(91, RAW_VARIANT)] == 3 # POI rows keep the raw variant
+ assert {variant for (ch, variant) in rows_on if ch == 5} == {1}
+
+ def test_raw_data_with_trailing_point(
+ self, spark: SparkSession, setup_raw_channels_db, mock_workspace_client
+ ):
+ tables = {
+ name: spark.read.table(f"spark_catalog.silver_raw.{name}")
+ for name in ("container_metrics", "channel_metrics", "channels")
+ }
+ db = MeasurementDB(MeasurementDBConfig.for_debug(tables), ws=mock_workspace_client)
+ off, on = _solve_both(
+ spark,
+ db,
+ lambda q: [q.channel(channel_name="Engine RPM").resample(2e6).alias("rpm")],
+ is_raw_data=True,
+ )
+ _assert_same(on, off)
+ rpm = on["rpm"].iloc[0]
+ assert rpm.tstarts[-1] == rpm.tends[-1] # the series ends on a point
+
+ @pytest.mark.parametrize("channel_id_type", ["int", "string"])
+ def test_channel_id_types(
+ self,
+ spark: SparkSession,
+ setup_key_value_store_db,
+ mock_workspace_client,
+ udf_rows,
+ channel_id_type,
+ ):
+ tables = {
+ name: spark.read.table(f"spark_catalog.silver_key_value_store.{name}")
+ for name in ("container_tags", "container_metrics", "channel_metrics", "channels")
+ }
+ for name in ("channel_metrics", "channels"):
+ tables[name] = tables[name].withColumn(
+ "channel_id", F.col("channel_id").cast(channel_id_type)
+ )
+ db = MeasurementDB(MeasurementDBConfig.for_debug(tables), ws=mock_workspace_client)
+ off, on = _solve_both(
+ spark,
+ db,
+ lambda q: [q.channel(channel_name="Engine RPM").resample(RATE).alias("rpm")],
+ config=_kvs_config(),
+ )
+ _assert_same(on, off)
+ assert all(np.isfinite(s.values).all() and len(s) > 1 for s in on["rpm"])
+ _, rows_on = udf_rows
+ assert {variant for _, variant in rows_on} == {1}
+
+
+class TestCalculatedChannels:
+ def test_calculated_channel_with_resample(
+ self, spark: SparkSession, key_value_store_db, udf_rows
+ ):
+ results = []
+ for enable_pushdown in (False, True):
+ q = key_value_store_db.query
+ rpm = q.channel(channel_name="Engine RPM")
+ channel = CalculatedChannel(rpm.resample(RATE) * 2, {"channel_name": "rpm_x2"})
+ solver = DefaultSolver(spark, config=_kvs_config(), enable_pushdown=enable_pushdown)
+ df = q.select(channel).solve_calculated_channels(spark, solver=solver)
+ results.append(
+ df.drop("identity")
+ .toPandas()
+ .sort_values(["container_id", "tstart"])
+ .reset_index(drop=True)
+ )
+ off, on = results
+ assert len(off) > 0
+ assert off["value"].gt(0).any()
+ pd.testing.assert_frame_equal(on, off)
+ assert set(udf_rows[1]) == {(5, 1)}
+
+
+class TestTimeSeriesCacheVariants:
+ """Channel 5 carries raw rows (variant 0) and ``resample(10)`` rows (variant 1)."""
+
+ @staticmethod
+ def _cache(rule, node) -> TimeSeriesCache:
+ pushdown = Classifier([rule])(node)
+ plan = PushdownPlan.build([rule], [pushdown], [pushdown.selector.selector_id])
+ pdf = pd.DataFrame(
+ {
+ "container_id": [1] * 5,
+ "channel_id": [5] * 5,
+ VARIANT_COL: [1, 1, 0, 0, 0],
+ "tstart": [0, 10, 0, 5, 10],
+ "tend": [10, 20, 5, 10, 20],
+ "value": [1.0, 3.0, 1.0, 2.0, 3.0],
+ "conversion_factor": [2.0] * 5,
+ "selector_ids": [[pushdown.selector.selector_id], None, None, None, None],
+ }
+ )
+ col_map = {
+ "cid": "container_id",
+ "ch": "channel_id",
+ "ts": "tstart",
+ "te": "tend",
+ "val": "value",
+ "conv": "conversion_factor",
+ }
+ return TimeSeriesCache(pdf, col_map=col_map, plan=plan)
+
+ def test_raw_and_derived_rows_are_served_separately(self):
+ node = _rpm().resample(10)
+ cache = self._cache(ResamplePushdownRule(), node)
+ assert VARIANT_COL not in cache.mdf.columns
+ nptest.assert_array_equal(cache.load_blob(1, 5).tstarts, [0, 5, 10])
+ derived = node.build(cache)
+ nptest.assert_array_equal(derived.tstarts, [0, 10])
+ nptest.assert_array_equal(derived.values, [1.0, 3.0])
+
+ def test_conversion_factor_applies_to_aliased_derived_rows(self):
+ node = _rpm(uses_alias=True).resample(10)
+ cache = self._cache(ResamplePushdownRule(), node)
+ nptest.assert_array_equal(node.build(cache).values, [2.0, 6.0])
+
+ def test_falls_back_without_derived_rows(self):
+ rpm = _rpm()
+ cache = self._cache(ResamplePushdownRule(), rpm.resample(10))
+ other_rate = rpm.resample(5) # not part of the plan
+ assert cache.materialized(other_rate) is None
+ nptest.assert_array_equal(other_rate.build(cache).tstarts, [0, 5, 10, 15])
+
+ def test_rule_decides_the_result_type(self):
+ class _IntervalsRule(ResamplePushdownRule):
+ def decode(self, rows, pushdown, cache):
+ return Intervals(
+ rows[cache.tstart_col].to_numpy(), rows[cache.tend_col].to_numpy()
+ )
+
+ node = _rpm().resample(10)
+ result = self._cache(_IntervalsRule(), node).materialized(node)
+ assert isinstance(result, Intervals)
+ nptest.assert_array_equal(result.tstarts, [0, 10])
+
+ def test_each_node_is_classified_once_per_container(self):
+ matched = []
+
+ class _Recording(ResamplePushdownRule):
+ def match(self, node, operand):
+ matched.append(node)
+ return super().match(node, operand)
+
+ resampled = _rpm().resample(10)
+ cache = self._cache(_Recording(), resampled)
+ matched.clear()
+ integral = resampled.cumtrapz().build(cache)
+ # The root and the served resample, each matched once across both lookups.
+ assert len(matched) == 2 and len({id(node) for node in matched}) == 2
+ nptest.assert_array_equal(integral.tstarts, [0, 10])
+
+ def test_optimizer_default_rules(self):
+ assert [type(r) for r in QueryOptimizer().rules] == [ResamplePushdownRule]
+
+
+def _rpm(uses_alias: bool = False) -> TimeSeriesSelector:
+ return TimeSeriesSelector(TagSelector("channel_name") == "rpm", uses_alias=uses_alias)
diff --git a/tests/impulse_reporting/integration/calculated_channel_test.py b/tests/impulse_reporting/integration/calculated_channel_test.py
index 5a426e8e..44ba2eb2 100644
--- a/tests/impulse_reporting/integration/calculated_channel_test.py
+++ b/tests/impulse_reporting/integration/calculated_channel_test.py
@@ -9,6 +9,7 @@
from unittest.mock import create_autospec
+import pandas as pd
import pyspark.sql.functions as F
import pyspark.sql.types as T
import pytest
@@ -614,3 +615,51 @@ def test_channel_metrics_custom_kpis(spark):
assert "max" in metrics.columns
assert "mean" not in metrics.columns
assert "duration" not in metrics.columns
+
+
+def test_resampled_channel_facts_are_identical_with_pushdown(spark, monkeypatch):
+ """A resample-only calculated channel is solved in Spark without the pandas UDF;
+ its gold facts equal the ones written without pushdown."""
+ from impulse_query_engine.analyze.query.solvers.default_solver import DefaultSolver
+
+ udf_runs = []
+ original = DefaultSolver._apply_grouped_map
+
+ def _counting(self, *args, **kwargs):
+ udf_runs[-1] += 1
+ return original(self, *args, **kwargs)
+
+ monkeypatch.setattr(DefaultSolver, "_apply_grouped_map", _counting)
+ facts = []
+ for enable_pushdown in (False, True):
+ udf_runs.append(0)
+ config = _config()
+ config.query_engine = QueryEngine(enable_pushdown=enable_pushdown)
+ report = Report(
+ name="calc_channel_report",
+ spark=spark,
+ workspace_client=create_autospec(WorkspaceClient),
+ config=dict(config),
+ )
+ q = report.get_db().query
+ report.add_calculated_channel(
+ CalculatedChannel(
+ name="speed_10s",
+ expr=q.channel(channel_name="Vehicle Speed Sensor").resample(1e7),
+ identity={"channel_name": "speed_10s", "data_key": "CALC"},
+ )
+ )
+ report.determine_report()
+ report.persist_results()
+ facts.append(
+ spark.read.table(_FACT)
+ .drop("_created_at")
+ .toPandas()
+ .sort_values(["container_id", "channel_id", "tstart"])
+ .reset_index(drop=True)
+ )
+ off, on = facts
+ assert udf_runs == [1, 0]
+ assert len(off) > 0
+ assert off["value"].gt(0).any()
+ pd.testing.assert_frame_equal(on, off, check_exact=True)
diff --git a/tests/impulse_reporting/unit/config/config_parser_test.py b/tests/impulse_reporting/unit/config/config_parser_test.py
index b935f0d0..287dc83b 100644
--- a/tests/impulse_reporting/unit/config/config_parser_test.py
+++ b/tests/impulse_reporting/unit/config/config_parser_test.py
@@ -532,6 +532,18 @@ def test_impulse_config_from_dict_no_query_engine_provided():
assert config.query_engine.solver == Solvers.DEFAULT_SOLVER
+@pytest.mark.parametrize(
+ ("query_engine", "expected"), [({}, False), ({"enable_pushdown": True}, True)]
+)
+def test_impulse_config_enable_pushdown(query_engine, expected):
+ """Spark pushdown is opt-in."""
+ config_json = {
+ **impulse_config_JSON,
+ "query_engine": {"solver": "DefaultSolver", **query_engine},
+ }
+ assert ImpulseConfig.model_validate(config_json).query_engine.enable_pushdown is expected
+
+
def test_impulse_config_from_dict_no_measurement_dim_provided():
"""Test ImpulseConfig with no measurement dimension info provided."""
config_json = impulse_config_JSON.copy()
diff --git a/tests/impulse_reporting/unit/core/report_test.py b/tests/impulse_reporting/unit/core/report_test.py
index 696125d4..a60493af 100644
--- a/tests/impulse_reporting/unit/core/report_test.py
+++ b/tests/impulse_reporting/unit/core/report_test.py
@@ -314,6 +314,25 @@ def test_create_solver_key_value_store_with_solver_config():
assert solver.config.project_id_col == "project_id"
+@pytest.mark.parametrize("enable_pushdown", [False, True])
+def test_create_solver_forwards_enable_pushdown(enable_pushdown):
+ """``query_engine.enable_pushdown`` decides whether the solver gets an optimizer."""
+ config = {
+ **DUMMY_KEY_VALUE_STORE_CONFIG,
+ "query_engine": {
+ **DUMMY_KEY_VALUE_STORE_CONFIG["query_engine"],
+ "enable_pushdown": enable_pushdown,
+ },
+ }
+ report = Report(
+ name="test_report",
+ spark=None,
+ workspace_client=create_autospec(WorkspaceClient),
+ config=config,
+ )
+ assert (report.get_solver().optimizer is not None) is enable_pushdown
+
+
def test_create_solver_delta_default_config():
"""DeltaSolver created without solver_config gets a default SolverConfig (no renames)."""
from impulse_query_engine.analyze.query.solvers.default_solver import DefaultSolver