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,192 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ import pandas as pd
7
+
8
+ from reader_workbench.domains.logic.treatment_columns import choose_treatment_column, normalize_treatment_series
9
+
10
+ LOG = logging.getLogger(__name__)
11
+
12
+ REQUIRED_BASE_COLS = ["position", "time", "channel", "value"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MappingConfig:
17
+ treatment_map: dict[str, str] # keys: "00","10","01","11" → exact data labels
18
+ case_sensitive: bool
19
+ treatment_column: str | None
20
+ design_by: list[str]
21
+ batch_col: str
22
+ response_channel: str
23
+ observation_stat: str # "mean" | "median"
24
+
25
+
26
+ def _assert_required_columns(df: pd.DataFrame, cfg: MappingConfig) -> None:
27
+ missing = [c for c in REQUIRED_BASE_COLS if c not in df.columns]
28
+ if missing:
29
+ raise ValueError(f"Input DataFrame missing required columns: {missing}")
30
+ meta_missing = [c for c in cfg.design_by if c not in df.columns]
31
+ if meta_missing:
32
+ raise ValueError(f"Missing design_by columns: {meta_missing}")
33
+ if cfg.batch_col not in df.columns:
34
+ raise ValueError(f"Missing required batch column '{cfg.batch_col}'")
35
+
36
+ # Batch must be numeric ordinal
37
+ try:
38
+ pd.to_numeric(df[cfg.batch_col])
39
+ except Exception as err:
40
+ raise ValueError(f"Batch column '{cfg.batch_col}' must be numeric (0,1,2,...)") from err
41
+
42
+ if cfg.response_channel not in df["channel"].unique().tolist():
43
+ raise ValueError(f"response_channel '{cfg.response_channel}' not present in 'channel' column")
44
+
45
+
46
+ def _observation_aggregate(series: pd.Series, how: str) -> float:
47
+ if how == "median":
48
+ return float(series.median())
49
+ return float(series.mean())
50
+
51
+
52
+ def _sd(series: pd.Series) -> float:
53
+ # population-like SD with ddof=1 when n>1; 0 otherwise
54
+ n = series.size
55
+ if n <= 1:
56
+ return 0.0
57
+ return float(series.std(ddof=1))
58
+
59
+
60
+ def _fail_if_multiple_times(df: pd.DataFrame, group_cols: list[str]) -> None:
61
+ """
62
+ Enforce: exactly ONE time per (design..., batch, treatment).
63
+ If violations exist, raise with the list of offending groups and their times.
64
+ """
65
+ chk = df.groupby(group_cols)["time"].nunique().reset_index(name="n_times")
66
+ bad = chk[chk["n_times"] > 1]
67
+ if not bad.empty:
68
+ offenders = []
69
+ for _, row in bad.iterrows():
70
+ filt = (df[group_cols] == row[group_cols]).all(axis=1)
71
+ times = sorted(df.loc[filt, "time"].unique().tolist())
72
+ key_str = ", ".join(f"{c}={row[c]!r}" for c in group_cols)
73
+ offenders.append(f"{key_str} → times={times}")
74
+ msg = "Snapshot violation: more than one 'time' for some (design…, batch, treatment).\n" + "\n".join(
75
+ offenders[:50]
76
+ )
77
+ raise ValueError(msg)
78
+
79
+
80
+ def resolve_and_aggregate(df: pd.DataFrame, cfg: MappingConfig) -> tuple[pd.DataFrame, pd.DataFrame]:
81
+ """
82
+ Returns:
83
+ points_df: one row per (design_by…, batch) with corner means, SDs, counts
84
+ per_corner_df: per (design…, batch, corner) aggregated table
85
+ """
86
+ _assert_required_columns(df, cfg)
87
+
88
+ # Filter to the channel
89
+ before = len(df)
90
+ df = df[df["channel"] == cfg.response_channel].copy()
91
+ LOG.info("• extract: filtered to channel %r → %d/%d rows", cfg.response_channel, len(df), before)
92
+ if df.empty:
93
+ raise ValueError(f"No rows for response_channel '{cfg.response_channel}'")
94
+
95
+ treatment_col = choose_treatment_column(
96
+ df,
97
+ cfg.treatment_map,
98
+ case_sensitive=cfg.case_sensitive,
99
+ preferred=cfg.treatment_column,
100
+ )
101
+
102
+ # Keep only rows whose treatment matches one of the mapped labels
103
+ map_vals = list(cfg.treatment_map.values())
104
+ if not cfg.case_sensitive:
105
+ df["_t_norm"] = normalize_treatment_series(df[treatment_col])
106
+ map_norm = [str(v).strip().casefold() for v in map_vals]
107
+ df = df[df["_t_norm"].isin(map_norm)].copy()
108
+ else:
109
+ df = df[df[treatment_col].astype(str).isin([str(v) for v in map_vals])].copy()
110
+ LOG.info(
111
+ "• extract: kept rows matching treatment_map labels → %d rows; unique treatments kept=%d",
112
+ len(df),
113
+ df[treatment_col].nunique(),
114
+ )
115
+
116
+ if df.empty:
117
+ counts = (
118
+ df[treatment_col].value_counts().to_string() if treatment_col in df.columns else "(no treatment column)"
119
+ )
120
+ raise ValueError(f"No rows match any treatment_map labels. Check spelling/case.\nAvailable counts:\n{counts}")
121
+
122
+ # Add 'corner' column via reverse lookup (value→corner)
123
+ rev = {}
124
+ seen = set()
125
+ for corner in ("00", "10", "01", "11"):
126
+ val = cfg.treatment_map.get(corner)
127
+ if val in seen:
128
+ raise ValueError(f"Duplicate treatment_map values detected: {val!r} is used by multiple corners")
129
+ seen.add(val)
130
+ rev[str(val) if cfg.case_sensitive else str(val).strip().casefold()] = corner
131
+
132
+ if cfg.case_sensitive:
133
+ df["corner"] = df[treatment_col].astype(str).map(lambda x: rev.get(x))
134
+ else:
135
+ df["corner"] = normalize_treatment_series(df[treatment_col]).map(lambda x: rev.get(x))
136
+
137
+ # Enforce snapshot rule per (design…, batch, treatment/corner)
138
+ group_cols = cfg.design_by + [cfg.batch_col, "corner"]
139
+ _fail_if_multiple_times(df, group_cols)
140
+
141
+ # Aggregate observations per corner at that unique time.
142
+ agg_rows = []
143
+ for keys, g in df.groupby(group_cols, dropna=False):
144
+ # There is exactly one time by construction; keep it
145
+ t_unique = float(g["time"].iloc[0])
146
+ val = pd.to_numeric(g["value"], errors="coerce").dropna()
147
+ if val.empty:
148
+ key_str = ", ".join(f"{c}={k!r}" for c, k in zip(group_cols, keys, strict=False))
149
+ raise ValueError(f"Non-numeric or missing 'value' for group: {key_str}")
150
+ mean_or_med = _observation_aggregate(val, cfg.observation_stat)
151
+ sd = _sd(val)
152
+ n = int(val.size)
153
+ record = dict(zip(group_cols, keys, strict=False))
154
+ record.update(time=t_unique, y_mean=mean_or_med, y_sd=sd, y_n=n)
155
+ agg_rows.append(record)
156
+
157
+ per_corner = pd.DataFrame.from_records(agg_rows)
158
+ LOG.info("• extract: per-corner aggregates = %d rows (design×batch×corner)", len(per_corner))
159
+
160
+ # Pivot to wide per (design…, batch)
161
+ idx_cols = cfg.design_by + [cfg.batch_col]
162
+ pivot_mean = per_corner.pivot_table(index=idx_cols, columns="corner", values="y_mean", aggfunc="first")
163
+ pivot_sd = per_corner.pivot_table(index=idx_cols, columns="corner", values="y_sd", aggfunc="first")
164
+ pivot_n = per_corner.pivot_table(index=idx_cols, columns="corner", values="y_n", aggfunc="first")
165
+
166
+ # Ensure all four corners present for every group (fail-fast)
167
+ required = ["00", "10", "01", "11"]
168
+ missing_groups = []
169
+ for idx, row in pivot_mean.iterrows():
170
+ missing = [c for c in required if pd.isna(row.get(c))]
171
+ if missing:
172
+ key_str = ", ".join(
173
+ f"{c}={v!r}" for c, v in zip(idx_cols, (idx if isinstance(idx, tuple) else (idx,)), strict=False)
174
+ )
175
+ missing_groups.append(f"{key_str} → missing corners: {missing}")
176
+ if missing_groups:
177
+ LOG.error("• extract: incomplete corner set for some groups (%d shown):", len(missing_groups))
178
+ for line in missing_groups[:10]:
179
+ LOG.error(" %s", line)
180
+ msg = "Incomplete corner set: some (design…, batch) groups lack one or more of {00,10,01,11}.\n" + "\n".join(
181
+ missing_groups[:50]
182
+ )
183
+ raise ValueError(msg)
184
+
185
+ pivot_mean = pivot_mean[required].rename(columns={"00": "b00", "10": "b10", "01": "b01", "11": "b11"})
186
+ pivot_sd = pivot_sd[required].rename(columns={"00": "sd00", "10": "sd10", "01": "sd01", "11": "sd11"})
187
+ pivot_n = pivot_n[required].rename(columns={"00": "n00", "10": "n10", "01": "n01", "11": "n11"}).astype(int)
188
+
189
+ points = pivot_mean.join(pivot_sd, how="left").join(pivot_n, how="left").reset_index()
190
+
191
+ LOG.info("• extract: wide table rows (designxbatch) = %d", len(points))
192
+ return points, per_corner
@@ -0,0 +1,236 @@
1
+ """Pure logic-symmetry summary and figure operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ import pandas as pd
8
+
9
+ from .encodings import EncodingConfig, apply_encodings
10
+ from .extract_corners import MappingConfig, resolve_and_aggregate
11
+ from .metrics import CornerStats, compute_metrics
12
+ from .overlay import OverlayStyle
13
+ from .prep import prepare_for_logic_symmetry
14
+ from .render import VisualConfig, draw_scatter
15
+
16
+ LOG = logging.getLogger(__name__)
17
+
18
+
19
+ def _dget(values: dict | None, key: str, default):
20
+ if values is None:
21
+ return default
22
+ if key in values and values[key] is not None:
23
+ return values[key]
24
+ return default
25
+
26
+
27
+ def _pick_baseline_corner(row: pd.Series) -> str:
28
+ corners = ("b00", "b10", "b01", "b11")
29
+ values = {corner: float(row[corner]) for corner in corners}
30
+ minimum = min(values.values())
31
+ for corner in corners:
32
+ if values[corner] == minimum:
33
+ return corner.removeprefix("b")
34
+ return "00"
35
+
36
+
37
+ def summarize_logic_symmetry(
38
+ df: pd.DataFrame,
39
+ *,
40
+ response_channel: str,
41
+ design_by: list[str] | None = None,
42
+ batch_col: str = "batch",
43
+ treatment_column: str | None = None,
44
+ treatment_map: dict[str, str] | None = None,
45
+ treatment_case_sensitive: bool = True,
46
+ observation_stat: str = "mean",
47
+ prep: dict | None = None,
48
+ ) -> pd.DataFrame:
49
+ """Compute one contract-ready logic-symmetry row per design and batch."""
50
+
51
+ design_by = list(design_by or ["design_id"])
52
+ if treatment_map is None or set(treatment_map) != {"00", "10", "01", "11"}:
53
+ raise ValueError(
54
+ "treatment_map must be provided with keys {'00','10','01','11'} and single exact labels as values"
55
+ )
56
+ if observation_stat not in {"mean", "median"}:
57
+ raise ValueError(f"observation_stat must be 'mean' or 'median', got {observation_stat!r}")
58
+
59
+ LOG.info("logic_symmetry: computing summary")
60
+ LOG.info("response_channel=%s | design_by=%s | batch_col=%s", response_channel, design_by, batch_col)
61
+ LOG.info(
62
+ "state_map: 00=%r | 10=%r | 01=%r | 11=%r | case_sensitive=%s",
63
+ treatment_map["00"],
64
+ treatment_map["10"],
65
+ treatment_map["01"],
66
+ treatment_map["11"],
67
+ treatment_case_sensitive,
68
+ )
69
+
70
+ if prep and bool(prep.get("enable", False)):
71
+ df = prepare_for_logic_symmetry(
72
+ df,
73
+ response_channel=response_channel,
74
+ design_by=design_by,
75
+ batch_col=batch_col,
76
+ treatment_map=treatment_map,
77
+ treatment_column=treatment_column,
78
+ mode=str(prep.get("mode", "last")),
79
+ target_time=prep.get("target_time"),
80
+ tolerance=float(prep.get("tolerance", 0.51)),
81
+ align_corners=bool(prep.get("align_corners", False)),
82
+ case_sensitive=bool(prep.get("case_sensitive_treatments", treatment_case_sensitive)),
83
+ time_column=str(prep.get("time_column", "time")),
84
+ )
85
+ LOG.info("logic_symmetry: rows after time selection=%d", len(df))
86
+
87
+ mapping = MappingConfig(
88
+ treatment_map=treatment_map,
89
+ case_sensitive=treatment_case_sensitive,
90
+ treatment_column=treatment_column,
91
+ design_by=design_by,
92
+ batch_col=batch_col,
93
+ response_channel=response_channel,
94
+ observation_stat=observation_stat,
95
+ )
96
+ points, _ = resolve_and_aggregate(df, mapping)
97
+ LOG.info("logic_symmetry: aggregated groups=%d", len(points))
98
+
99
+ metrics = []
100
+ for _, row in points.iterrows():
101
+ metrics.append(
102
+ compute_metrics(
103
+ CornerStats(
104
+ b00=float(row["b00"]),
105
+ b10=float(row["b10"]),
106
+ b01=float(row["b01"]),
107
+ b11=float(row["b11"]),
108
+ n00=int(row["n00"]),
109
+ n10=int(row["n10"]),
110
+ n01=int(row["n01"]),
111
+ n11=int(row["n11"]),
112
+ sd00=float(row["sd00"]),
113
+ sd10=float(row["sd10"]),
114
+ sd01=float(row["sd01"]),
115
+ sd11=float(row["sd11"]),
116
+ )
117
+ )
118
+ )
119
+ metric_table = pd.DataFrame.from_records(metrics, index=points.index)
120
+ summary = pd.concat([points.reset_index(drop=True), metric_table.reset_index(drop=True)], axis=1)
121
+ summary["baseline_corner"] = summary.apply(_pick_baseline_corner, axis=1).astype("string")
122
+ summary["baseline_value"] = summary[["b00", "b10", "b01", "b11"]].min(axis=1).astype(float)
123
+
124
+ value_columns = [
125
+ "n00",
126
+ "n10",
127
+ "n01",
128
+ "n11",
129
+ "b00",
130
+ "b10",
131
+ "b01",
132
+ "b11",
133
+ "sd00",
134
+ "sd10",
135
+ "sd01",
136
+ "sd11",
137
+ "r",
138
+ "log_r",
139
+ "cv",
140
+ "u00",
141
+ "u10",
142
+ "u01",
143
+ "u11",
144
+ "L",
145
+ "A",
146
+ "baseline_corner",
147
+ "baseline_value",
148
+ ]
149
+ identity_columns = list(dict.fromkeys([*design_by, batch_col]))
150
+ return summary[[*identity_columns, *value_columns]].copy()
151
+
152
+
153
+ def render_logic_symmetry(
154
+ table: pd.DataFrame,
155
+ *,
156
+ title: str = "Logic symmetry",
157
+ dispersion: str = "halo",
158
+ encodings: dict | None = None,
159
+ ideals_overlay: dict | None = None,
160
+ visuals: dict | None = None,
161
+ figsize: tuple[float, float] = (7, 6),
162
+ dpi: int = 300,
163
+ ):
164
+ """Render a logic-symmetry figure from a persisted summary table."""
165
+
166
+ if dispersion not in {"none", "bars", "halo"}:
167
+ raise ValueError(f"dispersion must be one of 'none'|'bars'|'halo', got {dispersion!r}")
168
+
169
+ hue = _dget(encodings, "hue", None)
170
+ if hue is None or str(hue).lower() in {"baseline", "baseline_corner", "min", "min_corner"}:
171
+ hue = "baseline_corner"
172
+ encoding = EncodingConfig(
173
+ size_by=str(_dget(encodings, "size_by", "log_r")),
174
+ size_fixed=float(_dget(encodings, "size_fixed", 80.0)),
175
+ hue=str(hue),
176
+ alpha_by=_dget(encodings, "alpha_by", "batch"),
177
+ alpha_min=float(_dget(encodings, "alpha_min", 0.35)),
178
+ alpha_max=float(_dget(encodings, "alpha_max", 1.0)),
179
+ shape_by=_dget(encodings, "shape_by", None),
180
+ shape_cycle=list(_dget(encodings, "shape_cycle", ["o", "s", "^", "D", "P", "X", "v", "*"])),
181
+ shape_max_categories=_dget(encodings, "shape_max_categories", None),
182
+ )
183
+ for column in {encoding.hue, encoding.alpha_by, encoding.shape_by} - {None}:
184
+ if column not in table.columns:
185
+ raise ValueError(f"Logic-symmetry encoding refers to missing column {column!r}")
186
+ encoded = apply_encodings(table, encoding)
187
+
188
+ overlay_enabled = bool(_dget(ideals_overlay, "enable", False))
189
+ overlay_gate_set = str(_dget(ideals_overlay, "gate_set", "logic_family"))
190
+ overlay_values = _dget(ideals_overlay, "style", {})
191
+ overlay_mode = _dget(overlay_values, "mode", None)
192
+ if overlay_mode is None:
193
+ overlay_mode = "tiles" if overlay_gate_set.startswith("tiles") else "dot"
194
+ overlay = OverlayStyle(
195
+ mode=str(overlay_mode),
196
+ alpha=float(_dget(overlay_values, "alpha", 0.25)),
197
+ size=float(_dget(overlay_values, "size", 40.0)),
198
+ face_color=str(_dget(overlay_values, "face_color", "#FFFFFF")),
199
+ edge_color=str(_dget(overlay_values, "edge_color", _dget(overlay_values, "color", "#888888"))),
200
+ show_labels=bool(_dget(overlay_values, "show_labels", True)),
201
+ label_offset=float(_dget(overlay_values, "label_offset", 0.02)),
202
+ label_line_height=float(_dget(overlay_values, "label_line_height", 0.018)),
203
+ label_fontsize=int(_dget(overlay_values, "label_fontsize", 12)),
204
+ tile_cell_w=float(_dget(overlay_values, "tile_cell_w", 0.035)),
205
+ tile_cell_h=float(_dget(overlay_values, "tile_cell_h", 0.035)),
206
+ tile_gap=float(_dget(overlay_values, "tile_gap", 0.0)),
207
+ tile_edge_width=float(_dget(overlay_values, "tile_edge_width", 0.6)),
208
+ tiles_stack_multiple=bool(_dget(overlay_values, "tiles_stack_multiple", True)),
209
+ )
210
+
211
+ visual = VisualConfig(
212
+ xlim=tuple(_dget(visuals, "xlim", (-1.02, 1.02))),
213
+ ylim=tuple(_dget(visuals, "ylim", (-1.02, 1.02))),
214
+ grid=bool(_dget(visuals, "grid", True)),
215
+ color=str(_dget(visuals, "color", "#6e6e6e")),
216
+ annotate_designs=bool(_dget(visuals, "annotate_designs", False)),
217
+ design_label_col=_dget(visuals, "design_label_col", None),
218
+ label_fontsize=int(_dget(visuals, "label_fontsize", 12)),
219
+ label_offset=float(_dget(visuals, "label_offset", 0.02)),
220
+ axis_label_fontsize=int(_dget(visuals, "axis_label_fontsize", 16)),
221
+ tick_label_fontsize=int(_dget(visuals, "tick_label_fontsize", 14)),
222
+ title_fontsize=int(_dget(visuals, "title_fontsize", 18)),
223
+ legend_fontsize=int(_dget(visuals, "legend_fontsize", 12)),
224
+ )
225
+ figure, _ = draw_scatter(
226
+ encoded,
227
+ hue_col=encoding.hue,
228
+ visuals=visual,
229
+ dispersion_mode=dispersion,
230
+ overlay_cfg=overlay if overlay_enabled else None,
231
+ overlay_gate_set=overlay_gate_set if overlay_enabled else None,
232
+ title=title,
233
+ figsize=figsize,
234
+ dpi=dpi,
235
+ )
236
+ return figure
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+ EPS_DEFAULT = 1e-9
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CornerStats:
12
+ """Aggregated per-corner numbers for a single (design..., batch)."""
13
+
14
+ b00: float
15
+ b10: float
16
+ b01: float
17
+ b11: float
18
+ n00: int
19
+ n10: int
20
+ n01: int
21
+ n11: int
22
+ sd00: float
23
+ sd10: float
24
+ sd01: float
25
+ sd11: float
26
+
27
+
28
+ def safe_log(x: np.ndarray | float, eps: float = EPS_DEFAULT) -> np.ndarray | float:
29
+ return np.log(np.maximum(x, eps))
30
+
31
+
32
+ def normalize_u(b00: float, b10: float, b01: float, b11: float, eps: float = EPS_DEFAULT) -> tuple[np.ndarray, float]:
33
+ """
34
+ Returns:
35
+ u: np.array([u00,u10,u01,u11]) in [0,1]
36
+ r: dynamic range (>=1)
37
+ """
38
+ b = np.array([b00, b10, b01, b11], dtype=float)
39
+ b = np.maximum(b, eps)
40
+
41
+ m = float(np.min(b))
42
+ M = float(np.max(b))
43
+ r = (M / m) if (M > 0 and m > 0) else 1.0
44
+
45
+ if r <= 1.0 + 1e-12:
46
+ # Degenerate range: place all corners mid-scale
47
+ return np.array([0.5, 0.5, 0.5, 0.5], dtype=float), 1.0
48
+
49
+ u = (safe_log(b, eps) - safe_log(m, eps)) / (safe_log(r, eps))
50
+ u = np.clip(u, 0.0, 1.0)
51
+ return u, r
52
+
53
+
54
+ def logic_asym(u: np.ndarray) -> tuple[float, float]:
55
+ """
56
+ L = u11 - 0.5*(u10+u01)
57
+ A = u10 - u01
58
+ """
59
+ u00, u10, u01, u11 = [float(x) for x in u]
60
+ L = float(u11 - 0.5 * (u10 + u01))
61
+ A = float(u10 - u01)
62
+ return float(np.clip(L, -1.0, 1.0)), float(np.clip(A, -1.0, 1.0))
63
+
64
+
65
+ def cv_corners(
66
+ sd00: float, sd10: float, sd01: float, sd11: float, b00: float, b10: float, b01: float, b11: float
67
+ ) -> float:
68
+ """
69
+ Mean CV across corners using per-corner SD / mean, ignoring corners with n<2 or mean<=0.
70
+ """
71
+ pairs = [(sd00, b00), (sd10, b10), (sd01, b01), (sd11, b11)]
72
+ cvs = []
73
+ for sd, mean in pairs:
74
+ if mean > 0:
75
+ cvs.append(float(sd) / float(mean))
76
+ if not cvs:
77
+ return 0.0
78
+ return float(np.mean(cvs))
79
+
80
+
81
+ def compute_metrics(cs: CornerStats, eps: float = EPS_DEFAULT) -> dict[str, float]:
82
+ u, r = normalize_u(cs.b00, cs.b10, cs.b01, cs.b11, eps=eps)
83
+ L, A = logic_asym(u)
84
+ cv = cv_corners(cs.sd00, cs.sd10, cs.sd01, cs.sd11, cs.b00, cs.b10, cs.b01, cs.b11)
85
+ out = {
86
+ "r": float(r),
87
+ "log_r": float(0.0 if r <= 1.0 else safe_log(r)),
88
+ "L": float(L),
89
+ "A": float(A),
90
+ "u00": float(u[0]),
91
+ "u10": float(u[1]),
92
+ "u01": float(u[2]),
93
+ "u11": float(u[3]),
94
+ "cv": float(max(cv, 0.0)),
95
+ }
96
+ return out
@@ -0,0 +1,129 @@
1
+ """
2
+ Ideal overlay definitions for the logic-symmetry plot.
3
+
4
+ Adds a tile-rendering mode (4-square horizontal strip for 00,10,01,11) and
5
+ a 'tiles_dual' gate set including antagonistic/negative counterparts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from .metrics import logic_asym
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class OverlayStyle:
20
+ # mode: "dot" or "tiles" (4-square horizontal strip for 00/10/01/11)
21
+ mode: str = "dot"
22
+
23
+ # shared appearance
24
+ alpha: float = 0.25
25
+ face_color: str = "#FFFFFF"
26
+ edge_color: str = "#888888"
27
+ show_labels: bool = True
28
+ label_offset: float = 0.02
29
+ label_line_height: float = 0.018
30
+ label_fontsize: int = 12
31
+
32
+ # dot-specific
33
+ size: float = 40.0
34
+
35
+ # tile-specific
36
+ tile_cell_w: float = 0.035
37
+ tile_cell_h: float = 0.035
38
+ tile_gap: float = 0.0
39
+ tile_edge_width: float = 0.6
40
+ tiles_stack_multiple: bool = True
41
+
42
+
43
+ # Base gate sets (u = [u00,u10,u01,u11])
44
+ _GATESETS: dict[str, dict[str, list[float]]] = {
45
+ "core": {
46
+ "AND": [0, 0, 0, 1],
47
+ "OR": [0, 1, 1, 1],
48
+ "XOR": [0, 1, 1, 0],
49
+ "SLOPE": [0, 0.5, 0.5, 1.0],
50
+ "SIG-A": [0, 1, 0, 1],
51
+ "SIG-B": [0, 0, 1, 1],
52
+ },
53
+ "logic_family": {
54
+ "AND": [0, 0, 0, 1],
55
+ "OR": [0, 1, 1, 1],
56
+ "XOR": [0, 1, 1, 0],
57
+ "NAND": [1, 1, 1, 0],
58
+ "NOR": [1, 0, 0, 0],
59
+ "XNOR": [1, 0, 0, 1],
60
+ "SIG-A": [0, 1, 0, 1],
61
+ "SIG-B": [0, 0, 1, 1],
62
+ "SLOPE": [0, 0.5, 0.5, 1.0],
63
+ },
64
+ "full16": {
65
+ "FALSE": [0, 0, 0, 0],
66
+ "TRUE": [1, 1, 1, 1],
67
+ "AND": [0, 0, 0, 1],
68
+ "OR": [0, 1, 1, 1],
69
+ "XOR": [0, 1, 1, 0],
70
+ "XNOR": [1, 0, 0, 1],
71
+ "NAND": [1, 1, 1, 0],
72
+ "NOR": [1, 0, 0, 0],
73
+ "A": [0, 1, 0, 1],
74
+ "NOT A": [1, 0, 1, 0],
75
+ "B": [0, 0, 1, 1],
76
+ "NOT B": [1, 1, 0, 0],
77
+ "A AND NOT B": [0, 1, 0, 0],
78
+ "NOT A AND B": [0, 0, 1, 0],
79
+ "A->B": [1, 1, 0, 1],
80
+ "B->A": [1, 0, 1, 1],
81
+ },
82
+ }
83
+
84
+
85
+ def _invert_u(u: list[float]) -> list[float]:
86
+ return [float(1.0 - float(x)) for x in u]
87
+
88
+
89
+ def _tiles_dual_set() -> dict[str, list[float]]:
90
+ """OR/NOR, XNOR/AND, SIG-A/NOT A, SIG-B/NOT B, SLOPE/SLOPE− (antagonistic)."""
91
+ base = {
92
+ "OR": [0, 1, 1, 1],
93
+ "NOR": [1, 0, 0, 0],
94
+ "XNOR": [1, 0, 0, 1],
95
+ "AND": [0, 0, 0, 1],
96
+ "SIG-A": [0, 1, 0, 1],
97
+ "SIG-B": [0, 0, 1, 1],
98
+ "SLOPE": [0, 0.5, 0.5, 1.0],
99
+ }
100
+ dual = {
101
+ "NAND": [1, 1, 1, 0],
102
+ "XOR": [0, 1, 1, 0],
103
+ "NOT A": [1, 0, 1, 0],
104
+ "NOT B": [1, 1, 0, 0],
105
+ "SLOPE−": _invert_u(base["SLOPE"]),
106
+ }
107
+ out = dict(base)
108
+ out.update(dual)
109
+ return out
110
+
111
+
112
+ def generate_overlay_points(gate_set: str) -> pd.DataFrame:
113
+ """
114
+ Return DataFrame with columns: label, L, A, u (u = [u00,u10,u01,u11])
115
+ """
116
+ if gate_set == "tiles_dual":
117
+ src = _tiles_dual_set()
118
+ else:
119
+ if gate_set not in _GATESETS:
120
+ valid = list(_GATESETS) + ["tiles_dual"]
121
+ raise ValueError(f"Unknown gate_set '{gate_set}'. Choose from {valid}")
122
+ src = _GATESETS[gate_set]
123
+
124
+ rows: list[dict[str, object]] = []
125
+ for label, u in src.items():
126
+ uu = [float(x) for x in u]
127
+ L, A = logic_asym(np.array(uu, dtype=float))
128
+ rows.append({"label": label, "L": L, "A": A, "u": uu})
129
+ return pd.DataFrame.from_records(rows)