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,283 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pandas as pd
6
+ import polars as pl
7
+
8
+ from ...ordering import order_levels
9
+ from .._data import alias_column, require_columns, warn_if_empty
10
+ from ..grouping import GroupMatch, resolve_groups
11
+ from ..panels.snapshot_data import select_snapshot_rows, summarize_snapshot_values
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class GroupFrame:
16
+ label: str
17
+ members: tuple[str | None, ...]
18
+ snapshot: pd.DataFrame
19
+ time_series: pd.DataFrame
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CompositeInputs:
24
+ ts_x_col: str
25
+ group_col: str | None
26
+ ts_hue_col: str
27
+ ts_style_col: str | None
28
+ snap_x_col: str
29
+ snap_hue_col: str | None
30
+ ts_channel: str
31
+ snap_channel: str
32
+ groups: tuple[GroupFrame, ...]
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class TimeSeriesPanelData:
37
+ frame: pd.DataFrame
38
+ sheet_lines: list[float] | None
39
+ segment_col: str | None
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class SnapshotPanelData:
44
+ frame: pd.DataFrame
45
+ stats: pd.DataFrame
46
+ time_used: float
47
+ x_order: list[str]
48
+ hue_order: list[str]
49
+ fell_back: bool
50
+ fallback_times_preview: str
51
+ fallback_delta: float
52
+
53
+
54
+ def resolve_level_order(*, observed: list[str], configured: list[str] | None, name: str) -> list[str]:
55
+ ordered_observed = order_levels([str(value) for value in observed])
56
+ if configured is None:
57
+ return ordered_observed
58
+ resolved = [str(value) for value in configured]
59
+ if len(set(resolved)) != len(resolved):
60
+ raise ValueError(f"ts_and_snap: {name} contains duplicate labels")
61
+ observed_set = set(ordered_observed)
62
+ missing = [value for value in resolved if value not in observed_set]
63
+ omitted = [value for value in ordered_observed if value not in resolved]
64
+ if missing:
65
+ raise ValueError(f"ts_and_snap: {name} includes missing label(s): {missing}")
66
+ if omitted:
67
+ raise ValueError(f"ts_and_snap: {name} omits observed label(s): {omitted}")
68
+ return resolved
69
+
70
+
71
+ def prepare_composite_inputs(
72
+ *,
73
+ df: pd.DataFrame,
74
+ group_on: str | None,
75
+ pool_sets: list[dict[str, list[str]]] | None,
76
+ pool_match: GroupMatch,
77
+ ts_x: str,
78
+ ts_channel: str,
79
+ ts_hue: str,
80
+ ts_style: str | None,
81
+ ts_time_window: list[float] | None,
82
+ snap_x: str,
83
+ snap_channel: str | None,
84
+ snap_hue: str | None,
85
+ ) -> CompositeInputs | None:
86
+ """Normalize and partition one composite request without creating a figure."""
87
+
88
+ ts_x_col = alias_column(df, ts_x)
89
+ group_col = alias_column(df, group_on) if group_on else None
90
+ ts_hue_col = alias_column(df, ts_hue)
91
+ ts_style_col = alias_column(df, ts_style) if ts_style else None
92
+ snap_x_col = alias_column(df, snap_x)
93
+ snap_hue_col = alias_column(df, snap_hue) if snap_hue else None
94
+ resolved_ts_channel = str(ts_channel)
95
+ resolved_snap_channel = str(snap_channel if snap_channel else ts_channel)
96
+
97
+ required = ["time", "channel", "value", "position", ts_x_col, ts_hue_col, snap_x_col]
98
+ if group_col:
99
+ required.append(group_col)
100
+ if snap_hue_col:
101
+ required.append(snap_hue_col)
102
+ if ts_style_col:
103
+ required.append(ts_style_col)
104
+ require_columns(df, required, where="ts_and_snap")
105
+
106
+ work_pl = pl.from_pandas(df)
107
+ value_expr = pl.col("value").cast(pl.Float64, strict=False)
108
+ value_expr = pl.when(value_expr.is_nan()).then(None).otherwise(value_expr)
109
+ time_expr = pl.col("time").cast(pl.Float64, strict=False)
110
+ time_expr = pl.when(time_expr.is_nan()).then(None).otherwise(time_expr)
111
+ work_pl = work_pl.with_columns(value_expr.alias("value"), time_expr.alias("time"))
112
+ work = work_pl.to_pandas(use_pyarrow_extension_array=False)
113
+ if warn_if_empty(work, where="ts_and_snap", detail="after numeric normalization"):
114
+ return None
115
+
116
+ ts_work_pl = work_pl
117
+ if ts_time_window:
118
+ lo, hi = float(ts_time_window[0]), float(ts_time_window[1])
119
+ ts_x_num = pl.col(ts_x_col).cast(pl.Float64, strict=False)
120
+ ts_work_pl = ts_work_pl.filter((ts_x_num >= lo) & (ts_x_num <= hi))
121
+ ts_work = ts_work_pl.to_pandas(use_pyarrow_extension_array=False)
122
+ if warn_if_empty(ts_work, where="ts_and_snap", detail="after time-series window filter"):
123
+ return None
124
+
125
+ available_ts_channels = sorted(ts_work["channel"].astype(str).unique().tolist())
126
+ available_snap_channels = sorted(work["channel"].astype(str).unique().tolist())
127
+ if resolved_ts_channel not in available_ts_channels:
128
+ raise ValueError(
129
+ f"ts_and_snap: ts_channel {resolved_ts_channel!r} not in data. Available: {available_ts_channels}"
130
+ )
131
+ if resolved_snap_channel not in available_snap_channels:
132
+ raise ValueError(
133
+ f"ts_and_snap: snap_channel {resolved_snap_channel!r} not in data. Available: {available_snap_channels}"
134
+ )
135
+ ts_work = ts_work[ts_work["channel"].astype(str) == resolved_ts_channel].copy()
136
+
137
+ if group_col:
138
+ universe = order_levels(work[group_col].astype(str).unique().tolist())
139
+ figure_groups = (
140
+ resolve_groups(universe, pool_sets, match=pool_match)
141
+ if pool_sets
142
+ else [(group, [group]) for group in universe]
143
+ )
144
+ else:
145
+ figure_groups = [("all", [None])]
146
+
147
+ groups: list[GroupFrame] = []
148
+ for label, members in figure_groups:
149
+ snapshot_frame = work.copy()
150
+ time_series_frame = ts_work.copy()
151
+ if group_col and members != [None]:
152
+ snapshot_frame = snapshot_frame[snapshot_frame[group_col].astype(str).isin(members)]
153
+ time_series_frame = time_series_frame[time_series_frame[group_col].astype(str).isin(members)]
154
+ if snapshot_frame.empty:
155
+ continue
156
+ groups.append(
157
+ GroupFrame(
158
+ label=str(label),
159
+ members=tuple(members),
160
+ snapshot=snapshot_frame,
161
+ time_series=time_series_frame,
162
+ )
163
+ )
164
+
165
+ if not groups:
166
+ return None
167
+ return CompositeInputs(
168
+ ts_x_col=ts_x_col,
169
+ group_col=group_col,
170
+ ts_hue_col=ts_hue_col,
171
+ ts_style_col=ts_style_col,
172
+ snap_x_col=snap_x_col,
173
+ snap_hue_col=snap_hue_col,
174
+ ts_channel=resolved_ts_channel,
175
+ snap_channel=resolved_snap_channel,
176
+ groups=tuple(groups),
177
+ )
178
+
179
+
180
+ def prepare_time_series_panel_data(
181
+ *,
182
+ frame: pd.DataFrame,
183
+ channel: str,
184
+ x_col: str,
185
+ add_sheet_lines: bool,
186
+ ) -> TimeSeriesPanelData:
187
+ selected = frame[frame["channel"].astype(str) == channel].copy()
188
+ sheet_lines = None
189
+ if add_sheet_lines and "sheet_index" in selected.columns:
190
+ starts = sorted(selected.groupby("sheet_index")[x_col].min().dropna().tolist())
191
+ sheet_lines = starts[1:] if len(starts) > 1 else []
192
+
193
+ segment_col = None
194
+ segment_parts = (
195
+ ["acquisition_segment_id"]
196
+ if "acquisition_segment_id" in selected.columns
197
+ else [
198
+ column for column in ("plate_id", "source_file", "sheet_name", "sheet_index") if column in selected.columns
199
+ ]
200
+ )
201
+ if segment_parts:
202
+ segment_col = "__plot_segment"
203
+ segments = selected[segment_parts].copy()
204
+ for column in segment_parts:
205
+ segments[column] = segments[column].astype(str)
206
+ selected[segment_col] = segments.agg("::".join, axis=1)
207
+ return TimeSeriesPanelData(frame=selected, sheet_lines=sheet_lines, segment_col=segment_col)
208
+
209
+
210
+ def prepare_snapshot_panel_data(
211
+ *,
212
+ frame: pd.DataFrame,
213
+ group_col: str | None,
214
+ snap_x_col: str,
215
+ snap_hue_col: str | None,
216
+ snap_channel: str,
217
+ snap_time: float,
218
+ snap_time_tolerance: float,
219
+ snap_dispersion: str,
220
+ order_x: list[str] | None,
221
+ order_snap_hue: list[str] | None,
222
+ order_hue: list[str] | None,
223
+ ts_hue_col: str,
224
+ ) -> SnapshotPanelData | None:
225
+ key_cols = [column for column in [group_col, snap_x_col, snap_hue_col, "channel", "position"] if column]
226
+ selection = select_snapshot_rows(
227
+ df=frame,
228
+ target_time=float(snap_time),
229
+ keys=key_cols,
230
+ channel=snap_channel,
231
+ tolerance=float(snap_time_tolerance),
232
+ )
233
+ if selection.rows.empty:
234
+ return None
235
+
236
+ group_cols = [snap_x_col] + ([snap_hue_col] if snap_hue_col else [])
237
+ stats = summarize_snapshot_values(
238
+ df=selection.rows,
239
+ group_cols=group_cols,
240
+ dispersion=snap_dispersion,
241
+ )
242
+ hue_order = (
243
+ resolve_level_order(
244
+ observed=stats[snap_hue_col].astype(str).unique().tolist(),
245
+ configured=(
246
+ order_snap_hue if order_snap_hue is not None else (order_hue if snap_hue_col == ts_hue_col else None)
247
+ ),
248
+ name=("order_snap_hue" if order_snap_hue is not None or snap_hue_col != ts_hue_col else "order_hue"),
249
+ )
250
+ if snap_hue_col
251
+ else ["_single"]
252
+ )
253
+ x_order = resolve_level_order(
254
+ observed=stats[snap_x_col].astype(str).unique().tolist(),
255
+ configured=order_x,
256
+ name="order_x",
257
+ )
258
+ return SnapshotPanelData(
259
+ frame=selection.rows,
260
+ stats=stats,
261
+ time_used=selection.time_used,
262
+ x_order=x_order,
263
+ hue_order=hue_order,
264
+ fell_back=selection.fell_back,
265
+ fallback_times_preview=selection.fallback_times_preview or "",
266
+ fallback_delta=float(selection.fallback_delta or 0.0),
267
+ )
268
+
269
+
270
+ def resolve_paired_hue_levels(
271
+ *, groups: tuple[GroupFrame, ...], hue_col: str, configured: list[str] | None
272
+ ) -> list[str]:
273
+ domains = [set(group.time_series[hue_col].astype(str).unique().tolist()) for group in groups]
274
+ if any(domain != domains[0] for domain in domains[1:]):
275
+ raise ValueError(
276
+ "ts_and_snap: group_layout='paired_row' requires identical ts_hue levels in every group "
277
+ "so one legend and color map remain truthful"
278
+ )
279
+ return resolve_level_order(
280
+ observed=groups[0].time_series[hue_col].astype(str).unique().tolist(),
281
+ configured=configured,
282
+ name="order_hue",
283
+ )
@@ -0,0 +1,29 @@
1
+ """Assay-neutral temporal selection and reduction primitives."""
2
+
3
+ from .aggregation import ObservationAggregationSpec
4
+ from .contracts import (
5
+ EndpointSelection,
6
+ IntervalSelection,
7
+ TemporalReductionSpec,
8
+ TemporalSupportPolicy,
9
+ parse_temporal_selection,
10
+ )
11
+ from .reduction import (
12
+ TemporalReductionResult,
13
+ combine_bound_kinds,
14
+ invert_bound_kind,
15
+ reduce_temporal_trace,
16
+ )
17
+
18
+ __all__ = [
19
+ "EndpointSelection",
20
+ "IntervalSelection",
21
+ "ObservationAggregationSpec",
22
+ "TemporalReductionResult",
23
+ "TemporalReductionSpec",
24
+ "TemporalSupportPolicy",
25
+ "combine_bound_kinds",
26
+ "invert_bound_kind",
27
+ "parse_temporal_selection",
28
+ "reduce_temporal_trace",
29
+ ]
@@ -0,0 +1,60 @@
1
+ """Aggregation policy kept separate from temporal trace reduction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ AggregationStatistic = Literal["mean", "median"]
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ObservationAggregationSpec:
14
+ """Separate within-unit observation reduction from the across-unit center."""
15
+
16
+ within_unit_statistic: AggregationStatistic
17
+ across_unit_statistic: AggregationStatistic
18
+
19
+ def __post_init__(self) -> None:
20
+ for field_name in ("within_unit_statistic", "across_unit_statistic"):
21
+ if getattr(self, field_name) not in {"mean", "median"}:
22
+ raise ValueError(f"observation_aggregation.{field_name} must be 'mean' or 'median'")
23
+
24
+ @classmethod
25
+ def from_mapping(cls, value: object) -> ObservationAggregationSpec:
26
+ if not isinstance(value, Mapping):
27
+ raise ValueError("observation_aggregation must be a mapping")
28
+ payload = {str(key): item for key, item in value.items()}
29
+ expected = {"within_unit_statistic", "across_unit_statistic"}
30
+ unknown = sorted(set(payload) - expected)
31
+ missing = sorted(expected - set(payload))
32
+ if unknown:
33
+ raise ValueError(f"observation_aggregation has unknown fields: {unknown}")
34
+ if missing:
35
+ raise ValueError(f"observation_aggregation is missing required fields: {missing}")
36
+ return cls(
37
+ within_unit_statistic=_statistic(
38
+ payload["within_unit_statistic"],
39
+ field="within_unit_statistic",
40
+ ),
41
+ across_unit_statistic=_statistic(
42
+ payload["across_unit_statistic"],
43
+ field="across_unit_statistic",
44
+ ),
45
+ )
46
+
47
+ def to_mapping(self) -> dict[str, str]:
48
+ return {
49
+ "within_unit_statistic": self.within_unit_statistic,
50
+ "across_unit_statistic": self.across_unit_statistic,
51
+ }
52
+
53
+
54
+ def _statistic(value: object, *, field: str) -> AggregationStatistic:
55
+ if value not in {"mean", "median"}:
56
+ raise ValueError(f"observation_aggregation.{field} must be 'mean' or 'median'")
57
+ return value # type: ignore[return-value]
58
+
59
+
60
+ __all__ = ["AggregationStatistic", "ObservationAggregationSpec"]