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,47 @@
1
+ from .model import (
2
+ AnnotationCollections,
3
+ AnnotationCollectionSpec,
4
+ AnnotationLabels,
5
+ AnnotationLabelSpec,
6
+ AnnotationOrders,
7
+ AnnotationOrderSpec,
8
+ AnnotationSemantics,
9
+ ExperimentEvidence,
10
+ ExperimentSemantics,
11
+ FileResourceEntry,
12
+ OrderedStateSpaces,
13
+ OrderedStateSpaceSpec,
14
+ OutputLayout,
15
+ RecordResourceEntry,
16
+ ReplicateKind,
17
+ ResolvedAnnotationLabelSpec,
18
+ ResolvedOrderedStateSpace,
19
+ ResolvedPlotPartition,
20
+ ResourceCatalog,
21
+ ResourceEntry,
22
+ ResourceKind,
23
+ )
24
+
25
+ __all__ = [
26
+ "AnnotationCollectionSpec",
27
+ "AnnotationCollections",
28
+ "AnnotationLabelSpec",
29
+ "AnnotationLabels",
30
+ "AnnotationOrderSpec",
31
+ "AnnotationOrders",
32
+ "AnnotationSemantics",
33
+ "ExperimentEvidence",
34
+ "ExperimentSemantics",
35
+ "FileResourceEntry",
36
+ "OrderedStateSpaceSpec",
37
+ "OrderedStateSpaces",
38
+ "OutputLayout",
39
+ "ResolvedAnnotationLabelSpec",
40
+ "ResolvedOrderedStateSpace",
41
+ "ResolvedPlotPartition",
42
+ "ResourceCatalog",
43
+ "ResourceEntry",
44
+ "ResourceKind",
45
+ "RecordResourceEntry",
46
+ "ReplicateKind",
47
+ ]
@@ -0,0 +1,381 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any, Literal
6
+
7
+ if TYPE_CHECKING:
8
+ from reader_workbench.protocols.model import ProtocolBinding, ProtocolSemanticProgram
9
+
10
+ ResourceKind = Literal["file", "record"]
11
+ ReplicateKind = Literal["biological", "technical", "mixed", "unknown", "not_applicable"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ExperimentEvidence:
16
+ data_class: str
17
+ data_class_reason: str
18
+ replicate_kind: ReplicateKind
19
+ replicate_identity_field: str | None = None
20
+
21
+ def __post_init__(self) -> None:
22
+ if not isinstance(self.data_class, str):
23
+ raise TypeError("data_class must be a string")
24
+ if not isinstance(self.data_class_reason, str):
25
+ raise TypeError("data_class_reason must be a string")
26
+ if not isinstance(self.replicate_kind, str):
27
+ raise TypeError("replicate_kind must be a string")
28
+ if self.replicate_identity_field is not None and not isinstance(self.replicate_identity_field, str):
29
+ raise TypeError("replicate_identity_field must be a string when provided")
30
+ data_class = self.data_class.strip()
31
+ data_class_reason = self.data_class_reason.strip()
32
+ replicate_identity_field = (
33
+ self.replicate_identity_field.strip() if self.replicate_identity_field is not None else None
34
+ )
35
+ if not data_class:
36
+ raise ValueError("data_class must be a non-empty string")
37
+ if not data_class_reason:
38
+ raise ValueError("data_class_reason must be a non-empty string")
39
+ if self.replicate_kind not in {"biological", "technical", "mixed", "unknown", "not_applicable"}:
40
+ raise ValueError(f"unsupported replicate_kind: {self.replicate_kind!r}")
41
+ if self.replicate_identity_field is not None and not replicate_identity_field:
42
+ raise ValueError("replicate_identity_field must be a non-empty string when provided")
43
+ if self.replicate_kind == "not_applicable" and self.replicate_identity_field is not None:
44
+ raise ValueError("replicate_identity_field cannot be set when replicate_kind is not_applicable")
45
+ object.__setattr__(self, "data_class", data_class)
46
+ object.__setattr__(self, "data_class_reason", data_class_reason)
47
+ object.__setattr__(self, "replicate_identity_field", replicate_identity_field)
48
+
49
+ def to_payload(self) -> dict[str, str | None]:
50
+ return {
51
+ "data_class": self.data_class,
52
+ "data_class_reason": self.data_class_reason,
53
+ "replicate_kind": self.replicate_kind,
54
+ "replicate_identity_field": self.replicate_identity_field,
55
+ }
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class AnnotationLabelSpec:
60
+ source: str
61
+ values: dict[str, str] = field(default_factory=dict)
62
+ output: str | None = None
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class ResolvedAnnotationLabelSpec:
67
+ ref: str
68
+ source: str
69
+ output: str | None
70
+ values: dict[str, str]
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class AnnotationLabels:
75
+ by_id: dict[str, AnnotationLabelSpec] = field(default_factory=dict)
76
+
77
+ def resolve(self, refs: list[str] | None = None) -> list[ResolvedAnnotationLabelSpec]:
78
+ requested = list(self.by_id) if refs is None else refs
79
+ if not requested:
80
+ return []
81
+ resolved: list[ResolvedAnnotationLabelSpec] = []
82
+ for raw_ref in requested:
83
+ ref = str(raw_ref).strip()
84
+ if not ref:
85
+ raise ValueError("label refs must be non-empty strings")
86
+ spec = self.by_id.get(ref)
87
+ if spec is None:
88
+ raise ValueError(f"annotations.labels missing key '{ref}'")
89
+ resolved.append(
90
+ ResolvedAnnotationLabelSpec(
91
+ ref=ref,
92
+ source=spec.source,
93
+ output=spec.output,
94
+ values=dict(spec.values),
95
+ )
96
+ )
97
+ return resolved
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class AnnotationOrderSpec:
102
+ column: str
103
+ values: list[str] = field(default_factory=list)
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class AnnotationOrders:
108
+ by_id: dict[str, AnnotationOrderSpec] = field(default_factory=dict)
109
+
110
+ def resolve(
111
+ self,
112
+ *,
113
+ order: list[str] | None,
114
+ order_ref: str | None,
115
+ column: str | None,
116
+ arg_name: str,
117
+ ) -> list[str] | None:
118
+ if order is not None and order_ref is not None:
119
+ raise ValueError(f"{arg_name} and {arg_name}_ref are mutually exclusive")
120
+ if order is not None:
121
+ if not order:
122
+ raise ValueError(f"{arg_name} must not be empty when provided")
123
+ return [str(item) for item in order]
124
+ if order_ref is None:
125
+ return None
126
+ ref = str(order_ref).strip()
127
+ if not ref:
128
+ raise ValueError(f"{arg_name}_ref must be a non-empty string")
129
+ spec = self.by_id.get(ref)
130
+ if spec is None:
131
+ options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
132
+ raise ValueError(
133
+ f"Unknown {arg_name}_ref '{ref}'. Define it under annotations.orders.{ref}. (available: {options})"
134
+ )
135
+ if column and spec.column and str(column) != str(spec.column):
136
+ raise ValueError(f"{arg_name}_ref '{ref}' targets column {spec.column!r}, but plot uses column {column!r}")
137
+ if not spec.values:
138
+ raise ValueError(f"annotations.orders.{ref}.values must not be empty")
139
+ return [str(item) for item in spec.values]
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class AnnotationCollectionSpec:
144
+ column: str
145
+ items: dict[str, list[str]] = field(default_factory=dict)
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class AnnotationCollections:
150
+ by_id: dict[str, AnnotationCollectionSpec] = field(default_factory=dict)
151
+
152
+ def resolve(self, *, ref: str) -> dict[str, Any]:
153
+ collection_ref = str(ref).strip()
154
+ if not collection_ref:
155
+ raise ValueError("collection_ref must be a non-empty string")
156
+ spec = self.by_id.get(collection_ref)
157
+ if spec is None:
158
+ options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
159
+ raise ValueError(
160
+ f"Unknown collection_ref '{collection_ref}'. "
161
+ f"Define it under annotations.collections.{collection_ref}. (available: {options})"
162
+ )
163
+ if not spec.items:
164
+ raise ValueError(f"annotations.collections.{collection_ref}.items must be a non-empty mapping")
165
+ normalized_items: list[dict[str, list[str]]] = []
166
+ for label, values in spec.items.items():
167
+ if not values:
168
+ raise ValueError(f"annotations.collections.{collection_ref}.items.{label} must be a non-empty list")
169
+ normalized_items.append({str(label): [str(value) for value in values]})
170
+ return {"column": spec.column, "items": normalized_items}
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class OrderedStateSpaceSpec:
175
+ column: str
176
+ state_order: tuple[str, ...]
177
+ source_values: dict[str, str]
178
+ case_sensitive: bool = True
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class ResolvedOrderedStateSpace:
183
+ ref: str
184
+ column: str
185
+ state_ids: tuple[str, ...]
186
+ source_values: dict[str, str]
187
+ case_sensitive: bool
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class OrderedStateSpaces:
192
+ by_id: dict[str, OrderedStateSpaceSpec] = field(default_factory=dict)
193
+
194
+ def resolve(self, *, ref: str) -> ResolvedOrderedStateSpace:
195
+ state_map_ref = str(ref).strip()
196
+ if not state_map_ref:
197
+ raise ValueError("state_map_ref must be a non-empty string")
198
+ spec = self.by_id.get(state_map_ref)
199
+ if spec is None:
200
+ options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
201
+ raise ValueError(
202
+ f"Unknown state_map_ref '{state_map_ref}'. "
203
+ f"Define it under annotations.ordered_state_spaces.{state_map_ref}. (available: {options})"
204
+ )
205
+ if not isinstance(spec.column, str) or not spec.column.strip():
206
+ raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref}.column must be a non-empty string")
207
+ state_ids = tuple(str(state_id).strip() for state_id in spec.state_order)
208
+ if not state_ids:
209
+ raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref}.state_order must not be empty")
210
+ if any(not state_id for state_id in state_ids):
211
+ raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} state ids must be non-empty")
212
+ if len(set(state_ids)) != len(state_ids):
213
+ raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} state ids must be unique")
214
+ if set(spec.source_values) != set(state_ids):
215
+ raise ValueError(
216
+ f"annotations.ordered_state_spaces.{state_map_ref}.values must have exactly the ids in state_order"
217
+ )
218
+ source_values = tuple(str(spec.source_values[state_id]) for state_id in state_ids)
219
+ if any(not source_value.strip() for source_value in source_values):
220
+ raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} source values must be non-empty")
221
+ comparison_values = (
222
+ source_values if spec.case_sensitive else tuple(value.strip().casefold() for value in source_values)
223
+ )
224
+ if len(set(comparison_values)) != len(comparison_values):
225
+ sensitivity = "true" if spec.case_sensitive else "false"
226
+ raise ValueError(
227
+ f"annotations.ordered_state_spaces.{state_map_ref} source values must be unique "
228
+ f"under case_sensitive={sensitivity}"
229
+ )
230
+ return ResolvedOrderedStateSpace(
231
+ ref=state_map_ref,
232
+ column=spec.column,
233
+ state_ids=state_ids,
234
+ source_values=dict(zip(state_ids, source_values, strict=True)),
235
+ case_sensitive=bool(spec.case_sensitive),
236
+ )
237
+
238
+
239
+ @dataclass(frozen=True)
240
+ class ResolvedPlotPartition:
241
+ group_by: str | None
242
+ collection_items: list[dict[str, list[str]]] | None
243
+ match: str
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class AnnotationSemantics:
248
+ labels: AnnotationLabels = field(default_factory=AnnotationLabels)
249
+ orders: AnnotationOrders = field(default_factory=AnnotationOrders)
250
+ collections: AnnotationCollections = field(default_factory=AnnotationCollections)
251
+ ordered_state_spaces: OrderedStateSpaces = field(default_factory=OrderedStateSpaces)
252
+
253
+ def resolve_label_specs(self, refs: list[str] | None = None) -> list[ResolvedAnnotationLabelSpec]:
254
+ return self.labels.resolve(refs)
255
+
256
+ def resolve_order_arg(
257
+ self,
258
+ *,
259
+ order: list[str] | None,
260
+ order_ref: str | None,
261
+ column: str | None,
262
+ arg_name: str,
263
+ ) -> list[str] | None:
264
+ return self.orders.resolve(order=order, order_ref=order_ref, column=column, arg_name=arg_name)
265
+
266
+ def resolve_ordered_state_space(self, *, ref: str) -> ResolvedOrderedStateSpace:
267
+ return self.ordered_state_spaces.resolve(ref=ref)
268
+
269
+ def resolve_plot_partition(self, *, partition: dict[str, Any] | Any | None) -> ResolvedPlotPartition:
270
+ if partition is None:
271
+ return ResolvedPlotPartition(group_by=None, collection_items=None, match="exact")
272
+ if hasattr(partition, "model_dump"):
273
+ partition = partition.model_dump()
274
+ if not isinstance(partition, dict):
275
+ raise ValueError("partition must resolve to a mapping")
276
+ group_by_raw = partition.get("by")
277
+ if group_by_raw is not None and (not isinstance(group_by_raw, str) or not group_by_raw.strip()):
278
+ raise ValueError("partition.by must be a non-empty string when provided")
279
+ group_by = str(group_by_raw).strip() if isinstance(group_by_raw, str) else None
280
+ collection_ref_raw = partition.get("collection_ref")
281
+ if collection_ref_raw is not None and (
282
+ not isinstance(collection_ref_raw, str) or not collection_ref_raw.strip()
283
+ ):
284
+ raise ValueError("partition.collection_ref must be a non-empty string when provided")
285
+ collection_ref = str(collection_ref_raw).strip() if isinstance(collection_ref_raw, str) else None
286
+ match = partition.get("match", "exact")
287
+ valid_match = {"exact", "contains", "startswith", "endswith", "regex"}
288
+ if not isinstance(match, str) or match not in valid_match:
289
+ raise ValueError(f"partition.match must be one of {sorted(valid_match)}")
290
+ if collection_ref is None:
291
+ return ResolvedPlotPartition(group_by=group_by, collection_items=None, match=match)
292
+ collection = self.collections.resolve(ref=collection_ref)
293
+ collection_column = collection["column"]
294
+ if group_by is not None and group_by != collection_column:
295
+ raise ValueError(
296
+ f"partition.collection_ref '{collection_ref}' targets column {collection_column!r}, "
297
+ f"but partition.by uses column {group_by!r}"
298
+ )
299
+ return ResolvedPlotPartition(
300
+ group_by=collection_column,
301
+ collection_items=collection["items"],
302
+ match=match,
303
+ )
304
+
305
+
306
+ @dataclass(frozen=True)
307
+ class FileResourceEntry:
308
+ kind: Literal["file"]
309
+ path: Path
310
+
311
+
312
+ @dataclass(frozen=True)
313
+ class RecordResourceEntry:
314
+ kind: Literal["record"]
315
+ experiment_id: str
316
+ record_id: str
317
+ experiment_root: Path
318
+ outputs_dir: Path
319
+
320
+
321
+ ResourceEntry = FileResourceEntry | RecordResourceEntry
322
+
323
+
324
+ @dataclass(frozen=True)
325
+ class ResourceCatalog:
326
+ by_id: dict[str, ResourceEntry] = field(default_factory=dict)
327
+
328
+ def get(self, resource_id: str) -> ResourceEntry | None:
329
+ return self.by_id.get(resource_id)
330
+
331
+ def require(self, resource_id: str) -> ResourceEntry:
332
+ resource = self.get(resource_id)
333
+ if resource is None:
334
+ options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
335
+ raise ValueError(f"Unknown resource '{resource_id}'. Declare it under resources. (available: {options})")
336
+ return resource
337
+
338
+ def require_file(self, resource_id: str) -> FileResourceEntry:
339
+ resource = self.require(resource_id)
340
+ if resource.kind != "file":
341
+ raise ValueError(f"Resource '{resource_id}' has kind '{resource.kind}', expected file")
342
+ return resource
343
+
344
+ def require_record(self, resource_id: str) -> RecordResourceEntry:
345
+ resource = self.require(resource_id)
346
+ if resource.kind != "record":
347
+ raise ValueError(f"Resource '{resource_id}' has kind '{resource.kind}', expected record")
348
+ return resource
349
+
350
+
351
+ @dataclass(frozen=True)
352
+ class OutputLayout:
353
+ outputs_dir: Path
354
+ plots_subdir: str
355
+ exports_subdir: str
356
+ notebooks_subdir: str
357
+
358
+ def subdir_path(self, key: Literal["plots", "exports", "notebooks"]) -> Path:
359
+ raw = {
360
+ "plots": self.plots_subdir,
361
+ "exports": self.exports_subdir,
362
+ "notebooks": self.notebooks_subdir,
363
+ }[key]
364
+ return self.outputs_dir if raw in ("", ".", "./") else self.outputs_dir / raw
365
+
366
+
367
+ @dataclass(frozen=True)
368
+ class ExperimentSemantics:
369
+ protocol: ProtocolBinding
370
+ annotations: AnnotationSemantics
371
+ resources: ResourceCatalog
372
+ layout: OutputLayout
373
+ protocol_program: ProtocolSemanticProgram
374
+ evidence: ExperimentEvidence | None = None
375
+
376
+ def __post_init__(self) -> None:
377
+ if self.protocol_program.protocol != self.protocol.id:
378
+ raise ValueError(
379
+ "ExperimentSemantics.protocol_program must target the bound protocol "
380
+ f"{self.protocol.id!r}, got {self.protocol_program.protocol!r}."
381
+ )
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from reader_workbench.errors import ConfigError
7
+ from reader_workbench.workbench.config import ReaderSpec
8
+ from reader_workbench.workbench.paths import resolve_path_within_root
9
+
10
+ _SCAFFOLD_NAMES = frozenset({"template", "templates"})
11
+ _SCAFFOLD_PREFIXES = ("template_", "scaffold", "_template")
12
+
13
+
14
+ def is_scaffold_dir(path: Path) -> bool:
15
+ name = path.name.strip().lower()
16
+ if not name:
17
+ return False
18
+ return name in _SCAFFOLD_NAMES or any(name.startswith(prefix) for prefix in _SCAFFOLD_PREFIXES)
19
+
20
+
21
+ def discover_experiment_dirs(root: Path, *, include_scaffolds: bool = False) -> list[Path]:
22
+ if not root.exists() or not root.is_dir():
23
+ return []
24
+ experiment_dirs: dict[Path, Path] = {}
25
+ for cfg in root.glob("**/config.yaml"):
26
+ relative = cfg.relative_to(root)
27
+ if "outputs" in relative.parts:
28
+ continue
29
+ exp_dir = cfg.parent.resolve()
30
+ try:
31
+ exp_dir.relative_to(root.resolve())
32
+ except ValueError:
33
+ continue
34
+ if not include_scaffolds and is_scaffold_dir(exp_dir):
35
+ continue
36
+ experiment_dirs[exp_dir] = cfg.resolve()
37
+ return sorted(experiment_dirs)
38
+
39
+
40
+ def discover_experiment_configs(root: Path, *, include_scaffolds: bool = False) -> list[Path]:
41
+ return [exp_dir / "config.yaml" for exp_dir in discover_experiment_dirs(root, include_scaffolds=include_scaffolds)]
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ExperimentLocation:
46
+ id: str
47
+ root: Path
48
+ config_path: Path
49
+ outputs_dir: Path
50
+
51
+
52
+ class ExperimentCatalog:
53
+ """Resolve experiment identities without assuming year or directory naming."""
54
+
55
+ def __init__(self, root: Path) -> None:
56
+ self.root = Path(root).expanduser().resolve()
57
+ if not self.root.is_dir():
58
+ raise ConfigError("The experiments workspace is missing or is not a directory")
59
+ self._locations: dict[str, list[ExperimentLocation]] | None = None
60
+ self._invalid_config_count = 0
61
+
62
+ @classmethod
63
+ def from_experiment_root(cls, experiment_root: Path) -> ExperimentCatalog:
64
+ return cls(find_experiments_root(experiment_root))
65
+
66
+ def resolve(self, experiment_id: str) -> ExperimentLocation:
67
+ identity = _safe_experiment_id(experiment_id)
68
+ if self._locations is None:
69
+ self._locations = self._build_index()
70
+ matches = self._locations.get(identity, [])
71
+ if len(matches) == 1:
72
+ return matches[0]
73
+ if len(matches) > 1:
74
+ rendered = ", ".join(str(item.config_path.relative_to(self.root)) for item in matches)
75
+ raise ConfigError(f"Experiment id {identity!r} is ambiguous in the experiments workspace: {rendered}")
76
+ suffix = (
77
+ f" ({self._invalid_config_count} invalid config(s) were ignored.)" if self._invalid_config_count else ""
78
+ )
79
+ raise ConfigError(f"Unknown experiment id {identity!r} in the experiments workspace.{suffix}")
80
+
81
+ def _build_index(self) -> dict[str, list[ExperimentLocation]]:
82
+ locations: dict[str, list[ExperimentLocation]] = {}
83
+ for config_path in discover_experiment_configs(self.root):
84
+ try:
85
+ spec = ReaderSpec.load(config_path)
86
+ except ConfigError:
87
+ self._invalid_config_count += 1
88
+ continue
89
+ experiment_root = config_path.parent.resolve()
90
+ try:
91
+ outputs_dir = resolve_path_within_root(spec.paths.outputs, root=experiment_root)
92
+ except ValueError:
93
+ self._invalid_config_count += 1
94
+ continue
95
+ locations.setdefault(spec.experiment.id, []).append(
96
+ ExperimentLocation(
97
+ id=spec.experiment.id,
98
+ root=experiment_root,
99
+ config_path=config_path.resolve(),
100
+ outputs_dir=outputs_dir,
101
+ )
102
+ )
103
+ return locations
104
+
105
+
106
+ def find_experiments_root(experiment_root: Path) -> Path:
107
+ resolved = Path(experiment_root).expanduser().resolve()
108
+ for candidate in (resolved, *resolved.parents):
109
+ if candidate.name == "experiments" and candidate.is_dir():
110
+ return candidate
111
+ raise ConfigError(
112
+ f"Experiment {resolved} is not inside a canonical experiments/ workspace; "
113
+ "cross-experiment record resources require that ownership boundary."
114
+ )
115
+
116
+
117
+ def _safe_experiment_id(value: str) -> str:
118
+ if not isinstance(value, str) or not value.strip():
119
+ raise ConfigError("Experiment id must be a non-empty string")
120
+ identity = value.strip()
121
+ if Path(identity).name != identity or identity in {".", ".."}:
122
+ raise ConfigError(f"Experiment id must be one safe path segment: {identity!r}")
123
+ return identity
124
+
125
+
126
+ __all__ = [
127
+ "ExperimentCatalog",
128
+ "ExperimentLocation",
129
+ "discover_experiment_configs",
130
+ "discover_experiment_dirs",
131
+ "find_experiments_root",
132
+ "is_scaffold_dir",
133
+ ]
@@ -0,0 +1,47 @@
1
+ from .nodes import PluginStep, RecipeSource, Workbench, ensure_unique_workbench_ids
2
+ from .normalize import materialize_workbench, normalize_input_binding, resolve_workbench, select_workbench_specs
3
+ from .refs import (
4
+ FileRef,
5
+ InputRef,
6
+ OutputRef,
7
+ ProvenanceInput,
8
+ RecordCollectionRef,
9
+ RecordRef,
10
+ ResourceRef,
11
+ SourceRecordRef,
12
+ input_ref_display,
13
+ input_ref_from_dict,
14
+ input_ref_to_dict,
15
+ output_ref_display,
16
+ output_ref_from_dict,
17
+ output_ref_to_dict,
18
+ provenance_input_from_dict,
19
+ provenance_input_to_dict,
20
+ )
21
+
22
+ __all__ = [
23
+ "FileRef",
24
+ "InputRef",
25
+ "OutputRef",
26
+ "PluginStep",
27
+ "ProvenanceInput",
28
+ "RecordCollectionRef",
29
+ "RecipeSource",
30
+ "RecordRef",
31
+ "ResourceRef",
32
+ "SourceRecordRef",
33
+ "Workbench",
34
+ "ensure_unique_workbench_ids",
35
+ "input_ref_display",
36
+ "input_ref_from_dict",
37
+ "input_ref_to_dict",
38
+ "materialize_workbench",
39
+ "normalize_input_binding",
40
+ "output_ref_display",
41
+ "output_ref_from_dict",
42
+ "output_ref_to_dict",
43
+ "provenance_input_from_dict",
44
+ "provenance_input_to_dict",
45
+ "resolve_workbench",
46
+ "select_workbench_specs",
47
+ ]