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,261 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cache
4
+
5
+ from .model import DataClassSpec, DopRegistry, ReadySpec
6
+
7
+ DOP_SCHEMA = "reader.dop/v1"
8
+
9
+ BUILTIN_DATA_CLASSES: tuple[DataClassSpec, ...] = (
10
+ DataClassSpec(
11
+ id="plate_reader_screen",
12
+ label="Plate-reader screen",
13
+ summary="Well-level plate-reader assay data with explicit channel, treatment, control, and plate/well semantics.",
14
+ decision_order=10,
15
+ protocol_candidates=(
16
+ "plate_reader/dual_reporter_screen",
17
+ "plate_reader/single_reporter_screen",
18
+ "plate_reader/growth_screen",
19
+ ),
20
+ minimum_capture=(
21
+ "raw plate-reader workbook or export",
22
+ "sample map with measured well coverage",
23
+ "channel labels and any denominator or derived-measurement meaning",
24
+ "treatment and control semantics",
25
+ "plate and well identifiers, plus any source-declared replicate and design identifiers",
26
+ ),
27
+ stop_conditions=(
28
+ "well coordinates or sample positions conflict",
29
+ "treatment or control meaning is incomplete",
30
+ "channel labels drift from the selected protocol",
31
+ "nearest protocol would silently change control semantics",
32
+ ),
33
+ transfer_rules=(
34
+ "stage the original workbook or export under inputs/",
35
+ "bind metadata files through resources",
36
+ "regenerate plots, exports, and records from source inputs",
37
+ ),
38
+ verification=(
39
+ "config schema and protocol binding validate",
40
+ "declared raw files and metadata resources exist",
41
+ "records catalog captures generated dataframe evidence",
42
+ ),
43
+ ),
44
+ DataClassSpec(
45
+ id="flow_cytometry_panel",
46
+ label="Flow-cytometry panel",
47
+ summary="Cytometry panel data with explicit FCS discovery, channel naming, and sample metadata.",
48
+ decision_order=20,
49
+ protocol_candidates=("cytometry/flow_panel",),
50
+ minimum_capture=(
51
+ "raw FCS files and their discovery policy",
52
+ "channel naming field",
53
+ "sample metadata",
54
+ "required panel metadata columns",
55
+ ),
56
+ stop_conditions=(
57
+ "FCS file discovery or mapping is ambiguous",
58
+ "channel naming field is unknown",
59
+ "sample metadata cannot be joined to events",
60
+ ),
61
+ transfer_rules=(
62
+ "stage raw FCS material under inputs/",
63
+ "declare FCS discovery under protocol inputs and bind metadata files through resources",
64
+ "keep generated review notebooks under outputs/notebooks/",
65
+ ),
66
+ verification=(
67
+ "declared or discovered FCS files resolve",
68
+ "configured metadata columns are present",
69
+ "records catalog captures panel dataframe evidence",
70
+ ),
71
+ ),
72
+ DataClassSpec(
73
+ id="four_state_logic_analysis",
74
+ label="Four-state logic analysis",
75
+ summary="Logic-response assay data with explicit response and intensity channels plus four ordered states.",
76
+ decision_order=30,
77
+ protocol_candidates=("logic/four_state_vector_screen",),
78
+ minimum_capture=(
79
+ "raw assay files",
80
+ "metadata map",
81
+ "response and intensity channel choices",
82
+ "reference design",
83
+ "ordered 00/10/01/11 states",
84
+ ),
85
+ stop_conditions=(
86
+ "reference design cannot be reconstructed",
87
+ "ordered state values are missing or contradictory",
88
+ "response or intensity channel choices are ambiguous",
89
+ ),
90
+ transfer_rules=(
91
+ "stage raw files under inputs/",
92
+ "encode ordered state spaces in annotations or metadata resources",
93
+ "regenerate four-state vector summaries from source inputs",
94
+ ),
95
+ verification=(
96
+ "logic reference config validates",
97
+ "ordered state-space annotations are present",
98
+ "records catalog captures vector summary evidence",
99
+ ),
100
+ ),
101
+ DataClassSpec(
102
+ id="record_collection_analysis",
103
+ label="Record-collection analysis",
104
+ summary="A new Reader experiment that analyzes exact dataframe-record revisions from prior experiments.",
105
+ decision_order=40,
106
+ protocol_candidates=("logic/four_state_vector_collection", "plate_reader/four_state_event_window"),
107
+ minimum_capture=(
108
+ "source experiment ids",
109
+ "exact dataframe record ids",
110
+ "analysis semantics",
111
+ "expected records and review outputs",
112
+ ),
113
+ stop_conditions=(
114
+ "source experiment ids are unknown",
115
+ "source records are missing, changed, or contract-incompatible",
116
+ "the selected collection protocol would change analysis meaning",
117
+ ),
118
+ transfer_rules=(
119
+ "declare source records as resources of kind record",
120
+ "reference source experiments instead of copying generated outputs",
121
+ "persist derived records and review artifacts through the canonical engine",
122
+ ),
123
+ verification=(
124
+ "source record contracts and exact revisions validate",
125
+ "derived records are present in the aggregate experiment catalog",
126
+ "generic Reader verify passes for the aggregate experiment",
127
+ ),
128
+ ),
129
+ DataClassSpec(
130
+ id="unsupported_long_tail_assay",
131
+ label="Unsupported long-tail assay",
132
+ summary="Assay data that does not yet fit an existing executable protocol contract.",
133
+ decision_order=50,
134
+ protocol_candidates=("workbench/generic",),
135
+ minimum_capture=(
136
+ "raw source path",
137
+ "intended analysis",
138
+ "required metadata",
139
+ "missing protocol decision",
140
+ "owner for follow-up",
141
+ ),
142
+ stop_conditions=(
143
+ "nearest protocol would change assay meaning",
144
+ "required metadata is unknown",
145
+ "execution contract is still being discovered",
146
+ ),
147
+ transfer_rules=(
148
+ "keep the experiment draft or template until semantics are clear",
149
+ "stage raw files without pretending they are runnable",
150
+ "add a protocol only after the metadata and execution contract stabilize",
151
+ ),
152
+ verification=(
153
+ "draft/template config shape validates with --no-files",
154
+ "missing protocol or metadata contract is documented",
155
+ "no generated outputs are treated as source material",
156
+ ),
157
+ ),
158
+ )
159
+
160
+ BUILTIN_READY_SPECS: tuple[ReadySpec, ...] = (
161
+ ReadySpec(
162
+ id="classified",
163
+ label="Classified",
164
+ summary="Dataset has a selected DOP data class and protocol candidate set.",
165
+ required_evidence=(
166
+ "DOP data class id",
167
+ "candidate reader protocol ids",
168
+ "reason the selected class fits the dataset",
169
+ ),
170
+ commands=("uv run reader dop classes",),
171
+ ),
172
+ ReadySpec(
173
+ id="metadata_ready",
174
+ label="Metadata ready",
175
+ summary="Required semantics for the selected data class have been captured before execution.",
176
+ required_evidence=(
177
+ "dataset identity",
178
+ "raw provenance",
179
+ "assay semantics",
180
+ "sample map",
181
+ "control semantics",
182
+ "canonical labels",
183
+ "requested outputs when they differ from protocol defaults",
184
+ ),
185
+ commands=("uv run reader protocols <protocol-id> --example-config",),
186
+ ),
187
+ ReadySpec(
188
+ id="staged",
189
+ label="Staged",
190
+ summary="Raw files, metadata resources, and hand-authored notes live in the standard experiment layout.",
191
+ required_evidence=(
192
+ "raw files under inputs/",
193
+ "resources entries for consumed files",
194
+ "hand-authored notes under notebooks/ when present",
195
+ "no copied generated outputs used as source material",
196
+ ),
197
+ commands=("uv run reader validate <config|dir|index> --no-files --format json",),
198
+ ),
199
+ ReadySpec(
200
+ id="preflight_ok",
201
+ label="Preflight OK",
202
+ summary="Schema, protocol binding, declared files, and dependencies pass reader validation.",
203
+ required_evidence=(
204
+ "config schema is reader/v8",
205
+ "protocol binding resolves",
206
+ "declared files and resources exist",
207
+ "runtime dependencies are available",
208
+ ),
209
+ accepted_readiness_states=("runnable", "uncataloged_outputs_present", "catalog_ready", "records_ready"),
210
+ commands=("uv run reader validate <config|dir|index> --format json",),
211
+ ),
212
+ ReadySpec(
213
+ id="runnable",
214
+ label="Runnable",
215
+ summary="The experiment is active and can run from authored source inputs.",
216
+ required_evidence=(
217
+ "reader readiness state allows run",
218
+ "run capability is true",
219
+ "next command is explicit",
220
+ ),
221
+ accepted_readiness_states=("runnable", "uncataloged_outputs_present", "catalog_ready", "records_ready"),
222
+ required_capabilities=("run",),
223
+ commands=("uv run reader run <config|dir|index> --dry-run --format json",),
224
+ ),
225
+ ReadySpec(
226
+ id="records_ready",
227
+ label="Records ready",
228
+ summary="Generated dataframe and file-bundle evidence is present in the records catalog.",
229
+ required_evidence=(
230
+ "records catalog exists",
231
+ "all current records use the verifiable schema",
232
+ "source and generated file digests match",
233
+ "config and exact upstream record revisions match",
234
+ ),
235
+ accepted_readiness_states=("records_ready",),
236
+ required_capabilities=("records", "verify"),
237
+ commands=("uv run reader verify <config|dir|index>",),
238
+ ),
239
+ ReadySpec(
240
+ id="review_ready",
241
+ label="Review ready",
242
+ summary="Records exist and review surfaces such as plots or notebooks can be inspected deliberately.",
243
+ required_evidence=(
244
+ "records catalog exists",
245
+ "selected plots or notebook scaffold are protocol-compatible",
246
+ "unresolved metadata assumptions remain visible in the handoff",
247
+ ),
248
+ accepted_readiness_states=("records_ready",),
249
+ required_capabilities=("records", "plot", "notebook_scaffold"),
250
+ commands=(
251
+ "uv run reader verify <config|dir|index>",
252
+ "uv run reader plot <config|dir|index> --list",
253
+ "uv run reader notebook <config|dir|index> --mode none",
254
+ ),
255
+ ),
256
+ )
257
+
258
+
259
+ @cache
260
+ def builtin_dop_registry() -> DopRegistry:
261
+ return DopRegistry(data_classes=BUILTIN_DATA_CLASSES, ready_specs=BUILTIN_READY_SPECS)
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from dataclasses import dataclass
5
+
6
+
7
+ def _clean_string(value: str, *, field_name: str) -> str:
8
+ if not isinstance(value, str) or not value.strip():
9
+ raise ValueError(f"{field_name} must be a non-empty string.")
10
+ return value.strip()
11
+
12
+
13
+ def _clean_tuple(values: Iterable[str], *, field_name: str, allow_empty: bool = False) -> tuple[str, ...]:
14
+ cleaned = tuple(str(value).strip() for value in values if str(value).strip())
15
+ if not allow_empty and not cleaned:
16
+ raise ValueError(f"{field_name} must include at least one value.")
17
+ if len(set(cleaned)) != len(cleaned):
18
+ raise ValueError(f"{field_name} must not include duplicate values.")
19
+ return cleaned
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class DataClassSpec:
24
+ id: str
25
+ label: str
26
+ summary: str
27
+ decision_order: int
28
+ protocol_candidates: tuple[str, ...]
29
+ minimum_capture: tuple[str, ...]
30
+ stop_conditions: tuple[str, ...]
31
+ transfer_rules: tuple[str, ...]
32
+ verification: tuple[str, ...]
33
+
34
+ def __post_init__(self) -> None:
35
+ object.__setattr__(self, "id", _clean_string(self.id, field_name="DataClassSpec.id"))
36
+ object.__setattr__(self, "label", _clean_string(self.label, field_name="DataClassSpec.label"))
37
+ object.__setattr__(self, "summary", _clean_string(self.summary, field_name="DataClassSpec.summary"))
38
+ if not isinstance(self.decision_order, int) or self.decision_order < 0:
39
+ raise ValueError("DataClassSpec.decision_order must be a non-negative integer.")
40
+ object.__setattr__(
41
+ self,
42
+ "protocol_candidates",
43
+ _clean_tuple(self.protocol_candidates, field_name="DataClassSpec.protocol_candidates"),
44
+ )
45
+ object.__setattr__(
46
+ self,
47
+ "minimum_capture",
48
+ _clean_tuple(self.minimum_capture, field_name="DataClassSpec.minimum_capture"),
49
+ )
50
+ object.__setattr__(
51
+ self,
52
+ "stop_conditions",
53
+ _clean_tuple(self.stop_conditions, field_name="DataClassSpec.stop_conditions"),
54
+ )
55
+ object.__setattr__(
56
+ self,
57
+ "transfer_rules",
58
+ _clean_tuple(self.transfer_rules, field_name="DataClassSpec.transfer_rules"),
59
+ )
60
+ object.__setattr__(
61
+ self,
62
+ "verification",
63
+ _clean_tuple(self.verification, field_name="DataClassSpec.verification"),
64
+ )
65
+
66
+ def to_payload(self) -> dict[str, object]:
67
+ return {
68
+ "id": self.id,
69
+ "label": self.label,
70
+ "summary": self.summary,
71
+ "decision_order": self.decision_order,
72
+ "protocol_candidates": list(self.protocol_candidates),
73
+ "minimum_capture": list(self.minimum_capture),
74
+ "stop_conditions": list(self.stop_conditions),
75
+ "transfer_rules": list(self.transfer_rules),
76
+ "verification": list(self.verification),
77
+ }
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ReadySpec:
82
+ id: str
83
+ label: str
84
+ summary: str
85
+ required_evidence: tuple[str, ...]
86
+ accepted_readiness_states: tuple[str, ...] = ()
87
+ required_capabilities: tuple[str, ...] = ()
88
+ commands: tuple[str, ...] = ()
89
+
90
+ def __post_init__(self) -> None:
91
+ object.__setattr__(self, "id", _clean_string(self.id, field_name="ReadySpec.id"))
92
+ object.__setattr__(self, "label", _clean_string(self.label, field_name="ReadySpec.label"))
93
+ object.__setattr__(self, "summary", _clean_string(self.summary, field_name="ReadySpec.summary"))
94
+ object.__setattr__(
95
+ self,
96
+ "required_evidence",
97
+ _clean_tuple(self.required_evidence, field_name="ReadySpec.required_evidence"),
98
+ )
99
+ object.__setattr__(
100
+ self,
101
+ "accepted_readiness_states",
102
+ _clean_tuple(
103
+ self.accepted_readiness_states,
104
+ field_name="ReadySpec.accepted_readiness_states",
105
+ allow_empty=True,
106
+ ),
107
+ )
108
+ object.__setattr__(
109
+ self,
110
+ "required_capabilities",
111
+ _clean_tuple(
112
+ self.required_capabilities,
113
+ field_name="ReadySpec.required_capabilities",
114
+ allow_empty=True,
115
+ ),
116
+ )
117
+ object.__setattr__(
118
+ self,
119
+ "commands",
120
+ _clean_tuple(self.commands, field_name="ReadySpec.commands", allow_empty=True),
121
+ )
122
+
123
+ def to_payload(self) -> dict[str, object]:
124
+ return {
125
+ "id": self.id,
126
+ "label": self.label,
127
+ "summary": self.summary,
128
+ "required_evidence": list(self.required_evidence),
129
+ "accepted_readiness_states": list(self.accepted_readiness_states),
130
+ "required_capabilities": list(self.required_capabilities),
131
+ "commands": list(self.commands),
132
+ }
133
+
134
+
135
+ class DopRegistry:
136
+ def __init__(self, *, data_classes: Iterable[DataClassSpec], ready_specs: Iterable[ReadySpec]):
137
+ data_class_items = tuple(sorted(data_classes, key=lambda item: item.decision_order))
138
+ ready_spec_items = tuple(ready_specs)
139
+ self._data_classes = data_class_items
140
+ self._ready_specs = ready_spec_items
141
+ self._data_classes_by_id = _index_by_id(data_class_items, kind="DOP data class")
142
+ self._ready_specs_by_id = _index_by_id(ready_spec_items, kind="DOP ready spec")
143
+ orders = [item.decision_order for item in data_class_items]
144
+ if len(set(orders)) != len(orders):
145
+ raise ValueError("DOP data class decision_order values must be unique.")
146
+
147
+ def data_classes(self) -> tuple[DataClassSpec, ...]:
148
+ return self._data_classes
149
+
150
+ def ready_specs(self) -> tuple[ReadySpec, ...]:
151
+ return self._ready_specs
152
+
153
+ def data_class(self, data_class_id: str) -> DataClassSpec:
154
+ key = str(data_class_id).strip()
155
+ try:
156
+ return self._data_classes_by_id[key]
157
+ except KeyError:
158
+ options = ", ".join(sorted(self._data_classes_by_id)) or "—"
159
+ raise ValueError(f"Unknown DOP data class {data_class_id!r}. Available classes: {options}") from None
160
+
161
+ def ready_spec(self, ready_spec_id: str) -> ReadySpec:
162
+ key = str(ready_spec_id).strip()
163
+ try:
164
+ return self._ready_specs_by_id[key]
165
+ except KeyError:
166
+ options = ", ".join(sorted(self._ready_specs_by_id)) or "—"
167
+ raise ValueError(f"Unknown DOP ready spec {ready_spec_id!r}. Available specs: {options}") from None
168
+
169
+ def data_classes_for_protocol(self, protocol_id: str) -> tuple[DataClassSpec, ...]:
170
+ key = str(protocol_id).strip()
171
+ return tuple(item for item in self._data_classes if key in item.protocol_candidates)
172
+
173
+ def validate_protocol_refs(self, protocol_ids: Iterable[str]) -> None:
174
+ known = {str(protocol_id).strip() for protocol_id in protocol_ids if str(protocol_id).strip()}
175
+ referenced = {
176
+ protocol_id for data_class in self._data_classes for protocol_id in data_class.protocol_candidates
177
+ }
178
+ missing = sorted(referenced - known)
179
+ if missing:
180
+ raise ValueError("DOP registry references unknown protocol ids: " + ", ".join(missing))
181
+
182
+ def validate_ready_refs(self, *, readiness_states: Iterable[str], capability_keys: Iterable[str]) -> None:
183
+ known_states = {str(state).strip() for state in readiness_states if str(state).strip()}
184
+ known_capabilities = {str(capability).strip() for capability in capability_keys if str(capability).strip()}
185
+ missing_states = sorted(
186
+ state for spec in self._ready_specs for state in spec.accepted_readiness_states if state not in known_states
187
+ )
188
+ missing_capabilities = sorted(
189
+ capability
190
+ for spec in self._ready_specs
191
+ for capability in spec.required_capabilities
192
+ if capability not in known_capabilities
193
+ )
194
+ errors = []
195
+ if missing_states:
196
+ errors.append("unknown readiness states: " + ", ".join(missing_states))
197
+ if missing_capabilities:
198
+ errors.append("unknown readiness capabilities: " + ", ".join(missing_capabilities))
199
+ if errors:
200
+ raise ValueError("DOP ready specs reference " + "; ".join(errors))
201
+
202
+
203
+ def _index_by_id(items: Iterable[DataClassSpec] | Iterable[ReadySpec], *, kind: str):
204
+ indexed = {}
205
+ for item in items:
206
+ if item.id in indexed:
207
+ raise ValueError(f"Duplicate {kind} id {item.id!r}.")
208
+ indexed[item.id] = item
209
+ return indexed
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from reader_workbench.workbench.registry import load_plugin_catalog
4
+
5
+ from .contracts import (
6
+ _assert_input_ports,
7
+ _assert_output_ports,
8
+ _resolve_output_labels,
9
+ _resolve_runtime_output_ports,
10
+ )
11
+ from .execution import execute_step, run_steps
12
+ from .inputs import _resolve_inputs
13
+ from .invocations import ExecutionResult, ProducedRecordRevision, SelectedSteps
14
+ from .planning import build_next_steps, explain
15
+ from .runtime import run_job, run_spec
16
+ from .setup import build_run_context, configure_logger, normalize_log_level, resolve_palette_book, slice_pipeline_steps
17
+ from .validation import validate, validation_summary
18
+
19
+ __all__ = [
20
+ "_assert_input_ports",
21
+ "_assert_output_ports",
22
+ "_resolve_inputs",
23
+ "_resolve_output_labels",
24
+ "_resolve_runtime_output_ports",
25
+ "build_run_context",
26
+ "build_next_steps",
27
+ "configure_logger",
28
+ "ExecutionResult",
29
+ "execute_step",
30
+ "explain",
31
+ "load_plugin_catalog",
32
+ "normalize_log_level",
33
+ "ProducedRecordRevision",
34
+ "resolve_palette_book",
35
+ "run_job",
36
+ "run_steps",
37
+ "run_spec",
38
+ "SelectedSteps",
39
+ "slice_pipeline_steps",
40
+ "validate",
41
+ "validation_summary",
42
+ ]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from typing import Any
6
+
7
+ from reader_workbench.runtime import ReaderRuntime, builtin_runtime
8
+ from reader_workbench.workbench.decl import WorkbenchDecl
9
+ from reader_workbench.workbench.graph import resolve_workbench
10
+
11
+
12
+ def digest_cfg(plugin_cfg: Any) -> str:
13
+ if hasattr(plugin_cfg, "model_dump"):
14
+ payload = plugin_cfg.model_dump(mode="json")
15
+ elif isinstance(plugin_cfg, dict):
16
+ payload = plugin_cfg
17
+ else:
18
+ payload = json.loads(json.dumps(plugin_cfg, default=str))
19
+ raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
20
+ return "sha256:" + hashlib.sha256(raw).hexdigest()
21
+
22
+
23
+ def needs_plot_palette(steps: list[Any], palette: str | None) -> bool:
24
+ if palette is None:
25
+ return False
26
+ return any(getattr(step, "plugin", "").startswith("plot/") for step in steps)
27
+
28
+
29
+ def collect_categories(steps: list[Any]) -> set[str]:
30
+ categories: set[str] = set()
31
+ for step in steps:
32
+ plugin = getattr(step, "plugin", "")
33
+ if "/" in plugin:
34
+ categories.add(plugin.split("/", 1)[0])
35
+ return categories
36
+
37
+
38
+ def has_cytometry_step(decl: WorkbenchDecl, *, runtime: ReaderRuntime | None = None) -> bool:
39
+ return pipeline_has_plugin(decl, runtime=runtime, domain="cytometry")
40
+
41
+
42
+ def pipeline_has_plugin(
43
+ decl: WorkbenchDecl,
44
+ *,
45
+ runtime: ReaderRuntime | None = None,
46
+ plugin: str | None = None,
47
+ domain: str | None = None,
48
+ family: str | None = None,
49
+ tag: str | None = None,
50
+ ) -> bool:
51
+ pipeline = list(resolve_workbench(decl).pipeline)
52
+ if not pipeline:
53
+ return False
54
+
55
+ registry = None
56
+ if domain is not None or family is not None or tag is not None:
57
+ runtime = runtime or builtin_runtime()
58
+ registry = runtime.plugins
59
+
60
+ for step in pipeline:
61
+ step_plugin = str(getattr(step, "plugin", ""))
62
+ if plugin is not None and step_plugin != plugin:
63
+ continue
64
+ if domain is None and family is None and tag is None:
65
+ return True
66
+ if registry is None:
67
+ continue
68
+ descriptor = registry.resolve_descriptor(step_plugin)
69
+ if domain is not None and descriptor.domain != domain:
70
+ continue
71
+ if family is not None and descriptor.family != family:
72
+ continue
73
+ if tag is not None and tag not in descriptor.tags:
74
+ continue
75
+ return True
76
+ return False