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,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import Field
6
+
7
+ from reader_workbench.errors import ExecutionError
8
+ from reader_workbench.plotting.sinks import PlotFigure
9
+ from reader_workbench.plotting.utils import slugify
10
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
11
+ from reader_workbench.workbench.ports import dataframe_input
12
+ from reader_workbench.workbench.registry import PluginConfig
13
+
14
+
15
+ class FourStateVectorDiagnosticCfg(PluginConfig):
16
+ state_map_ref: str = Field(min_length=1)
17
+ time_column: str = Field(default="time", min_length=1)
18
+ growth_channel: str = Field(default="OD600", min_length=1)
19
+ response_channel: str = Field(default="YFP/CFP", min_length=1)
20
+ design_ids: list[str] | None = None
21
+ trajectory_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
22
+ trajectory_resamples: int = Field(default=300, ge=1)
23
+ time_atol: float = Field(default=1e-9, ge=0.0)
24
+ title: str | None = Field(default=None, min_length=1)
25
+ filename: str = Field(default="four_state_vector_diagnostic", min_length=1)
26
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
27
+ dpi: int = Field(default=300, ge=1)
28
+ figsize: tuple[float, float] = (15.0, 5.5)
29
+
30
+
31
+ class FourStateVectorDiagnosticPlot(FigurePlotPlugin):
32
+ """Render a record-driven diagnostic without recomputing vector."""
33
+
34
+ ConfigModel = FourStateVectorDiagnosticCfg
35
+
36
+ @classmethod
37
+ def input_ports(cls):
38
+ return {
39
+ "df": dataframe_input("df", "plate_reader.annotated.v1"),
40
+ "vector": dataframe_input("vector", "logic.four_state_vector.v1"),
41
+ }
42
+
43
+ def render(self, ctx, inputs, cfg: FourStateVectorDiagnosticCfg) -> list[PlotFigure]:
44
+ if ctx is None or getattr(ctx, "experiment", None) is None:
45
+ raise ExecutionError("plot/four_state_vector_diagnostic requires experiment annotation semantics")
46
+ state_space = ctx.experiment.annotations.resolve_ordered_state_space(ref=cfg.state_map_ref)
47
+ if tuple(state_space.state_ids) != ("00", "10", "01", "11"):
48
+ raise ExecutionError("plot/four_state_vector_diagnostic requires ordered states 00, 10, 01, 11")
49
+
50
+ from reader_workbench.domains.logic.four_state_vector.diagnostic import ( # noqa: PLC0415
51
+ prepare_four_state_vector_diagnostics,
52
+ render_four_state_vector_diagnostic,
53
+ )
54
+
55
+ prepared = prepare_four_state_vector_diagnostics(
56
+ inputs["df"],
57
+ inputs["vector"],
58
+ treatment_column=state_space.column,
59
+ treatment_map=state_space.source_values,
60
+ treatment_case_sensitive=state_space.case_sensitive,
61
+ time_column=cfg.time_column,
62
+ growth_channel=cfg.growth_channel,
63
+ response_channel=cfg.response_channel,
64
+ design_ids=cfg.design_ids,
65
+ time_atol=cfg.time_atol,
66
+ trajectory_interval_mass=cfg.trajectory_interval_mass,
67
+ trajectory_resamples=cfg.trajectory_resamples,
68
+ )
69
+ filenames = [_design_filename(cfg.filename, item.design_id) for item in prepared]
70
+ if len(set(filenames)) != len(filenames):
71
+ raise ExecutionError("plot/four_state_vector_diagnostic design ids resolve to duplicate artifact filenames")
72
+
73
+ figures: list[PlotFigure] = []
74
+ for item, filename in zip(prepared, filenames, strict=True):
75
+ figure = render_four_state_vector_diagnostic(
76
+ item,
77
+ title=cfg.title,
78
+ figsize=cfg.figsize,
79
+ dpi=cfg.dpi,
80
+ )
81
+ description = (
82
+ f"Growth and response trajectories with persisted vector components for design {item.design_id} "
83
+ f"at {item.selected_time_h:g} hours."
84
+ )
85
+ figures.extend(
86
+ PlotFigure(
87
+ fig=figure,
88
+ filename=filename,
89
+ ext=extension,
90
+ dpi=cfg.dpi,
91
+ description=description,
92
+ )
93
+ for extension in cfg.format
94
+ )
95
+ return figures
96
+
97
+
98
+ def _design_filename(base: str, design_id: str) -> str:
99
+ base_slug = slugify(base)
100
+ if not base_slug:
101
+ raise ExecutionError(f"plot/four_state_vector_diagnostic cannot derive a filename from base {base!r}")
102
+ design_slug = slugify(design_id)
103
+ if not design_slug:
104
+ raise ExecutionError(f"plot/four_state_vector_diagnostic cannot derive a filename from design id {design_id!r}")
105
+ return f"{base_slug}--{design_slug}"
@@ -0,0 +1,63 @@
1
+ """
2
+ four-state vector heatmap plot plugin.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import pandas as pd
10
+ from pydantic import Field
11
+
12
+ from reader_workbench.errors import FourStateVectorError
13
+ from reader_workbench.plotting.sinks import PlotFigure
14
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
15
+ from reader_workbench.workbench.ports import dataframe_input
16
+ from reader_workbench.workbench.registry import PluginConfig
17
+
18
+
19
+ class FourStateVectorHeatmapCfg(PluginConfig):
20
+ experiment_id: str | None = None
21
+ title: str | None = None
22
+ max_y_tick_labels: int = 80
23
+ filename: str | None = None
24
+ format: list[str] = Field(default_factory=lambda: ["pdf"])
25
+ dpi: int = 300
26
+
27
+
28
+ class FourStateVectorHeatmapPlot(FigurePlotPlugin):
29
+ ConfigModel = FourStateVectorHeatmapCfg
30
+
31
+ @classmethod
32
+ def input_ports(cls):
33
+ return {"vector": dataframe_input("vector", "logic.four_state_vector.v1")}
34
+
35
+ def render(self, ctx, inputs, cfg: FourStateVectorHeatmapCfg) -> list[PlotFigure]:
36
+ vector: pd.DataFrame = inputs["vector"]
37
+ from reader_workbench.domains.logic.four_state_vector.heatmap import ( # noqa: PLC0415
38
+ render_experiment_four_state_vector_heatmap,
39
+ )
40
+
41
+ experiment_id = cfg.experiment_id or _experiment_id_from_context(ctx)
42
+ title = cfg.title or f"{experiment_id} four-state vector heatmap"
43
+ fig = render_experiment_four_state_vector_heatmap(
44
+ vector,
45
+ experiment_id=experiment_id,
46
+ title=title,
47
+ max_y_tick_labels=cfg.max_y_tick_labels,
48
+ )
49
+ filename = cfg.filename or "four_state_vector_heatmap"
50
+ return [PlotFigure(fig=fig, filename=filename, ext=ext, dpi=cfg.dpi) for ext in cfg.format]
51
+
52
+
53
+ def _experiment_id_from_context(ctx) -> str:
54
+ exp_dir = getattr(ctx, "exp_dir", None)
55
+ if isinstance(exp_dir, Path):
56
+ return exp_dir.name
57
+ if exp_dir is not None:
58
+ name = Path(str(exp_dir)).name
59
+ if name:
60
+ return name
61
+ raise FourStateVectorError(
62
+ "plot/four_state_vector_heatmap requires experiment_id when no experiment directory is available."
63
+ )
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import Field
6
+
7
+ from reader_workbench.plotting.sinks import PlotFigure
8
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
9
+ from reader_workbench.workbench.ports import dataframe_input
10
+ from reader_workbench.workbench.registry import PluginConfig
11
+
12
+
13
+ class LogicSymmetryPlotCfg(PluginConfig):
14
+ title: str = "Logic symmetry"
15
+ dispersion: Literal["none", "bars", "halo"] = "halo"
16
+ encodings: dict[str, Any] = Field(default_factory=dict)
17
+ ideals_overlay: dict[str, Any] = Field(default_factory=dict)
18
+ visuals: dict[str, Any] = Field(default_factory=dict)
19
+ filename: str = "logic_symmetry"
20
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["pdf"], min_length=1)
21
+ dpi: int = Field(300, gt=0)
22
+ figsize: tuple[float, float] = (7, 6)
23
+
24
+
25
+ class LogicSymmetryPlot(FigurePlotPlugin):
26
+ """Render a persisted logic-symmetry table without owning its computation."""
27
+
28
+ ConfigModel = LogicSymmetryPlotCfg
29
+
30
+ @classmethod
31
+ def input_ports(cls):
32
+ return {"table": dataframe_input("table", "logic_symmetry.v1")}
33
+
34
+ def render(self, ctx, inputs, cfg: LogicSymmetryPlotCfg) -> list[PlotFigure]:
35
+ from reader_workbench.domains.logic.logic_symmetry import render_logic_symmetry # noqa: PLC0415
36
+
37
+ figure = render_logic_symmetry(
38
+ inputs["table"],
39
+ title=cfg.title,
40
+ dispersion=cfg.dispersion,
41
+ encodings=cfg.encodings,
42
+ ideals_overlay=cfg.ideals_overlay,
43
+ visuals=cfg.visuals,
44
+ figsize=cfg.figsize,
45
+ dpi=cfg.dpi,
46
+ )
47
+ return [
48
+ PlotFigure(
49
+ fig=figure,
50
+ filename=cfg.filename,
51
+ ext=extension,
52
+ dpi=cfg.dpi,
53
+ description="Logic and asymmetry geometry over the configured four-state summary.",
54
+ )
55
+ for extension in cfg.format
56
+ ]
@@ -0,0 +1,279 @@
1
+ """Four-panel diagnostic over a persisted single-reporter ratio record."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any, Literal
7
+
8
+ import pandas as pd
9
+ from pydantic import BaseModel, Field, model_validator
10
+
11
+ from reader_workbench.domains.time_series import ObservationAggregationSpec, TemporalReductionSpec
12
+ from reader_workbench.plotting.sinks import PlotFigure
13
+ from reader_workbench.plotting.utils import slugify
14
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
15
+ from reader_workbench.workbench.ports import dataframe_input
16
+ from reader_workbench.workbench.registry import PluginConfig
17
+
18
+ _UNIT_DESCRIPTIONS = {
19
+ "declared_replicate": "temporally reduced declared-replicate values by condition",
20
+ "observation_only": "temporally reduced observation-only values by condition",
21
+ }
22
+
23
+
24
+ class SingleReporterDiagnosticFigureCfg(BaseModel):
25
+ figsize: tuple[float, float] = (14.0, 3.7)
26
+ axis_label_size: float = Field(default=9.5, gt=0.0)
27
+ tick_label_size: float = Field(default=8.5, gt=0.0)
28
+ legend_fontsize: float = Field(default=8.5, gt=0.0)
29
+ line_width: float = Field(default=1.7, gt=0.0)
30
+ point_size: float = Field(default=28.0, gt=0.0)
31
+ condition_tick_rotation: float = Field(default=20.0, ge=-90.0, le=90.0)
32
+
33
+ model_config = {"extra": "forbid"}
34
+
35
+ @model_validator(mode="after")
36
+ def validate_figure(self) -> SingleReporterDiagnosticFigureCfg:
37
+ if len(self.figsize) != 2 or any(
38
+ not math.isfinite(float(value)) or float(value) <= 0.0 for value in self.figsize
39
+ ):
40
+ raise ValueError("single_reporter_diagnostic: fig.figsize must contain two positive finite values")
41
+ return self
42
+
43
+
44
+ class SingleReporterObservationUnitCfg(BaseModel):
45
+ """Identify descriptive source observations without declaring replication."""
46
+
47
+ role: Literal["observation_only"]
48
+ column: str = Field(min_length=1)
49
+
50
+ model_config = {"extra": "forbid"}
51
+
52
+ @model_validator(mode="after")
53
+ def validate_observation_unit(self) -> SingleReporterObservationUnitCfg:
54
+ column = self.column.strip()
55
+ if not column:
56
+ raise ValueError("single_reporter_diagnostic: observation_unit.column must be a non-empty string")
57
+ self.column = column
58
+ return self
59
+
60
+
61
+ class SingleReporterIdentityScopeCfg(BaseModel):
62
+ """Bind unit identities to stable assay entities, independent of presentation."""
63
+
64
+ entity_columns: list[str] = Field(min_length=1)
65
+
66
+ model_config = {"extra": "forbid"}
67
+
68
+ @model_validator(mode="after")
69
+ def validate_identity_scope(self) -> SingleReporterIdentityScopeCfg:
70
+ columns = [str(column).strip() for column in self.entity_columns]
71
+ if not columns or any(not column for column in columns):
72
+ raise ValueError("single_reporter_diagnostic: identity_scope.entity_columns must be non-empty strings")
73
+ if len(set(columns)) != len(columns):
74
+ raise ValueError("single_reporter_diagnostic: identity_scope.entity_columns must not contain duplicates")
75
+ self.entity_columns = columns
76
+ return self
77
+
78
+
79
+ class SingleReporterDiagnosticCfg(PluginConfig):
80
+ partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
81
+ identity_scope: SingleReporterIdentityScopeCfg
82
+ condition_column: str = Field(default="treatment", min_length=1)
83
+ condition_order: list[str] | None = None
84
+ condition_order_ref: str | None = Field(default=None, min_length=1)
85
+ temporal_reduction: dict[str, Any]
86
+ observation_aggregation: dict[str, Any]
87
+ observation_unit: SingleReporterObservationUnitCfg | None = None
88
+ time_column: str = Field(default="time", min_length=1)
89
+ normalizer_channel: str = Field(min_length=1)
90
+ reporter_channel: str = Field(min_length=1)
91
+ ratio_channel: str = Field(min_length=1)
92
+ filename_prefix: str = Field(default="single_reporter_diagnostic", min_length=1)
93
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
94
+ dpi: int = Field(default=300, ge=1)
95
+ fig: SingleReporterDiagnosticFigureCfg = Field(default_factory=SingleReporterDiagnosticFigureCfg)
96
+
97
+ @model_validator(mode="after")
98
+ def validate_semantic_options(self) -> SingleReporterDiagnosticCfg:
99
+ for field_name in (
100
+ "condition_column",
101
+ "time_column",
102
+ "normalizer_channel",
103
+ "reporter_channel",
104
+ "ratio_channel",
105
+ "filename_prefix",
106
+ ):
107
+ value = str(getattr(self, field_name)).strip()
108
+ if not value:
109
+ raise ValueError(f"single_reporter_diagnostic: {field_name} must be a non-empty string")
110
+ setattr(self, field_name, value)
111
+ if len({self.normalizer_channel, self.reporter_channel, self.ratio_channel}) != 3:
112
+ raise ValueError("single_reporter_diagnostic: normalizer, reporter, and ratio channels must be distinct")
113
+ if self.condition_column in self.identity_scope.entity_columns:
114
+ raise ValueError(
115
+ "single_reporter_diagnostic: condition_column is already part of unit identity and must not be "
116
+ "repeated in identity_scope.entity_columns"
117
+ )
118
+ temporal = TemporalReductionSpec.from_mapping(self.temporal_reduction)
119
+ aggregation = ObservationAggregationSpec.from_mapping(self.observation_aggregation)
120
+ self.temporal_reduction = temporal.to_mapping()
121
+ self.observation_aggregation = aggregation.to_mapping()
122
+ if self.condition_order is not None and self.condition_order_ref is not None:
123
+ raise ValueError(
124
+ "single_reporter_diagnostic: condition_order and condition_order_ref are mutually exclusive"
125
+ )
126
+ if self.condition_order is not None:
127
+ values = [str(value).strip() for value in self.condition_order]
128
+ if not values or any(not value for value in values):
129
+ raise ValueError("single_reporter_diagnostic: condition_order must contain non-empty labels")
130
+ if len(set(values)) != len(values):
131
+ raise ValueError("single_reporter_diagnostic: condition_order contains duplicate labels")
132
+ self.condition_order = values
133
+ if len(set(self.format)) != len(self.format):
134
+ raise ValueError("single_reporter_diagnostic: format must not contain duplicates")
135
+ return self
136
+
137
+
138
+ class SingleReporterDiagnosticPlot(FigurePlotPlugin):
139
+ ConfigModel = SingleReporterDiagnosticCfg
140
+
141
+ @classmethod
142
+ def input_ports(cls):
143
+ return {"df": dataframe_input("df", "plate_reader.annotated.v1")}
144
+
145
+ @classmethod
146
+ def _resolve_condition_order(cls, *, experiment, cfg: SingleReporterDiagnosticCfg):
147
+ return experiment.annotations.resolve_order_arg(
148
+ order=cfg.condition_order,
149
+ order_ref=cfg.condition_order_ref,
150
+ column=cfg.condition_column,
151
+ arg_name="condition_order",
152
+ )
153
+
154
+ @classmethod
155
+ def validate_semantic_references(cls, *, experiment, cfg: SingleReporterDiagnosticCfg) -> None:
156
+ experiment.annotations.resolve_plot_partition(partition=cfg.partition)
157
+ cls._resolve_condition_order(experiment=experiment, cfg=cfg)
158
+
159
+ def render(self, ctx, inputs, cfg: SingleReporterDiagnosticCfg) -> list[PlotFigure]:
160
+ from reader_workbench.domains.plate_reader.plots.single_reporter_diagnostic import ( # noqa: PLC0415
161
+ prepare_single_reporter_diagnostics,
162
+ )
163
+ from reader_workbench.domains.plate_reader.plots.single_reporter_diagnostic_render import ( # noqa: PLC0415
164
+ render_single_reporter_diagnostic,
165
+ )
166
+
167
+ if ctx.experiment is None:
168
+ raise ValueError("single_reporter_diagnostic requires experiment semantics in the run context")
169
+ frame: pd.DataFrame = inputs["df"]
170
+ partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
171
+ condition_order = type(self)._resolve_condition_order(experiment=ctx.experiment, cfg=cfg)
172
+ unit_column, observation_column, unit_role = _reduction_columns(
173
+ ctx=ctx,
174
+ frame=frame,
175
+ observation_unit=cfg.observation_unit,
176
+ )
177
+ diagnostics = prepare_single_reporter_diagnostics(
178
+ frame,
179
+ group_on=partition.group_by,
180
+ collection_items=partition.collection_items,
181
+ group_match=partition.match,
182
+ condition_column=cfg.condition_column,
183
+ condition_order=condition_order,
184
+ entity_columns=cfg.identity_scope.entity_columns,
185
+ unit_column=unit_column,
186
+ observation_column=observation_column,
187
+ unit_role=unit_role,
188
+ time_column=cfg.time_column,
189
+ normalizer_channel=cfg.normalizer_channel,
190
+ reporter_channel=cfg.reporter_channel,
191
+ ratio_channel=cfg.ratio_channel,
192
+ temporal_reduction=TemporalReductionSpec.from_mapping(cfg.temporal_reduction),
193
+ observation_aggregation=ObservationAggregationSpec.from_mapping(cfg.observation_aggregation),
194
+ )
195
+ artifact_names = _artifact_names(diagnostics, filename_prefix=cfg.filename_prefix)
196
+ figures: list[PlotFigure] = []
197
+ palette_book = getattr(ctx, "palette_book", None)
198
+ for diagnostic in diagnostics:
199
+ colors = palette_book.colors(len(diagnostic.condition_order)) if palette_book is not None else None
200
+ figure = render_single_reporter_diagnostic(
201
+ diagnostic,
202
+ colors=colors,
203
+ **cfg.fig.model_dump(),
204
+ )
205
+ figures.extend(
206
+ PlotFigure(
207
+ fig=figure,
208
+ filename=artifact_names[diagnostic.group_label],
209
+ ext=extension,
210
+ dpi=cfg.dpi,
211
+ description=(
212
+ "Normalizer, reporter, and reporter-normalizer kinetics with an explicit endpoint or "
213
+ f"interval reduction, {_UNIT_DESCRIPTIONS[diagnostic.unit_role]}, and visible normalizer QC."
214
+ ),
215
+ )
216
+ for extension in cfg.format
217
+ )
218
+ return figures
219
+
220
+
221
+ def _reduction_columns(
222
+ *,
223
+ ctx,
224
+ frame: pd.DataFrame,
225
+ observation_unit: SingleReporterObservationUnitCfg | None,
226
+ ) -> tuple[str, str, Literal["declared_replicate", "observation_only"]]:
227
+ evidence = getattr(ctx.experiment, "evidence", None)
228
+ declared = getattr(evidence, "replicate_identity_field", None)
229
+ if declared is not None:
230
+ unit_column = str(declared).strip()
231
+ if unit_column not in frame.columns:
232
+ raise ValueError(
233
+ "single_reporter_diagnostic: declared evidence.replicate_identity_field "
234
+ f"{unit_column!r} is absent from the persisted ratio record"
235
+ )
236
+ observation_column = observation_unit.column if observation_unit is not None else unit_column
237
+ _require_observation_column(frame=frame, column=observation_column)
238
+ return unit_column, observation_column, "declared_replicate"
239
+ if observation_unit is None:
240
+ raise ValueError(
241
+ "single_reporter_diagnostic: no replicate identity is declared; configure "
242
+ "observation_unit with role='observation_only' to plot descriptive record observations"
243
+ )
244
+ _require_observation_column(frame=frame, column=observation_unit.column)
245
+ return observation_unit.column, observation_unit.column, "observation_only"
246
+
247
+
248
+ def _require_observation_column(*, frame: pd.DataFrame, column: str) -> None:
249
+ if column not in frame.columns:
250
+ raise ValueError(
251
+ f"single_reporter_diagnostic: observation_unit column {column!r} is absent from the persisted ratio record"
252
+ )
253
+
254
+
255
+ def _artifact_names(diagnostics, *, filename_prefix: str) -> dict[str, str]:
256
+ names: dict[str, str] = {}
257
+ owner_by_slug: dict[str, str] = {}
258
+ for diagnostic in diagnostics:
259
+ label = str(diagnostic.group_label)
260
+ filename = filename_prefix if label == "all" else f"{filename_prefix}__{label}"
261
+ normalized = slugify(filename)
262
+ prior = owner_by_slug.get(normalized)
263
+ if prior is not None:
264
+ raise ValueError(
265
+ "single_reporter_diagnostic: "
266
+ f"partitions {prior!r} and {label!r} resolve to the same artifact name {normalized!r}"
267
+ )
268
+ names[label] = filename
269
+ owner_by_slug[normalized] = label
270
+ return names
271
+
272
+
273
+ __all__ = [
274
+ "SingleReporterDiagnosticCfg",
275
+ "SingleReporterDiagnosticFigureCfg",
276
+ "SingleReporterIdentityScopeCfg",
277
+ "SingleReporterObservationUnitCfg",
278
+ "SingleReporterDiagnosticPlot",
279
+ ]
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ import pandas as pd
6
+ from pydantic import Field
7
+
8
+ from reader_workbench.plotting.sinks import PlotFigure
9
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
10
+ from reader_workbench.workbench.ports import dataframe_input
11
+ from reader_workbench.workbench.registry import PluginConfig
12
+
13
+
14
+ class SnapshotBarCfg(PluginConfig):
15
+ x: str
16
+ y: list[str] | str
17
+ hue: str | None = None
18
+ partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
19
+ time: float = Field(..., description="Snapshot time (hours); required for deterministic plotting.")
20
+ fig: dict[str, Any] = Field(default_factory=dict)
21
+ filename: str | None = None
22
+ agg: str = "mean" # median|mean
23
+ dispersion: Literal["sd", "iqr", "none"] = "sd"
24
+ time_tolerance: float = 0.51
25
+ panel_by: Literal["channel", "x", "group"] = "channel"
26
+ channel_select: str | None = None
27
+ file_by: Literal["auto", "channel"] = "auto"
28
+ show_legend: bool = False
29
+ legend_loc: str = "upper right"
30
+
31
+
32
+ class SnapshotBarplot(FigurePlotPlugin):
33
+ ConfigModel = SnapshotBarCfg
34
+
35
+ @classmethod
36
+ def input_ports(cls):
37
+ return {"df": dataframe_input("df", "tidy.v1")}
38
+
39
+ def render(self, ctx, inputs, cfg: SnapshotBarCfg) -> list[PlotFigure]:
40
+ df: pd.DataFrame = inputs["df"]
41
+ from reader_workbench.domains.plate_reader.plots.snapshot_barplot import plot_snapshot_barplot # noqa: PLC0415
42
+
43
+ partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
44
+
45
+ return plot_snapshot_barplot(
46
+ df=df,
47
+ x=cfg.x,
48
+ y=cfg.y,
49
+ hue=cfg.hue,
50
+ group_on=partition.group_by,
51
+ pool_sets=partition.collection_items,
52
+ time=cfg.time,
53
+ pool_match=partition.match, # type: ignore
54
+ fig_kwargs=cfg.fig,
55
+ filename=cfg.filename,
56
+ palette_book=ctx.palette_book,
57
+ agg=cfg.agg,
58
+ dispersion=cfg.dispersion,
59
+ time_tolerance=cfg.time_tolerance,
60
+ panel_by=cfg.panel_by,
61
+ channel_select=cfg.channel_select,
62
+ file_by=cfg.file_by,
63
+ show_legend=cfg.show_legend,
64
+ legend_loc=cfg.legend_loc,
65
+ )
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from typing import Any
5
+
6
+ import pandas as pd
7
+ from pydantic import Field, model_validator
8
+
9
+ from reader_workbench.plotting.sinks import PlotFigure
10
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
11
+ from reader_workbench.workbench.ports import dataframe_input
12
+ from reader_workbench.workbench.registry import PluginConfig
13
+
14
+
15
+ def _load_snapshot_heatmap_impl():
16
+ module = import_module("reader_workbench.domains.plate_reader.plots.snapshot_heatmap")
17
+ return module.plot_snapshot_heatmap, module.prepare_snapshot_heatmap_inputs
18
+
19
+
20
+ class HeatmapCfg(PluginConfig):
21
+ channel: str
22
+ time: float
23
+ x: str = "treatment"
24
+ y: str = "design_id"
25
+ order_x: list[str] | None = None
26
+ order_y: list[str] | None = None
27
+ order_x_ref: str | None = None
28
+ order_y_ref: str | None = None
29
+ square: bool = True
30
+ vmin: float | None = None
31
+ vmax: float | None = None
32
+ value_transform: str | None = "none" # "none" | "log2" | "log10"
33
+ time_tolerance: float = 0.51
34
+ fig: dict[str, Any] = Field(default_factory=dict)
35
+ filename: str | None = None
36
+
37
+ @model_validator(mode="after")
38
+ def validate_order_sources(self) -> HeatmapCfg:
39
+ if self.order_x is not None and self.order_x_ref is not None:
40
+ raise ValueError("snapshot_heatmap: order_x and order_x_ref are mutually exclusive")
41
+ if self.order_y is not None and self.order_y_ref is not None:
42
+ raise ValueError("snapshot_heatmap: order_y and order_y_ref are mutually exclusive")
43
+ return self
44
+
45
+
46
+ class SnapshotHeatmapPlot(FigurePlotPlugin):
47
+ ConfigModel = HeatmapCfg
48
+
49
+ @classmethod
50
+ def input_ports(cls):
51
+ return {
52
+ "df": dataframe_input("df", "tidy.v1", optional=True),
53
+ "fc": dataframe_input("fc", "fold_change.v1", optional=True),
54
+ }
55
+
56
+ def render(self, ctx, inputs, cfg: HeatmapCfg) -> list[PlotFigure]:
57
+ if ctx.experiment is None:
58
+ raise ValueError("snapshot_heatmap requires experiment semantics in the run context")
59
+ df_in: pd.DataFrame | None = inputs.get("df")
60
+ fc_in: pd.DataFrame | None = inputs.get("fc")
61
+ plot_snapshot_heatmap, prepare_snapshot_heatmap_inputs = _load_snapshot_heatmap_impl()
62
+
63
+ prepared = prepare_snapshot_heatmap_inputs(
64
+ df_in=df_in,
65
+ fc_in=fc_in,
66
+ channel=cfg.channel,
67
+ time=cfg.time,
68
+ time_tolerance=cfg.time_tolerance,
69
+ value_transform=cfg.value_transform,
70
+ fig=cfg.fig,
71
+ filename=cfg.filename,
72
+ logger=ctx.logger,
73
+ )
74
+ channel = str(cfg.channel)
75
+ df = prepared.frame
76
+ filename = prepared.filename
77
+ fig_kwargs = prepared.fig_kwargs
78
+ resolved_order_x = ctx.experiment.annotations.resolve_order_arg(
79
+ order=cfg.order_x,
80
+ order_ref=cfg.order_x_ref,
81
+ column=cfg.x,
82
+ arg_name="order_x",
83
+ )
84
+ resolved_order_y = ctx.experiment.annotations.resolve_order_arg(
85
+ order=cfg.order_y,
86
+ order_ref=cfg.order_y_ref,
87
+ column=cfg.y,
88
+ arg_name="order_y",
89
+ )
90
+ return plot_snapshot_heatmap(
91
+ df=df,
92
+ channel=channel,
93
+ time=cfg.time,
94
+ x=cfg.x,
95
+ y=cfg.y,
96
+ order_x=resolved_order_x,
97
+ order_y=resolved_order_y,
98
+ square=cfg.square,
99
+ vmin=cfg.vmin,
100
+ vmax=cfg.vmax,
101
+ fig_kwargs=fig_kwargs,
102
+ filename=filename,
103
+ logger=ctx.logger,
104
+ )