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,55 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from reader_workbench.errors import ExecutionError
9
+ from reader_workbench.plotting.sinks import PlotFigure, normalize_plot_figures, save_plot_figures
10
+ from reader_workbench.workbench.ports import file_bundle_output
11
+ from reader_workbench.workbench.records import PathDescription
12
+ from reader_workbench.workbench.registry import Plugin
13
+
14
+
15
+ class PlotPartitionCfg(BaseModel):
16
+ by: str | None = None
17
+ collection_ref: str | None = None
18
+ match: Literal["exact", "contains", "startswith", "endswith", "regex"] = "exact"
19
+
20
+ model_config = {"extra": "forbid"}
21
+
22
+
23
+ class FigurePlotPlugin(Plugin):
24
+ @classmethod
25
+ def output_ports(cls):
26
+ return {"artifacts": file_bundle_output("artifacts")}
27
+
28
+ @abstractmethod
29
+ def render(self, ctx, inputs, cfg) -> list[PlotFigure]:
30
+ raise NotImplementedError
31
+
32
+ def run(self, ctx, inputs, cfg):
33
+ return save_rendered_figures(ctx=ctx, figures=self.render(ctx, inputs, cfg), plot_key=self.plugin_key)
34
+
35
+
36
+ def resolve_plot_partition_cfg(*, ctx, partition: PlotPartitionCfg):
37
+ if ctx.experiment is None:
38
+ raise ValueError("plot partition resolution requires experiment semantics in the run context")
39
+ return ctx.experiment.annotations.resolve_plot_partition(partition=partition)
40
+
41
+
42
+ def save_rendered_figures(*, ctx, figures: list[PlotFigure], plot_key: str) -> dict[str, list[str | PathDescription]]:
43
+ normalized = normalize_plot_figures(figures, where=f"plot/{plot_key}")
44
+ if not normalized:
45
+ raise ExecutionError(
46
+ f"plot/{plot_key}: renderer produced no figures; check data filters, time windows, channels, and partitions"
47
+ )
48
+ saved = save_plot_figures(normalized, ctx.plots_dir)
49
+ artifacts: list[str | PathDescription] = []
50
+ for figure, path in zip(normalized, saved, strict=True):
51
+ if figure.description is None:
52
+ artifacts.append(str(path))
53
+ else:
54
+ artifacts.append(PathDescription(path=path, description=figure.description))
55
+ return {"artifacts": artifacts}
@@ -0,0 +1,54 @@
1
+ """Static cytometry gating diagnostic plot plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import Field
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
+ class CytometryDiagnosticCfg(PluginConfig):
16
+ title: str | None = Field(default=None, min_length=1)
17
+ max_events: int = Field(default=50_000, gt=0)
18
+ filename: str = Field(default="cytometry_diagnostic", min_length=1)
19
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
20
+ dpi: int = Field(default=300, gt=0)
21
+
22
+
23
+ class CytometryDiagnosticPlot(FigurePlotPlugin):
24
+ ConfigModel = CytometryDiagnosticCfg
25
+
26
+ @classmethod
27
+ def input_ports(cls):
28
+ return {
29
+ "original_events": dataframe_input("original_events", "tidy.v1"),
30
+ "gate_definition": dataframe_input("gate_definition", "cytometry.gate_definition.v1"),
31
+ "gated_events": dataframe_input("gated_events", "cytometry.gated_events.v1"),
32
+ }
33
+
34
+ def render(self, ctx, inputs, cfg: CytometryDiagnosticCfg):
35
+ del ctx
36
+ from reader_workbench.domains.cytometry.plots import render_cytometry_diagnostic # noqa: PLC0415
37
+
38
+ figure = render_cytometry_diagnostic(
39
+ inputs["original_events"],
40
+ inputs["gate_definition"],
41
+ inputs["gated_events"],
42
+ max_events=cfg.max_events,
43
+ title=cfg.title,
44
+ )
45
+ return [
46
+ PlotFigure(
47
+ fig=figure,
48
+ filename=cfg.filename,
49
+ ext=extension,
50
+ dpi=cfg.dpi,
51
+ description="Configured cells, singlets, fluorescence, and final-retention diagnostics.",
52
+ )
53
+ for extension in cfg.format
54
+ ]
@@ -0,0 +1,58 @@
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 DistributionsCfg(PluginConfig):
15
+ # what to draw
16
+ channels: list[str]
17
+ # modern grouping
18
+ partition: PlotPartitionCfg = Field(default_factory=lambda: PlotPartitionCfg(by="design_id"))
19
+ # layout
20
+ panel_by: Literal["channel", "group"] = "channel" # default: per-channel panels
21
+ hue: str | None = None
22
+ legend_loc: Literal["upper left", "upper right", "lower left", "lower right", "center", "best"] = "upper left"
23
+ # style/output
24
+ fig: dict[str, Any] = Field(default_factory=dict)
25
+ filename: str | None = None
26
+
27
+
28
+ class DistributionsPlot(FigurePlotPlugin):
29
+ ConfigModel = DistributionsCfg
30
+
31
+ @classmethod
32
+ def input_ports(cls):
33
+ return {
34
+ "df": dataframe_input("df", "tidy.v1"),
35
+ "blanks": dataframe_input("blanks", "tidy.v1", optional=True),
36
+ }
37
+
38
+ def render(self, ctx, inputs, cfg: DistributionsCfg) -> list[PlotFigure]:
39
+ df: pd.DataFrame = inputs["df"]
40
+ blanks: pd.DataFrame = inputs.get("blanks", df.iloc[0:0])
41
+ from reader_workbench.domains.plate_reader.plots.distributions import plot_distributions # noqa: PLC0415
42
+
43
+ partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
44
+
45
+ return plot_distributions(
46
+ df=df,
47
+ blanks=blanks,
48
+ channels=cfg.channels,
49
+ group_on=partition.group_by,
50
+ pool_sets=partition.collection_items,
51
+ pool_match=partition.match, # type: ignore[arg-type]
52
+ panel_by=cfg.panel_by,
53
+ hue=cfg.hue,
54
+ legend_loc=cfg.legend_loc,
55
+ fig_kwargs=cfg.fig,
56
+ filename=cfg.filename,
57
+ palette_book=ctx.palette_book,
58
+ )
@@ -0,0 +1,224 @@
1
+ """Persisted dual-reporter kinetics and endpoint triptych."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ import pandas as pd
8
+ from pydantic import Field, model_validator
9
+
10
+ from reader_workbench.plotting.sinks import PlotFigure
11
+ from reader_workbench.plotting.utils import slugify
12
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
13
+ from reader_workbench.workbench.ports import dataframe_input
14
+ from reader_workbench.workbench.registry import PluginConfig
15
+
16
+ _DESCRIPTION = (
17
+ "Growth and reporter-ratio kinetics with descriptive within-experiment resampling bands, "
18
+ "plus observed endpoint values and mean with sample standard deviation."
19
+ )
20
+
21
+
22
+ class DualReporterTriptychCfg(PluginConfig):
23
+ design_column: str = Field(default="design_id", min_length=1)
24
+ treatment_column: str = Field(default="treatment", min_length=1)
25
+ time_column: str = Field(default="time", min_length=1)
26
+ growth_channel: str = Field(default="OD600", min_length=1)
27
+ ratio_channel: str = Field(default="YFP/CFP", min_length=1)
28
+ snapshot_channel: str | None = Field(default=None, min_length=1)
29
+ snapshot_time_h: float
30
+ snapshot_time_mode: Literal["nearest", "last_before", "first_after", "exact"] = "nearest"
31
+ snapshot_time_tolerance_h: float | None = Field(default=0.51, ge=0.0)
32
+ treatment_order: list[str] | None = None
33
+ treatment_order_ref: str | None = Field(default=None, min_length=1)
34
+ trajectory_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
35
+ trajectory_resamples: int = Field(default=300, ge=1)
36
+ filename_prefix: str = Field(default="dual_reporter_triptych", min_length=1)
37
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
38
+ dpi: int = Field(default=300, ge=1)
39
+ figsize: tuple[float, float] = (10.5, 3.5)
40
+
41
+ @model_validator(mode="after")
42
+ def validate_semantic_options(self) -> DualReporterTriptychCfg:
43
+ filename_prefix = self.filename_prefix.strip()
44
+ if not filename_prefix:
45
+ raise ValueError("dual_reporter_triptych: filename_prefix must be a non-empty string")
46
+ self.filename_prefix = filename_prefix
47
+ if self.treatment_order is not None and self.treatment_order_ref is not None:
48
+ raise ValueError("dual_reporter_triptych: treatment_order and treatment_order_ref are mutually exclusive")
49
+ if self.treatment_order is not None:
50
+ normalized = [str(value).strip() for value in self.treatment_order]
51
+ if not normalized or any(not value for value in normalized):
52
+ raise ValueError("dual_reporter_triptych: treatment_order must contain non-empty strings")
53
+ if len(set(normalized)) != len(normalized):
54
+ raise ValueError("dual_reporter_triptych: treatment_order contains duplicate labels")
55
+ self.treatment_order = normalized
56
+ if any(not value > 0.0 for value in self.figsize):
57
+ raise ValueError("dual_reporter_triptych: figsize values must be positive")
58
+ if len(set(self.format)) != len(self.format):
59
+ raise ValueError("dual_reporter_triptych: format must not contain duplicates")
60
+ return self
61
+
62
+
63
+ class DualReporterTriptychPlot(FigurePlotPlugin):
64
+ ConfigModel = DualReporterTriptychCfg
65
+
66
+ @classmethod
67
+ def input_ports(cls):
68
+ return {"df": dataframe_input("df", "tidy.v1")}
69
+
70
+ @classmethod
71
+ def validate_semantic_references(cls, *, experiment, cfg: DualReporterTriptychCfg) -> None:
72
+ if cfg.treatment_order_ref is not None:
73
+ experiment.annotations.resolve_order_arg(
74
+ order=None,
75
+ order_ref=cfg.treatment_order_ref,
76
+ column=cfg.treatment_column,
77
+ arg_name="treatment_order",
78
+ )
79
+
80
+ def render(self, ctx, inputs, cfg: DualReporterTriptychCfg) -> list[PlotFigure]:
81
+ from reader_workbench.domains.plate_reader.analysis.timepoints import ( # noqa: PLC0415
82
+ infer_acquisition_transition_time_h,
83
+ )
84
+ from reader_workbench.domains.plate_reader.ordering import order_levels # noqa: PLC0415
85
+ from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych import ( # noqa: PLC0415
86
+ build_triptych_data,
87
+ choose_time,
88
+ )
89
+ from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych_render import ( # noqa: PLC0415
90
+ render_dual_reporter_triptych,
91
+ )
92
+
93
+ frame: pd.DataFrame = inputs["df"]
94
+ _require_columns(
95
+ frame,
96
+ (
97
+ cfg.design_column,
98
+ cfg.treatment_column,
99
+ cfg.time_column,
100
+ "channel",
101
+ "value",
102
+ "position",
103
+ ),
104
+ )
105
+ design_values = order_levels(_nonempty_strings(frame[cfg.design_column]))
106
+ if not design_values:
107
+ raise ValueError(f"dual_reporter_triptych: no non-empty values in {cfg.design_column!r}")
108
+ design_series = frame[cfg.design_column].astype("string").str.strip()
109
+ filename_by_design = _artifact_names_by_design(
110
+ design_values,
111
+ filename_prefix=cfg.filename_prefix,
112
+ )
113
+ treatment_order = _resolve_treatment_order(ctx=ctx, cfg=cfg)
114
+
115
+ figures: list[PlotFigure] = []
116
+ for design_value in design_values:
117
+ selected = frame[design_series == design_value].copy()
118
+ time_values = pd.to_numeric(selected[cfg.time_column], errors="coerce").dropna().unique().tolist()
119
+ selected_time = choose_time(time_values, cfg.snapshot_time_h, cfg.snapshot_time_mode)
120
+ if selected_time is None:
121
+ raise ValueError(
122
+ "dual_reporter_triptych: "
123
+ f"design {design_value!r} has no time matching {cfg.snapshot_time_h:g} h "
124
+ f"with mode {cfg.snapshot_time_mode!r}"
125
+ )
126
+ delta = abs(float(selected_time) - float(cfg.snapshot_time_h))
127
+ if cfg.snapshot_time_tolerance_h is not None and delta > cfg.snapshot_time_tolerance_h:
128
+ raise ValueError(
129
+ "dual_reporter_triptych: "
130
+ f"design {design_value!r} selected {selected_time:g} h for target "
131
+ f"{cfg.snapshot_time_h:g} h, outside snapshot_time_tolerance_h="
132
+ f"{cfg.snapshot_time_tolerance_h:g}"
133
+ )
134
+
135
+ data = build_triptych_data(
136
+ selected,
137
+ time_col=cfg.time_column,
138
+ treatment_col=cfg.treatment_column,
139
+ growth_channel=cfg.growth_channel,
140
+ ratio_channel=cfg.ratio_channel,
141
+ snapshot_channel=cfg.snapshot_channel,
142
+ snapshot_time=float(selected_time),
143
+ treatment_order=treatment_order,
144
+ trajectory_interval_mass=cfg.trajectory_interval_mass,
145
+ trajectory_resamples=cfg.trajectory_resamples,
146
+ )
147
+ palette_book = getattr(ctx, "palette_book", None)
148
+ colors = palette_book.colors(len(data.treatment_order)) if palette_book is not None else None
149
+ figure = render_dual_reporter_triptych(
150
+ data,
151
+ time_col=cfg.time_column,
152
+ treatment_col=cfg.treatment_column,
153
+ acquisition_transition_time_h=infer_acquisition_transition_time_h(
154
+ selected,
155
+ time_col=cfg.time_column,
156
+ ),
157
+ title=design_value,
158
+ colors=colors,
159
+ figsize=cfg.figsize,
160
+ )
161
+ filename = filename_by_design[design_value]
162
+ figures.extend(
163
+ PlotFigure(
164
+ fig=figure,
165
+ filename=filename,
166
+ ext=extension,
167
+ dpi=cfg.dpi,
168
+ description=_DESCRIPTION,
169
+ )
170
+ for extension in cfg.format
171
+ )
172
+ return figures
173
+
174
+
175
+ def _resolve_treatment_order(*, ctx, cfg: DualReporterTriptychCfg) -> list[str] | None:
176
+ if cfg.treatment_order_ref is None:
177
+ return cfg.treatment_order
178
+ experiment = getattr(ctx, "experiment", None)
179
+ if experiment is None:
180
+ raise ValueError("dual_reporter_triptych: treatment_order_ref requires experiment semantics")
181
+ return experiment.annotations.resolve_order_arg(
182
+ order=None,
183
+ order_ref=cfg.treatment_order_ref,
184
+ column=cfg.treatment_column,
185
+ arg_name="treatment_order",
186
+ )
187
+
188
+
189
+ def _nonempty_strings(series: pd.Series) -> list[str]:
190
+ values: list[str] = []
191
+ seen: set[str] = set()
192
+ for value in series.dropna().tolist():
193
+ text = str(value).strip()
194
+ if not text or text.casefold() in {"nan", "none"} or text in seen:
195
+ continue
196
+ values.append(text)
197
+ seen.add(text)
198
+ return values
199
+
200
+
201
+ def _artifact_names_by_design(design_values: list[str], *, filename_prefix: str) -> dict[str, str]:
202
+ by_design: dict[str, str] = {}
203
+ owner_by_name: dict[str, str] = {}
204
+ for design_value in design_values:
205
+ name = f"{filename_prefix}__{design_value}"
206
+ normalized_name = slugify(name)
207
+ prior = owner_by_name.get(normalized_name)
208
+ if prior is not None:
209
+ raise ValueError(
210
+ "dual_reporter_triptych: "
211
+ f"designs {prior!r} and {design_value!r} resolve to the same artifact name {normalized_name!r}"
212
+ )
213
+ by_design[design_value] = name
214
+ owner_by_name[normalized_name] = design_value
215
+ return by_design
216
+
217
+
218
+ def _require_columns(frame: pd.DataFrame, columns: tuple[str, ...]) -> None:
219
+ missing = [column for column in dict.fromkeys(columns) if column not in frame.columns]
220
+ if missing:
221
+ raise ValueError("dual_reporter_triptych: missing column(s): " + ", ".join(missing))
222
+
223
+
224
+ __all__ = ["DualReporterTriptychCfg", "DualReporterTriptychPlot"]
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
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 FourStateEventWindowDiagnosticSubject(BaseModel):
16
+ model_config = ConfigDict(extra="forbid")
17
+
18
+ source_experiment_id: str = Field(min_length=1)
19
+ design_id: str = Field(min_length=1)
20
+ filename: str = Field(min_length=1)
21
+ title: str | None = Field(default=None, min_length=1)
22
+
23
+ @model_validator(mode="after")
24
+ def _reject_blank_values(self) -> FourStateEventWindowDiagnosticSubject:
25
+ required = {
26
+ "source_experiment_id": self.source_experiment_id,
27
+ "design_id": self.design_id,
28
+ "filename": self.filename,
29
+ }
30
+ blank = [name for name, value in required.items() if not value.strip()]
31
+ if self.title is not None and not self.title.strip():
32
+ blank.append("title")
33
+ if blank:
34
+ raise ValueError(f"subject values must not be blank: {', '.join(blank)}")
35
+ normalized_filename = slugify(self.filename)
36
+ if not normalized_filename or not any(character.isalnum() for character in normalized_filename):
37
+ raise ValueError("subject filename must contain a filesystem-safe character")
38
+ if _is_windows_reserved_filename(normalized_filename):
39
+ raise ValueError("subject filename must not use a Windows reserved device name")
40
+ return self
41
+
42
+
43
+ class FourStateEventWindowAxisLabels(BaseModel):
44
+ """Protocol-derived labels for the three source trace panels."""
45
+
46
+ model_config = ConfigDict(extra="forbid")
47
+
48
+ growth: str = Field(min_length=1)
49
+ response: str = Field(min_length=1)
50
+ magnitude: str = Field(min_length=1)
51
+
52
+
53
+ class FourStateEventWindowDiagnosticCfg(PluginConfig):
54
+ subjects: list[FourStateEventWindowDiagnosticSubject] = Field(min_length=1)
55
+ primary_reduction_id: str = Field(min_length=1)
56
+ pre_window_duration_h: float | None = Field(default=None, gt=0.0)
57
+ axis_labels: FourStateEventWindowAxisLabels = Field(
58
+ default_factory=lambda: FourStateEventWindowAxisLabels(
59
+ growth="Signal",
60
+ response="log$_2$(response signal)",
61
+ magnitude="log$_2$(magnitude signal)",
62
+ )
63
+ )
64
+ state_labels: dict[str, str] = Field(default_factory=lambda: {"00": "00", "10": "10", "01": "01", "11": "11"})
65
+ reference_label: str = Field(default="reference", min_length=1)
66
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
67
+ dpi: int = Field(default=300, ge=1)
68
+
69
+ @model_validator(mode="after")
70
+ def _validate_subject_selection(self) -> FourStateEventWindowDiagnosticCfg:
71
+ identities = [(subject.source_experiment_id.strip(), subject.design_id.strip()) for subject in self.subjects]
72
+ if len(identities) != len(set(identities)):
73
+ raise ValueError("subjects must contain unique source/design identities")
74
+ filenames = [slugify(subject.filename).casefold() for subject in self.subjects]
75
+ if len(filenames) != len(set(filenames)):
76
+ raise ValueError("subjects must contain unique filenames")
77
+ if set(self.state_labels) != {"00", "10", "01", "11"}:
78
+ raise ValueError("state_labels must define exactly 00, 10, 01, and 11")
79
+ if any(not isinstance(value, str) or not value.strip() for value in self.state_labels.values()):
80
+ raise ValueError("state_labels values must be non-empty strings")
81
+ return self
82
+
83
+
84
+ class FourStateEventWindowDiagnosticPlot(FigurePlotPlugin):
85
+ ConfigModel = FourStateEventWindowDiagnosticCfg
86
+
87
+ @classmethod
88
+ def input_ports(cls):
89
+ return {
90
+ "traces": dataframe_input("traces", "plate_reader.four_state_event_window.traces.v3"),
91
+ "designs": dataframe_input("designs", "plate_reader.four_state_event_window.designs.v4"),
92
+ }
93
+
94
+ def render(self, ctx, inputs, cfg: FourStateEventWindowDiagnosticCfg):
95
+ from reader_workbench.domains.plate_reader.plots.four_state_event_window.diagnostic_render import ( # noqa: PLC0415
96
+ render_four_state_event_window_diagnostic,
97
+ )
98
+
99
+ rendered: list[PlotFigure] = []
100
+ for subject in cfg.subjects:
101
+ figure = render_four_state_event_window_diagnostic(
102
+ inputs["traces"],
103
+ inputs["designs"],
104
+ source_experiment_id=subject.source_experiment_id,
105
+ design_id=subject.design_id,
106
+ reduction_id=cfg.primary_reduction_id,
107
+ pre_window_duration_h=cfg.pre_window_duration_h,
108
+ axis_labels=cfg.axis_labels.model_dump(),
109
+ state_labels=cfg.state_labels,
110
+ reference_label=cfg.reference_label,
111
+ title=subject.title,
112
+ )
113
+ rendered.extend(
114
+ PlotFigure(
115
+ fig=figure,
116
+ filename=subject.filename,
117
+ ext=extension,
118
+ dpi=cfg.dpi,
119
+ description=(
120
+ "Event-relative growth, response, magnitude, and reduced components "
121
+ "for one source experiment and design."
122
+ ),
123
+ )
124
+ for extension in cfg.format
125
+ )
126
+ return rendered
127
+
128
+
129
+ def _is_windows_reserved_filename(filename: str) -> bool:
130
+ """Return whether a portable filename would address a Windows device."""
131
+
132
+ device_name = filename.split(".", maxsplit=1)[0]
133
+ return re.fullmatch(r"(?i:con|prn|aux|nul|com[1-9]|lpt[1-9])", device_name) is not None
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import 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 FourStateEventWindowSummaryCfg(PluginConfig):
14
+ primary_reduction_id: str = Field(min_length=1)
15
+ experiment_ids: list[str] | None = Field(default=None, min_length=1)
16
+ design_ids: list[str] | None = Field(default=None, min_length=1)
17
+ maximum_rows: int = Field(default=64, ge=1)
18
+ title: str = Field(default="Four-state event-window summary", min_length=1)
19
+ filename: str = Field(default="four_state_event_window_summary", min_length=1)
20
+ format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
21
+ dpi: int = Field(default=300, ge=1)
22
+
23
+
24
+ class FourStateEventWindowSummaryPlot(FigurePlotPlugin):
25
+ ConfigModel = FourStateEventWindowSummaryCfg
26
+
27
+ @classmethod
28
+ def input_ports(cls):
29
+ return {"designs": dataframe_input("designs", "plate_reader.four_state_event_window.designs.v4")}
30
+
31
+ def render(self, ctx, inputs, cfg):
32
+ from reader_workbench.domains.plate_reader.plots.four_state_event_window.summary import ( # noqa: PLC0415
33
+ render_four_state_event_window_summary,
34
+ )
35
+
36
+ figure = render_four_state_event_window_summary(
37
+ inputs["designs"],
38
+ primary_reduction_id=cfg.primary_reduction_id,
39
+ experiment_ids=cfg.experiment_ids,
40
+ design_ids=cfg.design_ids,
41
+ maximum_rows=cfg.maximum_rows,
42
+ title=cfg.title,
43
+ )
44
+ return [
45
+ PlotFigure(
46
+ fig=figure,
47
+ filename=cfg.filename,
48
+ ext=extension,
49
+ dpi=cfg.dpi,
50
+ description="Primary event-relative response and anchored-magnitude components by source and design.",
51
+ )
52
+ for extension in cfg.format
53
+ ]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+
5
+ from reader_workbench.plotting.sinks import PlotFigure
6
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin
7
+ from reader_workbench.workbench.ports import dataframe_input
8
+ from reader_workbench.workbench.registry import PluginConfig
9
+
10
+
11
+ class FourStateVectorCollectionHeatmapCfg(PluginConfig):
12
+ title: str = "Four-state vector collection"
13
+ max_y_tick_labels: int = 80
14
+ filename: str = "four_state_vector_collection"
15
+ format: list[str] = Field(default_factory=lambda: ["png"])
16
+ dpi: int = 300
17
+
18
+
19
+ class FourStateVectorCollectionHeatmapPlot(FigurePlotPlugin):
20
+ ConfigModel = FourStateVectorCollectionHeatmapCfg
21
+
22
+ @classmethod
23
+ def input_ports(cls):
24
+ return {"vectors": dataframe_input("vectors", "logic.four_state_vector_collection.v1")}
25
+
26
+ def render(self, ctx, inputs, cfg):
27
+ from reader_workbench.domains.logic.four_state_vector.collection import ( # noqa: PLC0415
28
+ render_four_state_vector_collection_heatmap,
29
+ )
30
+
31
+ figure = render_four_state_vector_collection_heatmap(
32
+ inputs["vectors"],
33
+ title=cfg.title,
34
+ max_y_tick_labels=cfg.max_y_tick_labels,
35
+ )
36
+ return [
37
+ PlotFigure(
38
+ fig=figure,
39
+ filename=cfg.filename,
40
+ ext=extension,
41
+ dpi=cfg.dpi,
42
+ description="Cross-experiment four-state vector heatmap.",
43
+ )
44
+ for extension in cfg.format
45
+ ]