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,435 @@
1
+ """Study-neutral preparation for the single-reporter diagnostic figure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from reader_workbench.domains.plate_reader.ordering import order_levels
12
+ from reader_workbench.domains.time_series import (
13
+ EndpointSelection,
14
+ IntervalSelection,
15
+ ObservationAggregationSpec,
16
+ TemporalReductionSpec,
17
+ reduce_temporal_trace,
18
+ )
19
+
20
+ from ._data import alias_column, require_columns
21
+ from .grouping import GroupMatch, resolve_groups
22
+
23
+ SummaryStat = Literal["mean", "median"]
24
+ UnitRole = Literal["declared_replicate", "observation_only"]
25
+ _PROVENANCE_COLUMNS = (
26
+ "value_policy_clipped",
27
+ "value_instrument_overflow",
28
+ "value_bound_kind",
29
+ )
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class SingleReporterSelection:
34
+ temporal_reduction: TemporalReductionSpec
35
+
36
+ @property
37
+ def endpoint_time_h(self) -> float | None:
38
+ selection = self.temporal_reduction.selection
39
+ return selection.time_h if isinstance(selection, EndpointSelection) else None
40
+
41
+ @property
42
+ def window_h(self) -> tuple[float, float] | None:
43
+ selection = self.temporal_reduction.selection
44
+ return (selection.start_h, selection.end_h) if isinstance(selection, IntervalSelection) else None
45
+
46
+ @property
47
+ def label(self) -> str:
48
+ selection = self.temporal_reduction.selection
49
+ method = {
50
+ "identity": "endpoint",
51
+ "observed_mean": "observed mean",
52
+ "observed_median": "observed median",
53
+ "geometric_time_mean": "time-weighted geometric mean",
54
+ "integrated_linear_mean": "time-weighted linear mean",
55
+ }[self.temporal_reduction.method]
56
+ suffix = "" if self.temporal_reduction.output_space == "linear" else " (log2 output)"
57
+ if isinstance(selection, EndpointSelection):
58
+ return f"{method} at {selection.time_h:g} h{suffix}"
59
+ return f"{method} over {selection.start_h:g}–{selection.end_h:g} h{suffix}"
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class SingleReporterDiagnosticData:
64
+ group_label: str
65
+ condition_column: str
66
+ entity_columns: tuple[str, ...]
67
+ unit_column: str
68
+ unit_role: UnitRole
69
+ time_column: str
70
+ normalizer_channel: str
71
+ reporter_channel: str
72
+ ratio_channel: str
73
+ condition_order: tuple[str, ...]
74
+ kinetics: pd.DataFrame
75
+ reduced_ratio: pd.DataFrame
76
+ reduced_normalizer: pd.DataFrame
77
+ selection: SingleReporterSelection
78
+ observation_aggregation: ObservationAggregationSpec
79
+
80
+
81
+ def prepare_single_reporter_diagnostics(
82
+ frame: pd.DataFrame,
83
+ *,
84
+ group_on: str | None,
85
+ collection_items: list[dict[str, list[str]]] | None,
86
+ group_match: GroupMatch,
87
+ condition_column: str,
88
+ condition_order: list[str] | None,
89
+ entity_columns: list[str],
90
+ unit_column: str,
91
+ observation_column: str,
92
+ unit_role: UnitRole,
93
+ time_column: str,
94
+ normalizer_channel: str,
95
+ reporter_channel: str,
96
+ ratio_channel: str,
97
+ temporal_reduction: TemporalReductionSpec,
98
+ observation_aggregation: ObservationAggregationSpec,
99
+ ) -> tuple[SingleReporterDiagnosticData, ...]:
100
+ """Prepare one diagnostic contract per resolved presentation partition."""
101
+
102
+ if temporal_reduction.selection.time_basis != "absolute":
103
+ raise ValueError("single_reporter_diagnostic: acquisition traces require an absolute temporal reduction")
104
+ if unit_role not in {"declared_replicate", "observation_only"}:
105
+ raise ValueError(f"single_reporter_diagnostic: unsupported unit_role {unit_role!r}")
106
+ resolved_condition = str(condition_column)
107
+ resolved_entities = tuple(str(column).strip() for column in entity_columns)
108
+ if not resolved_entities or any(not column for column in resolved_entities):
109
+ raise ValueError("single_reporter_diagnostic: entity_columns must contain non-empty strings")
110
+ if len(set(resolved_entities)) != len(resolved_entities):
111
+ raise ValueError("single_reporter_diagnostic: entity_columns must not contain duplicates")
112
+ if resolved_condition in resolved_entities:
113
+ raise ValueError("single_reporter_diagnostic: condition_column must not be repeated in entity_columns")
114
+ if unit_column == resolved_condition or unit_column in resolved_entities:
115
+ raise ValueError(
116
+ "single_reporter_diagnostic: unit_column must be distinct from condition_column and entity_columns"
117
+ )
118
+ resolved_group = str(alias_column(frame, group_on)) if group_on else None
119
+ required = [
120
+ time_column,
121
+ "channel",
122
+ "value",
123
+ resolved_condition,
124
+ *resolved_entities,
125
+ unit_column,
126
+ observation_column,
127
+ *([resolved_group] if resolved_group else []),
128
+ ]
129
+ if temporal_reduction.support.censored_values == "reject":
130
+ required.extend(_PROVENANCE_COLUMNS)
131
+ require_columns(frame, required, where="single_reporter_diagnostic")
132
+
133
+ work = frame.copy()
134
+ work[time_column] = pd.to_numeric(work[time_column], errors="coerce")
135
+ work["value"] = pd.to_numeric(work["value"], errors="coerce")
136
+ channels = (normalizer_channel, reporter_channel, ratio_channel)
137
+ work = work[work["channel"].astype(str).isin(channels)].copy()
138
+ if work.empty:
139
+ raise ValueError("single_reporter_diagnostic: no rows for the compiler-owned channels")
140
+ finite = np.isfinite(work[time_column].to_numpy(dtype=float)) & np.isfinite(work["value"].to_numpy(dtype=float))
141
+ if not finite.all():
142
+ raise ValueError(
143
+ "single_reporter_diagnostic: compiler-owned channel rows contain "
144
+ f"{int((~finite).sum())} non-finite time or value field(s)"
145
+ )
146
+
147
+ for column in (resolved_condition, *resolved_entities, unit_column, observation_column, resolved_group):
148
+ if column is not None:
149
+ _require_nonempty_identity(work, column=column)
150
+ _require_channels(work, channels=channels)
151
+
152
+ if resolved_group is None:
153
+ groups = [("all", [None])]
154
+ else:
155
+ universe = order_levels(work[resolved_group].astype(str).unique().tolist())
156
+ groups = (
157
+ resolve_groups(universe, collection_items, match=group_match)
158
+ if collection_items
159
+ else [(value, [value]) for value in universe]
160
+ )
161
+
162
+ diagnostics: list[SingleReporterDiagnosticData] = []
163
+ for label, members in groups:
164
+ if resolved_group is not None and not members:
165
+ raise ValueError(f"single_reporter_diagnostic: partition {label!r} selects no rows")
166
+ selected = work
167
+ if resolved_group is not None and members != [None]:
168
+ selected = work[work[resolved_group].astype(str).isin(members)].copy()
169
+ if selected.empty:
170
+ raise ValueError(f"single_reporter_diagnostic: partition {label!r} selects no rows")
171
+ entity_count = len(selected.loc[:, list(resolved_entities)].drop_duplicates())
172
+ if entity_count != 1:
173
+ raise ValueError(
174
+ "single_reporter_diagnostic: "
175
+ f"partition {label!r} spans multiple identity_scope entities ({entity_count}); "
176
+ f"partition by {list(resolved_entities)!r} or use an explicit comparison figure"
177
+ )
178
+
179
+ diagnostics.append(
180
+ _prepare_partition(
181
+ selected,
182
+ group_label=str(label),
183
+ condition_column=resolved_condition,
184
+ condition_order=_resolve_condition_order(
185
+ selected,
186
+ condition_column=resolved_condition,
187
+ configured=condition_order,
188
+ ),
189
+ entity_columns=resolved_entities,
190
+ unit_column=unit_column,
191
+ observation_column=observation_column,
192
+ unit_role=unit_role,
193
+ time_column=time_column,
194
+ normalizer_channel=normalizer_channel,
195
+ reporter_channel=reporter_channel,
196
+ ratio_channel=ratio_channel,
197
+ temporal_reduction=temporal_reduction,
198
+ observation_aggregation=observation_aggregation,
199
+ )
200
+ )
201
+
202
+ if not diagnostics:
203
+ raise ValueError("single_reporter_diagnostic: no diagnostic partitions were prepared")
204
+ return tuple(diagnostics)
205
+
206
+
207
+ def _prepare_partition(
208
+ frame: pd.DataFrame,
209
+ *,
210
+ group_label: str,
211
+ condition_column: str,
212
+ condition_order: list[str],
213
+ entity_columns: tuple[str, ...],
214
+ unit_column: str,
215
+ observation_column: str,
216
+ unit_role: UnitRole,
217
+ time_column: str,
218
+ normalizer_channel: str,
219
+ reporter_channel: str,
220
+ ratio_channel: str,
221
+ temporal_reduction: TemporalReductionSpec,
222
+ observation_aggregation: ObservationAggregationSpec,
223
+ ) -> SingleReporterDiagnosticData:
224
+ work = frame.copy()
225
+ work["__condition"] = work[condition_column].astype(str)
226
+ work["__unit"] = work[unit_column].astype(str)
227
+ work["__observation"] = work[observation_column].astype(str)
228
+ work["__segment"] = _segment_identity(work)
229
+
230
+ semantic_unit_keys = ["__condition", *entity_columns, "__unit"]
231
+ trace_keys = [*semantic_unit_keys, "__observation", "channel"]
232
+ duplicate_keys = [*trace_keys, time_column]
233
+ if work.duplicated(subset=duplicate_keys).any():
234
+ raise ValueError(f"single_reporter_diagnostic: partition {group_label!r} contains duplicate trace rows")
235
+ _require_aligned_channel_times(
236
+ work,
237
+ trace_identity=[*semantic_unit_keys, "__observation"],
238
+ time_column=time_column,
239
+ channels=(normalizer_channel, reporter_channel, ratio_channel),
240
+ where=f"partition {group_label!r}",
241
+ )
242
+
243
+ within_unit_stat = observation_aggregation.within_unit_statistic
244
+ kinetics = _reduce_rows(
245
+ work,
246
+ group_columns=["__segment", time_column, *semantic_unit_keys, "channel"],
247
+ statistic=within_unit_stat,
248
+ )
249
+ _require_complete_channels(
250
+ kinetics,
251
+ key_columns=["__segment", time_column, *semantic_unit_keys],
252
+ channels=(normalizer_channel, reporter_channel, ratio_channel),
253
+ where=f"partition {group_label!r} acquisition rows",
254
+ )
255
+
256
+ observation_reductions = _reduce_observation_traces(
257
+ work,
258
+ trace_keys=trace_keys,
259
+ time_column=time_column,
260
+ temporal_reduction=temporal_reduction,
261
+ group_label=group_label,
262
+ )
263
+ reduced = _reduce_rows(
264
+ observation_reductions,
265
+ group_columns=[*semantic_unit_keys, "channel"],
266
+ statistic=within_unit_stat,
267
+ )
268
+ _require_complete_channels(
269
+ reduced,
270
+ key_columns=semantic_unit_keys,
271
+ channels=(normalizer_channel, ratio_channel),
272
+ where=f"partition {group_label!r} reduction",
273
+ )
274
+
275
+ return SingleReporterDiagnosticData(
276
+ group_label=group_label,
277
+ condition_column=condition_column,
278
+ entity_columns=entity_columns,
279
+ unit_column=unit_column,
280
+ unit_role=unit_role,
281
+ time_column=time_column,
282
+ normalizer_channel=normalizer_channel,
283
+ reporter_channel=reporter_channel,
284
+ ratio_channel=ratio_channel,
285
+ condition_order=tuple(condition_order),
286
+ kinetics=kinetics.reset_index(drop=True),
287
+ reduced_ratio=reduced[reduced["channel"].astype(str) == ratio_channel].reset_index(drop=True),
288
+ reduced_normalizer=reduced[reduced["channel"].astype(str) == normalizer_channel].reset_index(drop=True),
289
+ selection=SingleReporterSelection(temporal_reduction=temporal_reduction),
290
+ observation_aggregation=observation_aggregation,
291
+ )
292
+
293
+
294
+ def _reduce_observation_traces(
295
+ frame: pd.DataFrame,
296
+ *,
297
+ trace_keys: list[str],
298
+ time_column: str,
299
+ temporal_reduction: TemporalReductionSpec,
300
+ group_label: str,
301
+ ) -> pd.DataFrame:
302
+ rows: list[dict[str, object]] = []
303
+ for identity, trace in frame.groupby(trace_keys, sort=True, dropna=False):
304
+ identity_values = identity if isinstance(identity, tuple) else (identity,)
305
+ trace_id = f"single_reporter_diagnostic:{group_label}:" + ":".join(map(str, identity_values))
306
+ provenance = {
307
+ "policy_clipped": (
308
+ trace["value_policy_clipped"].to_numpy(dtype=bool) if "value_policy_clipped" in trace.columns else None
309
+ ),
310
+ "instrument_overflow": (
311
+ trace["value_instrument_overflow"].to_numpy(dtype=bool)
312
+ if "value_instrument_overflow" in trace.columns
313
+ else None
314
+ ),
315
+ "bound_kinds": (
316
+ trace["value_bound_kind"].to_numpy(dtype=object) if "value_bound_kind" in trace.columns else None
317
+ ),
318
+ }
319
+ result = reduce_temporal_trace(
320
+ trace[time_column].to_numpy(dtype=float),
321
+ trace["value"].to_numpy(dtype=float),
322
+ spec=temporal_reduction,
323
+ trace_id=trace_id,
324
+ **provenance,
325
+ )
326
+ row = dict(zip(trace_keys, identity_values, strict=True))
327
+ row["value"] = result.value
328
+ rows.append(row)
329
+ return pd.DataFrame.from_records(rows)
330
+
331
+
332
+ def _reduce_rows(
333
+ frame: pd.DataFrame,
334
+ *,
335
+ group_columns: list[str],
336
+ statistic: SummaryStat,
337
+ ) -> pd.DataFrame:
338
+ grouped = frame.groupby(group_columns, dropna=False, sort=True)["value"]
339
+ values = grouped.mean() if statistic == "mean" else grouped.median()
340
+ return values.rename("value").reset_index()
341
+
342
+
343
+ def _segment_identity(frame: pd.DataFrame) -> pd.Series:
344
+ if "acquisition_segment_id" in frame.columns:
345
+ return frame["acquisition_segment_id"].astype(str)
346
+ columns = [column for column in ("source", "sheet_name", "sheet_index") if column in frame.columns]
347
+ if not columns:
348
+ return pd.Series("segment", index=frame.index, dtype="string")
349
+ parts = frame[columns].copy()
350
+ for column in columns:
351
+ parts[column] = parts[column].astype(str)
352
+ return parts.agg("::".join, axis=1)
353
+
354
+
355
+ def _resolve_condition_order(
356
+ frame: pd.DataFrame,
357
+ *,
358
+ condition_column: str,
359
+ configured: list[str] | None,
360
+ ) -> list[str]:
361
+ observed = order_levels(frame[condition_column].astype(str).unique().tolist())
362
+ if configured is None:
363
+ return observed
364
+ order = [str(value).strip() for value in configured]
365
+ if not order or any(not value for value in order):
366
+ raise ValueError("single_reporter_diagnostic: condition_order must contain non-empty labels")
367
+ if len(set(order)) != len(order):
368
+ raise ValueError("single_reporter_diagnostic: condition_order contains duplicate labels")
369
+ missing = [value for value in order if value not in observed]
370
+ omitted = [value for value in observed if value not in order]
371
+ if missing or omitted:
372
+ raise ValueError(
373
+ "single_reporter_diagnostic: condition_order must exactly match observed conditions "
374
+ f"(missing={missing}, omitted={omitted})"
375
+ )
376
+ return order
377
+
378
+
379
+ def _require_nonempty_identity(frame: pd.DataFrame, *, column: str) -> None:
380
+ values = frame[column]
381
+ invalid = values.isna() | values.astype(str).str.strip().str.casefold().isin({"", "nan", "none"})
382
+ if invalid.any():
383
+ raise ValueError(f"single_reporter_diagnostic: column {column!r} contains missing identities")
384
+
385
+
386
+ def _require_channels(frame: pd.DataFrame, *, channels: tuple[str, ...]) -> None:
387
+ observed = set(frame["channel"].astype(str).unique().tolist())
388
+ missing = [channel for channel in channels if channel not in observed]
389
+ if missing:
390
+ raise ValueError(
391
+ f"single_reporter_diagnostic: compiler-owned channel(s) missing: {missing}; available={sorted(observed)}"
392
+ )
393
+
394
+
395
+ def _require_aligned_channel_times(
396
+ frame: pd.DataFrame,
397
+ *,
398
+ trace_identity: list[str],
399
+ time_column: str,
400
+ channels: tuple[str, ...],
401
+ where: str,
402
+ ) -> None:
403
+ for identity, group in frame.groupby(trace_identity, sort=True, dropna=False):
404
+ times = {
405
+ channel: tuple(
406
+ sorted(group.loc[group["channel"].astype(str) == channel, time_column].to_numpy(dtype=float))
407
+ )
408
+ for channel in channels
409
+ }
410
+ if any(not values for values in times.values()) or len(set(times.values())) != 1:
411
+ raise ValueError(
412
+ f"single_reporter_diagnostic: {where} trace {identity!r} lacks exactly aligned channel times"
413
+ )
414
+
415
+
416
+ def _require_complete_channels(
417
+ frame: pd.DataFrame,
418
+ *,
419
+ key_columns: list[str],
420
+ channels: tuple[str, ...],
421
+ where: str,
422
+ ) -> None:
423
+ expected = set(channels)
424
+ observed = frame.groupby(key_columns, dropna=False)["channel"].agg(lambda values: set(map(str, values)))
425
+ incomplete = observed[~observed.map(expected.issubset)]
426
+ if not incomplete.empty:
427
+ example = incomplete.index[0]
428
+ raise ValueError(f"single_reporter_diagnostic: {where} lacks paired channel observations at {example!r}")
429
+
430
+
431
+ __all__ = [
432
+ "SingleReporterDiagnosticData",
433
+ "SingleReporterSelection",
434
+ "prepare_single_reporter_diagnostics",
435
+ ]