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,321 @@
1
+ """Pure four-state vector builder: tidy data to vector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from .config import FourStateVectorConfig, load_four_state_vector_config
13
+ from .math import compute_four_state_vector
14
+ from .reference import compute_reference_table, resolve_reference_design_id
15
+ from .selection import CornerizeResult, cornerize_and_aggregate
16
+
17
+
18
+ def _assert_same_times(time_a: float | None, time_b: float | None) -> None:
19
+ if time_a is None and time_b is None:
20
+ return
21
+ if time_a is None or time_b is None:
22
+ raise ValueError("four-state vector: logic and intensity selections produced different times.")
23
+ ta = float(time_a)
24
+ tb = float(time_b)
25
+ if not np.isclose(ta, tb, rtol=0, atol=1e-9):
26
+ raise ValueError(f"four-state vector: logic and intensity channels selected different times: {ta} vs {tb}")
27
+
28
+
29
+ def _attach_sequence(vector: pd.DataFrame, tidy_df: pd.DataFrame, *, design_by: list[str]) -> pd.DataFrame:
30
+ # If vector already has a usable 'sequence' column, keep it.
31
+ if "sequence" in vector.columns and vector["sequence"].notna().any():
32
+ return vector
33
+
34
+ if "sequence" not in tidy_df.columns:
35
+ return vector
36
+
37
+ idx_cols = [c for c in design_by if c in tidy_df.columns]
38
+ if not idx_cols:
39
+ return vector
40
+
41
+ seq_map = (
42
+ tidy_df[idx_cols + ["sequence"]].dropna(subset=["sequence"]).drop_duplicates(subset=idx_cols, keep="first")
43
+ )
44
+
45
+ merged = vector.merge(seq_map, on=idx_cols, how="left", suffixes=("", "_ref"))
46
+ # Prefer existing 'sequence' if present; otherwise use attached value
47
+ if "sequence_ref" in merged.columns:
48
+ if "sequence" not in merged.columns:
49
+ merged["sequence"] = merged["sequence_ref"]
50
+ else:
51
+ merged["sequence"] = merged["sequence"].fillna(merged["sequence_ref"])
52
+ merged = merged.drop(columns=["sequence_ref"])
53
+
54
+ return merged
55
+
56
+
57
+ def _reorder_and_filter(
58
+ vector: pd.DataFrame,
59
+ *,
60
+ design_by: list[str],
61
+ ref_design_id: str | None,
62
+ exclude_reference: bool,
63
+ ) -> pd.DataFrame:
64
+ """
65
+ Apply user-requested:
66
+ - drop REF design_id rows
67
+ - reorder columns to:
68
+ design_id, sequence, time_selected_h, reference_design_id, r_logic, v00, v10, v01, v11,
69
+ y00_star, y10_star, y01_star, y11_star, flat_logic (then keep remaining columns)
70
+ """
71
+ label_col = design_by[0]
72
+
73
+ df = vector.copy()
74
+
75
+ # Exclude REF (if specified)
76
+ if exclude_reference and ref_design_id is not None and label_col in df.columns:
77
+ df = df[df[label_col].astype(str) != str(ref_design_id)].copy()
78
+
79
+ # Ensure 'sequence' exists even if missing upstream
80
+ if "sequence" not in df.columns:
81
+ df["sequence"] = pd.NA
82
+
83
+ if "reference_sequence" in df.columns:
84
+ df = df.drop(columns=["reference_sequence"])
85
+
86
+ if label_col not in df.columns:
87
+ raise ValueError(f"four-state vector: expected label column '{label_col}' in vector output.")
88
+
89
+ desired = [
90
+ label_col,
91
+ "sequence",
92
+ "time_selected_h",
93
+ "reference_design_id",
94
+ "r_logic",
95
+ "v00",
96
+ "v10",
97
+ "v01",
98
+ "v11",
99
+ "y00_star",
100
+ "y10_star",
101
+ "y01_star",
102
+ "y11_star",
103
+ "flat_logic",
104
+ ]
105
+
106
+ # Reorder preferred columns first, then keep remaining diagnostics/metadata.
107
+ front = [c for c in desired if c in df.columns]
108
+ rest = [c for c in df.columns if c not in front]
109
+ out = df[front + rest].copy()
110
+
111
+ # Final tidy: drop duplicates just in case and reset index
112
+ out = out.drop_duplicates().reset_index(drop=True)
113
+ return out
114
+
115
+
116
+ def _anchors_payload(anchor_table: pd.DataFrame | None) -> dict[str, float] | None:
117
+ if anchor_table is None or anchor_table.empty:
118
+ return None
119
+ row = anchor_table.iloc[0]
120
+ return {
121
+ "00": float(row.get("b00")),
122
+ "10": float(row.get("b10")),
123
+ "01": float(row.get("b01")),
124
+ "11": float(row.get("b11")),
125
+ }
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class FourStateVectorBuildResult:
130
+ vector: pd.DataFrame
131
+ log: dict[str, Any]
132
+ cfg: FourStateVectorConfig
133
+ sel_logic: CornerizeResult
134
+ sel_int: CornerizeResult
135
+ ref_design_id: str | None
136
+
137
+
138
+ def build_four_state_vector_from_tidy(
139
+ tidy_df: pd.DataFrame, xform_cfg: Mapping[str, Any]
140
+ ) -> FourStateVectorBuildResult:
141
+ """
142
+ Shared four-state vector builder for pipeline + notebooks.
143
+ Returns vector + log payload + selection diagnostics without writing to disk.
144
+ """
145
+ cfg = load_four_state_vector_config(xform_cfg)
146
+
147
+ # 1) Cornerize + aggregate (LOGIC channel)
148
+ sel_logic = cornerize_and_aggregate(
149
+ tidy_df,
150
+ design_by=cfg.design_by,
151
+ treatment_map=cfg.treatment_map,
152
+ case_sensitive=cfg.treatment_case_sensitive,
153
+ preferred_treatment_column=cfg.treatment_column,
154
+ time_column=cfg.time_column,
155
+ channel=cfg.response.logic_channel,
156
+ target_time_h=cfg.target_time_h,
157
+ time_mode=cfg.time_mode,
158
+ time_tolerance_h=cfg.time_tolerance_h,
159
+ require_all_corners_per_design=cfg.require_all_corners_per_design,
160
+ )
161
+
162
+ # 2) Cornerize + aggregate (INTENSITY channel)
163
+ sel_int = cornerize_and_aggregate(
164
+ tidy_df,
165
+ design_by=cfg.design_by,
166
+ treatment_map=cfg.treatment_map,
167
+ case_sensitive=cfg.treatment_case_sensitive,
168
+ preferred_treatment_column=cfg.treatment_column,
169
+ time_column=cfg.time_column,
170
+ channel=cfg.response.intensity_channel,
171
+ target_time_h=cfg.target_time_h,
172
+ time_mode=cfg.time_mode,
173
+ time_tolerance_h=cfg.time_tolerance_h,
174
+ require_all_corners_per_design=cfg.require_all_corners_per_design,
175
+ )
176
+
177
+ # 3) Safety: same chosen snapshot times
178
+ _assert_same_times(sel_logic.chosen_time, sel_int.chosen_time)
179
+
180
+ tol_note = sel_logic.time_warning
181
+
182
+ # 4) Compute vector (logic from logic channel, y* from intensity channel + anchors)
183
+ ref_raw = resolve_reference_design_id(tidy_df, design_by=cfg.design_by, ref_label=cfg.reference.design_id)
184
+ vector = compute_four_state_vector(
185
+ points_logic=sel_logic.points,
186
+ points_intensity=sel_int.points,
187
+ per_corner_intensity=sel_int.per_corner,
188
+ design_by=cfg.design_by,
189
+ reference_design_id=ref_raw,
190
+ reference_stat=cfg.reference.observation_stat,
191
+ eps_ratio=cfg.eps_ratio,
192
+ eps_range=cfg.eps_range,
193
+ eps_ref=cfg.eps_ref,
194
+ eps_abs=cfg.eps_abs,
195
+ ref_add_alpha=cfg.ref_add_alpha,
196
+ log2_offset_delta=cfg.log2_offset_delta,
197
+ )
198
+
199
+ # 4a) Attach selected snapshot time
200
+ if sel_logic.chosen_time is not None:
201
+ vector["time_selected_h"] = float(sel_logic.chosen_time)
202
+ vector["time_selected_h"] = pd.to_numeric(vector["time_selected_h"], errors="coerce").astype(float)
203
+
204
+ # 4b) Attach 'sequence' if available in the tidy inputs
205
+ vector = _attach_sequence(vector, tidy_df, design_by=cfg.design_by)
206
+ # 4b.5) Persist intensity log2 offset delta for downstream enforcement
207
+ if "intensity_log2_offset_delta" not in vector.columns:
208
+ vector["intensity_log2_offset_delta"] = float(cfg.log2_offset_delta)
209
+ # Carry additional metadata columns if requested
210
+ idx_cols = [c for c in cfg.design_by if c in tidy_df.columns]
211
+ for col in cfg.carry_metadata or []:
212
+ if col in tidy_df.columns and col not in vector.columns:
213
+ meta = tidy_df[idx_cols + [col]].dropna(subset=[col]).drop_duplicates(subset=idx_cols, keep="first")
214
+ vector = vector.merge(meta, on=idx_cols, how="left", validate="m:1")
215
+
216
+ # 4c) Reference provenance columns
217
+ if "reference_design_id" not in vector.columns:
218
+ vector["reference_design_id"] = ref_raw
219
+
220
+ # 4d) Apply requested filtering and column order
221
+ vector_out = _reorder_and_filter(
222
+ vector,
223
+ design_by=cfg.design_by,
224
+ ref_design_id=ref_raw,
225
+ exclude_reference=cfg.exclude_reference_from_output,
226
+ )
227
+
228
+ # 5) Compose a log payload (unchanged)
229
+ # Compute r_logic distribution for quick provenance metrics
230
+ r_desc = vector_out["r_logic"].describe().to_dict() if "r_logic" in vector_out.columns else {}
231
+ flat_count = 0
232
+ flat_fraction = 0.0
233
+ flat_samples: list[str] = []
234
+ if "flat_logic" in vector_out.columns and len(vector_out) > 0:
235
+ flat_mask = vector_out["flat_logic"].astype(bool)
236
+ flat_count = int(flat_mask.sum())
237
+ flat_fraction = float(flat_count) / float(len(vector_out)) if len(vector_out) else 0.0
238
+ label_col = cfg.design_by[0] if cfg.design_by else None
239
+ if label_col and label_col in vector_out.columns and flat_count > 0:
240
+ flat_samples = vector_out.loc[flat_mask, label_col].astype(str).dropna().drop_duplicates().head(5).tolist()
241
+
242
+ log_payload: dict[str, Any] = {
243
+ "operation": "four_state_vector",
244
+ "design_by": cfg.design_by,
245
+ "time_column": cfg.time_column,
246
+ "channels": {
247
+ "logic": cfg.response.logic_channel,
248
+ "intensity": cfg.response.intensity_channel,
249
+ },
250
+ "treatment_map": cfg.treatment_map,
251
+ "treatment_case_sensitive": cfg.treatment_case_sensitive,
252
+ "time": {
253
+ "target_time_h": cfg.target_time_h,
254
+ "mode": cfg.time_mode,
255
+ "tolerance_h": cfg.time_tolerance_h,
256
+ "tolerance_policy": "soft-warning",
257
+ "chosen_time": sel_logic.chosen_time,
258
+ "out_of_tolerance": tol_note,
259
+ },
260
+ "reference": {
261
+ "design_id": cfg.reference.design_id,
262
+ "design_id_resolved": ref_raw,
263
+ "observation_stat": cfg.reference.observation_stat,
264
+ "anchors": _anchors_payload(
265
+ compute_reference_table(
266
+ sel_int.per_corner,
267
+ design_by=cfg.design_by,
268
+ ref_design_id=ref_raw,
269
+ stat=cfg.reference.observation_stat,
270
+ )
271
+ if ref_raw is not None
272
+ else None,
273
+ ),
274
+ },
275
+ "eps": {
276
+ "ratio": cfg.eps_ratio,
277
+ "range": cfg.eps_range,
278
+ "eta": cfg.eps_range,
279
+ "ref": cfg.eps_ref,
280
+ "abs": cfg.eps_abs,
281
+ },
282
+ "semantics": {
283
+ "v": {
284
+ "source_channel": cfg.response.logic_channel,
285
+ "transform": "u = log2(L); v = (u - min(u))/((max(u)-min(u)) + eta)",
286
+ "persisted_units": "unit-interval [0,1] (not log)",
287
+ "eta": cfg.eps_range,
288
+ },
289
+ "y_star": {
290
+ "source_channel": cfg.response.intensity_channel,
291
+ "transform": "y* = log2( ((I + eps_abs)/max(A + alpha, eps_ref)) + delta )",
292
+ "persisted_units": "log2",
293
+ "alpha": cfg.ref_add_alpha,
294
+ "delta": cfg.log2_offset_delta,
295
+ },
296
+ "r_logic": {
297
+ "definition": "per design dynamic range on LOGIC (linear, ε-guarded): max(L_i)/min(L_i)",
298
+ "epsilon_guard_ratio": cfg.eps_ratio,
299
+ "stats_over_vector": r_desc,
300
+ },
301
+ },
302
+ "rows": {
303
+ "per_corner_logic": int(len(sel_logic.per_corner)),
304
+ "per_corner_intensity": int(len(sel_int.per_corner)),
305
+ "points_logic": int(len(sel_logic.points)),
306
+ "points_intensity": int(len(sel_int.points)),
307
+ "vector": int(len(vector_out)),
308
+ },
309
+ "flat_logic_count": int(flat_count),
310
+ "flat_logic_fraction": float(flat_fraction),
311
+ "flat_logic_sample_design_ids": flat_samples,
312
+ }
313
+
314
+ return FourStateVectorBuildResult(
315
+ vector=vector_out,
316
+ log=log_payload,
317
+ cfg=cfg,
318
+ sel_logic=sel_logic,
319
+ sel_int=sel_int,
320
+ ref_design_id=ref_raw,
321
+ )
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from .constants import VECTOR_CHANNELS
4
+ from .model import FourStateVectorCollection, FourStateVectorSource
5
+ from .sources import collect_four_state_vector_sources
6
+
7
+
8
+ def render_four_state_vector_collection_heatmap(*args, **kwargs):
9
+ from .render import render_four_state_vector_collection_heatmap as render # noqa: PLC0415
10
+
11
+ return render(*args, **kwargs)
12
+
13
+
14
+ __all__ = [
15
+ "FourStateVectorCollection",
16
+ "FourStateVectorSource",
17
+ "VECTOR_CHANNELS",
18
+ "collect_four_state_vector_sources",
19
+ "render_four_state_vector_collection_heatmap",
20
+ ]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import pandas as pd
6
+
7
+ from reader_workbench.errors import FourStateVectorError
8
+
9
+ from ..validation import require_intensity_delta_column
10
+ from .constants import REQUIRED_VECTOR_COLUMNS
11
+
12
+
13
+ def require_vector_columns(frame: pd.DataFrame) -> None:
14
+ require_intensity_delta_column(frame)
15
+ missing = [column for column in REQUIRED_VECTOR_COLUMNS if column not in frame.columns]
16
+ if missing:
17
+ raise FourStateVectorError(f"four-state vector input requires columns: {', '.join(missing)}.")
18
+
19
+
20
+ def require_normalized_frame(frame: pd.DataFrame) -> None:
21
+ require_vector_columns(frame)
22
+ source_columns = {"source_resource_id", "source_experiment_id"} & set(frame.columns)
23
+ missing = ([] if source_columns else ["source_resource_id or source_experiment_id"]) + (
24
+ [] if "row_label" in frame.columns else ["row_label"]
25
+ )
26
+ if missing:
27
+ raise FourStateVectorError(f"four-state vector collection requires normalized columns: {', '.join(missing)}.")
28
+ if frame.empty:
29
+ raise FourStateVectorError("four-state vector collection has no rows to plot.")
30
+
31
+
32
+ def finite_numeric_column(
33
+ series: pd.Series,
34
+ *,
35
+ column: str,
36
+ source: str,
37
+ allow_nan: bool = False,
38
+ ) -> pd.Series:
39
+ values = pd.to_numeric(series, errors="coerce")
40
+ coerced_to_nan = series.notna() & values.isna()
41
+ nonfinite = values.notna() & ~values.fillna(0.0).map(lambda value: math.isfinite(float(value)))
42
+ invalid = coerced_to_nan | nonfinite
43
+ if not allow_nan:
44
+ invalid |= values.isna()
45
+ if invalid.any():
46
+ raise FourStateVectorError(
47
+ f"four-state vector collection column {column!r} must be finite numeric values in {source}."
48
+ )
49
+ return values.astype(float)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ VECTOR_CHANNELS = ("v00", "v10", "v01", "v11", "y00_star", "y10_star", "y01_star", "y11_star")
4
+ REQUIRED_VECTOR_COLUMNS = (
5
+ "design_id",
6
+ "reference_design_id",
7
+ "intensity_log2_offset_delta",
8
+ "r_logic",
9
+ *VECTOR_CHANNELS,
10
+ "flat_logic",
11
+ )
12
+
13
+ METADATA_COLUMNS = (
14
+ "source_index",
15
+ "source_resource_id",
16
+ "source_experiment_id",
17
+ "source_record_id",
18
+ "source_record_revision_digest",
19
+ "source_row_index",
20
+ "row_label",
21
+ "design_id",
22
+ "sequence",
23
+ "id",
24
+ "time_selected_h",
25
+ "reference_design_id",
26
+ "intensity_log2_offset_delta",
27
+ "r_logic",
28
+ "flat_logic",
29
+ )
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pandas as pd
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class FourStateVectorSource:
10
+ resource_id: str
11
+ experiment_id: str
12
+ record_id: str
13
+ revision_digest: str
14
+ frame: pd.DataFrame
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class FourStateVectorCollection:
19
+ frame: pd.DataFrame