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,479 @@
1
+ """Two-panel figure: (left) time series, (right) snapshot barplot,
2
+ driven by the same group selection."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+
8
+ import matplotlib.pyplot as plt
9
+ import pandas as pd
10
+
11
+ from reader_workbench.plotting.sinks import PlotFigure
12
+ from reader_workbench.plotting.style import DEFAULT_RC as _RC
13
+ from reader_workbench.plotting.style import PaletteBook, use_style
14
+
15
+ from ..common import colors_for, exact_observation_counts, plot_figure
16
+ from ..grouping import GroupMatch
17
+ from ..panels import (
18
+ draw_snapshot_panel,
19
+ draw_time_series_panel,
20
+ marker_map_for_levels,
21
+ )
22
+ from .planning import (
23
+ prepare_composite_inputs,
24
+ prepare_snapshot_panel_data,
25
+ prepare_time_series_panel_data,
26
+ resolve_level_order,
27
+ resolve_paired_hue_levels,
28
+ )
29
+
30
+ _FIG_STYLE_KEYS = {
31
+ "axis_label_size",
32
+ "tick_label_size",
33
+ "legend_fontsize",
34
+ "legend_marker_size",
35
+ "line_width",
36
+ "mean_marker_size",
37
+ "mean_marker_every",
38
+ "observation_marker_size",
39
+ "style_legend_loc",
40
+ "style_legend_title",
41
+ "snap_tick_rotation",
42
+ "ts_title",
43
+ "snap_title",
44
+ "line_alpha",
45
+ "mean_marker_alpha",
46
+ "observation_alpha",
47
+ "suptitle_y",
48
+ }
49
+
50
+
51
+ def _source_observation_count_frames(
52
+ *,
53
+ time_series: pd.DataFrame,
54
+ snapshot: pd.DataFrame | None,
55
+ ts_channel: str,
56
+ snap_channel: str,
57
+ ts_x_col: str,
58
+ ts_time_window: list[float] | None,
59
+ ) -> tuple[pd.DataFrame, ...]:
60
+ if snapshot is None or snapshot.empty:
61
+ return (time_series,)
62
+ if ts_channel != snap_channel:
63
+ return time_series, snapshot
64
+ if not ts_time_window:
65
+ return (time_series,)
66
+ lo, hi = (float(value) for value in ts_time_window)
67
+ snapshot_x = pd.to_numeric(snapshot[ts_x_col], errors="coerce")
68
+ snapshot_only = snapshot.loc[~snapshot_x.between(lo, hi, inclusive="both")]
69
+ return (time_series,) if snapshot_only.empty else (time_series, snapshot_only)
70
+
71
+
72
+ # -------------------------------- main API --------------------------------
73
+
74
+
75
+ def plot_ts_and_snap(
76
+ *,
77
+ df: pd.DataFrame,
78
+ # grouping
79
+ group_on: str | None,
80
+ pool_sets: list[dict[str, list[str]]] | None,
81
+ pool_match: GroupMatch = "exact",
82
+ group_layout: str = "separate",
83
+ # time series (left)
84
+ ts_x: str = "time",
85
+ ts_channel: str,
86
+ ts_hue: str,
87
+ ts_style: str | None = None,
88
+ order_hue: list[str] | None = None,
89
+ order_style: list[str] | None = None,
90
+ ts_time_window: list[float] | None = None,
91
+ ts_add_sheet_line: bool = False,
92
+ ts_sheet_line_kwargs: dict | None = None,
93
+ ts_mark_snap_time: bool = False,
94
+ ts_snap_line_kwargs: dict | None = None,
95
+ ts_log_transform: bool | list[str] = False,
96
+ ts_observation_interval_mass: float = 0.95,
97
+ ts_observation_interval_alpha: float = 0.15,
98
+ ts_observation_resamples: int = 100,
99
+ ts_observation_seed: int = 0,
100
+ ts_show_observations: bool = False,
101
+ ts_legend_loc: str = "upper right",
102
+ # snapshot (right)
103
+ snap_x: str = "treatment",
104
+ snap_channel: str | None = None, # defaults to ts_channel
105
+ snap_hue: str | None = None, # defaults to None (gray bars, white dots)
106
+ order_x: list[str] | None = None,
107
+ order_snap_hue: list[str] | None = None,
108
+ snap_time: float = 0.0,
109
+ snap_agg: str = "mean", # "mean" | "median"
110
+ snap_dispersion: str = "sd", # "sd" | "iqr" | "none"
111
+ snap_time_tolerance: float = 0.51,
112
+ snap_show_legend: bool = False,
113
+ snap_legend_loc: str = "upper right",
114
+ snap_color_by_x: bool = False,
115
+ square_panels: bool = False,
116
+ title: str | None = None,
117
+ # figure/style
118
+ fig_kwargs: dict | None = None,
119
+ filename: str | None = None,
120
+ palette_book: PaletteBook | None = None,
121
+ ) -> list[PlotFigure]:
122
+ """
123
+ Render time-series/snapshot pairs for each selected group. By default each
124
+ group receives its own two-panel figure; ``group_layout="paired_row"``
125
+ composes every group pair into one horizontal figure.
126
+
127
+ Hue handling:
128
+ • Left requires `ts_hue`
129
+ • Right uses `snap_hue` if provided; when `snap_hue == ts_hue`, colors are shared
130
+ • Otherwise snapshot bars are gray with white observation dots (no legend by default)
131
+ """
132
+ if snap_agg not in {"mean", "median"}:
133
+ raise ValueError("snap_agg must be 'mean' or 'median'")
134
+ if snap_dispersion not in {"sd", "iqr", "none"}:
135
+ raise ValueError("snap_dispersion must be 'sd', 'iqr', or 'none'")
136
+ if group_layout not in {"separate", "paired_row"}:
137
+ raise ValueError("group_layout must be 'separate' or 'paired_row'")
138
+ if snap_color_by_x and snap_hue is not None:
139
+ raise ValueError("snap_color_by_x requires snap_hue to be omitted")
140
+
141
+ fig_kwargs = fig_kwargs or {}
142
+
143
+ prepared = prepare_composite_inputs(
144
+ df=df,
145
+ group_on=group_on,
146
+ pool_sets=pool_sets,
147
+ pool_match=pool_match,
148
+ ts_x=ts_x,
149
+ ts_channel=ts_channel,
150
+ ts_hue=ts_hue,
151
+ ts_style=ts_style,
152
+ ts_time_window=ts_time_window,
153
+ snap_x=snap_x,
154
+ snap_channel=snap_channel,
155
+ snap_hue=snap_hue,
156
+ )
157
+ if prepared is None:
158
+ return []
159
+ ts_x_col = prepared.ts_x_col
160
+ group_col = prepared.group_col
161
+ ts_hue_col = prepared.ts_hue_col
162
+ ts_style_col = prepared.ts_style_col
163
+ snap_x_col = prepared.snap_x_col
164
+ snap_hue_col = prepared.snap_hue_col
165
+ ch_ts = prepared.ts_channel
166
+ ch_snap = prepared.snap_channel
167
+ group_frames = prepared.groups
168
+
169
+ figures: list[PlotFigure] = []
170
+ snap_fallbacks: list[dict[str, object]] = []
171
+ group_panel_counts: dict[str, tuple[int, int]] = {}
172
+
173
+ def _draw_group_pair(
174
+ *,
175
+ label: str,
176
+ d: pd.DataFrame,
177
+ ts_d: pd.DataFrame,
178
+ ax_ts,
179
+ ax_snap,
180
+ paired_row: bool,
181
+ show_ts_legend: bool,
182
+ shared_hue_levels: list[str] | None = None,
183
+ shared_color_map: dict[str, str] | None = None,
184
+ ):
185
+ """Draw one group pair into caller-owned axes."""
186
+
187
+ time_series_data = prepare_time_series_panel_data(
188
+ frame=ts_d,
189
+ channel=ch_ts,
190
+ x_col=ts_x_col,
191
+ add_sheet_lines=ts_add_sheet_line,
192
+ )
193
+ ts = time_series_data.frame
194
+
195
+ # TS hue levels + colors
196
+ hue_levels_ts = (
197
+ shared_hue_levels
198
+ if shared_hue_levels is not None
199
+ else resolve_level_order(
200
+ observed=ts[ts_hue_col].astype(str).unique().tolist(), configured=order_hue, name="order_hue"
201
+ )
202
+ )
203
+ style_levels_ts = (
204
+ resolve_level_order(
205
+ observed=ts[ts_style_col].astype(str).unique().tolist(),
206
+ configured=order_style,
207
+ name="order_style",
208
+ )
209
+ if ts_style_col
210
+ else []
211
+ )
212
+ marker_map = marker_map_for_levels(hue_levels_ts)
213
+ colors = colors_for(max(1, len(hue_levels_ts)), palette_book)
214
+ color_map = shared_color_map or {h: colors[i % len(colors)] for i, h in enumerate(hue_levels_ts)}
215
+
216
+ # If snapshot uses the same hue column, reuse colors; otherwise compute locally on demand
217
+ def _snap_color_map(
218
+ hue_levels_snap: list[str],
219
+ *,
220
+ color_map=color_map,
221
+ ) -> dict[str, str]:
222
+ if snap_hue_col and snap_hue_col == ts_hue_col:
223
+ fallback_colors = colors_for(len(hue_levels_snap), palette_book)
224
+ return {h: color_map.get(h, fallback_colors[i]) for i, h in enumerate(hue_levels_snap)}
225
+ snap_colors = colors_for(len(hue_levels_snap), palette_book)
226
+ return {h: snap_colors[i % len(snap_colors)] for i, h in enumerate(hue_levels_snap)}
227
+
228
+ # ---- Left: time series with descriptive observation-resampling band ----
229
+ if not ts.empty:
230
+ draw_time_series_panel(
231
+ ax_ts,
232
+ data=ts,
233
+ x_col=ts_x_col,
234
+ hue_col=ts_hue_col,
235
+ hue_levels=hue_levels_ts,
236
+ style_col=ts_style_col,
237
+ style_levels=style_levels_ts,
238
+ color_map=color_map,
239
+ marker_map=marker_map,
240
+ segment_col=time_series_data.segment_col,
241
+ show_observations=ts_show_observations,
242
+ observation_interval_mass=ts_observation_interval_mass,
243
+ observation_interval_alpha=ts_observation_interval_alpha,
244
+ observation_resamples=ts_observation_resamples,
245
+ observation_seed=ts_observation_seed,
246
+ line_alpha=float(fig_kwargs.get("line_alpha", 0.85)),
247
+ mean_marker_alpha=float(fig_kwargs.get("mean_marker_alpha", 0.75)),
248
+ observation_alpha=float(fig_kwargs.get("observation_alpha", 0.30)),
249
+ add_sheet_lines=ts_add_sheet_line,
250
+ sheet_lines=time_series_data.sheet_lines,
251
+ sheet_line_kwargs=ts_sheet_line_kwargs,
252
+ log_y=(ch_ts in ts_log_transform) if isinstance(ts_log_transform, list) else bool(ts_log_transform),
253
+ xlabel=("Time (h)" if str(ts_x).lower() == "time" else ts_x_col),
254
+ ylabel=ch_ts,
255
+ legend_loc=str(ts_legend_loc),
256
+ show_legend=show_ts_legend,
257
+ style_legend_loc=str(fig_kwargs.get("style_legend_loc", "lower right")),
258
+ style_legend_title=(
259
+ str(fig_kwargs["style_legend_title"]) if fig_kwargs.get("style_legend_title") is not None else None
260
+ ),
261
+ marked_time=(float(snap_time) if ts_mark_snap_time else None),
262
+ marked_time_kwargs=ts_snap_line_kwargs,
263
+ line_width=float(fig_kwargs.get("line_width", 1.8)),
264
+ mean_marker_size=float(fig_kwargs.get("mean_marker_size", 36.0)),
265
+ mean_marker_every=int(fig_kwargs.get("mean_marker_every", 1)),
266
+ observation_marker_size=float(fig_kwargs.get("observation_marker_size", 18.0)),
267
+ axis_label_size=float(fig_kwargs.get("axis_label_size", 10.0)),
268
+ tick_label_size=float(fig_kwargs.get("tick_label_size", 8.0)),
269
+ legend_fontsize=float(fig_kwargs.get("legend_fontsize", 8.0)),
270
+ legend_marker_size=float(fig_kwargs.get("legend_marker_size", 7.0)),
271
+ )
272
+ configured_ts_title = fig_kwargs.get("ts_title")
273
+ if paired_row:
274
+ title_parts = [str(label)]
275
+ if configured_ts_title is not None:
276
+ title_parts.append(str(configured_ts_title))
277
+ ax_ts.set_title("\n".join(title_parts), fontweight="normal")
278
+ elif configured_ts_title is not None:
279
+ ax_ts.set_title(str(configured_ts_title), fontweight="normal")
280
+ if ts_time_window:
281
+ ax_ts.set_xlim(float(ts_time_window[0]), float(ts_time_window[1]))
282
+
283
+ # ---- Right: snapshot barplot ----
284
+ snapshot_data = prepare_snapshot_panel_data(
285
+ frame=d,
286
+ group_col=group_col,
287
+ snap_x_col=snap_x_col,
288
+ snap_hue_col=snap_hue_col,
289
+ snap_channel=ch_snap,
290
+ snap_time=snap_time,
291
+ snap_time_tolerance=snap_time_tolerance,
292
+ snap_dispersion=snap_dispersion,
293
+ order_x=order_x,
294
+ order_snap_hue=order_snap_hue,
295
+ order_hue=order_hue,
296
+ ts_hue_col=ts_hue_col,
297
+ )
298
+ if snapshot_data is not None and snapshot_data.fell_back:
299
+ snap_fallbacks.append(
300
+ {
301
+ "label": str(label),
302
+ "times": snapshot_data.fallback_times_preview,
303
+ "delta": snapshot_data.fallback_delta,
304
+ }
305
+ )
306
+ if snapshot_data is not None:
307
+ hue_levels_snap = snapshot_data.hue_order
308
+ resolved_snap_x_order = snapshot_data.x_order
309
+ configured_snap_title = fig_kwargs.get("snap_title")
310
+ snap_title = (
311
+ f"{configured_snap_title} (t={snapshot_data.time_used:.2f} h)"
312
+ if configured_snap_title is not None
313
+ else f"t={snapshot_data.time_used:.2f} h"
314
+ )
315
+ if paired_row:
316
+ snap_title = f"{label}\n{snap_title}"
317
+ x_color_map = None
318
+ if snap_color_by_x:
319
+ if snap_x_col == ts_hue_col:
320
+ x_color_map = {value: color_map[value] for value in resolved_snap_x_order if value in color_map}
321
+ else:
322
+ snap_x_colors = colors_for(len(resolved_snap_x_order), palette_book)
323
+ x_color_map = {
324
+ value: snap_x_colors[idx % len(snap_x_colors)]
325
+ for idx, value in enumerate(resolved_snap_x_order)
326
+ }
327
+ draw_snapshot_panel(
328
+ ax_snap,
329
+ snapped=snapshot_data.frame,
330
+ stats=snapshot_data.stats,
331
+ x_col=snap_x_col,
332
+ hue_col=snap_hue_col,
333
+ agg=snap_agg,
334
+ dispersion=snap_dispersion,
335
+ palette_book=palette_book,
336
+ show_legend=snap_show_legend,
337
+ legend_loc=str(snap_legend_loc),
338
+ title=snap_title,
339
+ ylabel=ch_snap,
340
+ color_map=_snap_color_map(hue_levels_snap),
341
+ x_color_map=x_color_map,
342
+ x_order=resolved_snap_x_order,
343
+ hue_order=(hue_levels_snap if snap_hue_col else None),
344
+ tick_rotation=float(fig_kwargs.get("snap_tick_rotation", 45.0)),
345
+ axis_label_size=float(fig_kwargs.get("axis_label_size", 10.0)),
346
+ tick_label_size=float(fig_kwargs.get("tick_label_size", 8.0)),
347
+ legend_fontsize=float(fig_kwargs.get("legend_fontsize", 8.0)),
348
+ )
349
+
350
+ count_frames = _source_observation_count_frames(
351
+ time_series=ts,
352
+ snapshot=(snapshot_data.frame if snapshot_data is not None else None),
353
+ ts_channel=ch_ts,
354
+ snap_channel=ch_snap,
355
+ ts_x_col=ts_x_col,
356
+ ts_time_window=ts_time_window,
357
+ )
358
+ panel_values = pd.concat(count_frames, ignore_index=True)
359
+ group_panel_counts[str(label)] = exact_observation_counts(panel_values)
360
+
361
+ if square_panels:
362
+ ax_ts.set_box_aspect(1.0)
363
+ ax_snap.set_box_aspect(1.0)
364
+
365
+ def _figure_kwargs(*, pair_count: int) -> dict:
366
+ fkw = dict(fig_kwargs)
367
+ if "figsize" not in fkw:
368
+ base_w, base_h = _RC["figure_figsize"]
369
+ fkw["figsize"] = (base_w * 2.0 * pair_count, base_h)
370
+ return {key: value for key, value in fkw.items() if key not in {"rc", "ext", *_FIG_STYLE_KEYS}}
371
+
372
+ if group_layout == "paired_row":
373
+ figure_hue_levels = resolve_paired_hue_levels(
374
+ groups=group_frames,
375
+ hue_col=ts_hue_col,
376
+ configured=order_hue,
377
+ )
378
+ figure_colors = colors_for(max(1, len(figure_hue_levels)), palette_book)
379
+ figure_color_map = {
380
+ hue: figure_colors[index % len(figure_colors)] for index, hue in enumerate(figure_hue_levels)
381
+ }
382
+ with use_style(rc=fig_kwargs.get("rc"), color_cycle=figure_colors):
383
+ fig, axes = plt.subplots(1, 2 * len(group_frames), **_figure_kwargs(pair_count=len(group_frames)))
384
+ for idx, group in enumerate(group_frames):
385
+ ax_ts, ax_snap = axes[2 * idx : 2 * idx + 2]
386
+ _draw_group_pair(
387
+ label=group.label,
388
+ d=group.snapshot,
389
+ ts_d=group.time_series,
390
+ ax_ts=ax_ts,
391
+ ax_snap=ax_snap,
392
+ paired_row=True,
393
+ show_ts_legend=(idx == 0),
394
+ shared_hue_levels=figure_hue_levels,
395
+ shared_color_map=figure_color_map,
396
+ )
397
+ if title:
398
+ fig.suptitle(str(title), y=float(fig_kwargs.get("suptitle_y", 1.04)))
399
+ stub = filename or f"ts_snap__{ch_snap}__paired_row"
400
+ observed_count = sum(group_panel_counts[group.label][0] for group in group_frames)
401
+ omitted_count = sum(group_panel_counts[group.label][1] for group in group_frames)
402
+ description = None
403
+ if omitted_count:
404
+ description = (
405
+ f"Selected panel measurements: {observed_count} observed, {omitted_count} "
406
+ "omitted as bounded or non-finite; "
407
+ "affected time-series summaries were withheld."
408
+ )
409
+ figures.append(
410
+ plot_figure(
411
+ fig=fig,
412
+ filename=stub,
413
+ fig_kwargs=fig_kwargs,
414
+ description=description,
415
+ )
416
+ )
417
+ else:
418
+ for group in group_frames:
419
+ hue_levels = resolve_level_order(
420
+ observed=group.time_series[ts_hue_col].astype(str).unique().tolist(),
421
+ configured=order_hue,
422
+ name="order_hue",
423
+ )
424
+ figure_colors = colors_for(max(1, len(hue_levels)), palette_book)
425
+ with use_style(rc=fig_kwargs.get("rc"), color_cycle=figure_colors):
426
+ fig, axes = plt.subplots(1, 2, **_figure_kwargs(pair_count=1))
427
+ ax_ts, ax_snap = axes
428
+ _draw_group_pair(
429
+ label=group.label,
430
+ d=group.snapshot,
431
+ ts_d=group.time_series,
432
+ ax_ts=ax_ts,
433
+ ax_snap=ax_snap,
434
+ paired_row=False,
435
+ show_ts_legend=True,
436
+ )
437
+ fig.suptitle(
438
+ str(title if title is not None else group.label),
439
+ y=float(fig_kwargs.get("suptitle_y", 1.04)),
440
+ )
441
+ group_tag = f"__{str(group_col)}={str(group.label)}" if group_col and group.members != (None,) else None
442
+ if filename:
443
+ stub = f"{filename}{group_tag}" if group_tag else filename
444
+ else:
445
+ base = f"ts_snap__{ch_snap}"
446
+ stub = f"{base}{group_tag}" if group_tag else f"{base}__{group.label}"
447
+ observed_count, omitted_count = group_panel_counts[group.label]
448
+ description = None
449
+ if omitted_count:
450
+ description = (
451
+ f"Selected panel measurements: {observed_count} observed, "
452
+ f"{omitted_count} omitted as bounded or non-finite; "
453
+ "affected time-series summaries were withheld."
454
+ )
455
+ figures.append(
456
+ plot_figure(
457
+ fig=fig,
458
+ filename=stub,
459
+ fig_kwargs=fig_kwargs,
460
+ description=description,
461
+ )
462
+ )
463
+ if snap_fallbacks:
464
+ log = logging.getLogger("reader")
465
+ sample = []
466
+ for row in snap_fallbacks[:3]:
467
+ sample.append(f"{row['label']}: times={row['times']} Δ≈{float(row['delta']):.2f} h")
468
+ extra = f" (+{len(snap_fallbacks) - 3} more)" if len(snap_fallbacks) > 3 else ""
469
+ log.info(
470
+ "[warn]ts_and_snap:snapshot[/warn] • requested t=%.2f h; no rows within ±%.2f h for %d group(s) — "
471
+ "using nearest available per key (examples: %s%s)",
472
+ float(snap_time),
473
+ float(snap_time_tolerance),
474
+ len(snap_fallbacks),
475
+ "; ".join(sample),
476
+ extra,
477
+ )
478
+
479
+ return figures