reader-workbench 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reader_workbench/__init__.py +22 -0
- reader_workbench/__main__.py +4 -0
- reader_workbench/_version.py +17 -0
- reader_workbench/api/__init__.py +74 -0
- reader_workbench/api/_record_reads.py +75 -0
- reader_workbench/api/artifacts.py +79 -0
- reader_workbench/api/facade.py +538 -0
- reader_workbench/api/models.py +285 -0
- reader_workbench/api/notebooks.py +63 -0
- reader_workbench/contracts/__init__.py +18 -0
- reader_workbench/contracts/builtins/__init__.py +36 -0
- reader_workbench/contracts/builtins/cytometry.py +140 -0
- reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
- reader_workbench/contracts/builtins/generic.py +21 -0
- reader_workbench/contracts/builtins/logic.py +149 -0
- reader_workbench/contracts/builtins/plate_reader.py +47 -0
- reader_workbench/contracts/catalog.py +257 -0
- reader_workbench/contracts/model.py +109 -0
- reader_workbench/domains/__init__.py +1 -0
- reader_workbench/domains/cytometry/__init__.py +3 -0
- reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
- reader_workbench/domains/cytometry/analysis/events.py +182 -0
- reader_workbench/domains/cytometry/analysis/gating.py +175 -0
- reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
- reader_workbench/domains/cytometry/io/__init__.py +3 -0
- reader_workbench/domains/cytometry/io/fcs.py +135 -0
- reader_workbench/domains/cytometry/plots/__init__.py +5 -0
- reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
- reader_workbench/domains/logic/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
- reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
- reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
- reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
- reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
- reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
- reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
- reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
- reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
- reader_workbench/domains/logic/four_state_vector/config.py +214 -0
- reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
- reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
- reader_workbench/domains/logic/four_state_vector/math.py +191 -0
- reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
- reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
- reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
- reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
- reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
- reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
- reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
- reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
- reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
- reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
- reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
- reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
- reader_workbench/domains/logic/treatment_columns.py +42 -0
- reader_workbench/domains/plate_reader/__init__.py +1 -0
- reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
- reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
- reader_workbench/domains/plate_reader/io/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
- reader_workbench/domains/plate_reader/ordering.py +59 -0
- reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
- reader_workbench/domains/plate_reader/plots/_data.py +29 -0
- reader_workbench/domains/plate_reader/plots/common.py +346 -0
- reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
- reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
- reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
- reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
- reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
- reader_workbench/domains/time_series/__init__.py +29 -0
- reader_workbench/domains/time_series/aggregation.py +60 -0
- reader_workbench/domains/time_series/contracts.py +368 -0
- reader_workbench/domains/time_series/reduction.py +395 -0
- reader_workbench/errors.py +55 -0
- reader_workbench/maintenance/__init__.py +6 -0
- reader_workbench/maintenance/docs.py +335 -0
- reader_workbench/maintenance/model.py +28 -0
- reader_workbench/maintenance/release.py +39 -0
- reader_workbench/maintenance/skills.py +124 -0
- reader_workbench/plotting/__init__.py +20 -0
- reader_workbench/plotting/mpl.py +56 -0
- reader_workbench/plotting/sinks.py +69 -0
- reader_workbench/plotting/style.py +175 -0
- reader_workbench/plotting/utils.py +27 -0
- reader_workbench/plugins/__init__.py +1 -0
- reader_workbench/plugins/catalog.py +33 -0
- reader_workbench/plugins/export/__init__.py +0 -0
- reader_workbench/plugins/export/_paths.py +21 -0
- reader_workbench/plugins/export/csv.py +41 -0
- reader_workbench/plugins/export/xlsx.py +44 -0
- reader_workbench/plugins/ingest/__init__.py +0 -0
- reader_workbench/plugins/ingest/_discovery.py +58 -0
- reader_workbench/plugins/ingest/discovery_policy.py +66 -0
- reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
- reader_workbench/plugins/ingest/synergy_h1.py +234 -0
- reader_workbench/plugins/manifests/__init__.py +1 -0
- reader_workbench/plugins/manifests/export.py +29 -0
- reader_workbench/plugins/manifests/ingest.py +29 -0
- reader_workbench/plugins/manifests/plot.py +161 -0
- reader_workbench/plugins/manifests/transform.py +172 -0
- reader_workbench/plugins/manifests/validator.py +18 -0
- reader_workbench/plugins/plot/__init__.py +0 -0
- reader_workbench/plugins/plot/_shared.py +55 -0
- reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
- reader_workbench/plugins/plot/distributions.py +58 -0
- reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
- reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
- reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
- reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
- reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
- reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
- reader_workbench/plugins/plot/logic_symmetry.py +56 -0
- reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
- reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
- reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
- reader_workbench/plugins/plot/time_series.py +114 -0
- reader_workbench/plugins/plot/ts_and_snap.py +210 -0
- reader_workbench/plugins/transform/__init__.py +0 -0
- reader_workbench/plugins/transform/_four_state_vector.py +204 -0
- reader_workbench/plugins/transform/_labeling.py +109 -0
- reader_workbench/plugins/transform/alias.py +70 -0
- reader_workbench/plugins/transform/assay_labels.py +62 -0
- reader_workbench/plugins/transform/blank.py +79 -0
- reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
- reader_workbench/plugins/transform/cytometry_gating.py +120 -0
- reader_workbench/plugins/transform/fold_change.py +79 -0
- reader_workbench/plugins/transform/four_state_event_window.py +93 -0
- reader_workbench/plugins/transform/four_state_vector.py +62 -0
- reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
- reader_workbench/plugins/transform/logic_symmetry.py +67 -0
- reader_workbench/plugins/transform/outlier_filter.py +60 -0
- reader_workbench/plugins/transform/overflow.py +197 -0
- reader_workbench/plugins/transform/ratio.py +237 -0
- reader_workbench/plugins/transform/sample_map.py +170 -0
- reader_workbench/plugins/transform/sample_metadata.py +94 -0
- reader_workbench/plugins/validator/__init__.py +1 -0
- reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
- reader_workbench/protocols/__init__.py +80 -0
- reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
- reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
- reader_workbench/protocols/builtins.py +1656 -0
- reader_workbench/protocols/compiler.py +22 -0
- reader_workbench/protocols/compilers/__init__.py +1 -0
- reader_workbench/protocols/compilers/common.py +100 -0
- reader_workbench/protocols/compilers/cytometry.py +87 -0
- reader_workbench/protocols/compilers/generic.py +14 -0
- reader_workbench/protocols/compilers/logic.py +245 -0
- reader_workbench/protocols/compilers/plate_reader.py +937 -0
- reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
- reader_workbench/protocols/model.py +1486 -0
- reader_workbench/protocols/semantic_coverage.py +234 -0
- reader_workbench/runtime/__init__.py +12 -0
- reader_workbench/runtime/builtin.py +23 -0
- reader_workbench/runtime/model.py +42 -0
- reader_workbench/workbench/__init__.py +60 -0
- reader_workbench/workbench/assets/__init__.py +22 -0
- reader_workbench/workbench/assets/types.py +118 -0
- reader_workbench/workbench/audit/__init__.py +5 -0
- reader_workbench/workbench/audit/experiments.py +307 -0
- reader_workbench/workbench/audit/staging.py +187 -0
- reader_workbench/workbench/cli/__init__.py +51 -0
- reader_workbench/workbench/cli/_lazy.py +9 -0
- reader_workbench/workbench/cli/_records_view.py +150 -0
- reader_workbench/workbench/cli/_surface_execution.py +443 -0
- reader_workbench/workbench/cli/audit.py +95 -0
- reader_workbench/workbench/cli/automation.py +229 -0
- reader_workbench/workbench/cli/demo.py +46 -0
- reader_workbench/workbench/cli/dop.py +91 -0
- reader_workbench/workbench/cli/experiments.py +635 -0
- reader_workbench/workbench/cli/helpers.py +232 -0
- reader_workbench/workbench/cli/main.py +59 -0
- reader_workbench/workbench/cli/maintenance.py +82 -0
- reader_workbench/workbench/cli/notebooks.py +260 -0
- reader_workbench/workbench/cli/pagination.py +117 -0
- reader_workbench/workbench/cli/protocols.py +336 -0
- reader_workbench/workbench/cli/shared.py +309 -0
- reader_workbench/workbench/cli/surfaces.py +534 -0
- reader_workbench/workbench/cli/verification.py +128 -0
- reader_workbench/workbench/commands.py +10 -0
- reader_workbench/workbench/config/__init__.py +47 -0
- reader_workbench/workbench/config/identity.py +13 -0
- reader_workbench/workbench/config/load.py +405 -0
- reader_workbench/workbench/config/model.py +274 -0
- reader_workbench/workbench/context.py +26 -0
- reader_workbench/workbench/decl/__init__.py +31 -0
- reader_workbench/workbench/decl/build.py +190 -0
- reader_workbench/workbench/decl/model.py +81 -0
- reader_workbench/workbench/dop/__init__.py +12 -0
- reader_workbench/workbench/dop/builtins.py +261 -0
- reader_workbench/workbench/dop/model.py +209 -0
- reader_workbench/workbench/engine/__init__.py +42 -0
- reader_workbench/workbench/engine/_shared.py +76 -0
- reader_workbench/workbench/engine/contracts.py +283 -0
- reader_workbench/workbench/engine/execution.py +326 -0
- reader_workbench/workbench/engine/file_outputs.py +260 -0
- reader_workbench/workbench/engine/inputs.py +161 -0
- reader_workbench/workbench/engine/invocations.py +507 -0
- reader_workbench/workbench/engine/planning.py +72 -0
- reader_workbench/workbench/engine/runtime.py +464 -0
- reader_workbench/workbench/engine/setup.py +149 -0
- reader_workbench/workbench/engine/validation.py +684 -0
- reader_workbench/workbench/experiment/__init__.py +47 -0
- reader_workbench/workbench/experiment/model.py +381 -0
- reader_workbench/workbench/experiments.py +133 -0
- reader_workbench/workbench/graph/__init__.py +47 -0
- reader_workbench/workbench/graph/nodes.py +102 -0
- reader_workbench/workbench/graph/normalize.py +177 -0
- reader_workbench/workbench/graph/refs.py +148 -0
- reader_workbench/workbench/input_discovery.py +19 -0
- reader_workbench/workbench/inspection/__init__.py +3 -0
- reader_workbench/workbench/inspection/catalogs.py +128 -0
- reader_workbench/workbench/inspection/common.py +92 -0
- reader_workbench/workbench/inspection/dop.py +64 -0
- reader_workbench/workbench/inspection/experiments.py +449 -0
- reader_workbench/workbench/inspection/inventory.py +68 -0
- reader_workbench/workbench/inspection/protocols.py +368 -0
- reader_workbench/workbench/inspection/readiness.py +333 -0
- reader_workbench/workbench/inspection/reports.py +367 -0
- reader_workbench/workbench/inspection/results.py +166 -0
- reader_workbench/workbench/inspection/runtime.py +287 -0
- reader_workbench/workbench/inspection/semantics.py +192 -0
- reader_workbench/workbench/inspection/validation.py +30 -0
- reader_workbench/workbench/notebooks/__init__.py +17 -0
- reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
- reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
- reader_workbench/workbench/notebooks/components/__init__.py +21 -0
- reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
- reader_workbench/workbench/notebooks/components/overview.py +119 -0
- reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
- reader_workbench/workbench/notebooks/launch.py +274 -0
- reader_workbench/workbench/notebooks/presentation.py +136 -0
- reader_workbench/workbench/notebooks/scaffold.py +60 -0
- reader_workbench/workbench/ontology.py +78 -0
- reader_workbench/workbench/paths.py +44 -0
- reader_workbench/workbench/ports/__init__.py +31 -0
- reader_workbench/workbench/ports/model.py +168 -0
- reader_workbench/workbench/records/__init__.py +44 -0
- reader_workbench/workbench/records/epoch.py +329 -0
- reader_workbench/workbench/records/evidence.py +247 -0
- reader_workbench/workbench/records/identity.py +87 -0
- reader_workbench/workbench/records/locking.py +185 -0
- reader_workbench/workbench/records/model.py +711 -0
- reader_workbench/workbench/records/sources.py +73 -0
- reader_workbench/workbench/records/store.py +1022 -0
- reader_workbench/workbench/records/verification.py +998 -0
- reader_workbench/workbench/registry.py +333 -0
- reader_workbench/workbench/spec_overrides.py +215 -0
- reader_workbench-1.0.0.dist-info/METADATA +91 -0
- reader_workbench-1.0.0.dist-info/RECORD +293 -0
- reader_workbench-1.0.0.dist-info/WHEEL +5 -0
- reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
- reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
- reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Pure temporal trace reduction with explicit selection and support policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .contracts import EndpointSelection, IntervalSelection, TemporalReductionSpec
|
|
12
|
+
|
|
13
|
+
ValueBoundKind = Literal["exact", "lower", "upper", "indeterminate"]
|
|
14
|
+
_EXACT_TIME_ATOL_H = 1.0e-9
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class TemporalReductionResult:
|
|
19
|
+
value: float
|
|
20
|
+
observed_point_count: int
|
|
21
|
+
evaluation_point_count: int
|
|
22
|
+
max_interior_gap_h: float
|
|
23
|
+
policy_clipped_point_count: int
|
|
24
|
+
instrument_overflow_point_count: int
|
|
25
|
+
bound_kind: ValueBoundKind
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def reduce_temporal_trace(
|
|
29
|
+
times: np.ndarray,
|
|
30
|
+
values: np.ndarray,
|
|
31
|
+
*,
|
|
32
|
+
spec: TemporalReductionSpec,
|
|
33
|
+
trace_id: str,
|
|
34
|
+
origin_h: float | None = None,
|
|
35
|
+
policy_clipped: np.ndarray | None = None,
|
|
36
|
+
instrument_overflow: np.ndarray | None = None,
|
|
37
|
+
bound_kinds: np.ndarray | None = None,
|
|
38
|
+
) -> TemporalReductionResult:
|
|
39
|
+
"""Reduce one trace without extrapolation or implicit time semantics."""
|
|
40
|
+
|
|
41
|
+
x = np.asarray(times, dtype=float)
|
|
42
|
+
y = np.asarray(values, dtype=float)
|
|
43
|
+
if x.ndim != 1 or y.ndim != 1 or x.size != y.size or x.size == 0:
|
|
44
|
+
raise ValueError(f"{trace_id} must contain aligned one-dimensional time and value arrays")
|
|
45
|
+
if not np.isfinite(x).all() or not np.isfinite(y).all():
|
|
46
|
+
raise ValueError(f"{trace_id} contains non-finite time or signal values")
|
|
47
|
+
|
|
48
|
+
order = np.argsort(x, kind="mergesort")
|
|
49
|
+
x = x[order]
|
|
50
|
+
y = y[order]
|
|
51
|
+
if len(np.unique(x)) != len(x):
|
|
52
|
+
raise ValueError(f"{trace_id} contains duplicate time coordinates")
|
|
53
|
+
policy, overflow, bounds = _value_provenance(
|
|
54
|
+
size=len(x),
|
|
55
|
+
order=order,
|
|
56
|
+
policy_clipped=policy_clipped,
|
|
57
|
+
instrument_overflow=instrument_overflow,
|
|
58
|
+
bound_kinds=bound_kinds,
|
|
59
|
+
trace_id=trace_id,
|
|
60
|
+
)
|
|
61
|
+
origin = _resolved_origin(spec=spec, origin_h=origin_h)
|
|
62
|
+
if isinstance(spec.selection, EndpointSelection):
|
|
63
|
+
return _reduce_endpoint(
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
policy=policy,
|
|
67
|
+
overflow=overflow,
|
|
68
|
+
bounds=bounds,
|
|
69
|
+
spec=spec,
|
|
70
|
+
endpoint_h=origin + spec.selection.time_h,
|
|
71
|
+
trace_id=trace_id,
|
|
72
|
+
)
|
|
73
|
+
return _reduce_interval(
|
|
74
|
+
x,
|
|
75
|
+
y,
|
|
76
|
+
policy=policy,
|
|
77
|
+
overflow=overflow,
|
|
78
|
+
bounds=bounds,
|
|
79
|
+
spec=spec,
|
|
80
|
+
window_start_h=origin + spec.selection.start_h,
|
|
81
|
+
window_end_h=origin + spec.selection.end_h,
|
|
82
|
+
trace_id=trace_id,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolved_origin(*, spec: TemporalReductionSpec, origin_h: float | None) -> float:
|
|
87
|
+
selection = spec.selection
|
|
88
|
+
if selection.time_basis == "absolute":
|
|
89
|
+
if origin_h is not None and not math.isclose(float(origin_h), 0.0, abs_tol=0.0):
|
|
90
|
+
raise ValueError("absolute temporal reductions cannot declare a nonzero origin_h")
|
|
91
|
+
return 0.0
|
|
92
|
+
if origin_h is None or not math.isfinite(float(origin_h)):
|
|
93
|
+
raise ValueError("event-relative temporal reductions require a finite origin_h")
|
|
94
|
+
return float(origin_h)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _reduce_endpoint(
|
|
98
|
+
x: np.ndarray,
|
|
99
|
+
y: np.ndarray,
|
|
100
|
+
*,
|
|
101
|
+
policy: np.ndarray,
|
|
102
|
+
overflow: np.ndarray,
|
|
103
|
+
bounds: np.ndarray,
|
|
104
|
+
spec: TemporalReductionSpec,
|
|
105
|
+
endpoint_h: float,
|
|
106
|
+
trace_id: str,
|
|
107
|
+
) -> TemporalReductionResult:
|
|
108
|
+
selection = spec.selection
|
|
109
|
+
assert isinstance(selection, EndpointSelection)
|
|
110
|
+
delta = np.abs(x - endpoint_h)
|
|
111
|
+
if selection.mode == "exact":
|
|
112
|
+
candidates = np.flatnonzero(delta <= _EXACT_TIME_ATOL_H)
|
|
113
|
+
missing_message = f"{trace_id} has no exact observation at {endpoint_h:g} h"
|
|
114
|
+
else:
|
|
115
|
+
minimum = float(delta.min())
|
|
116
|
+
candidates = np.flatnonzero(np.isclose(delta, minimum, rtol=0.0, atol=1.0e-12))
|
|
117
|
+
missing_message = (
|
|
118
|
+
f"{trace_id} has no endpoint observation within {selection.tolerance_h:g} h of {endpoint_h:g} h"
|
|
119
|
+
)
|
|
120
|
+
if minimum > selection.tolerance_h:
|
|
121
|
+
candidates = np.asarray([], dtype=int)
|
|
122
|
+
if len(candidates) == 0:
|
|
123
|
+
raise ValueError(missing_message)
|
|
124
|
+
if len(candidates) != 1:
|
|
125
|
+
raise ValueError(f"{trace_id} endpoint selection is ambiguous at {endpoint_h:g} h")
|
|
126
|
+
index = int(candidates[0])
|
|
127
|
+
positive_values = y if spec.support.positive_value_scope == "entire_trace" else y[index : index + 1]
|
|
128
|
+
_require_positive(positive_values, spec=spec, trace_id=trace_id)
|
|
129
|
+
_reject_censored(
|
|
130
|
+
policy=policy[index : index + 1],
|
|
131
|
+
overflow=overflow[index : index + 1],
|
|
132
|
+
spec=spec,
|
|
133
|
+
trace_id=trace_id,
|
|
134
|
+
)
|
|
135
|
+
value = _project_output(float(y[index]), spec=spec, trace_id=trace_id)
|
|
136
|
+
return TemporalReductionResult(
|
|
137
|
+
value=value,
|
|
138
|
+
observed_point_count=1,
|
|
139
|
+
evaluation_point_count=1,
|
|
140
|
+
max_interior_gap_h=0.0,
|
|
141
|
+
policy_clipped_point_count=int(policy[index]),
|
|
142
|
+
instrument_overflow_point_count=int(overflow[index]),
|
|
143
|
+
bound_kind=combine_bound_kinds(bounds[index]),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _reduce_interval(
|
|
148
|
+
x: np.ndarray,
|
|
149
|
+
y: np.ndarray,
|
|
150
|
+
*,
|
|
151
|
+
policy: np.ndarray,
|
|
152
|
+
overflow: np.ndarray,
|
|
153
|
+
bounds: np.ndarray,
|
|
154
|
+
spec: TemporalReductionSpec,
|
|
155
|
+
window_start_h: float,
|
|
156
|
+
window_end_h: float,
|
|
157
|
+
trace_id: str,
|
|
158
|
+
) -> TemporalReductionResult:
|
|
159
|
+
selection = spec.selection
|
|
160
|
+
assert isinstance(selection, IntervalSelection)
|
|
161
|
+
if window_end_h <= window_start_h:
|
|
162
|
+
raise ValueError(f"{trace_id} interval resolves to a non-increasing absolute window")
|
|
163
|
+
|
|
164
|
+
support = spec.support
|
|
165
|
+
reduction_x = _normalized_interval_times(
|
|
166
|
+
x,
|
|
167
|
+
window_start_h=window_start_h,
|
|
168
|
+
window_end_h=window_end_h,
|
|
169
|
+
boundary_support=support.boundary_support,
|
|
170
|
+
trace_id=trace_id,
|
|
171
|
+
)
|
|
172
|
+
selected = (reduction_x >= window_start_h) & (reduction_x <= window_end_h)
|
|
173
|
+
selected_indexes = np.flatnonzero(selected)
|
|
174
|
+
if len(selected_indexes) < support.minimum_observations:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"{trace_id} has {len(selected_indexes)} observations in the selected interval; "
|
|
177
|
+
f"at least {support.minimum_observations} are required"
|
|
178
|
+
)
|
|
179
|
+
left_index, right_index = _support_bounds(
|
|
180
|
+
reduction_x,
|
|
181
|
+
selected_indexes=selected_indexes,
|
|
182
|
+
window_start_h=window_start_h,
|
|
183
|
+
window_end_h=window_end_h,
|
|
184
|
+
boundary_support=support.boundary_support,
|
|
185
|
+
trace_id=trace_id,
|
|
186
|
+
)
|
|
187
|
+
support_slice = slice(left_index, right_index + 1)
|
|
188
|
+
support_x = reduction_x[support_slice]
|
|
189
|
+
observed_max_gap = float(np.max(np.diff(support_x))) if len(support_x) > 1 else 0.0
|
|
190
|
+
if (
|
|
191
|
+
support.maximum_interior_gap_h is not None
|
|
192
|
+
and observed_max_gap > support.maximum_interior_gap_h + _EXACT_TIME_ATOL_H
|
|
193
|
+
):
|
|
194
|
+
raise ValueError(f"{trace_id} interior gap {observed_max_gap:g} h exceeds {support.maximum_interior_gap_h:g} h")
|
|
195
|
+
_reject_censored(
|
|
196
|
+
policy=policy[support_slice],
|
|
197
|
+
overflow=overflow[support_slice],
|
|
198
|
+
spec=spec,
|
|
199
|
+
trace_id=trace_id,
|
|
200
|
+
)
|
|
201
|
+
positive_values = y if support.positive_value_scope == "entire_trace" else y[support_slice]
|
|
202
|
+
_require_positive(positive_values, spec=spec, trace_id=trace_id)
|
|
203
|
+
|
|
204
|
+
method = spec.method
|
|
205
|
+
evaluation_count: int
|
|
206
|
+
with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
|
|
207
|
+
if method == "observed_mean":
|
|
208
|
+
reduced = float(np.mean(y[selected]))
|
|
209
|
+
evaluation_count = len(selected_indexes)
|
|
210
|
+
elif method == "observed_median":
|
|
211
|
+
reduced = float(np.median(y[selected]))
|
|
212
|
+
evaluation_count = len(selected_indexes)
|
|
213
|
+
else:
|
|
214
|
+
inside = (reduction_x > window_start_h) & (reduction_x < window_end_h)
|
|
215
|
+
window_x = np.concatenate(([window_start_h], reduction_x[inside], [window_end_h]))
|
|
216
|
+
duration = float(window_end_h - window_start_h)
|
|
217
|
+
if method == "geometric_time_mean":
|
|
218
|
+
log_values = np.log2(y)
|
|
219
|
+
window_values = np.concatenate(
|
|
220
|
+
(
|
|
221
|
+
[np.interp(window_start_h, reduction_x, log_values)],
|
|
222
|
+
log_values[inside],
|
|
223
|
+
[np.interp(window_end_h, reduction_x, log_values)],
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
reduced = float(np.trapezoid(window_values, window_x) / duration)
|
|
227
|
+
elif method == "integrated_linear_mean":
|
|
228
|
+
window_values = np.concatenate(
|
|
229
|
+
(
|
|
230
|
+
[np.interp(window_start_h, reduction_x, y)],
|
|
231
|
+
y[inside],
|
|
232
|
+
[np.interp(window_end_h, reduction_x, y)],
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
reduced = float(np.log2(float(np.trapezoid(window_values, window_x) / duration)))
|
|
236
|
+
else: # pragma: no cover - TemporalReductionSpec validates this pair.
|
|
237
|
+
raise ValueError(f"unsupported interval temporal reduction method: {method!r}")
|
|
238
|
+
evaluation_count = len(window_x)
|
|
239
|
+
if method in {"observed_mean", "observed_median"}:
|
|
240
|
+
reduced = _project_output(reduced, spec=spec, trace_id=trace_id)
|
|
241
|
+
if not math.isfinite(reduced):
|
|
242
|
+
raise ValueError(f"{trace_id} temporal reduction produced a non-finite value")
|
|
243
|
+
|
|
244
|
+
return TemporalReductionResult(
|
|
245
|
+
value=reduced,
|
|
246
|
+
observed_point_count=len(selected_indexes),
|
|
247
|
+
evaluation_point_count=evaluation_count,
|
|
248
|
+
max_interior_gap_h=observed_max_gap,
|
|
249
|
+
policy_clipped_point_count=int(policy[support_slice].sum()),
|
|
250
|
+
instrument_overflow_point_count=int(overflow[support_slice].sum()),
|
|
251
|
+
bound_kind=combine_bound_kinds(*bounds[support_slice]),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _normalized_interval_times(
|
|
256
|
+
x: np.ndarray,
|
|
257
|
+
*,
|
|
258
|
+
window_start_h: float,
|
|
259
|
+
window_end_h: float,
|
|
260
|
+
boundary_support: str,
|
|
261
|
+
trace_id: str,
|
|
262
|
+
) -> np.ndarray:
|
|
263
|
+
if boundary_support != "observed":
|
|
264
|
+
return x
|
|
265
|
+
normalized = x.copy()
|
|
266
|
+
start_indexes = np.flatnonzero(np.isclose(x, window_start_h, rtol=0.0, atol=_EXACT_TIME_ATOL_H))
|
|
267
|
+
end_indexes = np.flatnonzero(np.isclose(x, window_end_h, rtol=0.0, atol=_EXACT_TIME_ATOL_H))
|
|
268
|
+
if len(start_indexes) > 1:
|
|
269
|
+
raise ValueError(f"{trace_id} has ambiguous observations at the interval start {window_start_h:g} h")
|
|
270
|
+
if len(end_indexes) > 1:
|
|
271
|
+
raise ValueError(f"{trace_id} has ambiguous observations at the interval end {window_end_h:g} h")
|
|
272
|
+
if len(start_indexes) == 1 and len(end_indexes) == 1 and start_indexes[0] == end_indexes[0]:
|
|
273
|
+
raise ValueError(f"{trace_id} has one observation matching both interval boundaries")
|
|
274
|
+
if len(start_indexes) == 1:
|
|
275
|
+
normalized[start_indexes[0]] = window_start_h
|
|
276
|
+
if len(end_indexes) == 1:
|
|
277
|
+
normalized[end_indexes[0]] = window_end_h
|
|
278
|
+
return normalized
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _support_bounds(
|
|
282
|
+
x: np.ndarray,
|
|
283
|
+
*,
|
|
284
|
+
selected_indexes: np.ndarray,
|
|
285
|
+
window_start_h: float,
|
|
286
|
+
window_end_h: float,
|
|
287
|
+
boundary_support: str,
|
|
288
|
+
trace_id: str,
|
|
289
|
+
) -> tuple[int, int]:
|
|
290
|
+
if boundary_support == "covered":
|
|
291
|
+
if x[0] > window_start_h or x[-1] < window_end_h:
|
|
292
|
+
raise ValueError(f"{trace_id} does not cover [{window_start_h:g}, {window_end_h:g}] without extrapolation")
|
|
293
|
+
left_index = max(int(np.searchsorted(x, window_start_h, side="right")) - 1, 0)
|
|
294
|
+
right_index = min(int(np.searchsorted(x, window_end_h, side="left")), len(x) - 1)
|
|
295
|
+
return left_index, right_index
|
|
296
|
+
if boundary_support == "observed" and (
|
|
297
|
+
not np.any(np.isclose(x, window_start_h, rtol=0.0, atol=_EXACT_TIME_ATOL_H))
|
|
298
|
+
or not np.any(np.isclose(x, window_end_h, rtol=0.0, atol=_EXACT_TIME_ATOL_H))
|
|
299
|
+
):
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"{trace_id} requires observed interval boundaries at {window_start_h:g} and {window_end_h:g} h"
|
|
302
|
+
)
|
|
303
|
+
if len(selected_indexes) == 0:
|
|
304
|
+
raise ValueError(f"{trace_id} selected interval contains no observations")
|
|
305
|
+
return int(selected_indexes[0]), int(selected_indexes[-1])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _project_output(value: float, *, spec: TemporalReductionSpec, trace_id: str) -> float:
|
|
309
|
+
if spec.output_space == "linear":
|
|
310
|
+
return value
|
|
311
|
+
floor = spec.support.positive_floor
|
|
312
|
+
assert floor is not None
|
|
313
|
+
if value <= floor:
|
|
314
|
+
raise ValueError(f"{trace_id} reduced value is at or below the positive floor {floor:g}")
|
|
315
|
+
return float(np.log2(value))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _require_positive(values: np.ndarray, *, spec: TemporalReductionSpec, trace_id: str) -> None:
|
|
319
|
+
floor = spec.support.positive_floor
|
|
320
|
+
if floor is not None and np.any(values <= floor):
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"{trace_id} contains values at or below the positive floor {floor:g}; "
|
|
323
|
+
"log-space reduction requires strictly positive values"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _reject_censored(
|
|
328
|
+
*,
|
|
329
|
+
policy: np.ndarray,
|
|
330
|
+
overflow: np.ndarray,
|
|
331
|
+
spec: TemporalReductionSpec,
|
|
332
|
+
trace_id: str,
|
|
333
|
+
) -> None:
|
|
334
|
+
if spec.support.censored_values == "reject" and np.any(policy | overflow):
|
|
335
|
+
raise ValueError(f"{trace_id} selected support contains clipped or overflowed observations")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _value_provenance(
|
|
339
|
+
*,
|
|
340
|
+
size: int,
|
|
341
|
+
order: np.ndarray,
|
|
342
|
+
policy_clipped: np.ndarray | None,
|
|
343
|
+
instrument_overflow: np.ndarray | None,
|
|
344
|
+
bound_kinds: np.ndarray | None,
|
|
345
|
+
trace_id: str,
|
|
346
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
347
|
+
policy = np.zeros(size, dtype=bool) if policy_clipped is None else np.asarray(policy_clipped)
|
|
348
|
+
overflow = np.zeros(size, dtype=bool) if instrument_overflow is None else np.asarray(instrument_overflow)
|
|
349
|
+
bounds = np.full(size, "exact", dtype=object) if bound_kinds is None else np.asarray(bound_kinds, dtype=object)
|
|
350
|
+
for name, values in (("policy clipping", policy), ("instrument overflow", overflow), ("bounds", bounds)):
|
|
351
|
+
if values.ndim != 1 or len(values) != size:
|
|
352
|
+
raise ValueError(f"{trace_id} {name} provenance must align with time and value arrays")
|
|
353
|
+
if policy.dtype.kind != "b" or overflow.dtype.kind != "b":
|
|
354
|
+
raise ValueError(f"{trace_id} clipping and overflow provenance must be boolean")
|
|
355
|
+
allowed = {"exact", "lower", "upper", "indeterminate"}
|
|
356
|
+
unknown = sorted(set(map(str, bounds)) - allowed)
|
|
357
|
+
if unknown:
|
|
358
|
+
raise ValueError(f"{trace_id} contains unsupported value bounds: {unknown}")
|
|
359
|
+
if np.any((policy | overflow) & (bounds.astype(str) == "exact")):
|
|
360
|
+
raise ValueError(f"{trace_id} marks clipped or overflowed observations as exact")
|
|
361
|
+
return policy[order], overflow[order], bounds[order]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def combine_bound_kinds(*kinds: object) -> ValueBoundKind:
|
|
365
|
+
active = {str(kind) for kind in kinds} - {"exact"}
|
|
366
|
+
if not active:
|
|
367
|
+
return "exact"
|
|
368
|
+
if "indeterminate" in active or len(active) > 1:
|
|
369
|
+
return "indeterminate"
|
|
370
|
+
result = active.pop()
|
|
371
|
+
if result not in {"lower", "upper"}: # pragma: no cover - source validation owns this boundary.
|
|
372
|
+
raise ValueError(f"unsupported value bound kind: {result!r}")
|
|
373
|
+
return result # type: ignore[return-value]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def invert_bound_kind(kind: object) -> ValueBoundKind:
|
|
377
|
+
inverse: dict[str, ValueBoundKind] = {
|
|
378
|
+
"exact": "exact",
|
|
379
|
+
"lower": "upper",
|
|
380
|
+
"upper": "lower",
|
|
381
|
+
"indeterminate": "indeterminate",
|
|
382
|
+
}
|
|
383
|
+
try:
|
|
384
|
+
return inverse[str(kind)]
|
|
385
|
+
except KeyError as exc: # pragma: no cover - source validation owns this boundary.
|
|
386
|
+
raise ValueError(f"unsupported value bound kind: {kind!r}") from exc
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
__all__ = [
|
|
390
|
+
"TemporalReductionResult",
|
|
391
|
+
"ValueBoundKind",
|
|
392
|
+
"combine_bound_kinds",
|
|
393
|
+
"invert_bound_kind",
|
|
394
|
+
"reduce_temporal_trace",
|
|
395
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Package-wide exception taxonomy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReaderError(Exception): ...
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigError(ReaderError): ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegistryError(ReaderError): ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContractError(ReaderError): ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RecordError(ReaderError): ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProvenanceEpochChangedError(RecordError):
|
|
22
|
+
"""A bound record-store operation observed a different provenance epoch."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExecutionError(ReaderError): ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InvocationFinalizationError(ExecutionError):
|
|
29
|
+
"""Execution committed records but could not confirm its terminal ledger event."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str,
|
|
34
|
+
*,
|
|
35
|
+
invocation_id: str,
|
|
36
|
+
produced_record_revisions: tuple[dict[str, object], ...],
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.invocation_id = invocation_id
|
|
40
|
+
self.produced_record_revisions = produced_record_revisions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ParseError(ReaderError): ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MergeError(ReaderError): ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TransformError(ReaderError): ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PlotError(ReaderError): ...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FourStateVectorError(ReaderError): ...
|