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,937 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any
5
+
6
+ from reader_workbench.domains.plate_reader.analysis.four_state_event_window import FourStateEventWindowAnalysisSpec
7
+ from reader_workbench.domains.time_series import (
8
+ ObservationAggregationSpec,
9
+ TemporalReductionSpec,
10
+ )
11
+ from reader_workbench.errors import ConfigError
12
+ from reader_workbench.protocols.model import CompiledProtocolPlan
13
+ from reader_workbench.protocols.semantic_coverage import (
14
+ _plate_reader_growth_semantic_program,
15
+ _plate_reader_semantic_program,
16
+ _plate_reader_single_reporter_semantic_program,
17
+ )
18
+ from reader_workbench.workbench.decl.model import (
19
+ PluginStepDecl,
20
+ RecordCollectionInputDecl,
21
+ RecordInputDecl,
22
+ RecordOutputDecl,
23
+ )
24
+
25
+ from .common import (
26
+ _analysis_bool,
27
+ _analysis_channel,
28
+ _analysis_mapping,
29
+ _analysis_options,
30
+ _config_bool,
31
+ _deep_merge,
32
+ _input_mapping,
33
+ _step,
34
+ )
35
+ from .plate_reader_pipeline import (
36
+ compose_dual_reporter_pipeline,
37
+ compose_growth_pipeline,
38
+ compose_single_reporter_pipeline,
39
+ )
40
+
41
+ PLATE_READER_EXPORT_OUTPUTS = {"crosstalk_pairs_table"}
42
+
43
+
44
+ def compile_plate_reader_four_state_event_window(protocol: Any):
45
+ input_names = ("response_records", "magnitude_records", "trajectory_records")
46
+ effective_inputs = protocol.effective_inputs()
47
+ analysis = protocol.effective_analysis()
48
+ try:
49
+ FourStateEventWindowAnalysisSpec.from_mapping(analysis)
50
+ except ValueError as exc:
51
+ raise ConfigError(f"invalid plate-reader four-state event-window analysis policy: {exc}") from exc
52
+ resource_ids = {name: tuple(effective_inputs.get(name, ())) for name in input_names}
53
+ writes = {
54
+ name: RecordOutputDecl(record_id=f"four_state_event_window/{name}")
55
+ for name in ("wells", "designs", "descriptive_resampling_draws", "traces", "events", "dispositions")
56
+ }
57
+ pipeline = (
58
+ _step(
59
+ id="four_state_event_window",
60
+ plugin="transform/four_state_event_window",
61
+ reads={name: RecordCollectionInputDecl(resource_ids=resource_ids[name]) for name in input_names},
62
+ writes=writes,
63
+ with_=analysis,
64
+ ),
65
+ )
66
+ primary_reduction = _primary_reduction_config(analysis)
67
+ selected_plots = protocol.select_plot_outputs(
68
+ allowed={"four_state_event_window_summary", "four_state_event_window_diagnostic"}
69
+ )
70
+ plots = tuple(
71
+ _four_state_event_window_plot_output(
72
+ protocol,
73
+ output_id=output_id,
74
+ primary_reduction=primary_reduction,
75
+ analysis=analysis,
76
+ )
77
+ for output_id in selected_plots
78
+ )
79
+ selected_exports = protocol.select_export_outputs(
80
+ defaults=("designs_table", "events_table"),
81
+ allowed={"designs_table", "events_table"},
82
+ )
83
+ export_records = {
84
+ "designs_table": ("four_state_event_window/designs", "four_state_event_window_designs.csv"),
85
+ "events_table": ("four_state_event_window/events", "four_state_event_window_events.csv"),
86
+ }
87
+ exports = tuple(
88
+ _step(
89
+ id=artifact_id,
90
+ plugin="export/csv",
91
+ reads={"df": RecordInputDecl(record_id=export_records[artifact_id][0])},
92
+ with_=_deep_merge(
93
+ {"path": export_records[artifact_id][1]},
94
+ protocol.export_artifact_config(artifact_id=artifact_id),
95
+ ),
96
+ )
97
+ for artifact_id in selected_exports
98
+ )
99
+ return CompiledProtocolPlan(
100
+ pipeline=pipeline,
101
+ plots=plots,
102
+ exports=exports,
103
+ semantic_program=protocol.descriptor.semantic_program(),
104
+ )
105
+
106
+
107
+ def _four_state_event_window_plot_output(
108
+ protocol: Any,
109
+ *,
110
+ output_id: str,
111
+ primary_reduction: dict[str, Any],
112
+ analysis: dict[str, Any],
113
+ ) -> PluginStepDecl:
114
+ settings = protocol.plot_view_config(figure_id=output_id)
115
+ compiler_owned = {
116
+ "axis_labels",
117
+ "pre_window_duration_h",
118
+ "primary_reduction_id",
119
+ "reference_label",
120
+ "state_labels",
121
+ }
122
+ overridden = sorted(compiler_owned.intersection(settings))
123
+ if overridden:
124
+ raise ConfigError(
125
+ f"protocol.outputs.plots.views.{output_id} cannot override compiler-owned fields {overridden}; "
126
+ "mark exactly one protocol.analysis.reductions entry as primary instead"
127
+ )
128
+ primary_reduction_id = str(primary_reduction["id"])
129
+ if output_id == "four_state_event_window_summary":
130
+ return _step(
131
+ id=output_id,
132
+ plugin="plot/four_state_event_window_summary",
133
+ reads={"designs": RecordInputDecl(record_id="four_state_event_window/designs")},
134
+ with_=_deep_merge({"primary_reduction_id": primary_reduction_id}, settings),
135
+ )
136
+ if output_id == "four_state_event_window_diagnostic":
137
+ legacy_subject_fields = sorted(
138
+ {"source_experiment_id", "design_id", "filename", "title"}.intersection(settings)
139
+ )
140
+ if legacy_subject_fields:
141
+ raise ConfigError(
142
+ f"protocol.outputs.plots.views.{output_id} uses unsupported top-level subject fields "
143
+ f"{legacy_subject_fields}; declare every diagnostic under subjects"
144
+ )
145
+ subjects = settings.get("subjects")
146
+ if not isinstance(subjects, list) or not subjects:
147
+ raise ConfigError(f"protocol.outputs.plots.views.{output_id}.subjects must be a non-empty list")
148
+ return _step(
149
+ id=output_id,
150
+ plugin="plot/four_state_event_window_diagnostic",
151
+ reads={
152
+ "designs": RecordInputDecl(record_id="four_state_event_window/designs"),
153
+ "traces": RecordInputDecl(record_id="four_state_event_window/traces"),
154
+ },
155
+ with_=_deep_merge(
156
+ {
157
+ "primary_reduction_id": primary_reduction_id,
158
+ "pre_window_duration_h": primary_reduction.get("pre_window_duration_h"),
159
+ **_diagnostic_display_labels(analysis),
160
+ },
161
+ settings,
162
+ ),
163
+ )
164
+ raise ConfigError(f"Unsupported four-state event-window plot output {output_id!r}")
165
+
166
+
167
+ def _diagnostic_display_labels(analysis: dict[str, Any]) -> dict[str, object]:
168
+ source = analysis["source"]
169
+ return {
170
+ "axis_labels": {
171
+ "growth": str(source["growth_channel"]),
172
+ "response": f"log$_2$({source['response_channel']})",
173
+ "magnitude": f"log$_2$({source['magnitude_channel']})",
174
+ },
175
+ "state_labels": {
176
+ state: str(label) for state, label in source.get("state_labels", source["state_values"]).items()
177
+ },
178
+ "reference_label": str(source["reference_design_id"]),
179
+ }
180
+
181
+
182
+ def _primary_reduction_config(analysis: dict[str, Any]) -> dict[str, Any]:
183
+ reductions = analysis.get("reductions", ())
184
+ primary = [item for item in reductions if isinstance(item, dict) and item.get("role") == "primary"]
185
+ if len(primary) != 1 or not isinstance(primary[0].get("id"), str) or not primary[0]["id"].strip():
186
+ raise ConfigError("protocol.analysis.reductions must declare exactly one primary reduction id")
187
+ result = dict(primary[0])
188
+ result["id"] = result["id"].strip()
189
+ return result
190
+
191
+
192
+ def compile_plate_reader_dual_reporter_screen(protocol: Any):
193
+ analysis = _analysis_options(protocol)
194
+ include_fold_change = _configured_fold_change_enabled(protocol, analysis=analysis)
195
+ preprocessing = _analysis_mapping(analysis, key="preprocessing")
196
+ blank_cfg = _analysis_mapping(preprocessing, key="blank")
197
+ overflow_cfg = _analysis_mapping(preprocessing, key="overflow")
198
+ crosstalk_cfg = _analysis_mapping(analysis, key="crosstalk_pairs")
199
+ include_crosstalk_pairs = _config_bool(crosstalk_cfg, key="enabled", default=False)
200
+ include_crosstalk_export = _config_bool(crosstalk_cfg, key="export", default=include_crosstalk_pairs)
201
+ ingest_channels = _configured_ingest_channels(protocol, required=("OD600", "CFP", "YFP"))
202
+ pipeline = list(
203
+ compose_dual_reporter_pipeline(
204
+ ingest_channels=ingest_channels,
205
+ blank_config=blank_cfg,
206
+ overflow_config=overflow_cfg,
207
+ )
208
+ )
209
+ if include_fold_change:
210
+ _configured_fold_change_target(protocol, expected="YFP/CFP")
211
+ _configured_fold_change_report_times(protocol)
212
+ pipeline.append(_plate_reader_fold_change_step(measurement="yfp_cfp"))
213
+ if include_crosstalk_pairs:
214
+ if not include_fold_change:
215
+ raise ConfigError(
216
+ "plate_reader/dual_reporter_screen requires include_fold_change when crosstalk_pairs.enabled."
217
+ )
218
+ pipeline.append(_plate_reader_crosstalk_pairs_step(config=crosstalk_cfg))
219
+
220
+ selected_plots = protocol.select_plot_outputs(
221
+ allowed=_plate_reader_plot_output_ids(measurement="yfp_cfp"),
222
+ )
223
+ plots = [
224
+ _plate_reader_plot_output(
225
+ protocol,
226
+ output_id=deliverable_id,
227
+ measurement="yfp_cfp",
228
+ )
229
+ for deliverable_id in selected_plots
230
+ ]
231
+
232
+ default_exports = ("crosstalk_pairs_table",) if include_crosstalk_export else ()
233
+ selected_exports = protocol.select_export_outputs(
234
+ defaults=default_exports,
235
+ allowed=PLATE_READER_EXPORT_OUTPUTS,
236
+ )
237
+ exports = [_plate_reader_export_output(protocol, output_id=deliverable_id) for deliverable_id in selected_exports]
238
+
239
+ return CompiledProtocolPlan(
240
+ pipeline=tuple(pipeline),
241
+ plots=tuple(plots),
242
+ exports=tuple(exports),
243
+ semantic_program=_plate_reader_semantic_program(
244
+ protocol,
245
+ include_crosstalk_pairs=include_crosstalk_pairs,
246
+ include_fold_change=include_fold_change,
247
+ ),
248
+ )
249
+
250
+
251
+ def compile_plate_reader_single_reporter_screen(protocol: Any):
252
+ analysis = _analysis_options(protocol)
253
+ if analysis.get("temporal_reduction") is not None or analysis.get("observation_aggregation") is not None:
254
+ _single_reporter_diagnostic_policy(protocol)
255
+ reporter_channel = _analysis_channel(analysis, key="reporter_channel", default="RFP")
256
+ normalizer_channel = _analysis_channel(analysis, key="normalizer_channel", default="OD600")
257
+ if reporter_channel == normalizer_channel:
258
+ raise ConfigError(
259
+ "plate_reader/single_reporter_screen requires distinct reporter_channel and normalizer_channel."
260
+ )
261
+ include_fold_change = _configured_fold_change_enabled(protocol, analysis=analysis)
262
+ preprocessing = _analysis_mapping(analysis, key="preprocessing")
263
+ blank_cfg = _analysis_mapping(preprocessing, key="blank")
264
+ overflow_cfg = _analysis_mapping(preprocessing, key="overflow")
265
+ ingest_channels = _configured_ingest_channels(protocol, required=(normalizer_channel, reporter_channel))
266
+ ratio_label = _single_reporter_ratio_label(
267
+ reporter_channel=reporter_channel,
268
+ normalizer_channel=normalizer_channel,
269
+ )
270
+
271
+ pipeline = list(
272
+ compose_single_reporter_pipeline(
273
+ ingest_channels=ingest_channels,
274
+ reporter_channel=reporter_channel,
275
+ normalizer_channel=normalizer_channel,
276
+ blank_config=blank_cfg,
277
+ overflow_config=overflow_cfg,
278
+ )
279
+ )
280
+ if include_fold_change:
281
+ _configured_fold_change_target(protocol, expected=ratio_label)
282
+ _configured_fold_change_report_times(protocol)
283
+ pipeline.append(
284
+ _plate_reader_single_reporter_fold_change_step(
285
+ reporter_channel=reporter_channel,
286
+ normalizer_channel=normalizer_channel,
287
+ )
288
+ )
289
+
290
+ selected_plots = protocol.select_plot_outputs(allowed=_plate_reader_single_reporter_plot_output_ids())
291
+ plots = [
292
+ _plate_reader_single_reporter_plot_output(
293
+ protocol,
294
+ output_id=deliverable_id,
295
+ reporter_channel=reporter_channel,
296
+ normalizer_channel=normalizer_channel,
297
+ )
298
+ for deliverable_id in selected_plots
299
+ ]
300
+
301
+ selected_exports = protocol.select_export_outputs(defaults=(), allowed=set())
302
+ if selected_exports:
303
+ raise ConfigError("plate_reader/single_reporter_screen does not currently compile export artifacts.")
304
+
305
+ return CompiledProtocolPlan(
306
+ pipeline=tuple(pipeline),
307
+ plots=tuple(plots),
308
+ exports=(),
309
+ semantic_program=_plate_reader_single_reporter_semantic_program(
310
+ protocol,
311
+ reporter_channel=reporter_channel,
312
+ normalizer_channel=normalizer_channel,
313
+ include_fold_change=include_fold_change,
314
+ ),
315
+ )
316
+
317
+
318
+ def compile_plate_reader_growth_screen(protocol: Any):
319
+ """Compile one configured growth channel without inventing reporter semantics."""
320
+
321
+ analysis = _analysis_options(protocol)
322
+ growth_channel = _analysis_channel(analysis, key="growth_channel", default="OD600")
323
+ preprocessing = _analysis_mapping(analysis, key="preprocessing")
324
+ blank_cfg = _analysis_mapping(preprocessing, key="blank")
325
+ overflow_cfg = {"action": "none", **_analysis_mapping(preprocessing, key="overflow")}
326
+ ingest_channels = _configured_ingest_channels(protocol, required=(growth_channel,))
327
+ pipeline = compose_growth_pipeline(
328
+ ingest_channels=ingest_channels,
329
+ growth_channel=growth_channel,
330
+ blank_config=blank_cfg,
331
+ overflow_config=overflow_cfg,
332
+ )
333
+ selected_plots = protocol.select_plot_outputs(allowed=_plate_reader_growth_plot_output_ids())
334
+ plots = tuple(
335
+ _plate_reader_growth_plot_output(
336
+ protocol,
337
+ output_id=output_id,
338
+ growth_channel=growth_channel,
339
+ )
340
+ for output_id in selected_plots
341
+ )
342
+ selected_exports = protocol.select_export_outputs(defaults=(), allowed=set())
343
+ if selected_exports:
344
+ raise ConfigError("plate_reader/growth_screen does not currently compile export artifacts.")
345
+ return CompiledProtocolPlan(
346
+ pipeline=pipeline,
347
+ plots=plots,
348
+ exports=(),
349
+ semantic_program=_plate_reader_growth_semantic_program(
350
+ protocol,
351
+ growth_channel=growth_channel,
352
+ ),
353
+ )
354
+
355
+
356
+ def _configured_ingest_channels(protocol: Any, *, required: tuple[str, ...]) -> list[str]:
357
+ ingest = _input_mapping(protocol, key="ingest")
358
+ configured = ingest.get("channels")
359
+ if configured in (None, ()):
360
+ return list(required)
361
+ if not isinstance(configured, list) or any(not isinstance(item, str) or not item.strip() for item in configured):
362
+ raise ConfigError(f"protocol.inputs.ingest.channels for {protocol.id!r} must be a list of non-empty strings")
363
+ channels = [item.strip() for item in configured]
364
+ missing = [channel for channel in required if channel not in channels]
365
+ if missing:
366
+ missing_text = ", ".join(missing)
367
+ raise ConfigError(
368
+ f"protocol.inputs.ingest.channels for {protocol.id!r} must include required channel(s): {missing_text}"
369
+ )
370
+ return channels
371
+
372
+
373
+ def _configured_fold_change_target(protocol: Any, *, expected: str) -> str:
374
+ fold_change = _input_mapping(protocol, key="fold_change")
375
+ configured = fold_change.get("target")
376
+ if configured is None:
377
+ return expected
378
+ if not isinstance(configured, str) or not configured.strip():
379
+ raise ConfigError(f"protocol.inputs.fold_change.target for {protocol.id!r} must be a non-empty string")
380
+ target = configured.strip()
381
+ if target != expected:
382
+ raise ConfigError(
383
+ f"protocol.inputs.fold_change.target for {protocol.id!r} must match the compiled assay ratio "
384
+ f"{expected!r}, not {target!r}"
385
+ )
386
+ return target
387
+
388
+
389
+ def _configured_fold_change_enabled(protocol: Any, *, analysis: dict[str, Any]) -> bool:
390
+ if "include_fold_change" in analysis:
391
+ return _analysis_bool(analysis, key="include_fold_change", default=False)
392
+ fold_change = _input_mapping(protocol, key="fold_change")
393
+ return "report_times" in fold_change
394
+
395
+
396
+ def _configured_fold_change_report_times(protocol: Any) -> tuple[float, ...]:
397
+ fold_change = _input_mapping(protocol, key="fold_change")
398
+ configured = fold_change.get("report_times")
399
+ where = f"protocol.inputs.fold_change.report_times for {protocol.id!r}"
400
+ if not isinstance(configured, list) or not configured:
401
+ raise ConfigError(f"{where} must be an explicit non-empty list of finite hours")
402
+ report_times: list[float] = []
403
+ for value in configured:
404
+ if isinstance(value, bool) or not isinstance(value, int | float) or not math.isfinite(float(value)):
405
+ raise ConfigError(f"{where} must contain only finite numbers")
406
+ report_times.append(float(value))
407
+ return tuple(report_times)
408
+
409
+
410
+ def _require_plot_time(*, settings: dict[str, Any], output_id: str, key: str) -> float:
411
+ where = f"protocol.outputs.plots.views.{output_id}.{key}"
412
+ if key not in settings:
413
+ raise ConfigError(f"{where} must be explicit")
414
+ value = settings[key]
415
+ if isinstance(value, bool) or not isinstance(value, int | float) or not math.isfinite(float(value)):
416
+ raise ConfigError(f"{where} must be a finite number")
417
+ return float(value)
418
+
419
+
420
+ def _plate_reader_plot_output_ids(*, measurement: str) -> set[str]:
421
+ if measurement != "yfp_cfp":
422
+ raise ConfigError(f"Unsupported plate-reader measurement {measurement!r}")
423
+ return {
424
+ "dual_reporter_triptych",
425
+ "raw_kinetics",
426
+ "endpoint_by_condition",
427
+ "endpoint_by_design",
428
+ "state_summary",
429
+ "intensity_overview",
430
+ "ratio_overview",
431
+ "value_distributions",
432
+ "ratio_heatmap",
433
+ "support_heatmap",
434
+ }
435
+
436
+
437
+ def _plate_reader_single_reporter_plot_output_ids() -> set[str]:
438
+ return {
439
+ "raw_kinetics",
440
+ "single_reporter_diagnostic",
441
+ "endpoint_by_condition",
442
+ "endpoint_by_design",
443
+ "intensity_overview",
444
+ "subject_comparison",
445
+ "value_distributions",
446
+ }
447
+
448
+
449
+ def _plate_reader_growth_plot_output_ids() -> set[str]:
450
+ return {
451
+ "raw_kinetics",
452
+ "endpoint_by_condition",
453
+ "endpoint_by_design",
454
+ "growth_overview",
455
+ "value_distributions",
456
+ }
457
+
458
+
459
+ def _plate_reader_fold_change_step(*, measurement: str) -> PluginStepDecl:
460
+ if measurement != "yfp_cfp":
461
+ raise ConfigError(f"Unsupported fold-change measurement {measurement!r}")
462
+ return _step(
463
+ id="fold_change__yfp_over_cfp",
464
+ plugin="transform/fold_change",
465
+ reads={"df": RecordInputDecl(record_id="ratio_yfp_cfp/df")},
466
+ writes={"table": RecordOutputDecl(record_id="fold_change__yfp_over_cfp/table")},
467
+ )
468
+
469
+
470
+ def _plate_reader_single_reporter_fold_change_step(
471
+ *,
472
+ reporter_channel: str,
473
+ normalizer_channel: str,
474
+ ) -> PluginStepDecl:
475
+ return _step(
476
+ id="fold_change__single_reporter",
477
+ plugin="transform/fold_change",
478
+ reads={"df": RecordInputDecl(record_id="sample_measurements/df")},
479
+ with_={
480
+ "target": _single_reporter_ratio_label(
481
+ reporter_channel=reporter_channel, normalizer_channel=normalizer_channel
482
+ )
483
+ },
484
+ writes={"table": RecordOutputDecl(record_id="fold_change__single_reporter/table")},
485
+ )
486
+
487
+
488
+ def _plate_reader_crosstalk_pairs_step(*, config: dict[str, Any]) -> PluginStepDecl:
489
+ defaults = {
490
+ "value_column": "log2FC",
491
+ "value_scale": "log2",
492
+ "target": "YFP/CFP",
493
+ "time_mode": "all",
494
+ "design_column": "design_id",
495
+ "treatment_column": "treatment",
496
+ "mapping_mode": "explicit",
497
+ "require_self_treatment": True,
498
+ "require_self_is_top1": True,
499
+ "min_self": 1.0,
500
+ "max_cross": 0.5,
501
+ "min_selectivity_delta": 1.0,
502
+ }
503
+ with_block = _deep_merge(
504
+ defaults, {key: value for key, value in config.items() if key not in {"enabled", "export"}}
505
+ )
506
+ if with_block["target"] != "YFP/CFP":
507
+ raise ConfigError(
508
+ "protocol.analysis.crosstalk_pairs.target must remain 'YFP/CFP' for plate_reader/dual_reporter_screen"
509
+ )
510
+ return _step(
511
+ id="crosstalk_pairs",
512
+ plugin="transform/crosstalk_pairs",
513
+ reads={"table": RecordInputDecl(record_id="fold_change__yfp_over_cfp/table")},
514
+ with_=with_block,
515
+ writes={"table": RecordOutputDecl(record_id="crosstalk_pairs/table")},
516
+ )
517
+
518
+
519
+ def _plate_reader_plot_output(protocol: Any, *, output_id: str, measurement: str) -> PluginStepDecl:
520
+ settings = protocol.plot_view_config(figure_id=output_id)
521
+ plot_reads = _plate_reader_plot_reads(measurement=measurement)
522
+ if output_id == "dual_reporter_triptych":
523
+ if "snapshot_time_h" not in settings:
524
+ raise ConfigError("protocol.outputs.plots.views.dual_reporter_triptych.snapshot_time_h must be explicit")
525
+ snapshot_time_h = settings["snapshot_time_h"]
526
+ if (
527
+ isinstance(snapshot_time_h, bool)
528
+ or not isinstance(snapshot_time_h, int | float)
529
+ or not math.isfinite(float(snapshot_time_h))
530
+ ):
531
+ raise ConfigError(
532
+ "protocol.outputs.plots.views.dual_reporter_triptych.snapshot_time_h must be a finite number"
533
+ )
534
+ defaults = {
535
+ "design_column": "design_id",
536
+ "treatment_column": "treatment",
537
+ "time_column": "time",
538
+ "growth_channel": "OD600",
539
+ "ratio_channel": "YFP/CFP",
540
+ "snapshot_channel": "YFP/CFP",
541
+ "snapshot_time_mode": "nearest",
542
+ "snapshot_time_tolerance_h": 0.51,
543
+ }
544
+ return _step(
545
+ id="dual_reporter_triptych",
546
+ plugin="plot/dual_reporter_triptych",
547
+ reads={"df": plot_reads["df"]},
548
+ with_=_deep_merge(defaults, settings),
549
+ )
550
+ if output_id == "raw_kinetics":
551
+ defaults = {
552
+ "partition": {"by": "design_id"},
553
+ "hue": "treatment",
554
+ "y": ["OD600", "YFP", "YFP/CFP", "YFP/OD600"],
555
+ "add_sheet_line": True,
556
+ }
557
+ return _step(
558
+ id="raw_kinetics",
559
+ plugin="plot/time_series",
560
+ reads={"df": plot_reads["df"]},
561
+ with_=_deep_merge(defaults, settings),
562
+ )
563
+ if output_id == "endpoint_by_condition":
564
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
565
+ defaults = {
566
+ "x": "treatment",
567
+ "y": ["OD600", "YFP/OD600"],
568
+ "partition": {"by": "design_id"},
569
+ }
570
+ return _step(
571
+ id="endpoint_by_condition",
572
+ plugin="plot/snapshot_barplot",
573
+ reads={"df": plot_reads["df"]},
574
+ with_=_deep_merge(defaults, settings),
575
+ )
576
+ if output_id == "endpoint_by_design":
577
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
578
+ defaults = {
579
+ "x": "design_id",
580
+ "y": "YFP/OD600",
581
+ "hue": "treatment",
582
+ }
583
+ return _step(
584
+ id="endpoint_by_design",
585
+ plugin="plot/snapshot_barplot",
586
+ reads={"df": plot_reads["df"]},
587
+ with_=_deep_merge(defaults, settings),
588
+ )
589
+ if output_id == "state_summary":
590
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
591
+ defaults = {
592
+ "x": "treatment_alias",
593
+ "y": ["OD600", "CFP/OD600", "YFP/OD600", "YFP/CFP"],
594
+ "partition": {"by": "design_id_alias"},
595
+ }
596
+ return _step(
597
+ id="state_summary",
598
+ plugin="plot/snapshot_barplot",
599
+ reads={"df": plot_reads["df"]},
600
+ with_=_deep_merge(defaults, settings),
601
+ )
602
+ if output_id == "intensity_overview":
603
+ _require_plot_time(settings=settings, output_id=output_id, key="snap_time")
604
+ defaults = {
605
+ "partition": {"by": "design_id"},
606
+ "ts_channel": "YFP/OD600",
607
+ "ts_hue": "treatment",
608
+ "ts_add_sheet_line": True,
609
+ "ts_mark_snap_time": True,
610
+ "snap_channel": "YFP/OD600",
611
+ }
612
+ return _step(
613
+ id="intensity_overview",
614
+ plugin="plot/ts_and_snap",
615
+ reads={"df": plot_reads["df"]},
616
+ with_=_deep_merge(defaults, settings),
617
+ )
618
+ if output_id == "ratio_overview":
619
+ _require_plot_time(settings=settings, output_id=output_id, key="snap_time")
620
+ defaults = {
621
+ "partition": {"by": "design_id_alias"},
622
+ "ts_channel": "OD600",
623
+ "ts_hue": "treatment_alias",
624
+ "ts_add_sheet_line": True,
625
+ "ts_mark_snap_time": True,
626
+ "snap_x": "treatment_alias",
627
+ "snap_channel": "YFP/CFP",
628
+ }
629
+ return _step(
630
+ id="ratio_overview",
631
+ plugin="plot/ts_and_snap",
632
+ reads={"df": plot_reads["df"]},
633
+ with_=_deep_merge(defaults, settings),
634
+ )
635
+ if output_id == "value_distributions":
636
+ defaults = {"channels": ["YFP/CFP"], "partition": {"by": "design_id"}}
637
+ return _step(
638
+ id="value_distributions",
639
+ plugin="plot/distributions",
640
+ reads={"df": plot_reads["df"], "blanks": plot_reads["blanks"]},
641
+ with_=_deep_merge(defaults, settings),
642
+ )
643
+ if output_id == "ratio_heatmap":
644
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
645
+ defaults = {
646
+ "channel": "YFP/CFP",
647
+ "x": "treatment_alias",
648
+ "y": "design_id_alias",
649
+ }
650
+ return _step(
651
+ id="ratio_heatmap",
652
+ plugin="plot/snapshot_heatmap",
653
+ reads={"df": plot_reads["df"]},
654
+ with_=_deep_merge(defaults, settings),
655
+ )
656
+ if output_id == "support_heatmap":
657
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
658
+ defaults = {
659
+ "channel": "CFP/OD600",
660
+ "x": "treatment_alias",
661
+ "y": "design_id_alias",
662
+ }
663
+ return _step(
664
+ id="support_heatmap",
665
+ plugin="plot/snapshot_heatmap",
666
+ reads={"df": plot_reads["df"]},
667
+ with_=_deep_merge(defaults, settings),
668
+ )
669
+ raise ConfigError(f"Unknown plate-reader plot output {output_id!r}")
670
+
671
+
672
+ def _plate_reader_single_reporter_plot_output(
673
+ protocol: Any,
674
+ *,
675
+ output_id: str,
676
+ reporter_channel: str,
677
+ normalizer_channel: str,
678
+ ) -> PluginStepDecl:
679
+ settings = protocol.plot_view_config(figure_id=output_id)
680
+ plot_reads = _plate_reader_single_reporter_plot_reads()
681
+ ratio_label = _single_reporter_ratio_label(
682
+ reporter_channel=reporter_channel,
683
+ normalizer_channel=normalizer_channel,
684
+ )
685
+ if output_id == "single_reporter_diagnostic":
686
+ compiler_owned = {
687
+ "endpoint_time_h",
688
+ "endpoint_time_tolerance_h",
689
+ "normalizer_channel",
690
+ "observation_aggregation",
691
+ "reporter_channel",
692
+ "ratio_channel",
693
+ "summary_stat",
694
+ "temporal_reduction",
695
+ "time_column",
696
+ "window_h",
697
+ }
698
+ overridden = sorted(compiler_owned.intersection(settings))
699
+ if overridden:
700
+ raise ConfigError(
701
+ "protocol.outputs.plots.views.single_reporter_diagnostic cannot override compiler-owned fields "
702
+ f"{overridden}; configure protocol.analysis temporal_reduction, observation_aggregation, "
703
+ "reporter_channel, and normalizer_channel instead"
704
+ )
705
+ temporal_reduction, observation_aggregation = _single_reporter_diagnostic_policy(protocol)
706
+ defaults = {
707
+ **({} if "partition" in settings else {"partition": {"by": "design_id"}}),
708
+ "identity_scope": {"entity_columns": ["design_id"]},
709
+ "condition_column": "treatment",
710
+ "temporal_reduction": temporal_reduction,
711
+ "observation_aggregation": observation_aggregation,
712
+ "time_column": "time",
713
+ "normalizer_channel": normalizer_channel,
714
+ "reporter_channel": reporter_channel,
715
+ "ratio_channel": ratio_label,
716
+ }
717
+ return _step(
718
+ id="single_reporter_diagnostic",
719
+ plugin="plot/single_reporter_diagnostic",
720
+ reads={"df": RecordInputDecl(record_id="sample_measurements/df")},
721
+ with_=_deep_merge(defaults, settings),
722
+ )
723
+ if output_id == "raw_kinetics":
724
+ defaults = {
725
+ "partition": {"by": "design_id"},
726
+ "hue": "treatment",
727
+ "y": [normalizer_channel, reporter_channel, ratio_label],
728
+ "add_sheet_line": True,
729
+ }
730
+ return _step(
731
+ id="raw_kinetics",
732
+ plugin="plot/time_series",
733
+ reads={"df": plot_reads["df"]},
734
+ with_=_deep_merge(defaults, settings),
735
+ )
736
+ if output_id == "endpoint_by_condition":
737
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
738
+ defaults = {
739
+ "x": "treatment",
740
+ "y": [normalizer_channel, ratio_label],
741
+ "partition": {"by": "design_id"},
742
+ }
743
+ return _step(
744
+ id="endpoint_by_condition",
745
+ plugin="plot/snapshot_barplot",
746
+ reads={"df": plot_reads["df"]},
747
+ with_=_deep_merge(defaults, settings),
748
+ )
749
+ if output_id == "endpoint_by_design":
750
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
751
+ defaults = {
752
+ "x": "design_id",
753
+ "y": ratio_label,
754
+ "hue": "treatment",
755
+ }
756
+ return _step(
757
+ id="endpoint_by_design",
758
+ plugin="plot/snapshot_barplot",
759
+ reads={"df": plot_reads["df"]},
760
+ with_=_deep_merge(defaults, settings),
761
+ )
762
+ if output_id == "intensity_overview":
763
+ _require_plot_time(settings=settings, output_id=output_id, key="snap_time")
764
+ defaults = {
765
+ "partition": {"by": "design_id"},
766
+ "ts_channel": ratio_label,
767
+ "ts_hue": "treatment",
768
+ "ts_add_sheet_line": True,
769
+ "ts_mark_snap_time": True,
770
+ "snap_channel": ratio_label,
771
+ }
772
+ return _step(
773
+ id="intensity_overview",
774
+ plugin="plot/ts_and_snap",
775
+ reads={"df": plot_reads["df"]},
776
+ with_=_deep_merge(defaults, settings),
777
+ )
778
+ if output_id == "subject_comparison":
779
+ _require_plot_time(settings=settings, output_id=output_id, key="snap_time")
780
+ defaults = {
781
+ "ts_channel": normalizer_channel,
782
+ "ts_hue": "treatment",
783
+ "ts_style": "design_id",
784
+ "snap_x": "design_id",
785
+ "snap_channel": ratio_label,
786
+ "snap_hue": "treatment",
787
+ "square_panels": True,
788
+ "title": "Subject comparison",
789
+ }
790
+ return _step(
791
+ id="subject_comparison",
792
+ plugin="plot/ts_and_snap",
793
+ reads={"df": plot_reads["df"]},
794
+ with_=_deep_merge(defaults, settings),
795
+ )
796
+ if output_id == "value_distributions":
797
+ defaults = {"channels": [ratio_label], "partition": {"by": "design_id"}}
798
+ return _step(
799
+ id="value_distributions",
800
+ plugin="plot/distributions",
801
+ reads={"df": plot_reads["df"], "blanks": plot_reads["blanks"]},
802
+ with_=_deep_merge(defaults, settings),
803
+ )
804
+ raise ConfigError(f"Unknown single-reporter plot output {output_id!r}")
805
+
806
+
807
+ def _plate_reader_growth_plot_output(
808
+ protocol: Any,
809
+ *,
810
+ output_id: str,
811
+ growth_channel: str,
812
+ ) -> PluginStepDecl:
813
+ settings = protocol.plot_view_config(figure_id=output_id)
814
+ reads = {
815
+ "df": RecordInputDecl(record_id="sample_measurements/df"),
816
+ "blanks": RecordInputDecl(record_id="blank/blanks"),
817
+ }
818
+ if output_id == "raw_kinetics":
819
+ defaults = {
820
+ "partition": {"by": "design_id"},
821
+ "hue": "treatment",
822
+ "y": [growth_channel],
823
+ "add_sheet_line": True,
824
+ }
825
+ return _step(
826
+ id=output_id,
827
+ plugin="plot/time_series",
828
+ reads={"df": reads["df"]},
829
+ with_=_deep_merge(defaults, settings),
830
+ )
831
+ if output_id == "endpoint_by_condition":
832
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
833
+ defaults = {
834
+ "x": "treatment",
835
+ "y": growth_channel,
836
+ "partition": {"by": "design_id"},
837
+ }
838
+ return _step(
839
+ id=output_id,
840
+ plugin="plot/snapshot_barplot",
841
+ reads={"df": reads["df"]},
842
+ with_=_deep_merge(defaults, settings),
843
+ )
844
+ if output_id == "endpoint_by_design":
845
+ _require_plot_time(settings=settings, output_id=output_id, key="time")
846
+ defaults = {
847
+ "x": "design_id",
848
+ "y": growth_channel,
849
+ "hue": "treatment",
850
+ }
851
+ return _step(
852
+ id=output_id,
853
+ plugin="plot/snapshot_barplot",
854
+ reads={"df": reads["df"]},
855
+ with_=_deep_merge(defaults, settings),
856
+ )
857
+ if output_id == "growth_overview":
858
+ _require_plot_time(settings=settings, output_id=output_id, key="snap_time")
859
+ defaults = {
860
+ "partition": {"by": "design_id"},
861
+ "ts_channel": growth_channel,
862
+ "ts_hue": "treatment",
863
+ "ts_add_sheet_line": True,
864
+ "ts_mark_snap_time": True,
865
+ "snap_channel": growth_channel,
866
+ }
867
+ return _step(
868
+ id=output_id,
869
+ plugin="plot/ts_and_snap",
870
+ reads={"df": reads["df"]},
871
+ with_=_deep_merge(defaults, settings),
872
+ )
873
+ if output_id == "value_distributions":
874
+ defaults = {"channels": [growth_channel], "partition": {"by": "design_id"}}
875
+ return _step(
876
+ id=output_id,
877
+ plugin="plot/distributions",
878
+ reads=reads,
879
+ with_=_deep_merge(defaults, settings),
880
+ )
881
+ raise ConfigError(f"Unknown growth-screen plot output {output_id!r}")
882
+
883
+
884
+ def _single_reporter_diagnostic_policy(protocol: Any) -> tuple[dict[str, object], dict[str, str]]:
885
+ analysis = _analysis_options(protocol)
886
+ raw_temporal = analysis.get("temporal_reduction")
887
+ raw_aggregation = analysis.get("observation_aggregation")
888
+ if raw_temporal is None or raw_aggregation is None:
889
+ raise ConfigError(
890
+ "plate_reader/single_reporter_screen requires protocol.analysis.temporal_reduction and "
891
+ "protocol.analysis.observation_aggregation together when either policy is authored or "
892
+ "single_reporter_diagnostic is selected"
893
+ )
894
+ try:
895
+ temporal = TemporalReductionSpec.from_mapping(raw_temporal)
896
+ aggregation = ObservationAggregationSpec.from_mapping(raw_aggregation)
897
+ except ValueError as exc:
898
+ raise ConfigError(f"invalid single-reporter diagnostic reduction policy: {exc}") from exc
899
+ if temporal.selection.time_basis != "absolute":
900
+ raise ConfigError(
901
+ "plate_reader/single_reporter_screen temporal_reduction.selection.time_basis must be 'absolute'; "
902
+ "event-relative timing belongs to a protocol that declares an event"
903
+ )
904
+ return temporal.to_mapping(), aggregation.to_mapping()
905
+
906
+
907
+ def _plate_reader_export_output(protocol: Any, *, output_id: str) -> PluginStepDecl:
908
+ if output_id == "crosstalk_pairs_table":
909
+ defaults = {"path": "crosstalk_pairs.csv"}
910
+ settings = protocol.export_artifact_config(artifact_id=output_id)
911
+ return _step(
912
+ id="crosstalk_pairs_table",
913
+ plugin="export/csv",
914
+ reads={"df": RecordInputDecl(record_id="crosstalk_pairs/table")},
915
+ with_=_deep_merge(defaults, settings),
916
+ )
917
+ raise ConfigError(f"Unknown plate-reader export output {output_id!r}")
918
+
919
+
920
+ def _plate_reader_plot_reads(*, measurement: str) -> dict[str, RecordInputDecl]:
921
+ if measurement != "yfp_cfp":
922
+ raise ConfigError(f"Unsupported plate-reader measurement {measurement!r}")
923
+ return {
924
+ "df": RecordInputDecl(record_id="ratio_yfp_od600/df"),
925
+ "blanks": RecordInputDecl(record_id="blank/blanks"),
926
+ }
927
+
928
+
929
+ def _plate_reader_single_reporter_plot_reads() -> dict[str, RecordInputDecl]:
930
+ return {
931
+ "df": RecordInputDecl(record_id="ratio_reporter_normalizer/df"),
932
+ "blanks": RecordInputDecl(record_id="blank/blanks"),
933
+ }
934
+
935
+
936
+ def _single_reporter_ratio_label(*, reporter_channel: str, normalizer_channel: str) -> str:
937
+ return f"{reporter_channel}/{normalizer_channel}"