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.
Files changed (293) hide show
  1. reader_workbench/__init__.py +22 -0
  2. reader_workbench/__main__.py +4 -0
  3. reader_workbench/_version.py +17 -0
  4. reader_workbench/api/__init__.py +74 -0
  5. reader_workbench/api/_record_reads.py +75 -0
  6. reader_workbench/api/artifacts.py +79 -0
  7. reader_workbench/api/facade.py +538 -0
  8. reader_workbench/api/models.py +285 -0
  9. reader_workbench/api/notebooks.py +63 -0
  10. reader_workbench/contracts/__init__.py +18 -0
  11. reader_workbench/contracts/builtins/__init__.py +36 -0
  12. reader_workbench/contracts/builtins/cytometry.py +140 -0
  13. reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
  14. reader_workbench/contracts/builtins/generic.py +21 -0
  15. reader_workbench/contracts/builtins/logic.py +149 -0
  16. reader_workbench/contracts/builtins/plate_reader.py +47 -0
  17. reader_workbench/contracts/catalog.py +257 -0
  18. reader_workbench/contracts/model.py +109 -0
  19. reader_workbench/domains/__init__.py +1 -0
  20. reader_workbench/domains/cytometry/__init__.py +3 -0
  21. reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
  22. reader_workbench/domains/cytometry/analysis/events.py +182 -0
  23. reader_workbench/domains/cytometry/analysis/gating.py +175 -0
  24. reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
  25. reader_workbench/domains/cytometry/io/__init__.py +3 -0
  26. reader_workbench/domains/cytometry/io/fcs.py +135 -0
  27. reader_workbench/domains/cytometry/plots/__init__.py +5 -0
  28. reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
  29. reader_workbench/domains/logic/__init__.py +3 -0
  30. reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
  31. reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
  32. reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
  33. reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
  34. reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
  35. reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
  36. reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
  37. reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
  38. reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
  39. reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
  40. reader_workbench/domains/logic/four_state_vector/config.py +214 -0
  41. reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
  42. reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
  43. reader_workbench/domains/logic/four_state_vector/math.py +191 -0
  44. reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
  45. reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
  46. reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
  47. reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
  48. reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
  49. reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
  50. reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
  51. reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
  52. reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
  53. reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
  54. reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
  55. reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
  56. reader_workbench/domains/logic/treatment_columns.py +42 -0
  57. reader_workbench/domains/plate_reader/__init__.py +1 -0
  58. reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
  59. reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
  60. reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
  61. reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
  62. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
  63. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
  64. reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
  65. reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
  66. reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
  67. reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
  68. reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
  69. reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
  70. reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
  71. reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
  72. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
  73. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
  74. reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
  75. reader_workbench/domains/plate_reader/io/__init__.py +6 -0
  76. reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
  77. reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
  78. reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
  79. reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
  80. reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
  81. reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
  82. reader_workbench/domains/plate_reader/ordering.py +59 -0
  83. reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
  84. reader_workbench/domains/plate_reader/plots/_data.py +29 -0
  85. reader_workbench/domains/plate_reader/plots/common.py +346 -0
  86. reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
  87. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
  88. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
  89. reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
  90. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
  91. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
  92. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
  93. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
  94. reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
  95. reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
  96. reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
  97. reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
  98. reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
  99. reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
  100. reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
  101. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
  102. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
  103. reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
  104. reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
  105. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
  106. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
  107. reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
  108. reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
  109. reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
  110. reader_workbench/domains/time_series/__init__.py +29 -0
  111. reader_workbench/domains/time_series/aggregation.py +60 -0
  112. reader_workbench/domains/time_series/contracts.py +368 -0
  113. reader_workbench/domains/time_series/reduction.py +395 -0
  114. reader_workbench/errors.py +55 -0
  115. reader_workbench/maintenance/__init__.py +6 -0
  116. reader_workbench/maintenance/docs.py +335 -0
  117. reader_workbench/maintenance/model.py +28 -0
  118. reader_workbench/maintenance/release.py +39 -0
  119. reader_workbench/maintenance/skills.py +124 -0
  120. reader_workbench/plotting/__init__.py +20 -0
  121. reader_workbench/plotting/mpl.py +56 -0
  122. reader_workbench/plotting/sinks.py +69 -0
  123. reader_workbench/plotting/style.py +175 -0
  124. reader_workbench/plotting/utils.py +27 -0
  125. reader_workbench/plugins/__init__.py +1 -0
  126. reader_workbench/plugins/catalog.py +33 -0
  127. reader_workbench/plugins/export/__init__.py +0 -0
  128. reader_workbench/plugins/export/_paths.py +21 -0
  129. reader_workbench/plugins/export/csv.py +41 -0
  130. reader_workbench/plugins/export/xlsx.py +44 -0
  131. reader_workbench/plugins/ingest/__init__.py +0 -0
  132. reader_workbench/plugins/ingest/_discovery.py +58 -0
  133. reader_workbench/plugins/ingest/discovery_policy.py +66 -0
  134. reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
  135. reader_workbench/plugins/ingest/synergy_h1.py +234 -0
  136. reader_workbench/plugins/manifests/__init__.py +1 -0
  137. reader_workbench/plugins/manifests/export.py +29 -0
  138. reader_workbench/plugins/manifests/ingest.py +29 -0
  139. reader_workbench/plugins/manifests/plot.py +161 -0
  140. reader_workbench/plugins/manifests/transform.py +172 -0
  141. reader_workbench/plugins/manifests/validator.py +18 -0
  142. reader_workbench/plugins/plot/__init__.py +0 -0
  143. reader_workbench/plugins/plot/_shared.py +55 -0
  144. reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
  145. reader_workbench/plugins/plot/distributions.py +58 -0
  146. reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
  147. reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
  148. reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
  149. reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
  150. reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
  151. reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
  152. reader_workbench/plugins/plot/logic_symmetry.py +56 -0
  153. reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
  154. reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
  155. reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
  156. reader_workbench/plugins/plot/time_series.py +114 -0
  157. reader_workbench/plugins/plot/ts_and_snap.py +210 -0
  158. reader_workbench/plugins/transform/__init__.py +0 -0
  159. reader_workbench/plugins/transform/_four_state_vector.py +204 -0
  160. reader_workbench/plugins/transform/_labeling.py +109 -0
  161. reader_workbench/plugins/transform/alias.py +70 -0
  162. reader_workbench/plugins/transform/assay_labels.py +62 -0
  163. reader_workbench/plugins/transform/blank.py +79 -0
  164. reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
  165. reader_workbench/plugins/transform/cytometry_gating.py +120 -0
  166. reader_workbench/plugins/transform/fold_change.py +79 -0
  167. reader_workbench/plugins/transform/four_state_event_window.py +93 -0
  168. reader_workbench/plugins/transform/four_state_vector.py +62 -0
  169. reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
  170. reader_workbench/plugins/transform/logic_symmetry.py +67 -0
  171. reader_workbench/plugins/transform/outlier_filter.py +60 -0
  172. reader_workbench/plugins/transform/overflow.py +197 -0
  173. reader_workbench/plugins/transform/ratio.py +237 -0
  174. reader_workbench/plugins/transform/sample_map.py +170 -0
  175. reader_workbench/plugins/transform/sample_metadata.py +94 -0
  176. reader_workbench/plugins/validator/__init__.py +1 -0
  177. reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
  178. reader_workbench/protocols/__init__.py +80 -0
  179. reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
  180. reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
  181. reader_workbench/protocols/builtins.py +1656 -0
  182. reader_workbench/protocols/compiler.py +22 -0
  183. reader_workbench/protocols/compilers/__init__.py +1 -0
  184. reader_workbench/protocols/compilers/common.py +100 -0
  185. reader_workbench/protocols/compilers/cytometry.py +87 -0
  186. reader_workbench/protocols/compilers/generic.py +14 -0
  187. reader_workbench/protocols/compilers/logic.py +245 -0
  188. reader_workbench/protocols/compilers/plate_reader.py +937 -0
  189. reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
  190. reader_workbench/protocols/model.py +1486 -0
  191. reader_workbench/protocols/semantic_coverage.py +234 -0
  192. reader_workbench/runtime/__init__.py +12 -0
  193. reader_workbench/runtime/builtin.py +23 -0
  194. reader_workbench/runtime/model.py +42 -0
  195. reader_workbench/workbench/__init__.py +60 -0
  196. reader_workbench/workbench/assets/__init__.py +22 -0
  197. reader_workbench/workbench/assets/types.py +118 -0
  198. reader_workbench/workbench/audit/__init__.py +5 -0
  199. reader_workbench/workbench/audit/experiments.py +307 -0
  200. reader_workbench/workbench/audit/staging.py +187 -0
  201. reader_workbench/workbench/cli/__init__.py +51 -0
  202. reader_workbench/workbench/cli/_lazy.py +9 -0
  203. reader_workbench/workbench/cli/_records_view.py +150 -0
  204. reader_workbench/workbench/cli/_surface_execution.py +443 -0
  205. reader_workbench/workbench/cli/audit.py +95 -0
  206. reader_workbench/workbench/cli/automation.py +229 -0
  207. reader_workbench/workbench/cli/demo.py +46 -0
  208. reader_workbench/workbench/cli/dop.py +91 -0
  209. reader_workbench/workbench/cli/experiments.py +635 -0
  210. reader_workbench/workbench/cli/helpers.py +232 -0
  211. reader_workbench/workbench/cli/main.py +59 -0
  212. reader_workbench/workbench/cli/maintenance.py +82 -0
  213. reader_workbench/workbench/cli/notebooks.py +260 -0
  214. reader_workbench/workbench/cli/pagination.py +117 -0
  215. reader_workbench/workbench/cli/protocols.py +336 -0
  216. reader_workbench/workbench/cli/shared.py +309 -0
  217. reader_workbench/workbench/cli/surfaces.py +534 -0
  218. reader_workbench/workbench/cli/verification.py +128 -0
  219. reader_workbench/workbench/commands.py +10 -0
  220. reader_workbench/workbench/config/__init__.py +47 -0
  221. reader_workbench/workbench/config/identity.py +13 -0
  222. reader_workbench/workbench/config/load.py +405 -0
  223. reader_workbench/workbench/config/model.py +274 -0
  224. reader_workbench/workbench/context.py +26 -0
  225. reader_workbench/workbench/decl/__init__.py +31 -0
  226. reader_workbench/workbench/decl/build.py +190 -0
  227. reader_workbench/workbench/decl/model.py +81 -0
  228. reader_workbench/workbench/dop/__init__.py +12 -0
  229. reader_workbench/workbench/dop/builtins.py +261 -0
  230. reader_workbench/workbench/dop/model.py +209 -0
  231. reader_workbench/workbench/engine/__init__.py +42 -0
  232. reader_workbench/workbench/engine/_shared.py +76 -0
  233. reader_workbench/workbench/engine/contracts.py +283 -0
  234. reader_workbench/workbench/engine/execution.py +326 -0
  235. reader_workbench/workbench/engine/file_outputs.py +260 -0
  236. reader_workbench/workbench/engine/inputs.py +161 -0
  237. reader_workbench/workbench/engine/invocations.py +507 -0
  238. reader_workbench/workbench/engine/planning.py +72 -0
  239. reader_workbench/workbench/engine/runtime.py +464 -0
  240. reader_workbench/workbench/engine/setup.py +149 -0
  241. reader_workbench/workbench/engine/validation.py +684 -0
  242. reader_workbench/workbench/experiment/__init__.py +47 -0
  243. reader_workbench/workbench/experiment/model.py +381 -0
  244. reader_workbench/workbench/experiments.py +133 -0
  245. reader_workbench/workbench/graph/__init__.py +47 -0
  246. reader_workbench/workbench/graph/nodes.py +102 -0
  247. reader_workbench/workbench/graph/normalize.py +177 -0
  248. reader_workbench/workbench/graph/refs.py +148 -0
  249. reader_workbench/workbench/input_discovery.py +19 -0
  250. reader_workbench/workbench/inspection/__init__.py +3 -0
  251. reader_workbench/workbench/inspection/catalogs.py +128 -0
  252. reader_workbench/workbench/inspection/common.py +92 -0
  253. reader_workbench/workbench/inspection/dop.py +64 -0
  254. reader_workbench/workbench/inspection/experiments.py +449 -0
  255. reader_workbench/workbench/inspection/inventory.py +68 -0
  256. reader_workbench/workbench/inspection/protocols.py +368 -0
  257. reader_workbench/workbench/inspection/readiness.py +333 -0
  258. reader_workbench/workbench/inspection/reports.py +367 -0
  259. reader_workbench/workbench/inspection/results.py +166 -0
  260. reader_workbench/workbench/inspection/runtime.py +287 -0
  261. reader_workbench/workbench/inspection/semantics.py +192 -0
  262. reader_workbench/workbench/inspection/validation.py +30 -0
  263. reader_workbench/workbench/notebooks/__init__.py +17 -0
  264. reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
  265. reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
  266. reader_workbench/workbench/notebooks/components/__init__.py +21 -0
  267. reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
  268. reader_workbench/workbench/notebooks/components/overview.py +119 -0
  269. reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
  270. reader_workbench/workbench/notebooks/launch.py +274 -0
  271. reader_workbench/workbench/notebooks/presentation.py +136 -0
  272. reader_workbench/workbench/notebooks/scaffold.py +60 -0
  273. reader_workbench/workbench/ontology.py +78 -0
  274. reader_workbench/workbench/paths.py +44 -0
  275. reader_workbench/workbench/ports/__init__.py +31 -0
  276. reader_workbench/workbench/ports/model.py +168 -0
  277. reader_workbench/workbench/records/__init__.py +44 -0
  278. reader_workbench/workbench/records/epoch.py +329 -0
  279. reader_workbench/workbench/records/evidence.py +247 -0
  280. reader_workbench/workbench/records/identity.py +87 -0
  281. reader_workbench/workbench/records/locking.py +185 -0
  282. reader_workbench/workbench/records/model.py +711 -0
  283. reader_workbench/workbench/records/sources.py +73 -0
  284. reader_workbench/workbench/records/store.py +1022 -0
  285. reader_workbench/workbench/records/verification.py +998 -0
  286. reader_workbench/workbench/registry.py +333 -0
  287. reader_workbench/workbench/spec_overrides.py +215 -0
  288. reader_workbench-1.0.0.dist-info/METADATA +91 -0
  289. reader_workbench-1.0.0.dist-info/RECORD +293 -0
  290. reader_workbench-1.0.0.dist-info/WHEEL +5 -0
  291. reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
  292. reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  293. reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,368 @@
