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,70 @@
1
+ """Alias mappings for categorical columns. Either replace in-place or create
2
+ <column>_alias columns. Prints a succinct per-column summary of applied aliases."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Mapping
7
+
8
+ from reader_workbench.plugins.transform._labeling import apply_label_mappings
9
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
10
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
11
+
12
+
13
+ class AliasCfg(PluginConfig):
14
+ """
15
+ mappings:
16
+ <column_name>:
17
+ <raw_value>: <alias_value>
18
+ ...
19
+ in_place: if true, mutate <column_name> directly; else create <column_name>_alias
20
+ case_insensitive: map using casefold() on incoming values (keys in 'aliases' are matched case-insensitively)
21
+ """
22
+
23
+ mappings: Mapping[str, Mapping[str, str]] | None = None
24
+ in_place: bool = False
25
+ case_insensitive: bool = True
26
+ suffix: str = "_alias"
27
+
28
+
29
+ class AliasTransform(Plugin):
30
+ ConfigModel = AliasCfg
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 cls.passthrough_output_ports(
39
+ outputs={"df": dataframe_output("df", "tidy.v1")},
40
+ passthrough={"df": "df"},
41
+ promoted_examples={"df": ("plate_reader.annotated.v1",)},
42
+ )
43
+
44
+ def resolve_output_ports(self, *, inputs, outputs, cfg, where):
45
+ del cfg
46
+ return self.inherit_dataframe_output_ports(
47
+ inputs=inputs,
48
+ outputs=outputs,
49
+ passthrough={"df": "df"},
50
+ where=where,
51
+ )
52
+
53
+ def run(self, ctx, inputs, cfg: AliasCfg):
54
+ if cfg.mappings is None:
55
+ raise ValueError("alias: provide with.mappings")
56
+ if not isinstance(cfg.mappings, Mapping):
57
+ raise ValueError("alias: mappings must be a mapping of column -> {raw: alias}")
58
+ mappings = {str(col): mapping for col, mapping in cfg.mappings.items()}
59
+ output_names = {str(col): f"{col}{cfg.suffix}" for col in mappings}
60
+ return {
61
+ "df": apply_label_mappings(
62
+ ctx=ctx,
63
+ df=inputs["df"],
64
+ mappings=mappings,
65
+ output_names=output_names,
66
+ in_place=cfg.in_place,
67
+ case_insensitive=cfg.case_insensitive,
68
+ label="alias",
69
+ )
70
+ }
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from reader_workbench.plugins.transform._labeling import apply_label_mappings
4
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
5
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
6
+
7
+
8
+ class AnnotationLabelsCfg(PluginConfig):
9
+ refs: list[str] | None = None
10
+ in_place: bool = False
11
+ case_insensitive: bool = True
12
+ suffix: str = "_alias"
13
+
14
+
15
+ class AnnotationLabelsTransform(Plugin):
16
+ ConfigModel = AnnotationLabelsCfg
17
+
18
+ @classmethod
19
+ def input_ports(cls):
20
+ return {"df": dataframe_input("df", "tidy.v1")}
21
+
22
+ @classmethod
23
+ def output_ports(cls):
24
+ return cls.passthrough_output_ports(
25
+ outputs={"df": dataframe_output("df", "tidy.v1")},
26
+ passthrough={"df": "df"},
27
+ promoted_examples={"df": ("plate_reader.annotated.v1",)},
28
+ )
29
+
30
+ def resolve_output_ports(self, *, inputs, outputs, cfg, where):
31
+ del cfg
32
+ return self.inherit_dataframe_output_ports(
33
+ inputs=inputs,
34
+ outputs=outputs,
35
+ passthrough={"df": "df"},
36
+ where=where,
37
+ )
38
+
39
+ def run(self, ctx, inputs, cfg: AnnotationLabelsCfg):
40
+ if ctx.experiment is None:
41
+ raise ValueError("assay_labels requires experiment annotations in the run context")
42
+ label_specs = ctx.experiment.annotations.resolve_label_specs(cfg.refs)
43
+ if not label_specs:
44
+ raise ValueError("assay_labels: no annotations.labels are configured")
45
+
46
+ mappings: dict[str, dict[str, str]] = {}
47
+ output_names: dict[str, str] = {}
48
+ for spec in label_specs:
49
+ mappings[spec.source] = dict(spec.values)
50
+ output_names[spec.source] = spec.output or f"{spec.source}{cfg.suffix}"
51
+
52
+ return {
53
+ "df": apply_label_mappings(
54
+ ctx=ctx,
55
+ df=inputs["df"],
56
+ mappings=mappings,
57
+ output_names=output_names,
58
+ in_place=cfg.in_place,
59
+ case_insensitive=cfg.case_insensitive,
60
+ label="assay_labels",
61
+ )
62
+ }
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+
5
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
6
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
7
+
8
+
9
+ class BlankCfg(PluginConfig):
10
+ method: str = "disregard" # disregard | subtract
11
+ capture_blanks: bool = True
12
+
13
+
14
+ def _detect_blanks(df: pd.DataFrame) -> pd.DataFrame:
15
+ out = df.copy()
16
+ mask = False
17
+ for col in ["is_blank", "blank", "isBlank"]:
18
+ if col in out.columns:
19
+ try:
20
+ mask = mask | out[col].astype(bool)
21
+ except Exception:
22
+ mask = mask | out[col].astype(str).str.lower().isin({"true", "1", "t", "yes"})
23
+ for col in ["treatment", "genotype", "sample_type"]:
24
+ if col in out.columns:
25
+ mask = mask | out[col].astype(str).str.contains("blank", case=False, na=False)
26
+ return out[mask].copy() if isinstance(mask, pd.Series) else out.iloc[0:0].copy()
27
+
28
+
29
+ class BlankCorrection(Plugin):
30
+ ConfigModel = BlankCfg
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 cls.passthrough_output_ports(
39
+ outputs={
40
+ "df": dataframe_output("df", "tidy.v1"),
41
+ "blanks": dataframe_output("blanks", "tidy.v1"),
42
+ },
43
+ passthrough={"df": "df", "blanks": "df"},
44
+ promoted_examples={
45
+ "df": ("plate_reader.annotated.v1",),
46
+ "blanks": ("plate_reader.annotated.v1",),
47
+ },
48
+ )
49
+
50
+ def resolve_output_ports(self, *, inputs, outputs, cfg, where):
51
+ del cfg
52
+ return self.inherit_dataframe_output_ports(
53
+ inputs=inputs,
54
+ outputs=outputs,
55
+ passthrough={"df": "df", "blanks": "df"},
56
+ where=where,
57
+ )
58
+
59
+ def run(self, ctx, inputs, cfg: BlankCfg):
60
+ df: pd.DataFrame = inputs["df"].copy()
61
+ blanks = _detect_blanks(df)
62
+ if cfg.method == "disregard":
63
+ return {"df": df, "blanks": blanks}
64
+ if cfg.method == "subtract":
65
+ if blanks.empty:
66
+ return {"df": df, "blanks": blanks}
67
+ corr = (
68
+ blanks.assign(value=pd.to_numeric(blanks["value"], errors="coerce"))
69
+ .groupby("channel")["value"]
70
+ .median()
71
+ .rename("__blank__")
72
+ )
73
+ out = df.copy()
74
+ out["value"] = pd.to_numeric(out["value"], errors="coerce")
75
+ out = out.join(corr, on="channel")
76
+ out["value"] = out["value"] - out["__blank__"].fillna(0.0)
77
+ out = out.drop(columns="__blank__")
78
+ return {"df": out, "blanks": blanks}
79
+ raise ValueError(f"unknown method {cfg.method}")
@@ -0,0 +1,180 @@
1
+ """Crosstalk pairing transform over fold_change.v1 tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ import pandas as pd
8
+ from pydantic import Field
9
+
10
+ from reader_workbench.domains.logic.crosstalk import compute_crosstalk_pairs
11
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
12
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
13
+
14
+
15
+ def _pick_alias(df: pd.DataFrame, base: str | None) -> str | None:
16
+ if not base:
17
+ return None
18
+ alias = f"{base}_alias"
19
+ if base in df.columns:
20
+ return base
21
+ if alias in df.columns:
22
+ return alias
23
+ return None
24
+
25
+
26
+ class CrosstalkPairsCfg(PluginConfig):
27
+ value_column: str = "log2FC"
28
+ value_scale: Literal["log2", "linear"] = Field(..., description="Scale of value_column.")
29
+
30
+ target: str | None = None
31
+ time_mode: Literal["single", "exact", "nearest", "latest", "all"] = Field(
32
+ ..., description="How to select time(s) from the fold_change table."
33
+ )
34
+ time_policy: Literal["per_time", "all"] = Field(
35
+ "per_time", description="How to handle multiple times after selection."
36
+ )
37
+ time: float | None = None
38
+ times: list[float] | None = None
39
+ time_tolerance: float = 0.51
40
+ time_column: str = "time"
41
+
42
+ mapping_mode: Literal["explicit", "column", "top1"] = Field(
43
+ ..., description="How to map design_id -> cognate treatment."
44
+ )
45
+ design_column: str = "design_id"
46
+ treatment_column: str = "treatment"
47
+ design_treatment_column: str | None = None
48
+ design_treatment_map: dict[str, str] = Field(default_factory=dict)
49
+ design_treatment_overrides: dict[str, str] = Field(default_factory=dict)
50
+ top1_tie_policy: Literal["error", "alphabetical"] = "error"
51
+ top1_tie_tolerance: float = 0.0
52
+
53
+ agg: Literal["median", "mean"] = "median"
54
+
55
+ require_self_treatment: bool = True
56
+ require_self_is_top1: bool = False
57
+
58
+ min_self: float | None = None
59
+ max_cross: float | None = None
60
+ max_other: float | None = None
61
+ min_self_minus_best_other: float | None = None
62
+ min_self_ratio_best_other: float | None = None
63
+ min_selectivity_delta: float | None = None
64
+ min_selectivity_ratio: float | None = None
65
+
66
+ only_passing: bool = True
67
+ top_n: int = 10
68
+
69
+
70
+ class CrosstalkPairs(Plugin):
71
+ """Compute crosstalk-safe design pairings from a fold_change.v1 table."""
72
+
73
+ ConfigModel = CrosstalkPairsCfg
74
+
75
+ @classmethod
76
+ def input_ports(cls):
77
+ return {"table": dataframe_input("table", "fold_change.v1")}
78
+
79
+ @classmethod
80
+ def output_ports(cls):
81
+ return {"table": dataframe_output("table", "crosstalk_pairs.v1")}
82
+
83
+ def run(self, ctx, inputs: dict[str, Any], cfg: CrosstalkPairsCfg) -> dict[str, Any]:
84
+ df = inputs["table"].copy()
85
+
86
+ dcol = _pick_alias(df, cfg.design_column)
87
+ tcol = _pick_alias(df, cfg.treatment_column)
88
+ if dcol is None:
89
+ raise ValueError(f"crosstalk_pairs: design column '{cfg.design_column}' (or its alias) is missing")
90
+ if tcol is None:
91
+ raise ValueError(f"crosstalk_pairs: treatment column '{cfg.treatment_column}' (or its alias) is missing")
92
+
93
+ map_col = _pick_alias(df, cfg.design_treatment_column) if cfg.design_treatment_column else None
94
+
95
+ result = compute_crosstalk_pairs(
96
+ df,
97
+ design_col=dcol,
98
+ treatment_col=tcol,
99
+ value_col=cfg.value_column,
100
+ value_scale=cfg.value_scale,
101
+ target=cfg.target,
102
+ time_mode=cfg.time_mode,
103
+ time_policy=cfg.time_policy,
104
+ time=cfg.time,
105
+ times=cfg.times,
106
+ time_column=cfg.time_column,
107
+ time_tolerance=cfg.time_tolerance,
108
+ mapping_mode=cfg.mapping_mode,
109
+ design_treatment_column=map_col,
110
+ design_treatment_map=cfg.design_treatment_map,
111
+ design_treatment_overrides=cfg.design_treatment_overrides,
112
+ top1_tie_policy=cfg.top1_tie_policy,
113
+ top1_tie_tolerance=cfg.top1_tie_tolerance,
114
+ require_self_treatment=cfg.require_self_treatment,
115
+ require_self_is_top1=cfg.require_self_is_top1,
116
+ agg=cfg.agg,
117
+ min_self=cfg.min_self,
118
+ max_cross=cfg.max_cross,
119
+ max_other=cfg.max_other,
120
+ min_self_minus_best_other=cfg.min_self_minus_best_other,
121
+ min_self_ratio_best_other=cfg.min_self_ratio_best_other,
122
+ min_selectivity_delta=cfg.min_selectivity_delta,
123
+ min_selectivity_ratio=cfg.min_selectivity_ratio,
124
+ only_passing=cfg.only_passing,
125
+ logger=ctx.logger,
126
+ )
127
+
128
+ pairs = result.pairs
129
+ designs = result.designs
130
+
131
+ try:
132
+ total_designs = int(designs["design_id"].nunique()) if "design_id" in designs.columns else 0
133
+ time_count = len(result.times_used)
134
+ total_pairs = total_designs * (total_designs - 1) // 2 * max(1, time_count)
135
+ passing = int(pairs.shape[0])
136
+ ctx.logger.info(
137
+ "crosstalk_pairs - designs=%d - times=%d - pairs=%d - passing=%d - metric=%s",
138
+ total_designs,
139
+ time_count,
140
+ total_pairs,
141
+ passing,
142
+ cfg.value_column,
143
+ )
144
+ except Exception:
145
+ pass
146
+
147
+ try:
148
+ if not pairs.empty:
149
+ preview = pairs.copy()
150
+ preview["pair_score"] = pd.to_numeric(preview["pair_score"], errors="coerce")
151
+ preview = preview[preview["pair_score"].notna()].sort_values("pair_score", ascending=False)
152
+ if not preview.empty:
153
+ n_show = max(1, int(cfg.top_n))
154
+ ctx.logger.info("crosstalk_pairs - top %d pairs by pair_score:", n_show)
155
+ for _, row in preview.head(n_show).iterrows():
156
+ ctx.logger.info(
157
+ " - t=%s | %s <-> %s | self=%s/%s | cross=%s/%s | score=%.3g",
158
+ _fmt(row.get("time")),
159
+ row.get("design_a"),
160
+ row.get("design_b"),
161
+ _fmt(row.get("a_self_value")),
162
+ _fmt(row.get("b_self_value")),
163
+ _fmt(row.get("a_cross_to_b")),
164
+ _fmt(row.get("b_cross_to_a")),
165
+ float(row.get("pair_score")) if pd.notna(row.get("pair_score")) else float("nan"),
166
+ )
167
+ except Exception:
168
+ pass
169
+
170
+ return {"table": pairs}
171
+
172
+
173
+ def _fmt(val: object) -> str:
174
+ try:
175
+ f = float(val)
176
+ if pd.isna(f):
177
+ return "nan"
178
+ return f"{f:.3g}"
179
+ except Exception:
180
+ return str(val)
@@ -0,0 +1,120 @@
1
+ """Explicit cytometry gating transform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import Field, model_validator
8
+
9
+ from reader_workbench.domains.cytometry.analysis import GateSpec, ThresholdSpec
10
+ from reader_workbench.domains.cytometry.analysis.workflow import (
11
+ CytometryGatingRequest,
12
+ CytometryQCSpec,
13
+ run_cytometry_gating,
14
+ )
15
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
16
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
17
+
18
+
19
+ class CytometryGatingCfg(PluginConfig):
20
+ cells_x_channel: str = Field(min_length=1)
21
+ cells_y_channel: str = Field(min_length=1)
22
+ cells_x_range: tuple[float, float]
23
+ cells_y_range: tuple[float, float]
24
+ singlet_x_channel: str = Field(min_length=1)
25
+ singlet_y_channel: str = Field(min_length=1)
26
+ singlet_ratio_range: tuple[float, float]
27
+ cells_enabled: bool
28
+ singlets_enabled: bool
29
+ fluorescence_channel: str = Field(min_length=1)
30
+ threshold_mode: Literal["manual", "from_control_quantile"]
31
+ threshold_value: float | None
32
+ threshold_group_column: str | None
33
+ threshold_control_value: str | None
34
+ threshold_quantile: float | None
35
+ group_column: str | None
36
+ minimum_final_events: int = Field(ge=0)
37
+ minimum_final_percent: float = Field(ge=0.0, le=100.0)
38
+ maximum_nonpositive_percent: float = Field(ge=0.0, le=100.0)
39
+ nonpositive_scope: Literal["all_events", "gated_events"]
40
+
41
+ @model_validator(mode="after")
42
+ def _validate_threshold_policy(self):
43
+ if self.threshold_mode == "manual":
44
+ if self.threshold_value is None:
45
+ raise ValueError("threshold_value is required for manual thresholding")
46
+ if any(
47
+ value is not None
48
+ for value in (self.threshold_group_column, self.threshold_control_value, self.threshold_quantile)
49
+ ):
50
+ raise ValueError("manual thresholding may not declare control threshold fields")
51
+ return self
52
+ if self.threshold_value is not None:
53
+ raise ValueError("threshold_value must be null for control-quantile thresholding")
54
+ if not self.threshold_group_column:
55
+ raise ValueError("threshold_group_column is required for control-quantile thresholding")
56
+ if not self.threshold_control_value:
57
+ raise ValueError("threshold_control_value is required for control-quantile thresholding")
58
+ if self.threshold_quantile is None or not 0.0 <= self.threshold_quantile <= 1.0:
59
+ raise ValueError("threshold_quantile must be between 0 and 1 for control-quantile thresholding")
60
+ return self
61
+
62
+
63
+ class CytometryGatingTransform(Plugin):
64
+ ConfigModel = CytometryGatingCfg
65
+
66
+ @classmethod
67
+ def input_ports(cls):
68
+ return {"events": dataframe_input("events", "tidy.v1")}
69
+
70
+ @classmethod
71
+ def output_ports(cls):
72
+ return {
73
+ "gate_definition": dataframe_output("gate_definition", "cytometry.gate_definition.v1"),
74
+ "gated_events": dataframe_output("gated_events", "cytometry.gated_events.v1"),
75
+ "sample_stats": dataframe_output("sample_stats", "cytometry.sample_stats.v1"),
76
+ "group_stats": dataframe_output("group_stats", "cytometry.group_stats.v1"),
77
+ "qc": dataframe_output("qc", "cytometry.qc.v1"),
78
+ }
79
+
80
+ def run(self, ctx, inputs, cfg: CytometryGatingCfg):
81
+ del ctx
82
+ threshold = ThresholdSpec(
83
+ channel=cfg.fluorescence_channel,
84
+ value=float(cfg.threshold_value or 0.0),
85
+ mode=cfg.threshold_mode,
86
+ group_column=cfg.threshold_group_column,
87
+ control_value=cfg.threshold_control_value,
88
+ quantile=float(cfg.threshold_quantile or 0.0),
89
+ )
90
+ result = run_cytometry_gating(
91
+ inputs["events"],
92
+ CytometryGatingRequest(
93
+ gate=GateSpec(
94
+ cells_x_channel=cfg.cells_x_channel,
95
+ cells_y_channel=cfg.cells_y_channel,
96
+ cells_x_range=cfg.cells_x_range,
97
+ cells_y_range=cfg.cells_y_range,
98
+ singlet_x_channel=cfg.singlet_x_channel,
99
+ singlet_y_channel=cfg.singlet_y_channel,
100
+ singlet_ratio_range=cfg.singlet_ratio_range,
101
+ cells_enabled=cfg.cells_enabled,
102
+ singlets_enabled=cfg.singlets_enabled,
103
+ ),
104
+ threshold=threshold,
105
+ group_column=cfg.group_column,
106
+ qc=CytometryQCSpec(
107
+ minimum_final_events=cfg.minimum_final_events,
108
+ minimum_final_percent=cfg.minimum_final_percent,
109
+ maximum_nonpositive_percent=cfg.maximum_nonpositive_percent,
110
+ nonpositive_scope=cfg.nonpositive_scope,
111
+ ),
112
+ ),
113
+ )
114
+ return {
115
+ "gate_definition": result.gate_definition.to_pandas(),
116
+ "gated_events": result.gated_events.to_pandas(),
117
+ "sample_stats": result.sample_stats.to_pandas(),
118
+ "group_stats": result.group_stats.to_pandas(),
119
+ "qc": result.qc.to_pandas(),
120
+ }
@@ -0,0 +1,79 @@
1
+ """Fold-change report:
2
+ • Selects the nearest snapshot time(s) per group
3
+ • Computes FC against explicit baselines (global or per-group overrides)
4
+ • Emits a validated artifact (fold_change.v1) — no mutation of the main tidy df
5
+ • Prints a concise, rich stdout summary (via logger) for quick inspection"""
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import Field
12
+
13
+ from reader_workbench.domains.plate_reader.analysis import FoldChangeAnalysisSpec, compute_fold_change_table
14
+ from reader_workbench.workbench.ports import dataframe_input, dataframe_output
15
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
16
+
17
+ # ----------------------------- config model -----------------------------
18
+
19
+
20
+ class FoldChangeCfg(PluginConfig):
21
+ # What to compute
22
+ target: str # e.g., "YFP/CFP" or "YFP/OD600"
23
+ report_times: list[float] # e.g., [8.0, 14.0]
24
+ time_tolerance: float = 0.51 # nearest-time selection tolerance (h)
25
+ observation_stat: Literal["median", "mean"] = "median"
26
+
27
+ # Grouping and labels
28
+ treatment_column: str = "treatment" # we will prefer '<col>_alias' when present
29
+ group_by: list[str] = Field(default_factory=lambda: ["design_id"])
30
+ expected_treatments: list[str] = Field(default_factory=list)
31
+
32
+ # Baseline policy
33
+ use_global_baseline: bool = False
34
+ global_baseline_value: str | None = None # used when use_global_baseline==True
35
+ # overrides: list of maps; any keys matching group_by columns define a match; each must
36
+ # include 'baseline_value'. Example:
37
+ # - { design_id: "araBADp", baseline_value: "0 uM arabinose" }
38
+ overrides: list[dict[str, Any]] = Field(default_factory=list)
39
+
40
+ # Output columns (names)
41
+ fc_column: str = "FC"
42
+ log2fc_column: str = "log2FC"
43
+
44
+ # Attach extra metadata columns if present (won't be required by contract, just carried through)
45
+ attach_metadata: list[str] = Field(default_factory=lambda: ["batch"])
46
+
47
+
48
+ class FoldChange(Plugin):
49
+ """Contract-driven transform that emits a fold_change.v1 table."""
50
+
51
+ ConfigModel = FoldChangeCfg
52
+
53
+ @classmethod
54
+ def input_ports(cls):
55
+ return {"df": dataframe_input("df", "tidy.v1")}
56
+
57
+ @classmethod
58
+ def output_ports(cls):
59
+ return {"table": dataframe_output("table", "fold_change.v1")}
60
+
61
+ # ------------------------------- run --------------------------------
62
+
63
+ def run(self, ctx, inputs, cfg: FoldChangeCfg):
64
+ spec = FoldChangeAnalysisSpec(
65
+ target=cfg.target,
66
+ report_times=tuple(cfg.report_times),
67
+ time_tolerance=cfg.time_tolerance,
68
+ observation_stat=cfg.observation_stat,
69
+ treatment_column=cfg.treatment_column,
70
+ group_by=tuple(cfg.group_by),
71
+ expected_treatments=tuple(cfg.expected_treatments),
72
+ use_global_baseline=cfg.use_global_baseline,
73
+ global_baseline_value=cfg.global_baseline_value,
74
+ overrides=tuple(dict(rule) for rule in cfg.overrides),
75
+ fc_column=cfg.fc_column,
76
+ log2fc_column=cfg.log2fc_column,
77
+ attach_metadata=tuple(cfg.attach_metadata),
78
+ )
79
+ return {"table": compute_fold_change_table(inputs["df"], spec=spec, logger=ctx.logger)}
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+
7
+ from reader_workbench.domains.plate_reader.analysis.four_state_event_window.contracts import (
8
+ FourStateEventWindowAnalysisSpec,
9
+ )
10
+ from reader_workbench.domains.plate_reader.analysis.four_state_event_window.materialize import materialize_experiment
11
+ from reader_workbench.domains.plate_reader.analysis.four_state_event_window.sources import build_experiment_source
12
+ from reader_workbench.workbench.ports import dataframe_output, record_collection_input
13
+ from reader_workbench.workbench.records import SourceRecordCollection
14
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
15
+
16
+
17
+ class FourStateEventWindowCfg(PluginConfig):
18
+ source: dict[str, Any]
19
+ event: dict[str, Any]
20
+ reductions: list[dict[str, Any]]
21
+ aggregation: dict[str, Any]
22
+ quality: dict[str, Any]
23
+
24
+
25
+ class FourStateEventWindowTransform(Plugin):
26
+ """Materialize event-relative summaries from aligned source record collections."""
27
+
28
+ ConfigModel = FourStateEventWindowCfg
29
+
30
+ @classmethod
31
+ def input_ports(cls):
32
+ source = "plate_reader.annotated.v1"
33
+ return {
34
+ "response_records": record_collection_input("response_records", source),
35
+ "magnitude_records": record_collection_input("magnitude_records", source),
36
+ "trajectory_records": record_collection_input("trajectory_records", source),
37
+ }
38
+
39
+ @classmethod
40
+ def output_ports(cls):
41
+ return {
42
+ "wells": dataframe_output("wells", "plate_reader.four_state_event_window.wells.v3"),
43
+ "designs": dataframe_output("designs", "plate_reader.four_state_event_window.designs.v4"),
44
+ "descriptive_resampling_draws": dataframe_output(
45
+ "descriptive_resampling_draws",
46
+ "plate_reader.four_state_event_window.descriptive_resampling_draws.v3",
47
+ ),
48
+ "traces": dataframe_output("traces", "plate_reader.four_state_event_window.traces.v3"),
49
+ "events": dataframe_output("events", "plate_reader.four_state_event_window.events.v2"),
50
+ "dispositions": dataframe_output(
51
+ "dispositions",
52
+ "plate_reader.four_state_event_window.dispositions.v1",
53
+ ),
54
+ }
55
+
56
+ def run(self, ctx, inputs, cfg):
57
+ spec = FourStateEventWindowAnalysisSpec.from_mapping(cfg.model_dump())
58
+ response: SourceRecordCollection = inputs["response_records"]
59
+ magnitude: SourceRecordCollection = inputs["magnitude_records"]
60
+ trajectory: SourceRecordCollection = inputs["trajectory_records"]
61
+ experiment_ids = tuple(item.ref.experiment_id for item in response)
62
+ if len(set(experiment_ids)) != len(experiment_ids):
63
+ raise ValueError("four-state event-window source experiments must be unique")
64
+ spec.source.require_known_experiment_ids(experiment_ids)
65
+ for label, collection in (("magnitude", magnitude), ("trajectory", trajectory)):
66
+ observed = tuple(item.ref.experiment_id for item in collection)
67
+ if observed != experiment_ids:
68
+ raise ValueError(
69
+ f"four-state event-window {label} source order must match response source experiments: "
70
+ f"expected {experiment_ids}, got {observed}"
71
+ )
72
+
73
+ materialized = []
74
+ for response_item, magnitude_item, trajectory_item in zip(
75
+ response,
76
+ magnitude,
77
+ trajectory,
78
+ strict=True,
79
+ ):
80
+ source = build_experiment_source(
81
+ experiment_id=response_item.ref.experiment_id,
82
+ response_frame=response_item.load_dataframe(),
83
+ magnitude_frame=magnitude_item.load_dataframe(),
84
+ trajectory_frame=trajectory_item.load_dataframe(),
85
+ source_spec=spec.source,
86
+ event_spec=spec.event,
87
+ )
88
+ materialized.append(materialize_experiment(source, request=spec))
89
+ names = ("wells", "designs", "descriptive_resampling_draws", "traces", "events", "dispositions")
90
+ return {
91
+ name: pd.concat([frames[index] for frames in materialized], ignore_index=True)
92
+ for index, name in enumerate(names)
93
+ }