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,161 @@
1
+ """Shared snapshot selection and drawing primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from reader_workbench.domains.plate_reader.ordering import order_levels
9
+ from reader_workbench.domains.plate_reader.plots.common import colors_for
10
+ from reader_workbench.plotting.style import PaletteBook
11
+
12
+
13
+ def draw_snapshot_panel(
14
+ ax,
15
+ *,
16
+ snapped: pd.DataFrame,
17
+ stats: pd.DataFrame,
18
+ x_col: str,
19
+ hue_col: str | None,
20
+ agg: str,
21
+ dispersion: str,
22
+ palette_book: PaletteBook | None,
23
+ show_legend: bool,
24
+ legend_loc: str,
25
+ title: str | None,
26
+ ylabel: str,
27
+ color_map: dict[str, str] | None = None,
28
+ x_color_map: dict[str, str] | None = None,
29
+ x_order: list[str] | None = None,
30
+ hue_order: list[str] | None = None,
31
+ tick_rotation: float = 45.0,
32
+ axis_label_size: float | None = None,
33
+ tick_label_size: float | None = None,
34
+ legend_fontsize: float | None = None,
35
+ observation_seed: int = 0,
36
+ ) -> None:
37
+ x_levels = stats[x_col].astype(str).unique().tolist()
38
+ resolved_x_order = [str(value) for value in x_order] if x_order is not None else order_levels(x_levels)
39
+ observed_x = set(map(str, x_levels))
40
+ missing_x = [value for value in resolved_x_order if value not in observed_x]
41
+ omitted_x = [value for value in observed_x if value not in resolved_x_order]
42
+ if missing_x:
43
+ raise ValueError(f"snapshot: x order includes missing label(s): {missing_x}")
44
+ if omitted_x:
45
+ raise ValueError(f"snapshot: x order omits observed label(s): {sorted(omitted_x)}")
46
+ if len(set(resolved_x_order)) != len(resolved_x_order):
47
+ raise ValueError("snapshot: x order contains duplicate labels")
48
+ observed_hue_levels = order_levels(stats[hue_col].astype(str).unique().tolist()) if hue_col else ["_single"]
49
+ hue_levels = [str(value) for value in hue_order] if hue_col and hue_order is not None else observed_hue_levels
50
+ observed_hue = set(observed_hue_levels)
51
+ missing_hue = [value for value in hue_levels if value not in observed_hue]
52
+ omitted_hue = [value for value in observed_hue if value not in hue_levels]
53
+ if missing_hue:
54
+ raise ValueError(f"snapshot: hue order includes missing label(s): {missing_hue}")
55
+ if omitted_hue:
56
+ raise ValueError(f"snapshot: hue order omits observed label(s): {sorted(omitted_hue)}")
57
+ if len(set(hue_levels)) != len(hue_levels):
58
+ raise ValueError("snapshot: hue order contains duplicate labels")
59
+ if color_map is None:
60
+ if hue_col:
61
+ colors = colors_for(len(hue_levels), palette_book)
62
+ color_map = {hue: colors[idx % len(colors)] for idx, hue in enumerate(hue_levels)}
63
+ else:
64
+ color_map = {"_single": "#D9D9D9"}
65
+
66
+ n_x = len(resolved_x_order)
67
+ base_pos = np.arange(n_x, dtype=float)
68
+ num_hues = len(hue_levels) if hue_col else 1
69
+ has_hue = hue_col is not None and num_hues > 1
70
+ width = 0.8 if not has_hue else min(0.85 / max(num_hues, 1), 0.8)
71
+ offsets = (np.arange(num_hues) - (num_hues - 1) / 2.0) * width if has_hue else np.array([0.0])
72
+ hue_index = {hue: idx for idx, hue in enumerate(hue_levels)}
73
+
74
+ ax.grid(False)
75
+ ax.yaxis.grid(True, which="major")
76
+ ax.xaxis.grid(False)
77
+
78
+ legend_handles: dict[str, object] = {}
79
+ rng = np.random.default_rng(int(observation_seed))
80
+ for j, x_value in enumerate(resolved_x_order):
81
+ x_center = base_pos[j]
82
+ for hue in hue_levels:
83
+ sub = stats[stats[x_col].astype(str) == str(x_value)]
84
+ if hue_col:
85
+ sub = sub[sub[hue_col].astype(str) == str(hue)]
86
+ if sub.empty:
87
+ continue
88
+ row = sub.iloc[0]
89
+ height = float(row[agg])
90
+
91
+ yerr = None
92
+ if dispersion == "sd":
93
+ dispersion_value = float(row.get("std", np.nan))
94
+ yerr = None if not np.isfinite(dispersion_value) else dispersion_value
95
+ elif dispersion == "iqr":
96
+ q1 = row.get("q1", np.nan)
97
+ q3 = row.get("q3", np.nan)
98
+ if np.isfinite(q1) and np.isfinite(q3):
99
+ if agg == "median":
100
+ lower = max(height - float(q1), 0.0)
101
+ upper = max(float(q3) - height, 0.0)
102
+ yerr = [[lower], [upper]]
103
+ else:
104
+ yerr = max(0.5 * (float(q3) - float(q1)), 0.0)
105
+
106
+ error_kw = {"capsize": 3, "elinewidth": 1.0, "alpha": 0.9} if yerr is not None else None
107
+ xpos = x_center + offsets[hue_index[hue]]
108
+ bar_color = color_map[hue] if hue_col else ((x_color_map or {}).get(str(x_value), "#D9D9D9"))
109
+ bars = ax.bar(
110
+ [xpos],
111
+ [height],
112
+ width=width,
113
+ color=bar_color,
114
+ edgecolor="#C0C0C0",
115
+ zorder=1,
116
+ yerr=yerr,
117
+ **({"error_kw": error_kw} if error_kw else {}),
118
+ label=(str(hue) if (show_legend and hue not in legend_handles) else None),
119
+ )
120
+ if show_legend and hue not in legend_handles and len(bars.patches) > 0:
121
+ legend_handles[hue] = bars.patches[0]
122
+
123
+ rr = snapped[snapped[x_col].astype(str) == str(x_value)]
124
+ if hue_col:
125
+ rr = rr[rr[hue_col].astype(str) == str(hue)]
126
+ if not rr.empty:
127
+ jitter = float(width) * (0.08 if has_hue else 0.12)
128
+ xj = xpos + (rng.random(len(rr)) - 0.5) * (2.0 * jitter)
129
+ ax.scatter(
130
+ xj,
131
+ rr["value"],
132
+ s=34,
133
+ zorder=3,
134
+ facecolors="#FFFFFF",
135
+ edgecolors="#C0C0C0",
136
+ linewidths=0.7,
137
+ color=None,
138
+ )
139
+
140
+ ax.set_xticks(base_pos)
141
+ horizontal_alignment = "center" if float(tick_rotation) == 0.0 else "right"
142
+ tick_kwargs = {"fontsize": tick_label_size} if tick_label_size is not None else {}
143
+ ax.set_xticklabels(resolved_x_order, rotation=float(tick_rotation), ha=horizontal_alignment, **tick_kwargs)
144
+ ax.set_xlabel("")
145
+ ylabel_kwargs = {"fontsize": axis_label_size} if axis_label_size is not None else {}
146
+ ax.set_ylabel(ylabel, **ylabel_kwargs)
147
+ if tick_label_size is not None:
148
+ ax.tick_params(axis="y", labelsize=tick_label_size)
149
+ if title:
150
+ ax.set_title(title, fontweight="normal")
151
+
152
+ if show_legend and hue_col and len(hue_levels) > 1 and legend_handles:
153
+ legend_kwargs = {"fontsize": legend_fontsize} if legend_fontsize is not None else {}
154
+ ax.legend(
155
+ handles=list(legend_handles.values()),
156
+ labels=list(legend_handles.keys()),
157
+ loc=str(legend_loc),
158
+ title=None,
159
+ frameon=False,
160
+ **legend_kwargs,
161
+ )
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+
6
+ import pandas as pd
7
+ import polars as pl
8
+
9
+ from reader_workbench.domains.plate_reader.analysis.timepoints import nearest_time_per_key
10
+
11
+ from ..common import select_exact_observations
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class SnapshotSelection:
16
+ rows: pd.DataFrame
17
+ time_used: float
18
+ fell_back: bool
19
+ fallback_delta: float | None = None
20
+ fallback_times_preview: str | None = None
21
+
22
+
23
+ def select_snapshot_rows(
24
+ *,
25
+ df: pd.DataFrame,
26
+ target_time: float,
27
+ keys: Sequence[str],
28
+ channel: str,
29
+ tolerance: float,
30
+ ) -> SnapshotSelection:
31
+ snapped = nearest_time_per_key(df, target_time=float(target_time), keys=list(keys), tol=float(tolerance))
32
+ snapped = snapped[snapped["channel"].astype(str) == str(channel)].copy()
33
+ if snapped.empty:
34
+ fallback = nearest_time_per_key(df, target_time=float(target_time), keys=list(keys), tol=float("inf"))
35
+ fallback = fallback[fallback["channel"].astype(str) == str(channel)].copy()
36
+ fallback = select_exact_observations(fallback, where="snapshot summary")
37
+ if fallback.empty:
38
+ return SnapshotSelection(rows=fallback, time_used=float(target_time), fell_back=True)
39
+ times_used = pd.to_numeric(fallback["time"], errors="coerce").dropna()
40
+ unique_times = sorted(times_used.unique().tolist())
41
+ representative_time = unique_times[0] if len(unique_times) == 1 else float(pd.Series(unique_times).median())
42
+ preview = ", ".join(f"{time:.2f}" for time in unique_times[:6]) + (" …" if len(unique_times) > 6 else "")
43
+ return SnapshotSelection(
44
+ rows=fallback,
45
+ time_used=float(times_used.median()) if not times_used.empty else float(target_time),
46
+ fell_back=True,
47
+ fallback_delta=abs(float(representative_time) - float(target_time)),
48
+ fallback_times_preview=preview,
49
+ )
50
+
51
+ snapped = select_exact_observations(snapped, where="snapshot summary")
52
+ times_used = pd.to_numeric(snapped["time"], errors="coerce").dropna()
53
+ time_used = float(times_used.median()) if not times_used.empty else float(target_time)
54
+ return SnapshotSelection(rows=snapped, time_used=time_used, fell_back=False)
55
+
56
+
57
+ def summarize_snapshot_values(
58
+ *,
59
+ df: pd.DataFrame,
60
+ group_cols: Sequence[str],
61
+ dispersion: str,
62
+ ) -> pd.DataFrame:
63
+ stats_pl = (
64
+ pl.from_pandas(df)
65
+ .with_columns(
66
+ pl.when(pl.col("value").cast(pl.Float64, strict=False).is_nan())
67
+ .then(None)
68
+ .otherwise(pl.col("value").cast(pl.Float64, strict=False))
69
+ .alias("value")
70
+ )
71
+ .group_by(list(group_cols))
72
+ .agg(
73
+ pl.col("value").count().alias("n"),
74
+ pl.col("value").mean().alias("mean"),
75
+ pl.col("value").median().alias("median"),
76
+ pl.col("value").std(ddof=1).alias("std"),
77
+ )
78
+ )
79
+
80
+ if dispersion == "iqr":
81
+ quantiles = (
82
+ pl.from_pandas(df)
83
+ .group_by(list(group_cols))
84
+ .agg(
85
+ pl.col("value").quantile(0.25, interpolation="linear").alias("q1"),
86
+ pl.col("value").quantile(0.75, interpolation="linear").alias("q3"),
87
+ )
88
+ )
89
+ stats_pl = stats_pl.join(quantiles, on=list(group_cols), how="left")
90
+
91
+ return stats_pl.sort(list(group_cols)).to_pandas(use_pyarrow_extension_array=False)
@@ -0,0 +1,296 @@
1
+ """Shared time-series drawing primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from matplotlib.lines import Line2D
10
+
11
+ from ..common import descriptive_mean_resampling_interval, select_exact_observations
12
+
13
+ _MARKERS = ["o", "s", "^", "D", "P", "X", "v", "<", ">", "h", "H"]
14
+
15
+
16
+ def marker_map_for_levels(levels: Sequence[str]) -> dict[str, str]:
17
+ return {str(level): _MARKERS[idx % len(_MARKERS)] for idx, level in enumerate(levels)}
18
+
19
+
20
+ def _maybe_log(ax, enable: bool) -> None:
21
+ if enable:
22
+ ax.set_yscale("log")
23
+ ymin, ymax = ax.get_ylim()
24
+ if ymin <= 0:
25
+ ax.set_ylim(bottom=max(1e-12, ymin))
26
+
27
+
28
+ def _summarize_time_series_lines(
29
+ *,
30
+ data: pd.DataFrame,
31
+ x_col: str,
32
+ hue_col: str,
33
+ hue_levels: Sequence[str],
34
+ segment_col: str | None,
35
+ observation_interval_mass: float,
36
+ observation_resamples: int,
37
+ observation_seed: int,
38
+ style_col: str | None = None,
39
+ style_levels: Sequence[str] | None = None,
40
+ ) -> dict[str | tuple[str, str], pd.DataFrame]:
41
+ if data.empty:
42
+ return {}
43
+
44
+ columns = [x_col, hue_col, "value"]
45
+ if style_col is not None and style_col in data.columns:
46
+ columns.append(style_col)
47
+ if segment_col is not None and segment_col in data.columns:
48
+ columns.append(segment_col)
49
+ plot_data = data.loc[:, columns].copy()
50
+ plot_data[hue_col] = plot_data[hue_col].astype(str)
51
+ active_style_col = style_col if style_col is not None and style_col in plot_data.columns else None
52
+ if active_style_col is not None:
53
+ plot_data[active_style_col] = plot_data[active_style_col].astype(str)
54
+ active_segment_col = segment_col if segment_col is not None and segment_col in plot_data.columns else None
55
+ if active_segment_col is not None:
56
+ plot_data[active_segment_col] = plot_data[active_segment_col].astype(str)
57
+ else:
58
+ active_segment_col = "__segment_id"
59
+ plot_data[active_segment_col] = "segment"
60
+ plot_data["value"] = pd.to_numeric(plot_data["value"], errors="coerce")
61
+ group_columns = [hue_col]
62
+ if active_style_col is not None:
63
+ group_columns.append(active_style_col)
64
+ group_columns.extend([active_segment_col, x_col])
65
+ grouped = plot_data.groupby(group_columns, dropna=False, sort=True)["value"]
66
+ rng = np.random.default_rng(int(observation_seed))
67
+
68
+ if active_style_col is None:
69
+ summary_keys: list[str | tuple[str, str]] = [str(hue) for hue in hue_levels]
70
+ else:
71
+ resolved_style_levels = [str(value) for value in (style_levels or [])]
72
+ if not resolved_style_levels:
73
+ resolved_style_levels = sorted(plot_data[active_style_col].dropna().astype(str).unique().tolist())
74
+ summary_keys = [(str(hue), style) for hue in hue_levels for style in resolved_style_levels]
75
+ summaries: dict[str | tuple[str, str], dict[str, list[object]]] = {
76
+ key: {x_col: [], "segment": [], "mean": [], "lower": [], "upper": []} for key in summary_keys
77
+ }
78
+ for group_key, series in grouped:
79
+ if active_style_col is None:
80
+ hue, segment_id, x_value = group_key
81
+ summary_key: str | tuple[str, str] = str(hue)
82
+ else:
83
+ hue, style, segment_id, x_value = group_key
84
+ summary_key = (str(hue), str(style))
85
+ payload = summaries.get(summary_key)
86
+ if payload is None:
87
+ continue
88
+ values = series.to_numpy(dtype=float, copy=False)
89
+ values = values[~np.isnan(values)]
90
+ if values.size == 0:
91
+ continue
92
+ mean, lower, upper = descriptive_mean_resampling_interval(
93
+ values,
94
+ interval_mass=observation_interval_mass,
95
+ resamples=observation_resamples,
96
+ rng=rng,
97
+ )
98
+ payload[x_col].append(x_value)
99
+ payload["segment"].append(str(segment_id))
100
+ payload["mean"].append(mean)
101
+ payload["lower"].append(lower)
102
+ payload["upper"].append(upper)
103
+
104
+ out: dict[str | tuple[str, str], pd.DataFrame] = {}
105
+ for key in summary_keys:
106
+ payload = summaries[key]
107
+ if not payload[x_col]:
108
+ continue
109
+ out[key] = pd.DataFrame(payload).sort_values(["segment", x_col], kind="stable")
110
+ return out
111
+
112
+
113
+ def draw_time_series_panel(
114
+ ax,
115
+ *,
116
+ data: pd.DataFrame,
117
+ x_col: str,
118
+ hue_col: str,
119
+ hue_levels: list[str],
120
+ color_map: dict[str, str],
121
+ marker_map: dict[str, str],
122
+ segment_col: str | None,
123
+ show_observations: bool,
124
+ observation_interval_mass: float,
125
+ observation_interval_alpha: float,
126
+ observation_resamples: int,
127
+ observation_seed: int,
128
+ line_alpha: float,
129
+ mean_marker_alpha: float,
130
+ observation_alpha: float,
131
+ add_sheet_lines: bool,
132
+ sheet_lines: list[float] | None,
133
+ sheet_line_kwargs: dict | None,
134
+ log_y: bool,
135
+ xlabel: str,
136
+ ylabel: str,
137
+ legend_loc: str,
138
+ show_legend: bool,
139
+ legend_label_map: Mapping[str, str] | None = None,
140
+ marked_time: float | None = None,
141
+ marked_time_kwargs: dict | None = None,
142
+ line_width: float = 1.8,
143
+ mean_marker_size: float = 36.0,
144
+ mean_marker_every: int = 1,
145
+ observation_marker_size: float = 18.0,
146
+ axis_label_size: float = 10.0,
147
+ tick_label_size: float = 8.0,
148
+ legend_fontsize: float = 8.0,
149
+ legend_marker_size: float = 7.0,
150
+ style_col: str | None = None,
151
+ style_levels: list[str] | None = None,
152
+ linestyle_map: dict[str, str] | None = None,
153
+ style_legend_loc: str = "lower right",
154
+ style_legend_title: str | None = None,
155
+ ) -> None:
156
+ ax.grid(False)
157
+ ax.yaxis.grid(True, which="major")
158
+ ax.xaxis.grid(True, which="major")
159
+ data = select_exact_observations(data, where="time-series summary")
160
+
161
+ if show_observations:
162
+ for hue in hue_levels:
163
+ rr = data[data[hue_col].astype(str) == str(hue)]
164
+ ax.scatter(
165
+ rr[x_col],
166
+ rr["value"],
167
+ s=observation_marker_size,
168
+ alpha=observation_alpha,
169
+ zorder=3,
170
+ linewidths=0.0,
171
+ edgecolors="none",
172
+ marker=marker_map[hue],
173
+ c=color_map[hue],
174
+ )
175
+
176
+ summary_by_hue = _summarize_time_series_lines(
177
+ data=data,
178
+ x_col=x_col,
179
+ hue_col=hue_col,
180
+ hue_levels=hue_levels,
181
+ segment_col=segment_col,
182
+ observation_interval_mass=observation_interval_mass,
183
+ observation_resamples=observation_resamples,
184
+ observation_seed=observation_seed,
185
+ style_col=style_col,
186
+ style_levels=style_levels,
187
+ )
188
+ resolved_style_levels = [str(value) for value in (style_levels or [])]
189
+ if style_col is not None and not resolved_style_levels:
190
+ resolved_style_levels = sorted(data[style_col].dropna().astype(str).unique().tolist())
191
+ if style_col is None:
192
+ resolved_style_levels = []
193
+ style_cycle = ["-", "--", ":", "-."]
194
+ resolved_linestyle_map = linestyle_map or {
195
+ style: style_cycle[idx % len(style_cycle)] for idx, style in enumerate(resolved_style_levels)
196
+ }
197
+ for hue in hue_levels:
198
+ styles_for_hue: list[str | None] = resolved_style_levels if style_col is not None else [None]
199
+ for style in styles_for_hue:
200
+ summary_key: str | tuple[str, str] = str(hue) if style is None else (str(hue), str(style))
201
+ mm = summary_by_hue.get(summary_key)
202
+ if mm is None or mm.empty:
203
+ continue
204
+ for _, segment_df in mm.groupby("segment", sort=False):
205
+ if float(observation_interval_mass) > 0:
206
+ ax.fill_between(
207
+ segment_df[x_col],
208
+ segment_df["lower"],
209
+ segment_df["upper"],
210
+ alpha=float(observation_interval_alpha),
211
+ color=color_map[hue],
212
+ linewidth=0.0,
213
+ zorder=1,
214
+ )
215
+ ax.plot(
216
+ segment_df[x_col],
217
+ segment_df["mean"],
218
+ color=color_map[hue],
219
+ linestyle=(resolved_linestyle_map[str(style)] if style is not None else "-"),
220
+ linewidth=line_width,
221
+ alpha=line_alpha,
222
+ zorder=1.5,
223
+ )
224
+ marker_rows = segment_df.iloc[:: max(1, int(mean_marker_every))]
225
+ ax.scatter(
226
+ marker_rows[x_col],
227
+ marker_rows["mean"],
228
+ s=mean_marker_size,
229
+ zorder=2.5,
230
+ marker=marker_map[hue],
231
+ alpha=mean_marker_alpha,
232
+ edgecolors="none",
233
+ linewidths=0.0,
234
+ c=color_map[hue],
235
+ )
236
+
237
+ if add_sheet_lines and sheet_lines:
238
+ style = {"color": "#9E9E9E", "linestyle": "--", "linewidth": 0.8, "alpha": 0.9, "zorder": 0.5}
239
+ style.update(sheet_line_kwargs or {})
240
+ for sheet_x in sheet_lines:
241
+ ax.axvline(float(sheet_x), **style)
242
+
243
+ if marked_time is not None:
244
+ style = {"color": "#9E9E9E", "linestyle": "--", "linewidth": 0.9, "alpha": 1.0, "zorder": 0.8}
245
+ style.update(marked_time_kwargs or {})
246
+ ax.axvline(float(marked_time), **style)
247
+
248
+ _maybe_log(ax, log_y)
249
+ ax.set_xlabel(xlabel, fontsize=axis_label_size)
250
+ ax.set_ylabel(ylabel, fontsize=axis_label_size)
251
+ ax.tick_params(axis="x", labelsize=tick_label_size)
252
+ ax.tick_params(axis="y", labelsize=tick_label_size)
253
+
254
+ if show_legend:
255
+ handles = [
256
+ Line2D(
257
+ [0],
258
+ [0],
259
+ color=color_map[hue],
260
+ marker=marker_map[hue],
261
+ markersize=legend_marker_size,
262
+ linestyle="-",
263
+ linewidth=line_width,
264
+ alpha=mean_marker_alpha,
265
+ label=(str(legend_label_map.get(str(hue), hue)) if legend_label_map is not None else str(hue)),
266
+ )
267
+ for hue in hue_levels
268
+ ]
269
+ hue_legend = ax.legend(
270
+ handles=handles,
271
+ loc=legend_loc,
272
+ title=None,
273
+ frameon=False,
274
+ fontsize=legend_fontsize,
275
+ )
276
+ if resolved_style_levels:
277
+ ax.add_artist(hue_legend)
278
+ style_handles = [
279
+ Line2D(
280
+ [0],
281
+ [0],
282
+ color="#333333",
283
+ linestyle=resolved_linestyle_map[style],
284
+ linewidth=line_width,
285
+ label=style,
286
+ )
287
+ for style in resolved_style_levels
288
+ ]
289
+ ax.legend(
290
+ handles=style_handles,
291
+ loc=style_legend_loc,
292
+ title=style_legend_title,
293
+ frameon=False,
294
+ fontsize=legend_fontsize,
295
+ title_fontsize=legend_fontsize,
296
+ )