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,661 @@
1
+ """Crosstalk pairing utilities built on fold-change tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ TimeMode = Literal["single", "exact", "nearest", "latest", "all"]
12
+ TimePolicy = Literal["per_time", "all"]
13
+ MappingMode = Literal["explicit", "column", "top1"]
14
+ Top1TiePolicy = Literal["error", "alphabetical"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class CrosstalkResult:
19
+ pairs: pd.DataFrame
20
+ designs: pd.DataFrame
21
+ times_used: list[float]
22
+ target_used: str | None
23
+ value_column: str
24
+ value_scale: Literal["log2", "linear"]
25
+
26
+
27
+ def _log(logger, level: str, msg: str, *args) -> None:
28
+ if logger is None:
29
+ return
30
+ try:
31
+ getattr(logger, level)(msg, *args)
32
+ except Exception:
33
+ return
34
+
35
+
36
+ def _select_target(df: pd.DataFrame, target: str | None, logger=None) -> tuple[pd.DataFrame, str | None]:
37
+ if "target" not in df.columns:
38
+ if target is not None:
39
+ raise ValueError("crosstalk_pairs: target specified but 'target' column is missing")
40
+ return df, None
41
+ targets = sorted(df["target"].astype(str).dropna().unique().tolist())
42
+ if target is None:
43
+ if len(targets) > 1:
44
+ raise ValueError(f"crosstalk_pairs: multiple targets present {targets}; set target explicitly")
45
+ if len(targets) == 1:
46
+ return df[df["target"].astype(str) == targets[0]].copy(), targets[0]
47
+ return df.copy(), None
48
+ if target not in targets:
49
+ raise ValueError(f"crosstalk_pairs: target {target!r} not found in table")
50
+ _log(logger, "info", "crosstalk_pairs - target=%s", target)
51
+ return df[df["target"].astype(str) == str(target)].copy(), str(target)
52
+
53
+
54
+ def _select_times(
55
+ df: pd.DataFrame,
56
+ *,
57
+ time_mode: TimeMode,
58
+ time: float | None,
59
+ times: list[float] | None,
60
+ time_column: str,
61
+ tolerance: float,
62
+ logger=None,
63
+ ) -> list[float]:
64
+ if time_column not in df.columns:
65
+ raise ValueError(f"crosstalk_pairs: time column {time_column!r} not found")
66
+ unique = pd.to_numeric(df[time_column], errors="coerce").dropna().unique()
67
+ if unique.size == 0:
68
+ raise ValueError("crosstalk_pairs: no valid time values")
69
+ unique = np.asarray(sorted(unique.astype(float)), float)
70
+
71
+ if time_mode == "single":
72
+ if time is not None or times is not None:
73
+ raise ValueError("crosstalk_pairs: time/time(s) must be omitted when time_mode='single'")
74
+ if len(unique) != 1:
75
+ raise ValueError(f"crosstalk_pairs: expected exactly one time, found {unique.tolist()}")
76
+ return [float(unique[0])]
77
+
78
+ if time_mode == "latest":
79
+ if time is not None or times is not None:
80
+ raise ValueError("crosstalk_pairs: time/time(s) must be omitted when time_mode='latest'")
81
+ return [float(unique[-1])]
82
+
83
+ if time_mode == "all":
84
+ if time is not None or times is not None:
85
+ raise ValueError("crosstalk_pairs: time/time(s) must be omitted when time_mode='all'")
86
+ return [float(x) for x in unique]
87
+
88
+ if time_mode not in {"exact", "nearest"}:
89
+ raise ValueError(f"crosstalk_pairs: unsupported time_mode {time_mode!r}")
90
+
91
+ if (time is None and not times) or (time is not None and times is not None):
92
+ raise ValueError("crosstalk_pairs: provide exactly one of time or times")
93
+
94
+ requested = [float(time)] if time is not None else [float(x) for x in (times or [])]
95
+ selected: list[float] = []
96
+ for req in requested:
97
+ deltas = np.abs(unique - req)
98
+ idx = int(np.nanargmin(deltas))
99
+ if time_mode == "exact":
100
+ candidates = unique[unique == float(req)]
101
+ if candidates.size == 0:
102
+ raise ValueError(f"crosstalk_pairs: time {req} not found in available times {unique.tolist()}")
103
+ selected.append(float(candidates[0]))
104
+ else:
105
+ # nearest must also be within tolerance
106
+ if float(deltas[idx]) > float(tolerance):
107
+ raise ValueError(
108
+ f"crosstalk_pairs: nearest time to {req} is {float(unique[idx])} (delta={float(deltas[idx])}),"
109
+ f" outside tolerance {tolerance}"
110
+ )
111
+ selected.append(float(unique[idx]))
112
+
113
+ out = sorted(set(selected))
114
+ _log(logger, "info", "crosstalk_pairs - times selected: %s", out)
115
+ return out
116
+
117
+
118
+ def _mapping_from_column(
119
+ df: pd.DataFrame,
120
+ *,
121
+ design_col: str,
122
+ mapping_col: str,
123
+ overrides: dict[str, str] | None,
124
+ logger=None,
125
+ ) -> dict[str, str]:
126
+ if mapping_col not in df.columns:
127
+ raise ValueError(f"crosstalk_pairs: mapping column {mapping_col!r} not found")
128
+ mapping: dict[str, str] = {}
129
+ for design, sub in df[[design_col, mapping_col]].dropna().groupby(design_col, dropna=False):
130
+ vals = sorted(sub[mapping_col].astype(str).dropna().unique().tolist())
131
+ if len(vals) == 0:
132
+ continue
133
+ if len(vals) > 1:
134
+ raise ValueError(f"crosstalk_pairs: design {design!r} has multiple mapping values in {mapping_col}: {vals}")
135
+ mapping[str(design)] = str(vals[0])
136
+
137
+ overrides = overrides or {}
138
+ for k, v in overrides.items():
139
+ if k in mapping and mapping[k] != v:
140
+ _log(logger, "warning", "crosstalk_pairs: mapping override for %r (%r -> %r)", k, mapping[k], v)
141
+ if k not in mapping:
142
+ _log(logger, "warning", "crosstalk_pairs: mapping override for unknown design %r", k)
143
+ mapping[str(k)] = str(v)
144
+ return mapping
145
+
146
+
147
+ def _mapping_from_explicit(
148
+ mapping_map: dict[str, str] | None,
149
+ ) -> dict[str, str]:
150
+ mapping_map = mapping_map or {}
151
+ if not mapping_map:
152
+ raise ValueError("crosstalk_pairs: explicit mapping_mode requires design_treatment_map")
153
+ return {str(k): str(v) for k, v in mapping_map.items()}
154
+
155
+
156
+ def _mapping_from_top1(
157
+ matrix: pd.DataFrame,
158
+ *,
159
+ tie_policy: Top1TiePolicy,
160
+ tie_tolerance: float,
161
+ ) -> dict[str, str]:
162
+ mapping: dict[str, str] = {}
163
+ for design, row in matrix.iterrows():
164
+ vals = row.dropna()
165
+ if vals.empty:
166
+ raise ValueError(f"crosstalk_pairs: design {design!r} has no values for top1 mapping")
167
+ if tie_policy == "alphabetical":
168
+ ordered = sorted(vals.items(), key=lambda kv: (-float(kv[1]), str(kv[0])))
169
+ mapping[str(design)] = str(ordered[0][0])
170
+ continue
171
+
172
+ sorted_vals = vals.sort_values(ascending=False)
173
+ top1_value = float(sorted_vals.iloc[0])
174
+ if len(sorted_vals) >= 2:
175
+ top2_value = float(sorted_vals.iloc[1])
176
+ if abs(top1_value - top2_value) <= float(tie_tolerance):
177
+ raise ValueError(
178
+ f"crosstalk_pairs: tie for top1 in design {design!r} (values {top1_value} vs {top2_value})"
179
+ )
180
+ mapping[str(design)] = str(sorted_vals.index[0])
181
+ return mapping
182
+
183
+
184
+ def _aggregate_values(
185
+ df: pd.DataFrame,
186
+ *,
187
+ design_col: str,
188
+ treatment_col: str,
189
+ value_col: str,
190
+ agg: Literal["median", "mean"],
191
+ ) -> pd.DataFrame:
192
+ if agg not in {"median", "mean"}:
193
+ raise ValueError(f"crosstalk_pairs: agg must be 'median' or 'mean', got {agg!r}")
194
+ work = df[[design_col, treatment_col, value_col]].copy()
195
+ work[value_col] = pd.to_numeric(work[value_col], errors="coerce")
196
+ grouped = work.groupby([design_col, treatment_col], dropna=False)[value_col].agg(agg).reset_index()
197
+ return grouped
198
+
199
+
200
+ def _compute_design_summary(
201
+ matrix: pd.DataFrame,
202
+ *,
203
+ mapping_mode: MappingMode,
204
+ mapping: dict[str, str] | None,
205
+ value_scale: Literal["log2", "linear"],
206
+ tie_policy: Top1TiePolicy,
207
+ tie_tolerance: float,
208
+ time_value: float,
209
+ ) -> pd.DataFrame:
210
+ rows: list[dict[str, object]] = []
211
+ for design, row in matrix.iterrows():
212
+ vals = row.dropna()
213
+ if vals.empty:
214
+ raise ValueError(f"crosstalk_pairs: design {design!r} has no values")
215
+
216
+ if tie_policy == "alphabetical":
217
+ ordered = sorted(vals.items(), key=lambda kv: (-float(kv[1]), str(kv[0])))
218
+ top1_treatment, top1_value = ordered[0][0], float(ordered[0][1])
219
+ if len(ordered) >= 2:
220
+ top2_treatment, top2_value = ordered[1][0], float(ordered[1][1])
221
+ else:
222
+ top2_treatment, top2_value = None, float("nan")
223
+ else:
224
+ sorted_vals = vals.sort_values(ascending=False)
225
+ top1_treatment = sorted_vals.index[0]
226
+ top1_value = float(sorted_vals.iloc[0])
227
+ top2_treatment = sorted_vals.index[1] if len(sorted_vals) >= 2 else None
228
+ top2_value = float(sorted_vals.iloc[1]) if len(sorted_vals) >= 2 else float("nan")
229
+ if len(sorted_vals) >= 2 and abs(top1_value - top2_value) <= float(tie_tolerance):
230
+ raise ValueError(
231
+ f"crosstalk_pairs: tie for top1 in design {design!r} (values {top1_value} vs {top2_value})"
232
+ )
233
+
234
+ delta = float(top1_value) - float(top2_value) if np.isfinite(top2_value) else float("nan")
235
+ if value_scale == "log2":
236
+ ratio = float(2**delta) if np.isfinite(delta) else float("nan")
237
+ else:
238
+ ratio = (
239
+ float(top1_value) / float(top2_value) if np.isfinite(top2_value) and top2_value != 0 else float("nan")
240
+ )
241
+
242
+ if mapping_mode == "top1":
243
+ self_treatment = str(top1_treatment)
244
+ else:
245
+ if mapping is None:
246
+ raise ValueError("crosstalk_pairs: mapping is required for mapping_mode 'explicit' or 'column'")
247
+ self_treatment = mapping.get(str(design))
248
+
249
+ self_value = float(row.get(self_treatment)) if self_treatment in row.index else float("nan")
250
+
251
+ other_vals = vals.drop(index=self_treatment, errors="ignore")
252
+ if other_vals.empty:
253
+ best_other_treatment = None
254
+ best_other_value = float("nan")
255
+ else:
256
+ ordered = sorted(other_vals.items(), key=lambda kv: (-float(kv[1]), str(kv[0])))
257
+ best_other_treatment = str(ordered[0][0])
258
+ best_other_value = float(ordered[0][1])
259
+
260
+ if np.isfinite(self_value) and np.isfinite(best_other_value):
261
+ self_minus_best_other = float(self_value) - float(best_other_value)
262
+ if value_scale == "log2":
263
+ self_ratio_best_other = float(2**self_minus_best_other)
264
+ else:
265
+ self_ratio_best_other = (
266
+ float(self_value) / float(best_other_value) if float(best_other_value) != 0 else float("nan")
267
+ )
268
+ else:
269
+ self_minus_best_other = float("nan")
270
+ self_ratio_best_other = float("nan")
271
+
272
+ rows.append(
273
+ {
274
+ "design_id": str(design),
275
+ "time": float(time_value),
276
+ "self_treatment": self_treatment,
277
+ "self_value": self_value,
278
+ "best_other_treatment": best_other_treatment,
279
+ "best_other_value": best_other_value,
280
+ "self_minus_best_other": self_minus_best_other,
281
+ "self_ratio_best_other": self_ratio_best_other,
282
+ "self_is_top1": bool(self_treatment == top1_treatment) if self_treatment is not None else False,
283
+ "top1_treatment": str(top1_treatment) if top1_treatment is not None else None,
284
+ "top1_value": top1_value,
285
+ "top2_treatment": str(top2_treatment) if top2_treatment is not None else None,
286
+ "top2_value": top2_value,
287
+ "selectivity_delta": float(delta),
288
+ "selectivity_ratio": float(ratio),
289
+ "n_treatments": int(len(vals)),
290
+ }
291
+ )
292
+ return pd.DataFrame(rows)
293
+
294
+
295
+ def _empty_pairs_frame() -> pd.DataFrame:
296
+ dtypes = {
297
+ "design_a": "string",
298
+ "design_b": "string",
299
+ "treatment_a": "string",
300
+ "treatment_b": "string",
301
+ "target": "string",
302
+ "time": "float",
303
+ "value_column": "string",
304
+ "value_scale": "string",
305
+ "a_self_value": "float",
306
+ "b_self_value": "float",
307
+ "a_cross_to_b": "float",
308
+ "b_cross_to_a": "float",
309
+ "a_best_other_treatment": "string",
310
+ "b_best_other_treatment": "string",
311
+ "a_best_other_value": "float",
312
+ "b_best_other_value": "float",
313
+ "a_self_minus_best_other": "float",
314
+ "b_self_minus_best_other": "float",
315
+ "a_self_ratio_best_other": "float",
316
+ "b_self_ratio_best_other": "float",
317
+ "a_self_is_top1": "bool",
318
+ "b_self_is_top1": "bool",
319
+ "a_top1_treatment": "string",
320
+ "a_top2_treatment": "string",
321
+ "b_top1_treatment": "string",
322
+ "b_top2_treatment": "string",
323
+ "a_top1_value": "float",
324
+ "a_top2_value": "float",
325
+ "b_top1_value": "float",
326
+ "b_top2_value": "float",
327
+ "a_selectivity_delta": "float",
328
+ "b_selectivity_delta": "float",
329
+ "a_selectivity_ratio": "float",
330
+ "b_selectivity_ratio": "float",
331
+ "pair_score": "float",
332
+ "pair_ratio": "float",
333
+ "passes_filters": "bool",
334
+ }
335
+ return pd.DataFrame({col: pd.Series(dtype=dt) for col, dt in dtypes.items()})
336
+
337
+
338
+ def _ensure_mapping_coverage(
339
+ mapping: dict[str, str] | None,
340
+ *,
341
+ designs: list[str],
342
+ require: bool,
343
+ mapping_mode: MappingMode,
344
+ ) -> None:
345
+ if not require or mapping_mode == "top1":
346
+ return
347
+ mapping = mapping or {}
348
+ missing = sorted(set(designs) - set(mapping.keys()))
349
+ if missing:
350
+ raise ValueError(f"crosstalk_pairs: mapping missing for designs {missing}")
351
+
352
+
353
+ def compute_crosstalk_pairs(
354
+ df: pd.DataFrame,
355
+ *,
356
+ design_col: str,
357
+ treatment_col: str,
358
+ value_col: str,
359
+ value_scale: Literal["log2", "linear"],
360
+ target: str | None = None,
361
+ time_mode: TimeMode,
362
+ time_policy: TimePolicy = "per_time",
363
+ time: float | None = None,
364
+ times: list[float] | None = None,
365
+ time_column: str = "time",
366
+ time_tolerance: float = 0.51,
367
+ mapping_mode: MappingMode,
368
+ design_treatment_column: str | None = None,
369
+ design_treatment_map: dict[str, str] | None = None,
370
+ design_treatment_overrides: dict[str, str] | None = None,
371
+ top1_tie_policy: Top1TiePolicy = "error",
372
+ top1_tie_tolerance: float = 0.0,
373
+ agg: Literal["median", "mean"] = "median",
374
+ require_self_treatment: bool = True,
375
+ require_self_is_top1: bool = False,
376
+ min_self: float | None = None,
377
+ max_cross: float | None = None,
378
+ max_other: float | None = None,
379
+ min_self_minus_best_other: float | None = None,
380
+ min_self_ratio_best_other: float | None = None,
381
+ min_selectivity_delta: float | None = None,
382
+ min_selectivity_ratio: float | None = None,
383
+ only_passing: bool = True,
384
+ logger=None,
385
+ ) -> CrosstalkResult:
386
+ if design_col not in df.columns:
387
+ raise ValueError(f"crosstalk_pairs: design column {design_col!r} not found")
388
+ if treatment_col not in df.columns:
389
+ raise ValueError(f"crosstalk_pairs: treatment column {treatment_col!r} not found")
390
+ if value_col not in df.columns:
391
+ raise ValueError(f"crosstalk_pairs: value column {value_col!r} not found")
392
+
393
+ work, target_used = _select_target(df, target, logger=logger)
394
+ selected_times = _select_times(
395
+ work,
396
+ time_mode=time_mode,
397
+ time=time,
398
+ times=times,
399
+ time_column=time_column,
400
+ tolerance=time_tolerance,
401
+ logger=logger,
402
+ )
403
+ if time_policy not in {"per_time", "all"}:
404
+ raise ValueError(f"crosstalk_pairs: unsupported time_policy {time_policy!r}")
405
+ _log(
406
+ logger,
407
+ "info",
408
+ "crosstalk_pairs - time_mode=%s - time_policy=%s - mapping_mode=%s",
409
+ time_mode,
410
+ time_policy,
411
+ mapping_mode,
412
+ )
413
+
414
+ if mapping_mode not in {"explicit", "column", "top1"}:
415
+ raise ValueError(f"crosstalk_pairs: unsupported mapping_mode {mapping_mode!r}")
416
+
417
+ if mapping_mode == "explicit":
418
+ if design_treatment_column is not None or design_treatment_overrides:
419
+ raise ValueError("crosstalk_pairs: mapping_mode='explicit' forbids design_treatment_column/overrides")
420
+ base_mapping = _mapping_from_explicit(design_treatment_map)
421
+ elif mapping_mode == "column":
422
+ if design_treatment_column is None:
423
+ raise ValueError("crosstalk_pairs: mapping_mode='column' requires design_treatment_column")
424
+ base_mapping = _mapping_from_column(
425
+ work,
426
+ design_col=design_col,
427
+ mapping_col=design_treatment_column,
428
+ overrides=design_treatment_overrides,
429
+ logger=logger,
430
+ )
431
+ else:
432
+ if design_treatment_column is not None or design_treatment_map or design_treatment_overrides:
433
+ raise ValueError("crosstalk_pairs: mapping_mode='top1' forbids explicit mapping inputs")
434
+ base_mapping = {}
435
+
436
+ if mapping_mode in {"explicit", "column"}:
437
+ treatments_present = set(work[treatment_col].astype(str).dropna().tolist())
438
+ unknown = sorted(set(base_mapping.values()) - treatments_present)
439
+ if unknown:
440
+ raise ValueError(f"crosstalk_pairs: mapping values not present in treatment column: {unknown}")
441
+
442
+ all_pairs: list[pd.DataFrame] = []
443
+ all_designs: list[pd.DataFrame] = []
444
+
445
+ for tsel in selected_times:
446
+ sub = work[pd.to_numeric(work[time_column], errors="coerce") == float(tsel)].copy()
447
+ grouped = _aggregate_values(
448
+ sub, design_col=design_col, treatment_col=treatment_col, value_col=value_col, agg=agg
449
+ )
450
+ if grouped.empty:
451
+ raise ValueError(f"crosstalk_pairs: no rows for time {tsel}")
452
+
453
+ matrix = grouped.pivot(index=design_col, columns=treatment_col, values=value_col)
454
+ matrix.index = matrix.index.astype(str)
455
+ matrix.columns = matrix.columns.astype(str)
456
+
457
+ if mapping_mode == "top1":
458
+ mapping = _mapping_from_top1(matrix, tie_policy=top1_tie_policy, tie_tolerance=top1_tie_tolerance)
459
+ else:
460
+ mapping = base_mapping
461
+
462
+ _log(
463
+ logger,
464
+ "info",
465
+ "crosstalk_pairs - t=%.2f - designs=%d - mapping=%d",
466
+ float(tsel),
467
+ int(len(matrix.index)),
468
+ int(len(mapping)) if mapping is not None else 0,
469
+ )
470
+
471
+ _ensure_mapping_coverage(
472
+ mapping, designs=matrix.index.tolist(), require=require_self_treatment, mapping_mode=mapping_mode
473
+ )
474
+
475
+ design_summary = _compute_design_summary(
476
+ matrix,
477
+ mapping_mode=mapping_mode,
478
+ mapping=mapping,
479
+ value_scale=value_scale,
480
+ tie_policy=top1_tie_policy,
481
+ tie_tolerance=top1_tie_tolerance,
482
+ time_value=float(tsel),
483
+ )
484
+ all_designs.append(design_summary)
485
+
486
+ designs = design_summary["design_id"].tolist()
487
+ if len(designs) < 2:
488
+ all_pairs.append(_empty_pairs_frame())
489
+ continue
490
+
491
+ summary_index = design_summary.set_index("design_id")
492
+ rows: list[dict[str, object]] = []
493
+ for i, a in enumerate(designs):
494
+ for b in designs[i + 1 :]:
495
+ a_row = summary_index.loc[a]
496
+ b_row = summary_index.loc[b]
497
+
498
+ treatment_a = a_row["self_treatment"]
499
+ treatment_b = b_row["self_treatment"]
500
+
501
+ a_self = float(a_row["self_value"])
502
+ b_self = float(b_row["self_value"])
503
+
504
+ a_cross = float("nan")
505
+ b_cross = float("nan")
506
+ if treatment_b in matrix.columns and a in matrix.index:
507
+ a_cross = float(matrix.loc[a, treatment_b])
508
+ if treatment_a in matrix.columns and b in matrix.index:
509
+ b_cross = float(matrix.loc[b, treatment_a])
510
+
511
+ min_self_val = (
512
+ float(np.nanmin([a_self, b_self])) if np.isfinite([a_self, b_self]).all() else float("nan")
513
+ )
514
+ max_cross_val = (
515
+ float(np.nanmax([a_cross, b_cross])) if np.isfinite([a_cross, b_cross]).all() else float("nan")
516
+ )
517
+
518
+ if value_scale == "log2":
519
+ pair_score = (
520
+ min_self_val - max_cross_val
521
+ if np.isfinite(min_self_val) and np.isfinite(max_cross_val)
522
+ else float("nan")
523
+ )
524
+ pair_ratio = float(2**pair_score) if np.isfinite(pair_score) else float("nan")
525
+ else:
526
+ pair_score = (
527
+ min_self_val - max_cross_val
528
+ if np.isfinite(min_self_val) and np.isfinite(max_cross_val)
529
+ else float("nan")
530
+ )
531
+ pair_ratio = (
532
+ float(min_self_val) / float(max_cross_val)
533
+ if np.isfinite(min_self_val) and np.isfinite(max_cross_val) and max_cross_val != 0
534
+ else float("nan")
535
+ )
536
+
537
+ passes = True
538
+ if require_self_treatment and (
539
+ not (np.isfinite(a_self) and np.isfinite(b_self) and np.isfinite(a_cross) and np.isfinite(b_cross))
540
+ ):
541
+ passes = False
542
+ if require_self_is_top1 and not (bool(a_row["self_is_top1"]) and bool(b_row["self_is_top1"])):
543
+ passes = False
544
+ if min_self is not None and (
545
+ not (np.isfinite(a_self) and np.isfinite(b_self))
546
+ or a_self < float(min_self)
547
+ or b_self < float(min_self)
548
+ ):
549
+ passes = False
550
+ if max_cross is not None and (
551
+ not (np.isfinite(a_cross) and np.isfinite(b_cross))
552
+ or a_cross > float(max_cross)
553
+ or b_cross > float(max_cross)
554
+ ):
555
+ passes = False
556
+ if min_selectivity_delta is not None and (
557
+ not (np.isfinite(a_row["selectivity_delta"]) and np.isfinite(b_row["selectivity_delta"]))
558
+ or float(a_row["selectivity_delta"]) < float(min_selectivity_delta)
559
+ or float(b_row["selectivity_delta"]) < float(min_selectivity_delta)
560
+ ):
561
+ passes = False
562
+ if min_selectivity_ratio is not None and (
563
+ not (np.isfinite(a_row["selectivity_ratio"]) and np.isfinite(b_row["selectivity_ratio"]))
564
+ or float(a_row["selectivity_ratio"]) < float(min_selectivity_ratio)
565
+ or float(b_row["selectivity_ratio"]) < float(min_selectivity_ratio)
566
+ ):
567
+ passes = False
568
+ if max_other is not None and (
569
+ not (np.isfinite(a_row["best_other_value"]) and np.isfinite(b_row["best_other_value"]))
570
+ or float(a_row["best_other_value"]) > float(max_other)
571
+ or float(b_row["best_other_value"]) > float(max_other)
572
+ ):
573
+ passes = False
574
+ if min_self_minus_best_other is not None and (
575
+ not (np.isfinite(a_row["self_minus_best_other"]) and np.isfinite(b_row["self_minus_best_other"]))
576
+ or float(a_row["self_minus_best_other"]) < float(min_self_minus_best_other)
577
+ or float(b_row["self_minus_best_other"]) < float(min_self_minus_best_other)
578
+ ):
579
+ passes = False
580
+ if min_self_ratio_best_other is not None and (
581
+ not (np.isfinite(a_row["self_ratio_best_other"]) and np.isfinite(b_row["self_ratio_best_other"]))
582
+ or float(a_row["self_ratio_best_other"]) < float(min_self_ratio_best_other)
583
+ or float(b_row["self_ratio_best_other"]) < float(min_self_ratio_best_other)
584
+ ):
585
+ passes = False
586
+
587
+ row = {
588
+ "design_a": a,
589
+ "design_b": b,
590
+ "treatment_a": treatment_a,
591
+ "treatment_b": treatment_b,
592
+ "target": target_used,
593
+ "time": float(tsel),
594
+ "value_column": value_col,
595
+ "value_scale": value_scale,
596
+ "a_self_value": a_self,
597
+ "b_self_value": b_self,
598
+ "a_cross_to_b": a_cross,
599
+ "b_cross_to_a": b_cross,
600
+ "a_best_other_treatment": a_row["best_other_treatment"],
601
+ "b_best_other_treatment": b_row["best_other_treatment"],
602
+ "a_best_other_value": float(a_row["best_other_value"]),
603
+ "b_best_other_value": float(b_row["best_other_value"]),
604
+ "a_self_minus_best_other": float(a_row["self_minus_best_other"]),
605
+ "b_self_minus_best_other": float(b_row["self_minus_best_other"]),
606
+ "a_self_ratio_best_other": float(a_row["self_ratio_best_other"]),
607
+ "b_self_ratio_best_other": float(b_row["self_ratio_best_other"]),
608
+ "a_self_is_top1": bool(a_row["self_is_top1"]),
609
+ "b_self_is_top1": bool(b_row["self_is_top1"]),
610
+ "a_top1_treatment": a_row["top1_treatment"],
611
+ "a_top2_treatment": a_row["top2_treatment"],
612
+ "b_top1_treatment": b_row["top1_treatment"],
613
+ "b_top2_treatment": b_row["top2_treatment"],
614
+ "a_top1_value": float(a_row["top1_value"]),
615
+ "a_top2_value": float(a_row["top2_value"]),
616
+ "b_top1_value": float(b_row["top1_value"]),
617
+ "b_top2_value": float(b_row["top2_value"]),
618
+ "a_selectivity_delta": float(a_row["selectivity_delta"]),
619
+ "b_selectivity_delta": float(b_row["selectivity_delta"]),
620
+ "a_selectivity_ratio": float(a_row["selectivity_ratio"]),
621
+ "b_selectivity_ratio": float(b_row["selectivity_ratio"]),
622
+ "pair_score": pair_score,
623
+ "pair_ratio": pair_ratio,
624
+ "passes_filters": bool(passes),
625
+ }
626
+ rows.append(row)
627
+
628
+ pairs = pd.DataFrame(rows) if rows else _empty_pairs_frame()
629
+ if not pairs.empty:
630
+ pairs = pairs[_empty_pairs_frame().columns]
631
+ all_pairs.append(pairs)
632
+
633
+ pairs_out = pd.concat(all_pairs, ignore_index=True) if all_pairs else _empty_pairs_frame()
634
+ if not pairs_out.empty:
635
+ pairs_out = pairs_out[_empty_pairs_frame().columns]
636
+
637
+ if time_policy == "all" and len(selected_times) > 1 and not pairs_out.empty:
638
+ key_cols = ["design_a", "design_b", "treatment_a", "treatment_b", "target", "value_column", "value_scale"]
639
+ grouped = pairs_out.groupby(key_cols, dropna=False)
640
+ summary = grouped.agg(time_count=("time", "nunique"), all_pass=("passes_filters", "all")).reset_index()
641
+ summary = summary[(summary["time_count"] == len(selected_times)) & (summary["all_pass"])]
642
+ if summary.empty:
643
+ pairs_out = _empty_pairs_frame()
644
+ else:
645
+ pairs_out = pairs_out.merge(summary[key_cols], on=key_cols, how="inner")
646
+
647
+ if only_passing and not pairs_out.empty:
648
+ pairs_out = pairs_out[pairs_out["passes_filters"]].copy()
649
+ if pairs_out.empty:
650
+ pairs_out = _empty_pairs_frame()
651
+
652
+ designs_out = pd.concat(all_designs, ignore_index=True) if all_designs else pd.DataFrame(columns=["design_id"])
653
+
654
+ return CrosstalkResult(
655
+ pairs=pairs_out,
656
+ designs=designs_out,
657
+ times_used=[float(x) for x in selected_times],
658
+ target_used=target_used,
659
+ value_column=value_col,
660
+ value_scale=value_scale,
661
+ )
@@ -0,0 +1,7 @@
1
+ """four-state vector internals used by the transform plugin."""
2
+
3
+ from .math import compute_four_state_vector
4
+ from .reference import resolve_reference_design_id
5
+ from .selection import cornerize_and_aggregate
6
+
7
+ __all__ = ["compute_four_state_vector", "cornerize_and_aggregate", "resolve_reference_design_id"]