1
+ """Strict contracts for assay-neutral temporal reductions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal
9
+
10
+ TimeBasis = Literal["absolute", "event_relative"]
11
+ EndpointMode = Literal["exact", "nearest"]
12
+ IntervalBoundary = Literal["inclusive"]
13
+ TemporalReductionMethod = Literal[
14
+ "identity",
15
+ "observed_mean",
16
+ "observed_median",
17
+ "geometric_time_mean",
18
+ "integrated_linear_mean",
19
+ ]
20
+ OutputSpace = Literal["linear", "log2"]
21
+ BoundarySupport = Literal["none", "covered", "observed"]
22
+ CensoredValuePolicy = Literal["allow", "reject"]
23
+ PositiveValueScope = Literal["selected_support", "entire_trace"]
24
+
25
+
26
+ def _mapping(value: object, *, context: str) -> dict[str, Any]:
27
+ if not isinstance(value, Mapping):
28
+ raise ValueError(f"{context} must be a mapping")
29
+ return {str(key): item for key, item in value.items()}
30
+
31
+
32
+ def _exact_fields(
33
+ value: object,
34
+ *,
35
+ context: str,
36
+ required: set[str],
37
+ optional: set[str] | None = None,
38
+ ) -> dict[str, Any]:
39
+ payload = _mapping(value, context=context)
40
+ allowed = required | (optional or set())
41
+ unknown = sorted(set(payload) - allowed)
42
+ if unknown:
43
+ raise ValueError(f"{context} has unknown fields: {unknown}")
44
+ missing = sorted(required - set(payload))
45
+ if missing:
46
+ raise ValueError(f"{context} is missing required fields: {missing}")
47
+ return payload
48
+
49
+
50
+ def _choice(value: object, *, context: str, allowed: set[str]) -> str:
51
+ if not isinstance(value, str) or value not in allowed:
52
+ options = ", ".join(sorted(allowed))
53
+ raise ValueError(f"{context} must be one of: {options}")
54
+ return value
55
+
56
+
57
+ def _finite(value: object, *, context: str) -> float:
58
+ if isinstance(value, bool) or not isinstance(value, int | float):
59
+ raise ValueError(f"{context} must be a finite number")
60
+ result = float(value)
61
+ if not math.isfinite(result):
62
+ raise ValueError(f"{context} must be a finite number")
63
+ return result
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class EndpointSelection:
68
+ """One recorded or event-relative endpoint."""
69
+
70
+ time_basis: TimeBasis
71
+ time_h: float
72
+ mode: EndpointMode
73
+ tolerance_h: float
74
+
75
+ def __post_init__(self) -> None:
76
+ if self.time_basis not in {"absolute", "event_relative"}:
77
+ raise ValueError("endpoint.time_basis must be 'absolute' or 'event_relative'")
78
+ if self.mode not in {"exact", "nearest"}:
79
+ raise ValueError("endpoint.mode must be 'exact' or 'nearest'")
80
+ if not math.isfinite(float(self.time_h)):
81
+ raise ValueError("endpoint.time_h must be finite")
82
+ if not math.isfinite(float(self.tolerance_h)) or float(self.tolerance_h) < 0.0:
83
+ raise ValueError("endpoint.tolerance_h must be finite and non-negative")
84
+ if self.mode == "exact" and float(self.tolerance_h) != 0.0:
85
+ raise ValueError("endpoint.tolerance_h must be zero when mode is 'exact'")
86
+
87
+ @classmethod
88
+ def from_mapping(cls, value: object) -> EndpointSelection:
89
+ payload = _exact_fields(
90
+ value,
91
+ context="temporal_reduction.selection",
92
+ required={"kind", "time_basis", "time_h", "mode", "tolerance_h"},
93
+ )
94
+ if payload["kind"] != "endpoint":
95
+ raise ValueError("temporal_reduction.selection.kind must be 'endpoint'")
96
+ return cls(
97
+ time_basis=_choice(
98
+ payload["time_basis"],
99
+ context="temporal_reduction.selection.time_basis",
100
+ allowed={"absolute", "event_relative"},
101
+ ), # type: ignore[arg-type]
102
+ time_h=_finite(payload["time_h"], context="temporal_reduction.selection.time_h"),
103
+ mode=_choice(
104
+ payload["mode"],
105
+ context="temporal_reduction.selection.mode",
106
+ allowed={"exact", "nearest"},
107
+ ), # type: ignore[arg-type]
108
+ tolerance_h=_finite(
109
+ payload["tolerance_h"],
110
+ context="temporal_reduction.selection.tolerance_h",
111
+ ),
112
+ )
113
+
114
+ def to_mapping(self) -> dict[str, object]:
115
+ return {
116
+ "kind": "endpoint",
117
+ "time_basis": self.time_basis,
118
+ "time_h": self.time_h,
119
+ "mode": self.mode,
120
+ "tolerance_h": self.tolerance_h,
121
+ }
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class IntervalSelection:
126
+ """One closed recorded or event-relative interval."""
127
+
128
+ time_basis: TimeBasis
129
+ start_h: float
130
+ end_h: float
131
+ boundary: IntervalBoundary = "inclusive"
132
+
133
+ def __post_init__(self) -> None:
134
+ if self.time_basis not in {"absolute", "event_relative"}:
135
+ raise ValueError("interval.time_basis must be 'absolute' or 'event_relative'")
136
+ if self.boundary != "inclusive":
137
+ raise ValueError("interval.boundary must be 'inclusive'")
138
+ if not math.isfinite(float(self.start_h)) or not math.isfinite(float(self.end_h)):
139
+ raise ValueError("interval bounds must be finite")
140
+ if float(self.end_h) <= float(self.start_h):
141
+ raise ValueError("interval requires start_h < end_h")
142
+
143
+ @classmethod
144
+ def from_mapping(cls, value: object) -> IntervalSelection:
145
+ payload = _exact_fields(
146
+ value,
147
+ context="temporal_reduction.selection",
148
+ required={"kind", "time_basis", "start_h", "end_h", "boundary"},
149
+ )
150
+ if payload["kind"] != "interval":
151
+ raise ValueError("temporal_reduction.selection.kind must be 'interval'")
152
+ return cls(
153
+ time_basis=_choice(
154
+ payload["time_basis"],
155
+ context="temporal_reduction.selection.time_basis",
156
+ allowed={"absolute", "event_relative"},
157
+ ), # type: ignore[arg-type]
158
+ start_h=_finite(payload["start_h"], context="temporal_reduction.selection.start_h"),
159
+ end_h=_finite(payload["end_h"], context="temporal_reduction.selection.end_h"),
160
+ boundary=_choice(
161
+ payload["boundary"],
162
+ context="temporal_reduction.selection.boundary",
163
+ allowed={"inclusive"},
164
+ ), # type: ignore[arg-type]
165
+ )
166
+
167
+ def to_mapping(self) -> dict[str, object]:
168
+ return {
169
+ "kind": "interval",
170
+ "time_basis": self.time_basis,
171
+ "start_h": self.start_h,
172
+ "end_h": self.end_h,
173
+ "boundary": self.boundary,
174
+ }
175
+
176
+
177
+ type TemporalSelection = EndpointSelection | IntervalSelection
178
+
179
+
180
+ def parse_temporal_selection(value: object) -> TemporalSelection:
181
+ payload = _mapping(value, context="temporal_reduction.selection")
182
+ kind = payload.get("kind")
183
+ if kind == "endpoint":
184
+ return EndpointSelection.from_mapping(payload)
185
+ if kind == "interval":
186
+ return IntervalSelection.from_mapping(payload)
187
+ raise ValueError("temporal_reduction.selection.kind must be 'endpoint' or 'interval'")
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class TemporalSupportPolicy:
192
+ """Trace support and value-provenance requirements."""
193
+
194
+ boundary_support: BoundarySupport
195
+ minimum_observations: int
196
+ maximum_interior_gap_h: float | None
197
+ positive_floor: float | None
198
+ positive_value_scope: PositiveValueScope
199
+ censored_values: CensoredValuePolicy
200
+
201
+ def __post_init__(self) -> None:
202
+ if self.boundary_support not in {"none", "covered", "observed"}:
203
+ raise ValueError("support.boundary_support must be 'none', 'covered', or 'observed'")
204
+ if (
205
+ isinstance(self.minimum_observations, bool)
206
+ or not isinstance(self.minimum_observations, int)
207
+ or self.minimum_observations < 0
208
+ ):
209
+ raise ValueError("support.minimum_observations must be a non-negative integer")
210
+ if self.maximum_interior_gap_h is not None and (
211
+ not math.isfinite(float(self.maximum_interior_gap_h)) or float(self.maximum_interior_gap_h) <= 0.0
212
+ ):
213
+ raise ValueError("support.maximum_interior_gap_h must be null or a positive finite number")
214
+ if self.positive_floor is not None and (
215
+ not math.isfinite(float(self.positive_floor)) or float(self.positive_floor) <= 0.0
216
+ ):
217
+ raise ValueError("support.positive_floor must be null or a positive finite number")
218
+ if self.positive_value_scope not in {"selected_support", "entire_trace"}:
219
+ raise ValueError("support.positive_value_scope must be 'selected_support' or 'entire_trace'")
220
+ if self.censored_values not in {"allow", "reject"}:
221
+ raise ValueError("support.censored_values must be 'allow' or 'reject'")
222
+
223
+ @classmethod
224
+ def from_mapping(cls, value: object) -> TemporalSupportPolicy:
225
+ payload = _exact_fields(
226
+ value,
227
+ context="temporal_reduction.support",
228
+ required={
229
+ "boundary_support",
230
+ "minimum_observations",
231
+ "maximum_interior_gap_h",
232
+ "positive_floor",
233
+ "positive_value_scope",
234
+ "censored_values",
235
+ },
236
+ )
237
+ minimum = payload["minimum_observations"]
238
+ if isinstance(minimum, bool) or not isinstance(minimum, int):
239
+ raise ValueError("temporal_reduction.support.minimum_observations must be an integer")
240
+ maximum_gap = payload["maximum_interior_gap_h"]
241
+ positive_floor = payload["positive_floor"]
242
+ return cls(
243
+ boundary_support=_choice(
244
+ payload["boundary_support"],
245
+ context="temporal_reduction.support.boundary_support",
246
+ allowed={"none", "covered", "observed"},
247
+ ), # type: ignore[arg-type]
248
+ minimum_observations=minimum,
249
+ maximum_interior_gap_h=(
250
+ None
251
+ if maximum_gap is None
252
+ else _finite(maximum_gap, context="temporal_reduction.support.maximum_interior_gap_h")
253
+ ),
254
+ positive_floor=(
255
+ None
256
+ if positive_floor is None
257
+ else _finite(positive_floor, context="temporal_reduction.support.positive_floor")
258
+ ),
259
+ positive_value_scope=_choice(
260
+ payload["positive_value_scope"],
261
+ context="temporal_reduction.support.positive_value_scope",
262
+ allowed={"selected_support", "entire_trace"},
263
+ ), # type: ignore[arg-type]
264
+ censored_values=_choice(
265
+ payload["censored_values"],
266
+ context="temporal_reduction.support.censored_values",
267
+ allowed={"allow", "reject"},
268
+ ), # type: ignore[arg-type]
269
+ )
270
+
271
+ def to_mapping(self) -> dict[str, object]:
272
+ return {
273
+ "boundary_support": self.boundary_support,
274
+ "minimum_observations": self.minimum_observations,
275
+ "maximum_interior_gap_h": self.maximum_interior_gap_h,
276
+ "positive_floor": self.positive_floor,
277
+ "positive_value_scope": self.positive_value_scope,
278
+ "censored_values": self.censored_values,
279
+ }
280
+
281
+
282
+ @dataclass(frozen=True)
283
+ class TemporalReductionSpec:
284
+ """One selection, numerical method, value space, and support policy."""
285
+
286
+ selection: TemporalSelection
287
+ method: TemporalReductionMethod
288
+ output_space: OutputSpace
289
+ support: TemporalSupportPolicy
290
+
291
+ def __post_init__(self) -> None:
292
+ methods = {
293
+ "identity",
294
+ "observed_mean",
295
+ "observed_median",
296
+ "geometric_time_mean",
297
+ "integrated_linear_mean",
298
+ }
299
+ if self.method not in methods:
300
+ raise ValueError(f"temporal_reduction.method is unsupported: {self.method!r}")
301
+ if self.output_space not in {"linear", "log2"}:
302
+ raise ValueError("temporal_reduction.output_space must be 'linear' or 'log2'")
303
+ if isinstance(self.selection, EndpointSelection):
304
+ if self.method != "identity":
305
+ raise ValueError("endpoint temporal reductions require method 'identity'")
306
+ if self.support.boundary_support != "none" or self.support.maximum_interior_gap_h is not None:
307
+ raise ValueError("endpoint temporal reductions cannot declare interval boundary or gap support")
308
+ if self.support.minimum_observations != 1:
309
+ raise ValueError("endpoint temporal reductions require exactly one minimum observation")
310
+ elif self.method == "identity":
311
+ raise ValueError("interval temporal reductions cannot use method 'identity'")
312
+ if self.method in {"observed_mean", "observed_median"} and self.support.minimum_observations < 1:
313
+ raise ValueError(f"{self.method} requires at least one minimum observation")
314
+ if self.method in {"geometric_time_mean", "integrated_linear_mean"}:
315
+ if self.output_space != "log2":
316
+ raise ValueError(f"{self.method} requires output_space 'log2'")
317
+ if self.support.boundary_support == "none":
318
+ raise ValueError(f"{self.method} requires covered or observed interval boundaries")
319
+ if self.support.maximum_interior_gap_h is None or self.support.positive_floor is None:
320
+ raise ValueError(f"{self.method} requires maximum_interior_gap_h and positive_floor")
321
+ if self.output_space == "log2" and self.support.positive_floor is None:
322
+ raise ValueError("log2 temporal reductions require support.positive_floor")
323
+
324
+ @classmethod
325
+ def from_mapping(cls, value: object) -> TemporalReductionSpec:
326
+ payload = _exact_fields(
327
+ value,
328
+ context="temporal_reduction",
329
+ required={"selection", "method", "output_space", "support"},
330
+ )
331
+ return cls(
332
+ selection=parse_temporal_selection(payload["selection"]),
333
+ method=_choice(
334
+ payload["method"],
335
+ context="temporal_reduction.method",
336
+ allowed={
337
+ "identity",
338
+ "observed_mean",
339
+ "observed_median",
340
+ "geometric_time_mean",
341
+ "integrated_linear_mean",
342
+ },
343
+ ), # type: ignore[arg-type]
344
+ output_space=_choice(
345
+ payload["output_space"],
346
+ context="temporal_reduction.output_space",
347
+ allowed={"linear", "log2"},
348
+ ), # type: ignore[arg-type]
349
+ support=TemporalSupportPolicy.from_mapping(payload["support"]),
350
+ )
351
+
352
+ def to_mapping(self) -> dict[str, object]:
353
+ return {
354
+ "selection": self.selection.to_mapping(),
355
+ "method": self.method,
356
+ "output_space": self.output_space,
357
+ "support": self.support.to_mapping(),
358
+ }
359
+
360
+
361
+ __all__ = [
362
+ "EndpointSelection",
363
+ "IntervalSelection",
364
+ "TemporalReductionSpec",
365
+ "TemporalSelection",
366
+ "TemporalSupportPolicy",
367
+ "parse_temporal_selection",
368
+ ]