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,170 @@
1
+ """Merge tidy measurement table with a sample metadata map. Cleans the map by:
2
+ 1) dropping all-empty columns
3
+ 2) dropping positions that carry no metadata beyond 'position'
4
+ 3) asserting remaining raw positions exist in the map
5
+ Then merges many:1 on 'position'."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import pandas as pd
12
+ from pydantic import Field
13
+
14
+ from reader_workbench.domains.plate_reader.io.sample_map import parse_sample_map
15
+ from reader_workbench.errors import MergeError
16
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output, file_path_input
17
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
18
+
19
+
20
+ class SampleMapCfg(PluginConfig):
21
+ """
22
+ Flexible plate-map merge.
23
+ - require_columns: metadata columns that MUST exist after merge (presence-only by default).
24
+ - require_non_null: if true, also assert these columns are non-null for all merged rows.
25
+ """
26
+
27
+ require_columns: list[str] = Field(default_factory=list)
28
+ require_non_null: bool = False
29
+
30
+
31
+ class SampleMapMerge(Plugin):
32
+ ConfigModel = SampleMapCfg
33
+
34
+ @classmethod
35
+ def input_ports(cls):
36
+ return {
37
+ "df": dataframe_input("df", "tidy.v1"),
38
+ "sample_map": file_path_input("sample_map"),
39
+ }
40
+
41
+ @classmethod
42
+ def output_ports(cls):
43
+ return cls.promoted_output_ports(
44
+ outputs={"df": dataframe_output("df", "tidy.v1")},
45
+ promotions={"df": ("plate_reader.annotated.v1",)},
46
+ note="promotion requires mapped metadata columns that satisfy the richer contract",
47
+ )
48
+
49
+ def resolve_output_ports(self, *, inputs, outputs, cfg, where):
50
+ del inputs, cfg
51
+ resolved = dict(self.output_ports())
52
+ merged = outputs.get("df")
53
+ if not isinstance(merged, pd.DataFrame):
54
+ return resolved
55
+ try:
56
+ self.contracts.validate(merged, contract_id="plate_reader.annotated.v1", where=f"{where}:df")
57
+ except Exception:
58
+ return resolved
59
+ resolved["df"] = dataframe_output("df", "plate_reader.annotated.v1", surface=resolved["df"].surface)
60
+ return resolved
61
+
62
+ def _clean_plate_map(self, plate_map: pd.DataFrame) -> pd.DataFrame:
63
+ if "position" not in plate_map.columns:
64
+ raise MergeError("Plate map must contain a 'position' column")
65
+
66
+ # 1) drop all-empty columns (keeps 'position' even if empty by policy)
67
+ pm = plate_map.copy()
68
+ non_all_empty = [c for c in pm.columns if c == "position" or not pm[c].isna().all()]
69
+ pm = pm[non_all_empty]
70
+
71
+ # 2) drop rows with no metadata beyond 'position'
72
+ meta_cols = [c for c in pm.columns if c != "position"]
73
+ if not meta_cols:
74
+ # map has only 'position' with no metadata → nothing to merge
75
+ return pm.iloc[0:0].copy()
76
+
77
+ no_meta = pm[meta_cols].isna().all(axis=1)
78
+ pm = pm.loc[~no_meta].copy()
79
+
80
+ return pm
81
+
82
+ def run(self, ctx, inputs, cfg: SampleMapCfg):
83
+ df: pd.DataFrame = inputs["df"]
84
+ sm_path: Path = inputs["sample_map"]
85
+
86
+ try:
87
+ sm_raw = parse_sample_map(sm_path)
88
+ sm = self._clean_plate_map(sm_raw)
89
+ if sm.empty:
90
+ raise MergeError("Plate map has no usable metadata rows after cleaning")
91
+
92
+ # A position row with no metadata explicitly excludes that well from annotated output.
93
+ removed_positions = sorted(set(sm_raw["position"].astype(str)) - set(sm["position"].astype(str)))
94
+ if removed_positions:
95
+ before = len(df)
96
+ df = df[~df["position"].astype(str).isin(removed_positions)].copy()
97
+ after = len(df)
98
+ try:
99
+ ctx.logger.info(
100
+ "[muted]sample_map: dropped %d raw rows (%d positions without metadata)[/muted]",
101
+ before - after,
102
+ len(removed_positions),
103
+ )
104
+ head = ", ".join(removed_positions[:20])
105
+ tail = " …" if len(removed_positions) > 20 else ""
106
+ ctx.logger.debug("sample_map: removed positions: %s%s", head, tail)
107
+ # Optional arithmetic trace (best-effort; relies on tidy schema)
108
+ try:
109
+ chans = df["channel"].astype(str).nunique()
110
+ avg_rows_per_pos = (before - after) / max(len(removed_positions), 1)
111
+ approx_time_slices = round(avg_rows_per_pos / max(chans, 1))
112
+ ctx.logger.debug(
113
+ "sample_map: consistency hint • removed_positions=%d • channels=%d • ~time_slices_per_channel=%d",
114
+ len(removed_positions),
115
+ chans,
116
+ approx_time_slices,
117
+ )
118
+ except Exception:
119
+ pass
120
+ except Exception:
121
+ pass
122
+
123
+ # 3) ensure all remaining raw positions exist in the (cleaned) map
124
+ raw_positions = set(df["position"].astype(str).unique())
125
+ map_positions = set(sm["position"].astype(str).unique())
126
+ missing = sorted(raw_positions - map_positions)
127
+ if missing:
128
+ raise MergeError(
129
+ f"Plate map missing entries for positions: {missing[:40]}{'…' if len(missing) > 40 else ''}"
130
+ )
131
+
132
+ merged = df.merge(sm, on="position", how="left", validate="m:1")
133
+
134
+ # Optional dtype normalization for 'batch' when present
135
+ if "batch" in merged.columns:
136
+ try:
137
+ merged["batch"] = pd.to_numeric(merged["batch"], errors="raise").astype("Int64")
138
+ except Exception as e:
139
+ raise MergeError(f"'batch' must be integer-typed: {e}") from e
140
+
141
+ # Assert required metadata columns per-experiment (config-driven)
142
+ missing_cols = [c for c in cfg.require_columns if c not in merged.columns]
143
+ if missing_cols:
144
+ raise MergeError(f"Required metadata column(s) missing after merge: {missing_cols}")
145
+ if cfg.require_non_null and cfg.require_columns:
146
+ nulls = {c: int(merged[c].isna().sum()) for c in cfg.require_columns}
147
+ bad = {c: n for c, n in nulls.items() if n > 0}
148
+ if bad:
149
+ raise MergeError(f"Required metadata column(s) contain NaN: {bad}")
150
+
151
+ # Concise merge summary.
152
+ try:
153
+ added_cols = [c for c in merged.columns if c not in df.columns]
154
+ ctx.logger.info(
155
+ "sample_map • positions: raw=%d • map=%d • intersect=%d • added_cols=%d [%s]",
156
+ len(raw_positions),
157
+ len(map_positions),
158
+ len(raw_positions & map_positions),
159
+ len(added_cols),
160
+ ", ".join(added_cols[:6]) + (" …" if len(added_cols) > 6 else ""),
161
+ )
162
+ except Exception:
163
+ pass
164
+
165
+ except MergeError:
166
+ raise
167
+ except Exception as e:
168
+ raise MergeError(str(e)) from e
169
+
170
+ return {"df": merged}
@@ -0,0 +1,94 @@
1
+ """Merge tidy measurements with a sample metadata table (keyed by sample_id by default)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ from pydantic import Field
9
+
10
+ from reader_workbench.errors import MergeError
11
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output, file_path_input
12
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
13
+
14
+
15
+ class SampleMetadataCfg(PluginConfig):
16
+ key: str = "sample_id"
17
+ require_columns: list[str] = Field(default_factory=list)
18
+ require_non_null: bool = False
19
+
20
+
21
+ class SampleMetadataMerge(Plugin):
22
+ ConfigModel = SampleMetadataCfg
23
+
24
+ @classmethod
25
+ def input_ports(cls):
26
+ return {
27
+ "df": dataframe_input("df", "tidy.v1"),
28
+ "metadata": file_path_input("metadata"),
29
+ }
30
+
31
+ @classmethod
32
+ def output_ports(cls):
33
+ return {"df": dataframe_output("df", "tidy.v1")}
34
+
35
+ def _load_metadata(self, path: Path) -> pd.DataFrame:
36
+ if not path.exists():
37
+ raise MergeError(f"Metadata file does not exist: {path}")
38
+ if not path.is_file():
39
+ raise MergeError(f"Metadata path must be a regular file: {path}")
40
+ suffix = path.suffix.lower()
41
+ if suffix == ".xlsx":
42
+ return pd.read_excel(path)
43
+ if suffix == ".csv":
44
+ return pd.read_csv(path)
45
+ raise MergeError(f"Unsupported metadata format {suffix or '<none>'!r}; expected .csv or .xlsx")
46
+
47
+ def run(self, ctx, inputs, cfg: SampleMetadataCfg):
48
+ df: pd.DataFrame = inputs["df"]
49
+ meta_path: Path = inputs["metadata"]
50
+ key = str(cfg.key)
51
+
52
+ try:
53
+ meta = self._load_metadata(meta_path)
54
+ except Exception as e:
55
+ raise MergeError(f"Failed to read metadata file {meta_path}: {e}") from e
56
+
57
+ if key not in df.columns:
58
+ raise MergeError(f"Metadata merge key '{key}' missing from input dataframe")
59
+ if key not in meta.columns:
60
+ raise MergeError(f"Metadata merge key '{key}' missing from metadata file")
61
+ missing_keys = meta[key].isna() | meta[key].astype("string").str.strip().eq("")
62
+ if missing_keys.any():
63
+ rows = [int(index) + 2 for index in meta.index[missing_keys].tolist()]
64
+ raise MergeError(f"Metadata merge key '{key}' is blank at file rows: {rows}")
65
+ duplicate_keys = sorted(meta.loc[meta[key].duplicated(keep=False), key].astype(str).unique().tolist())
66
+ if duplicate_keys:
67
+ raise MergeError(f"Metadata merge key '{key}' must be unique; duplicates: {duplicate_keys}")
68
+
69
+ try:
70
+ merged = df.merge(meta, on=key, how="left", validate="m:1")
71
+ except (TypeError, ValueError, pd.errors.MergeError) as exc:
72
+ raise MergeError(f"Metadata merge failed for key '{key}': {exc}") from exc
73
+
74
+ missing_cols = [c for c in cfg.require_columns if c not in merged.columns]
75
+ if missing_cols:
76
+ raise MergeError(f"Required metadata column(s) missing after merge: {missing_cols}")
77
+ if cfg.require_non_null and cfg.require_columns:
78
+ nulls = {c: int(merged[c].isna().sum()) for c in cfg.require_columns}
79
+ bad = {c: n for c, n in nulls.items() if n > 0}
80
+ if bad:
81
+ raise MergeError(f"Required metadata column(s) contain NaN: {bad}")
82
+
83
+ try:
84
+ added_cols = [c for c in merged.columns if c not in df.columns]
85
+ ctx.logger.info(
86
+ "sample_metadata • rows=%d • added_cols=%d [%s]",
87
+ len(merged),
88
+ len(added_cols),
89
+ ", ".join(added_cols[:6]) + (" …" if len(added_cols) > 6 else ""),
90
+ )
91
+ except Exception:
92
+ pass
93
+
94
+ return {"df": merged}
@@ -0,0 +1 @@
1
+ """Built-in validators."""
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import suppress
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from pydantic import Field
8
+
9
+ from reader_workbench.errors import ExecutionError
10
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
11
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
12
+
13
+
14
+ class PromoteCfg(PluginConfig):
15
+ require_columns: list[str] = Field(default_factory=lambda: ["treatment", "design_id"])
16
+ require_non_null: bool = True # be strict when promoting
17
+ trim_and_require_non_blank: list[str] = Field(default_factory=list)
18
+ require_finite: list[str] = Field(default_factory=list)
19
+ allow_instrument_overflow: list[str] = Field(default_factory=list)
20
+ # Only promote a subset of rows (e.g., samples). If provided, we require the column to exist.
21
+ type_column: str = "type"
22
+ include_types: list[str] = Field(default_factory=list) # e.g., ["SAMPLE"]
23
+ # Deterministically drop rows with NULL in these columns before assertions.
24
+ drop_where_null_in: list[str] = Field(default_factory=list)
25
+ synthesize_batch: bool = False
26
+ synthesized_batch_value: int = 0
27
+
28
+
29
+ class PromoteToTidyPlusMap(Plugin):
30
+ ConfigModel = PromoteCfg
31
+
32
+ @classmethod
33
+ def input_ports(cls):
34
+ return {"df": dataframe_input("df", "tidy.v1")}
35
+
36
+ @classmethod
37
+ def output_ports(cls):
38
+ return {"df": dataframe_output("df", "plate_reader.annotated.v1")}
39
+
40
+ def run(self, ctx, inputs, cfg: PromoteCfg):
41
+ df: pd.DataFrame = inputs["df"].copy()
42
+
43
+ # 0) Optional row filtering by "type" (case-insensitive exact match)
44
+ if cfg.include_types:
45
+ if cfg.type_column not in df.columns:
46
+ raise ExecutionError(
47
+ f"to_tidy_plus_map: include_types was provided but column {cfg.type_column!r} is missing"
48
+ )
49
+ keep = {str(x).casefold() for x in cfg.include_types}
50
+ before = len(df)
51
+ df = df[df[cfg.type_column].astype(str).str.casefold().isin(keep)].copy()
52
+ with suppress(Exception):
53
+ ctx.logger.info(
54
+ "to_tidy_plus_map: filtered rows by %r ∈ %s → kept %d/%d",
55
+ cfg.type_column,
56
+ sorted(cfg.include_types),
57
+ len(df),
58
+ before,
59
+ )
60
+ if df.empty:
61
+ raise ExecutionError(
62
+ f"to_tidy_plus_map: no rows remain after filtering by {cfg.type_column} ∈ {cfg.include_types}"
63
+ )
64
+
65
+ # 0b) Optional: drop rows with NULL in specific columns
66
+ if cfg.drop_where_null_in:
67
+ missing_cols = [c for c in cfg.drop_where_null_in if c not in df.columns]
68
+ if missing_cols:
69
+ raise ExecutionError(f"to_tidy_plus_map: drop_where_null_in refers to missing columns: {missing_cols}")
70
+ before = len(df)
71
+ df = df.dropna(subset=list(cfg.drop_where_null_in)).copy()
72
+ with suppress(Exception):
73
+ ctx.logger.info(
74
+ "to_tidy_plus_map: dropped %d row(s) with NULL in %s",
75
+ before - len(df),
76
+ list(cfg.drop_where_null_in),
77
+ )
78
+
79
+ if "batch" not in df.columns or df["batch"].isna().all():
80
+ if cfg.synthesize_batch:
81
+ df = df.copy()
82
+ df["batch"] = int(cfg.synthesized_batch_value)
83
+ with suppress(Exception):
84
+ ctx.logger.info(
85
+ "to_tidy_plus_map: 'batch' missing → added constant %d for all rows (explicit config)",
86
+ int(cfg.synthesized_batch_value),
87
+ )
88
+ elif df["batch"].isna().any() and cfg.synthesize_batch:
89
+ df = df.copy()
90
+ df["batch"] = df["batch"].fillna(int(cfg.synthesized_batch_value))
91
+ missing = [c for c in cfg.require_columns if c not in df.columns]
92
+ if missing:
93
+ raise ExecutionError(f"Cannot promote to plate_reader.annotated.v1; missing columns: {missing}")
94
+ validation_columns = (
95
+ set(cfg.trim_and_require_non_blank) | set(cfg.require_finite) | set(cfg.allow_instrument_overflow)
96
+ )
97
+ missing_validation = sorted(validation_columns - set(df.columns))
98
+ if missing_validation:
99
+ raise ExecutionError(
100
+ "Cannot promote to plate_reader.annotated.v1; configured validation refers to "
101
+ f"missing columns: {missing_validation}"
102
+ )
103
+ unvalidated_overflow = sorted(set(cfg.allow_instrument_overflow) - set(cfg.require_finite))
104
+ if unvalidated_overflow:
105
+ raise ExecutionError(
106
+ f"Cannot promote; allow_instrument_overflow must name columns in require_finite: {unvalidated_overflow}"
107
+ )
108
+ if cfg.require_non_null:
109
+ bad = {c: int(df[c].isna().sum()) for c in cfg.require_columns if df[c].isna().any()}
110
+ if bad:
111
+ raise ExecutionError(f"Cannot promote; required columns contain NaN: {bad}")
112
+ blank: dict[str, int] = {}
113
+ for column in cfg.trim_and_require_non_blank:
114
+ normalized = df[column].astype("string").str.strip()
115
+ invalid = normalized.isna() | normalized.eq("")
116
+ if invalid.any():
117
+ blank[column] = int(invalid.sum())
118
+ df[column] = normalized
119
+ if blank:
120
+ raise ExecutionError(f"Cannot promote; configured identity columns contain blank values: {blank}")
121
+ nonfinite: dict[str, int] = {}
122
+ for column in cfg.require_finite:
123
+ numeric = pd.to_numeric(df[column], errors="coerce").to_numpy(dtype=float, na_value=np.nan)
124
+ invalid = ~np.isfinite(numeric)
125
+ if column in cfg.allow_instrument_overflow:
126
+ overflow_column = f"{column}_instrument_overflow"
127
+ bound_column = f"{column}_bound_kind"
128
+ missing_provenance = [name for name in (overflow_column, bound_column) if name not in df.columns]
129
+ if missing_provenance:
130
+ raise ExecutionError(
131
+ "Cannot promote; instrument-overflow validation requires provenance columns: "
132
+ f"{missing_provenance}"
133
+ )
134
+ raw_flags = df[overflow_column]
135
+ if raw_flags.isna().any() or not raw_flags.map(lambda value: isinstance(value, (bool, np.bool_))).all():
136
+ raise ExecutionError(
137
+ f"Cannot promote; {overflow_column!r} must contain booleans without missing values"
138
+ )
139
+ declared_overflow = (
140
+ np.isposinf(numeric)
141
+ & raw_flags.to_numpy(dtype=bool)
142
+ & df[bound_column].astype("string").eq("lower").to_numpy(dtype=bool)
143
+ )
144
+ invalid &= ~declared_overflow
145
+ invalid_count = int(invalid.sum())
146
+ if invalid_count:
147
+ nonfinite[column] = invalid_count
148
+ if nonfinite:
149
+ raise ExecutionError(
150
+ f"Cannot promote; configured measurement columns contain non-finite values: {nonfinite}"
151
+ )
152
+ # dtype normalization for 'batch' (if present)
153
+ if "batch" in df.columns:
154
+ df["batch"] = pd.to_numeric(df["batch"], errors="raise").astype("Int64")
155
+ return {"df": df}
@@ -0,0 +1,80 @@
1
+ from .builtins import BUILTIN_PROTOCOLS, builtin_protocol_catalog
2
+ from .compiler import (
3
+ compile_cytometry_flow_panel,
4
+ compile_generic_protocol,
5
+ compile_logic_four_state_vector_screen,
6
+ compile_plate_reader_dual_reporter_screen,
7
+ compile_plate_reader_growth_screen,
8
+ compile_plate_reader_single_reporter_screen,
9
+ )
10
+ from .model import (
11
+ BoundProtocol,
12
+ CompiledProtocolPlan,
13
+ FigureKind,
14
+ MetricStage,
15
+ ProtocolArtifactSpec,
16
+ ProtocolBinding,
17
+ ProtocolBindingValueRef,
18
+ ProtocolCatalog,
19
+ ProtocolCompiler,
20
+ ProtocolConfigFieldSpec,
21
+ ProtocolControlRule,
22
+ ProtocolDescriptor,
23
+ ProtocolEffectSignSpec,
24
+ ProtocolExecutionPlan,
25
+ ProtocolFactorSpec,
26
+ ProtocolFigureSpec,
27
+ ProtocolMetricSpec,
28
+ ProtocolPlotProfileSpec,
29
+ ProtocolPluginDefaultsSpec,
30
+ ProtocolRankingSpec,
31
+ ProtocolResourceSpec,
32
+ ProtocolSemanticExecution,
33
+ ProtocolSemanticNode,
34
+ ProtocolSemanticProfileOverride,
35
+ ProtocolSemanticProfileSpec,
36
+ ProtocolSemanticProgram,
37
+ ProtocolWindowSpec,
38
+ RankingDirection,
39
+ binding_value,
40
+ )
41
+
42
+ __all__ = [
43
+ "BUILTIN_PROTOCOLS",
44
+ "BoundProtocol",
45
+ "CompiledProtocolPlan",
46
+ "FigureKind",
47
+ "MetricStage",
48
+ "ProtocolArtifactSpec",
49
+ "ProtocolBinding",
50
+ "ProtocolBindingValueRef",
51
+ "ProtocolCatalog",
52
+ "ProtocolCompiler",
53
+ "ProtocolConfigFieldSpec",
54
+ "ProtocolControlRule",
55
+ "ProtocolDescriptor",
56
+ "ProtocolExecutionPlan",
57
+ "ProtocolEffectSignSpec",
58
+ "ProtocolFactorSpec",
59
+ "ProtocolFigureSpec",
60
+ "ProtocolMetricSpec",
61
+ "ProtocolPlotProfileSpec",
62
+ "ProtocolPluginDefaultsSpec",
63
+ "ProtocolRankingSpec",
64
+ "ProtocolResourceSpec",
65
+ "ProtocolSemanticExecution",
66
+ "ProtocolSemanticNode",
67
+ "ProtocolSemanticProfileOverride",
68
+ "ProtocolSemanticProfileSpec",
69
+ "ProtocolSemanticProgram",
70
+ "ProtocolWindowSpec",
71
+ "RankingDirection",
72
+ "binding_value",
73
+ "builtin_protocol_catalog",
74
+ "compile_cytometry_flow_panel",
75
+ "compile_generic_protocol",
76
+ "compile_logic_four_state_vector_screen",
77
+ "compile_plate_reader_dual_reporter_screen",
78
+ "compile_plate_reader_growth_screen",
79
+ "compile_plate_reader_single_reporter_screen",
80
+ ]
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import replace
5
+
6
+ from reader_workbench.workbench.input_discovery import (
7
+ DEFAULT_INPUT_EXCLUDE as DEFAULT_EXCLUDE,
8
+ )
9
+ from reader_workbench.workbench.input_discovery import (
10
+ DEFAULT_WORKBOOK_INCLUDE as DEFAULT_INCLUDE,
11
+ )
12
+
13
+ from .compiler import compile_plate_reader_growth_screen
14
+ from .model import (
15
+ ProtocolConfigFieldSpec,
16
+ ProtocolDescriptor,
17
+ ProtocolExecutionPlan,
18
+ ProtocolFigureSpec,
19
+ ProtocolMetricSpec,
20
+ ProtocolPlotProfileSpec,
21
+ ProtocolPluginDefaultsSpec,
22
+ ProtocolResourceSpec,
23
+ ProtocolSemanticProfileSpec,
24
+ binding_value,
25
+ )
26
+
27
+
28
+ def build_plate_reader_growth_protocol(
29
+ *,
30
+ dual_reporter_protocol: ProtocolDescriptor,
31
+ field_builder: Callable[..., ProtocolConfigFieldSpec],
32
+ ) -> ProtocolDescriptor:
33
+ """Build the one-channel growth protocol from shared plate-reader inputs."""
34
+
35
+ field = field_builder
36
+ ingest_field = next(item for item in dual_reporter_protocol.input_fields if item.key == "ingest")
37
+ channel_map_field = next(item for item in ingest_field.children if item.key == "channel_map")
38
+ growth_ingest = replace(
39
+ ingest_field,
40
+ children=tuple(
41
+ replace(
42
+ item,
43
+ summary=(
44
+ "Optional raw workbook label to canonical channel mapping. Leave null for "
45
+ "map-free kinetic discovery; snapshot and mixed parsing require a mapping."
46
+ ),
47
+ default={"OD600:600": "OD600"},
48
+ )
49
+ if item is channel_map_field
50
+ else item
51
+ for item in ingest_field.children
52
+ ),
53
+ )
54
+ inherited_preprocessing = next(
55
+ item for item in dual_reporter_protocol.analysis_fields if item.key == "preprocessing"
56
+ )
57
+ preprocessing_field = replace(
58
+ inherited_preprocessing,
59
+ children=tuple(
60
+ replace(
61
+ item,
62
+ children=tuple(
63
+ replace(child, default="none") if child.key == "action" else child for child in item.children
64
+ ),
65
+ )
66
+ if item.key == "overflow"
67
+ else item
68
+ for item in inherited_preprocessing.children
69
+ ),
70
+ )
71
+
72
+ return ProtocolDescriptor(
73
+ protocol="plate_reader/growth_screen",
74
+ domain="plate_reader",
75
+ family="screen_analysis",
76
+ summary="One-channel plate-reader growth assay with explicit sample and treatment metadata.",
77
+ tags=("plate_reader", "growth", "screen", "single_channel"),
78
+ resources=(
79
+ ProtocolResourceSpec(
80
+ id="sample_map",
81
+ path="./inputs/metadata.xlsx",
82
+ summary="Well-to-sample metadata for the plate-reader workbook.",
83
+ ),
84
+ ),
85
+ input_fields=(growth_ingest,),
86
+ analysis_fields=(
87
+ field(
88
+ "growth_channel",
89
+ "Measured channel used as the growth readout.",
90
+ kind="string",
91
+ default="OD600",
92
+ ),
93
+ preprocessing_field,
94
+ ),
95
+ factors=dual_reporter_protocol.factors,
96
+ semantic_profiles=(
97
+ ProtocolSemanticProfileSpec(
98
+ id="growth_raw",
99
+ family="growth_panel",
100
+ summary="Growth-screen semantics over one configured measurement channel.",
101
+ primary_metric="Growth",
102
+ primary_readout="configured growth channel",
103
+ tags=("growth", "plate_reader", "panel"),
104
+ ),
105
+ ),
106
+ metrics=(
107
+ ProtocolMetricSpec(
108
+ id="Growth",
109
+ stage="raw",
110
+ summary="Raw configured growth trace.",
111
+ formula="configured_growth_channel",
112
+ profiles=("growth_raw",),
113
+ ),
114
+ ),
115
+ figures=(
116
+ ProtocolFigureSpec(
117
+ id="raw_kinetics",
118
+ kind="qc",
119
+ summary="Raw growth trajectories by sample and treatment.",
120
+ primary=True,
121
+ ),
122
+ ProtocolFigureSpec(
123
+ id="endpoint_by_condition",
124
+ kind="summary",
125
+ summary="Explicit-time growth comparison grouped by treatment or condition.",
126
+ ),
127
+ ProtocolFigureSpec(
128
+ id="endpoint_by_design",
129
+ kind="summary",
130
+ summary="Explicit-time growth comparison grouped by sample or design.",
131
+ ),
132
+ ProtocolFigureSpec(
133
+ id="growth_overview",
134
+ kind="kinetics",
135
+ summary="Growth trajectories and an explicitly timed endpoint in one view.",
136
+ ),
137
+ ProtocolFigureSpec(
138
+ id="value_distributions",
139
+ kind="qc",
140
+ summary="Distribution view of the configured growth channel.",
141
+ ),
142
+ ),
143
+ plot_profiles=(
144
+ ProtocolPlotProfileSpec(
145
+ id="screen_overview",
146
+ summary="Explicit-time growth screen review.",
147
+ figures=("raw_kinetics", "endpoint_by_condition", "endpoint_by_design", "growth_overview"),
148
+ ),
149
+ ProtocolPlotProfileSpec(
150
+ id="kinetics_qc",
151
+ summary="Growth trajectories and value distributions without an inferred endpoint.",
152
+ figures=("raw_kinetics", "value_distributions"),
153
+ ),
154
+ ),
155
+ default_plot_profile="kinetics_qc",
156
+ execution=ProtocolExecutionPlan(
157
+ plugin_defaults=(
158
+ ProtocolPluginDefaultsSpec(
159
+ plugin="ingest/synergy_h1",
160
+ summary="Generic one-channel plate-reader ingest settings.",
161
+ with_={
162
+ "mode": binding_value("ingest.mode", "kinetic_only"),
163
+ "channel_map": binding_value("ingest.channel_map", {"OD600:600": "OD600"}),
164
+ "sheet_names": binding_value("ingest.sheet_names", None),
165
+ "time_round_decimals": binding_value("ingest.time_round_decimals", 12),
166
+ "time_step_h": binding_value("ingest.time_step_h", None),
167
+ "time_offset_h": binding_value("ingest.time_offset_h", 0.0),
168
+ "auto_roots": binding_value("ingest.auto_roots", None),
169
+ "auto_include": binding_value("ingest.auto_include", list(DEFAULT_INCLUDE)),
170
+ "auto_exclude": binding_value("ingest.auto_exclude", list(DEFAULT_EXCLUDE)),
171
+ "auto_pick": binding_value("ingest.auto_pick", "single"),
172
+ "auto_recursive": binding_value("ingest.auto_recursive", False),
173
+ "print_summary": binding_value("ingest.print_summary", True),
174
+ },
175
+ ),
176
+ ),
177
+ compiler=compile_plate_reader_growth_screen,
178
+ ),
179
+ )