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,175 @@
1
+ """Shared palette + subplot layout helpers for plotting modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+
9
+ import matplotlib as mpl
10
+ import matplotlib.pyplot as plt
11
+
12
+ _PALETTES: dict[str, list[str]] = {
13
+ "colorblind": [
14
+ "#0072B2",
15
+ "#E69F00",
16
+ "#009E73",
17
+ "#CC79A7",
18
+ "#56B4E9",
19
+ "#D55E00",
20
+ "#F0E442",
21
+ "#000000",
22
+ ],
23
+ "muted": [
24
+ "#4878CF",
25
+ "#6ACC65",
26
+ "#D65F5F",
27
+ "#B47CC7",
28
+ "#C4AD66",
29
+ "#77BEDB",
30
+ "#4D4D4D",
31
+ "#A0A0A0",
32
+ ],
33
+ "tableau": [
34
+ "#4E79A7",
35
+ "#F28E2B",
36
+ "#E15759",
37
+ "#76B7B2",
38
+ "#59A14F",
39
+ "#EDC948",
40
+ "#B07AA1",
41
+ "#FF9DA7",
42
+ ],
43
+ }
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class PaletteBook:
48
+ """Small helper carried in RunContext (see workbench/context.py)."""
49
+
50
+ name: str = "colorblind"
51
+
52
+ def colors(self, n: int) -> list[str]:
53
+ if self.name not in _PALETTES:
54
+ opts = ", ".join(sorted(_PALETTES))
55
+ raise ValueError(f"Unknown palette '{self.name}'. Available: {opts}")
56
+ palette = _PALETTES[self.name]
57
+ if n <= len(palette):
58
+ return palette[:n]
59
+ out = []
60
+ while len(out) < n:
61
+ out.extend(palette)
62
+ return out[:n]
63
+
64
+
65
+ def available_palettes() -> list[str]:
66
+ return sorted(_PALETTES)
67
+
68
+
69
+ DEFAULT_RC = {
70
+ "figure_figsize": (5, 5),
71
+ "savefig_dpi": 300,
72
+ "axes_spines_top": False,
73
+ "axes_spines_right": False,
74
+ "axes_titleweight": "bold",
75
+ "axes_labelweight": "regular",
76
+ "axes_grid": True,
77
+ "grid_alpha": 0.25,
78
+ "grid_linestyle": "-",
79
+ "grid_color": "#B0B0B0",
80
+ "axes_axisbelow": True,
81
+ "font_size": 13.0,
82
+ "axes_labelsize": 13.0,
83
+ "axes_titlesize": 14.0,
84
+ "xtick_labelsize": 12.0,
85
+ "ytick_labelsize": 12.0,
86
+ "legend_fontsize": 12.0,
87
+ "legend_title_fontsize": 12.0,
88
+ "xtick_direction": "out",
89
+ "ytick_direction": "out",
90
+ "legend_frameon": False,
91
+ "pdf_fonttype": 42,
92
+ "pdf_compression": 9,
93
+ "path_simplify": True,
94
+ "path_simplify_threshold": 0.0,
95
+ "agg_path_chunksize": 20000,
96
+ }
97
+
98
+
99
+ @contextlib.contextmanager
100
+ def use_style(rc: dict | None = None, color_cycle: Iterable[str] | None = None):
101
+ """Context manager to push a small, opinionated Matplotlib style."""
102
+ rc = {**DEFAULT_RC, **(rc or {})}
103
+ scale = float(rc.pop("font_scale", 1.0))
104
+
105
+ def _scaled(key: str) -> float:
106
+ value = float(rc.get(key, DEFAULT_RC[key]))
107
+ return value * scale
108
+
109
+ with mpl.rc_context():
110
+ mpl.rcParams.update(
111
+ {
112
+ "figure.figsize": rc["figure_figsize"],
113
+ "savefig.dpi": rc["savefig_dpi"],
114
+ "axes.spines.top": rc["axes_spines_top"],
115
+ "axes.spines.right": rc["axes_spines_right"],
116
+ "axes.titleweight": rc["axes_titleweight"],
117
+ "axes.labelweight": rc["axes_labelweight"],
118
+ "axes.grid": rc["axes_grid"],
119
+ "grid.alpha": rc["grid_alpha"],
120
+ "grid.linestyle": rc["grid_linestyle"],
121
+ "grid.color": rc["grid_color"],
122
+ "axes.axisbelow": rc["axes_axisbelow"],
123
+ "font.size": _scaled("font_size"),
124
+ "axes.labelsize": _scaled("axes_labelsize"),
125
+ "axes.titlesize": _scaled("axes_titlesize"),
126
+ "xtick.labelsize": _scaled("xtick_labelsize"),
127
+ "ytick.labelsize": _scaled("ytick_labelsize"),
128
+ "legend.fontsize": _scaled("legend_fontsize"),
129
+ "legend.title_fontsize": _scaled("legend_title_fontsize"),
130
+ "xtick.direction": rc["xtick_direction"],
131
+ "ytick.direction": rc["ytick_direction"],
132
+ "legend.frameon": rc["legend_frameon"],
133
+ "pdf.fonttype": rc["pdf_fonttype"],
134
+ "pdf.compression": int(rc["pdf_compression"]),
135
+ "path.simplify": bool(rc["path_simplify"]),
136
+ "path.simplify_threshold": float(rc["path_simplify_threshold"]),
137
+ "agg.path.chunksize": int(rc["agg_path_chunksize"]),
138
+ }
139
+ )
140
+ if color_cycle is not None:
141
+ mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=list(color_cycle))
142
+ yield
143
+
144
+
145
+ def new_fig_ax(fig_kwargs: dict | None = None):
146
+ """
147
+ Consistent figure construction.
148
+ Only pass kwargs that Matplotlib's Figure/subplots actually understand.
149
+ Plot- or style-level options must be consumed by the caller and not forwarded.
150
+ """
151
+ fig_kwargs = dict(fig_kwargs or {})
152
+ allowed = {
153
+ "num",
154
+ "figsize",
155
+ "dpi",
156
+ "facecolor",
157
+ "edgecolor",
158
+ "frameon",
159
+ "clear",
160
+ "constrained_layout",
161
+ "layout",
162
+ "squeeze",
163
+ "subplot_kw",
164
+ "gridspec_kw",
165
+ "sharex",
166
+ "sharey",
167
+ }
168
+ fig_kwargs = {key: value for key, value in fig_kwargs.items() if key in allowed}
169
+ if "figsize" not in fig_kwargs:
170
+ fig_kwargs["figsize"] = DEFAULT_RC["figure_figsize"]
171
+ fig_kwargs.setdefault("constrained_layout", True)
172
+ return plt.subplots(**fig_kwargs)
173
+
174
+
175
+ __all__ = ["DEFAULT_RC", "PaletteBook", "available_palettes", "new_fig_ax", "use_style"]
@@ -0,0 +1,27 @@
1
+ """Shared plotting utilities (filesystem + naming helpers)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+
9
+ def ensure_dir(path: Path) -> None:
10
+ path.mkdir(parents=True, exist_ok=True)
11
+
12
+
13
+ def slugify(value: str) -> str:
14
+ value = str(value).strip()
15
+ value = re.sub(r"[^\w\-.]+", "_", value)
16
+ return re.sub(r"_{2,}", "_", value).strip("_")
17
+
18
+
19
+ def save_figure(fig, output_dir: Path, filename_stub: str, ext: str = "pdf", dpi: int | None = None) -> Path:
20
+ """
21
+ Save figures as PDF by default (print-friendly, vector).
22
+ Use ext="png" if you explicitly need rasters.
23
+ """
24
+ ensure_dir(output_dir)
25
+ out = output_dir / f"{slugify(filename_stub)}.{ext}"
26
+ fig.savefig(out, bbox_inches="tight", dpi=dpi)
27
+ return out
@@ -0,0 +1 @@
1
+ """Built-in workbench plugins shipped with Reader."""
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from typing import cast
5
+
6
+ from reader_workbench.workbench.assets import AssetCatalog, AssetDescriptor
7
+
8
+ _CATEGORY_MODULES = {
9
+ "ingest": "reader_workbench.plugins.manifests.ingest",
10
+ "transform": "reader_workbench.plugins.manifests.transform",
11
+ "plot": "reader_workbench.plugins.manifests.plot",
12
+ "export": "reader_workbench.plugins.manifests.export",
13
+ "validator": "reader_workbench.plugins.manifests.validator",
14
+ }
15
+
16
+
17
+ def _category_descriptors(category: str) -> tuple[AssetDescriptor, ...]:
18
+ module = importlib.import_module(_CATEGORY_MODULES[category])
19
+ return cast(tuple[AssetDescriptor, ...], module.BUILTIN_PLUGIN_DESCRIPTORS)
20
+
21
+
22
+ def builtin_plugin_catalog(*, categories: set[str] | None = None) -> AssetCatalog:
23
+ selected = (
24
+ tuple(_CATEGORY_MODULES)
25
+ if categories is None
26
+ else tuple(category for category in _CATEGORY_MODULES if category in categories)
27
+ )
28
+ descriptors = [descriptor for category in selected for descriptor in _category_descriptors(category)]
29
+ return AssetCatalog(descriptors)
30
+
31
+
32
+ def builtin_plugin_descriptors(*, categories: set[str] | None = None) -> tuple[AssetDescriptor, ...]:
33
+ return builtin_plugin_catalog(categories=categories).all()
File without changes
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from reader_workbench.errors import ExecutionError
6
+ from reader_workbench.workbench.paths import resolve_path_within_root
7
+
8
+
9
+ def resolve_export_path(raw: str, *, exports_dir: Path) -> Path:
10
+ path = Path(raw).expanduser()
11
+ if path.is_absolute():
12
+ raise ExecutionError("Export paths must be relative to the experiment exports directory")
13
+ try:
14
+ resolved = resolve_path_within_root(path, root=exports_dir)
15
+ except ValueError as exc:
16
+ raise ExecutionError(
17
+ "Export paths must stay under the experiment exports directory after resolving symlinks"
18
+ ) from exc
19
+ if resolved == exports_dir.resolve():
20
+ raise ExecutionError("Export paths must identify a file below the experiment exports directory")
21
+ return resolved
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+ from pydantic import Field
7
+
8
+ from reader_workbench.errors import ExecutionError
9
+ from reader_workbench.workbench.ports import dataframe_input, file_path_output
10
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
11
+
12
+ from ._paths import resolve_export_path
13
+
14
+
15
+ class ExportCsvCfg(PluginConfig):
16
+ path: str = Field(..., description="Output CSV path relative to the experiment exports directory.")
17
+ index: bool = False
18
+ sep: str = ","
19
+ na_rep: str | None = None
20
+
21
+
22
+ class ExportCsv(Plugin):
23
+ ConfigModel = ExportCsvCfg
24
+
25
+ @classmethod
26
+ def input_ports(cls):
27
+ return {"df": dataframe_input("df")}
28
+
29
+ @classmethod
30
+ def output_ports(cls):
31
+ return {"artifact": file_path_output("artifact")}
32
+
33
+ def run(self, ctx, inputs: dict[str, Any], cfg: ExportCsvCfg) -> dict[str, Any]:
34
+ df = inputs["df"]
35
+ if not isinstance(df, pd.DataFrame):
36
+ raise ExecutionError(f"export/csv expects a DataFrame input, got {type(df).__name__}")
37
+ out_path = resolve_export_path(cfg.path, exports_dir=ctx.exports_dir)
38
+ out_path.parent.mkdir(parents=True, exist_ok=True)
39
+ df.to_csv(out_path, index=cfg.index, sep=cfg.sep, na_rep=cfg.na_rep)
40
+ ctx.logger.info("export • csv → %s", out_path)
41
+ return {"artifact": out_path}
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+ from pydantic import Field
7
+
8
+ from reader_workbench.errors import ExecutionError
9
+ from reader_workbench.workbench.ports import dataframe_input, file_path_output
10
+ from reader_workbench.workbench.registry import Plugin, PluginConfig
11
+
12
+ from ._paths import resolve_export_path
13
+
14
+
15
+ class ExportXlsxCfg(PluginConfig):
16
+ path: str = Field(..., description="Output XLSX path relative to the experiment exports directory.")
17
+ sheet_name: str = "Sheet1"
18
+ index: bool = False
19
+
20
+
21
+ class ExportXlsx(Plugin):
22
+ ConfigModel = ExportXlsxCfg
23
+
24
+ @classmethod
25
+ def input_ports(cls):
26
+ return {"df": dataframe_input("df")}
27
+
28
+ @classmethod
29
+ def output_ports(cls):
30
+ return {"artifact": file_path_output("artifact")}
31
+
32
+ def run(self, ctx, inputs: dict[str, Any], cfg: ExportXlsxCfg) -> dict[str, Any]:
33
+ df = inputs["df"]
34
+ if not isinstance(df, pd.DataFrame):
35
+ raise ExecutionError(f"export/xlsx expects a DataFrame input, got {type(df).__name__}")
36
+ out_path = resolve_export_path(cfg.path, exports_dir=ctx.exports_dir)
37
+ out_path.parent.mkdir(parents=True, exist_ok=True)
38
+ try:
39
+ with pd.ExcelWriter(out_path, engine="openpyxl") as writer:
40
+ df.to_excel(writer, index=cfg.index, sheet_name=cfg.sheet_name)
41
+ except ImportError as e:
42
+ raise ExecutionError("export/xlsx requires the 'openpyxl' dependency to write .xlsx files.") from e
43
+ ctx.logger.info("export • xlsx → %s", out_path)
44
+ return {"artifact": out_path}
File without changes
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from reader_workbench.errors import ParseError
7
+
8
+ from .discovery_policy import DEFAULT_ROOTS, discover_files
9
+
10
+
11
+ def auto_pick_discovered_file(
12
+ files: Sequence[Path],
13
+ mode: str,
14
+ *,
15
+ singular_label: str,
16
+ ) -> Path:
17
+ if mode == "single":
18
+ if len(files) != 1:
19
+ raise ParseError(
20
+ f"Auto-discovery expected exactly one {singular_label}, found "
21
+ f"{len(files)}:\n- " + "\n- ".join(str(path) for path in files)
22
+ )
23
+ return files[0]
24
+ if mode == "latest":
25
+ return max(files, key=lambda path: path.stat().st_mtime)
26
+ raise ParseError(f"Unknown auto_pick mode {mode!r} (expected: single|latest|merge)")
27
+
28
+
29
+ def discover_auto_input_files(
30
+ *,
31
+ exp_dir: Path,
32
+ auto_roots: Sequence[str] | None,
33
+ auto_include: Sequence[str],
34
+ auto_exclude: Sequence[str],
35
+ auto_recursive: bool,
36
+ auto_pick: str,
37
+ discovery_label: str,
38
+ singular_label: str,
39
+ ) -> list[Path]:
40
+ roots = list(auto_roots or DEFAULT_ROOTS)
41
+ files = discover_files(
42
+ exp_dir,
43
+ roots=roots,
44
+ include=list(auto_include),
45
+ exclude=list(auto_exclude),
46
+ recursive=auto_recursive,
47
+ )
48
+ if not files:
49
+ raise ParseError(
50
+ f"No {discovery_label} discovered under {roots} "
51
+ f"(include={list(auto_include)}, exclude={list(auto_exclude)}).\n"
52
+ "Hint: put raw files under ./inputs (default), or set auto_roots / reads.raw explicitly."
53
+ )
54
+ if auto_pick in {"single", "latest"}:
55
+ return [auto_pick_discovered_file(files, auto_pick, singular_label=singular_label)]
56
+ if auto_pick == "merge":
57
+ return files
58
+ raise ParseError(f"Unknown auto_pick mode {auto_pick!r} (expected: single|latest|merge)")
@@ -0,0 +1,66 @@
1
+ """Raw-file autodiscovery policy for ingest plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ from collections.abc import Iterable, Sequence
7
+ from pathlib import Path
8
+
9
+ from reader_workbench.errors import ParseError
10
+ from reader_workbench.workbench.input_discovery import (
11
+ DEFAULT_INPUT_EXCLUDE as DEFAULT_EXCLUDE,
12
+ )
13
+ from reader_workbench.workbench.input_discovery import (
14
+ DEFAULT_INPUT_ROOTS as DEFAULT_ROOTS,
15
+ )
16
+ from reader_workbench.workbench.input_discovery import (
17
+ DEFAULT_WORKBOOK_INCLUDE as DEFAULT_INCLUDE,
18
+ )
19
+ from reader_workbench.workbench.paths import resolve_path_within_root
20
+
21
+
22
+ def _iter_candidates(root: Path, patterns: Sequence[str], recursive: bool) -> Iterable[Path]:
23
+ if not root.exists():
24
+ return []
25
+ if recursive:
26
+ for path in root.rglob("*"):
27
+ if path.is_file() and any(fnmatch.fnmatch(path.name, pat) for pat in patterns):
28
+ yield path
29
+ return
30
+ for path in root.glob("*"):
31
+ if path.is_file() and any(fnmatch.fnmatch(path.name, pat) for pat in patterns):
32
+ yield path
33
+
34
+
35
+ def discover_files(
36
+ base: Path,
37
+ *,
38
+ roots: Sequence[str] | None = None,
39
+ include: Sequence[str] = DEFAULT_INCLUDE,
40
+ exclude: Sequence[str] = DEFAULT_EXCLUDE,
41
+ recursive: bool = False,
42
+ ) -> list[Path]:
43
+ roots = list(roots or DEFAULT_ROOTS)
44
+ out: list[Path] = []
45
+ seen: set[Path] = set()
46
+ for root in roots:
47
+ try:
48
+ root_path = resolve_path_within_root(root, root=base)
49
+ except ValueError as err:
50
+ raise ParseError(
51
+ f"auto_roots entry {root!r} must stay under the experiment root after resolving symlinks."
52
+ ) from err
53
+ for path in _iter_candidates(root_path, include, recursive):
54
+ try:
55
+ confined_path = resolve_path_within_root(path, root=base)
56
+ except ValueError as err:
57
+ raise ParseError(
58
+ f"Discovered input {path!s} must stay under the experiment root after resolving symlinks."
59
+ ) from err
60
+ if any(fnmatch.fnmatch(path.name, pat) for pat in exclude):
61
+ continue
62
+ if confined_path in seen:
63
+ continue
64
+ out.append(confined_path)
65
+ seen.add(confined_path)
66
+ return sorted(out)
@@ -0,0 +1,139 @@
1
+ """Flow cytometer ingest for .fcs files (snapshot data)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from collections.abc import Mapping
7
+ from contextlib import suppress
8
+ from typing import Literal
9
+
10
+ import pandas as pd
11
+ from pydantic import Field
12
+
13
+ from reader_workbench.domains.cytometry.io.fcs import parse_fcs_file
14
+ from reader_workbench.errors import ParseError
15
+ from reader_workbench.plugins.ingest._discovery import discover_auto_input_files
16
+ from reader_workbench.plugins.ingest.discovery_policy import DEFAULT_EXCLUDE
17
+ from reader_workbench.workbench.ports import dataframe_output, file_set_input
18
+ from reader_workbench.workbench.registry import Plugin, PluginConfig, PreflightIssue
19
+
20
+ DEFAULT_FCS_INCLUDE = ("*.fcs", "*.FCS")
21
+
22
+
23
+ class FlowCytometerCfg(PluginConfig):
24
+ # auto-discovery knobs
25
+ auto_roots: list[str] | None = None
26
+ auto_include: list[str] = Field(default_factory=lambda: list(DEFAULT_FCS_INCLUDE))
27
+ auto_exclude: list[str] = Field(default_factory=lambda: list(DEFAULT_EXCLUDE))
28
+ auto_pick: Literal["single", "latest", "merge"] = "merge"
29
+ auto_recursive: bool = False
30
+
31
+ # channel naming / output shaping
32
+ channel_name_field: str = "pns" # field in FCS channel metadata (e.g., pns or pnn)
33
+ channel_map: Mapping[str, str] | None = None
34
+ drop_channels: list[str] | None = None
35
+ sample_id_from: Literal["stem", "name"] = "stem"
36
+ time_value: float = 0.0
37
+
38
+ # logging
39
+ print_summary: bool = True
40
+
41
+
42
+ class FlowCytometerIngest(Plugin):
43
+ """Ingest .fcs files into tidy.v1 (snapshot; time is constant)."""
44
+
45
+ ConfigModel = FlowCytometerCfg
46
+
47
+ @classmethod
48
+ def input_ports(cls):
49
+ return {"raw": file_set_input("raw", optional=True)}
50
+
51
+ @classmethod
52
+ def output_ports(cls):
53
+ return {
54
+ "df": dataframe_output("df", "tidy.v1"),
55
+ "channels": dataframe_output("channels", "cytometer.channels.v1"),
56
+ }
57
+
58
+ @classmethod
59
+ def preflight_readiness(cls, *, exp_dir, cfg: FlowCytometerCfg, reads):
60
+ issues: list[PreflightIssue] = []
61
+ if importlib.util.find_spec("flowio") is None:
62
+ issues.append(
63
+ PreflightIssue(
64
+ kind="dependency",
65
+ message="flowio is required for ingest/flow_cytometer. Re-sync the core reader environment.",
66
+ )
67
+ )
68
+ if "raw" not in reads:
69
+ try:
70
+ discover_auto_input_files(
71
+ exp_dir=exp_dir,
72
+ auto_roots=cfg.auto_roots,
73
+ auto_include=cfg.auto_include,
74
+ auto_exclude=cfg.auto_exclude,
75
+ auto_recursive=cfg.auto_recursive,
76
+ auto_pick=cfg.auto_pick,
77
+ discovery_label=".fcs files",
78
+ singular_label=".fcs file",
79
+ )
80
+ except ParseError as err:
81
+ issues.append(PreflightIssue(kind="file", message=str(err)))
82
+ return tuple(issues)
83
+
84
+ @classmethod
85
+ def resolve_missing_file_inputs(cls, *, exp_dir, cfg: FlowCytometerCfg, inputs):
86
+ if "raw" in inputs:
87
+ return {}
88
+ files = discover_auto_input_files(
89
+ exp_dir=exp_dir,
90
+ auto_roots=cfg.auto_roots,
91
+ auto_include=cfg.auto_include,
92
+ auto_exclude=cfg.auto_exclude,
93
+ auto_recursive=cfg.auto_recursive,
94
+ auto_pick=cfg.auto_pick,
95
+ discovery_label=".fcs files",
96
+ singular_label=".fcs file",
97
+ )
98
+ return {"raw": tuple(files)}
99
+
100
+ def run(self, ctx, inputs, cfg: FlowCytometerCfg):
101
+ files = tuple(inputs["raw"])
102
+ channel_map = {str(k): str(v) for k, v in (cfg.channel_map or {}).items()}
103
+ drop_channels = {str(c) for c in (cfg.drop_channels or [])}
104
+
105
+ frames: list[pd.DataFrame] = []
106
+ channel_meta_frames: list[pd.DataFrame] = []
107
+ for f in files:
108
+ long, channels_meta = parse_fcs_file(
109
+ f,
110
+ channel_name_field=cfg.channel_name_field,
111
+ channel_map=channel_map,
112
+ drop_channels=drop_channels,
113
+ sample_id_from=cfg.sample_id_from,
114
+ time_value=cfg.time_value,
115
+ )
116
+ frames.append(long)
117
+ if not channels_meta.empty:
118
+ channel_meta_frames.append(channels_meta)
119
+
120
+ if not frames:
121
+ raise ParseError("No cytometer frames parsed from selected files")
122
+ out = pd.concat(frames, ignore_index=True)
123
+
124
+ if cfg.print_summary:
125
+ with suppress(Exception):
126
+ ctx.logger.info(
127
+ "flow_cytometer ingest • files=%d • rows=%d • channels=%d • samples=%d",
128
+ len(files),
129
+ len(out),
130
+ out["channel"].nunique(),
131
+ out["sample_id"].nunique(),
132
+ )
133
+
134
+ if channel_meta_frames:
135
+ channels_meta = pd.concat(channel_meta_frames, ignore_index=True)
136
+ else:
137
+ channels_meta = pd.DataFrame(columns=["sample_id", "channel_index", "channel_name"])
138
+
139
+ return {"df": out, "channels": channels_meta}