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.
- reader_workbench/__init__.py +22 -0
- reader_workbench/__main__.py +4 -0
- reader_workbench/_version.py +17 -0
- reader_workbench/api/__init__.py +74 -0
- reader_workbench/api/_record_reads.py +75 -0
- reader_workbench/api/artifacts.py +79 -0
- reader_workbench/api/facade.py +538 -0
- reader_workbench/api/models.py +285 -0
- reader_workbench/api/notebooks.py +63 -0
- reader_workbench/contracts/__init__.py +18 -0
- reader_workbench/contracts/builtins/__init__.py +36 -0
- reader_workbench/contracts/builtins/cytometry.py +140 -0
- reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
- reader_workbench/contracts/builtins/generic.py +21 -0
- reader_workbench/contracts/builtins/logic.py +149 -0
- reader_workbench/contracts/builtins/plate_reader.py +47 -0
- reader_workbench/contracts/catalog.py +257 -0
- reader_workbench/contracts/model.py +109 -0
- reader_workbench/domains/__init__.py +1 -0
- reader_workbench/domains/cytometry/__init__.py +3 -0
- reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
- reader_workbench/domains/cytometry/analysis/events.py +182 -0
- reader_workbench/domains/cytometry/analysis/gating.py +175 -0
- reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
- reader_workbench/domains/cytometry/io/__init__.py +3 -0
- reader_workbench/domains/cytometry/io/fcs.py +135 -0
- reader_workbench/domains/cytometry/plots/__init__.py +5 -0
- reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
- reader_workbench/domains/logic/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
- reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
- reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
- reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
- reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
- reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
- reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
- reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
- reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
- reader_workbench/domains/logic/four_state_vector/config.py +214 -0
- reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
- reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
- reader_workbench/domains/logic/four_state_vector/math.py +191 -0
- reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
- reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
- reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
- reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
- reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
- reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
- reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
- reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
- reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
- reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
- reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
- reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
- reader_workbench/domains/logic/treatment_columns.py +42 -0
- reader_workbench/domains/plate_reader/__init__.py +1 -0
- reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
- reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
- reader_workbench/domains/plate_reader/io/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
- reader_workbench/domains/plate_reader/ordering.py +59 -0
- reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
- reader_workbench/domains/plate_reader/plots/_data.py +29 -0
- reader_workbench/domains/plate_reader/plots/common.py +346 -0
- reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
- reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
- reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
- reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
- reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
- reader_workbench/domains/time_series/__init__.py +29 -0
- reader_workbench/domains/time_series/aggregation.py +60 -0
- reader_workbench/domains/time_series/contracts.py +368 -0
- reader_workbench/domains/time_series/reduction.py +395 -0
- reader_workbench/errors.py +55 -0
- reader_workbench/maintenance/__init__.py +6 -0
- reader_workbench/maintenance/docs.py +335 -0
- reader_workbench/maintenance/model.py +28 -0
- reader_workbench/maintenance/release.py +39 -0
- reader_workbench/maintenance/skills.py +124 -0
- reader_workbench/plotting/__init__.py +20 -0
- reader_workbench/plotting/mpl.py +56 -0
- reader_workbench/plotting/sinks.py +69 -0
- reader_workbench/plotting/style.py +175 -0
- reader_workbench/plotting/utils.py +27 -0
- reader_workbench/plugins/__init__.py +1 -0
- reader_workbench/plugins/catalog.py +33 -0
- reader_workbench/plugins/export/__init__.py +0 -0
- reader_workbench/plugins/export/_paths.py +21 -0
- reader_workbench/plugins/export/csv.py +41 -0
- reader_workbench/plugins/export/xlsx.py +44 -0
- reader_workbench/plugins/ingest/__init__.py +0 -0
- reader_workbench/plugins/ingest/_discovery.py +58 -0
- reader_workbench/plugins/ingest/discovery_policy.py +66 -0
- reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
- reader_workbench/plugins/ingest/synergy_h1.py +234 -0
- reader_workbench/plugins/manifests/__init__.py +1 -0
- reader_workbench/plugins/manifests/export.py +29 -0
- reader_workbench/plugins/manifests/ingest.py +29 -0
- reader_workbench/plugins/manifests/plot.py +161 -0
- reader_workbench/plugins/manifests/transform.py +172 -0
- reader_workbench/plugins/manifests/validator.py +18 -0
- reader_workbench/plugins/plot/__init__.py +0 -0
- reader_workbench/plugins/plot/_shared.py +55 -0
- reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
- reader_workbench/plugins/plot/distributions.py +58 -0
- reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
- reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
- reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
- reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
- reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
- reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
- reader_workbench/plugins/plot/logic_symmetry.py +56 -0
- reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
- reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
- reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
- reader_workbench/plugins/plot/time_series.py +114 -0
- reader_workbench/plugins/plot/ts_and_snap.py +210 -0
- reader_workbench/plugins/transform/__init__.py +0 -0
- reader_workbench/plugins/transform/_four_state_vector.py +204 -0
- reader_workbench/plugins/transform/_labeling.py +109 -0
- reader_workbench/plugins/transform/alias.py +70 -0
- reader_workbench/plugins/transform/assay_labels.py +62 -0
- reader_workbench/plugins/transform/blank.py +79 -0
- reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
- reader_workbench/plugins/transform/cytometry_gating.py +120 -0
- reader_workbench/plugins/transform/fold_change.py +79 -0
- reader_workbench/plugins/transform/four_state_event_window.py +93 -0
- reader_workbench/plugins/transform/four_state_vector.py +62 -0
- reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
- reader_workbench/plugins/transform/logic_symmetry.py +67 -0
- reader_workbench/plugins/transform/outlier_filter.py +60 -0
- reader_workbench/plugins/transform/overflow.py +197 -0
- reader_workbench/plugins/transform/ratio.py +237 -0
- reader_workbench/plugins/transform/sample_map.py +170 -0
- reader_workbench/plugins/transform/sample_metadata.py +94 -0
- reader_workbench/plugins/validator/__init__.py +1 -0
- reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
- reader_workbench/protocols/__init__.py +80 -0
- reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
- reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
- reader_workbench/protocols/builtins.py +1656 -0
- reader_workbench/protocols/compiler.py +22 -0
- reader_workbench/protocols/compilers/__init__.py +1 -0
- reader_workbench/protocols/compilers/common.py +100 -0
- reader_workbench/protocols/compilers/cytometry.py +87 -0
- reader_workbench/protocols/compilers/generic.py +14 -0
- reader_workbench/protocols/compilers/logic.py +245 -0
- reader_workbench/protocols/compilers/plate_reader.py +937 -0
- reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
- reader_workbench/protocols/model.py +1486 -0
- reader_workbench/protocols/semantic_coverage.py +234 -0
- reader_workbench/runtime/__init__.py +12 -0
- reader_workbench/runtime/builtin.py +23 -0
- reader_workbench/runtime/model.py +42 -0
- reader_workbench/workbench/__init__.py +60 -0
- reader_workbench/workbench/assets/__init__.py +22 -0
- reader_workbench/workbench/assets/types.py +118 -0
- reader_workbench/workbench/audit/__init__.py +5 -0
- reader_workbench/workbench/audit/experiments.py +307 -0
- reader_workbench/workbench/audit/staging.py +187 -0
- reader_workbench/workbench/cli/__init__.py +51 -0
- reader_workbench/workbench/cli/_lazy.py +9 -0
- reader_workbench/workbench/cli/_records_view.py +150 -0
- reader_workbench/workbench/cli/_surface_execution.py +443 -0
- reader_workbench/workbench/cli/audit.py +95 -0
- reader_workbench/workbench/cli/automation.py +229 -0
- reader_workbench/workbench/cli/demo.py +46 -0
- reader_workbench/workbench/cli/dop.py +91 -0
- reader_workbench/workbench/cli/experiments.py +635 -0
- reader_workbench/workbench/cli/helpers.py +232 -0
- reader_workbench/workbench/cli/main.py +59 -0
- reader_workbench/workbench/cli/maintenance.py +82 -0
- reader_workbench/workbench/cli/notebooks.py +260 -0
- reader_workbench/workbench/cli/pagination.py +117 -0
- reader_workbench/workbench/cli/protocols.py +336 -0
- reader_workbench/workbench/cli/shared.py +309 -0
- reader_workbench/workbench/cli/surfaces.py +534 -0
- reader_workbench/workbench/cli/verification.py +128 -0
- reader_workbench/workbench/commands.py +10 -0
- reader_workbench/workbench/config/__init__.py +47 -0
- reader_workbench/workbench/config/identity.py +13 -0
- reader_workbench/workbench/config/load.py +405 -0
- reader_workbench/workbench/config/model.py +274 -0
- reader_workbench/workbench/context.py +26 -0
- reader_workbench/workbench/decl/__init__.py +31 -0
- reader_workbench/workbench/decl/build.py +190 -0
- reader_workbench/workbench/decl/model.py +81 -0
- reader_workbench/workbench/dop/__init__.py +12 -0
- reader_workbench/workbench/dop/builtins.py +261 -0
- reader_workbench/workbench/dop/model.py +209 -0
- reader_workbench/workbench/engine/__init__.py +42 -0
- reader_workbench/workbench/engine/_shared.py +76 -0
- reader_workbench/workbench/engine/contracts.py +283 -0
- reader_workbench/workbench/engine/execution.py +326 -0
- reader_workbench/workbench/engine/file_outputs.py +260 -0
- reader_workbench/workbench/engine/inputs.py +161 -0
- reader_workbench/workbench/engine/invocations.py +507 -0
- reader_workbench/workbench/engine/planning.py +72 -0
- reader_workbench/workbench/engine/runtime.py +464 -0
- reader_workbench/workbench/engine/setup.py +149 -0
- reader_workbench/workbench/engine/validation.py +684 -0
- reader_workbench/workbench/experiment/__init__.py +47 -0
- reader_workbench/workbench/experiment/model.py +381 -0
- reader_workbench/workbench/experiments.py +133 -0
- reader_workbench/workbench/graph/__init__.py +47 -0
- reader_workbench/workbench/graph/nodes.py +102 -0
- reader_workbench/workbench/graph/normalize.py +177 -0
- reader_workbench/workbench/graph/refs.py +148 -0
- reader_workbench/workbench/input_discovery.py +19 -0
- reader_workbench/workbench/inspection/__init__.py +3 -0
- reader_workbench/workbench/inspection/catalogs.py +128 -0
- reader_workbench/workbench/inspection/common.py +92 -0
- reader_workbench/workbench/inspection/dop.py +64 -0
- reader_workbench/workbench/inspection/experiments.py +449 -0
- reader_workbench/workbench/inspection/inventory.py +68 -0
- reader_workbench/workbench/inspection/protocols.py +368 -0
- reader_workbench/workbench/inspection/readiness.py +333 -0
- reader_workbench/workbench/inspection/reports.py +367 -0
- reader_workbench/workbench/inspection/results.py +166 -0
- reader_workbench/workbench/inspection/runtime.py +287 -0
- reader_workbench/workbench/inspection/semantics.py +192 -0
- reader_workbench/workbench/inspection/validation.py +30 -0
- reader_workbench/workbench/notebooks/__init__.py +17 -0
- reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
- reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
- reader_workbench/workbench/notebooks/components/__init__.py +21 -0
- reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
- reader_workbench/workbench/notebooks/components/overview.py +119 -0
- reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
- reader_workbench/workbench/notebooks/launch.py +274 -0
- reader_workbench/workbench/notebooks/presentation.py +136 -0
- reader_workbench/workbench/notebooks/scaffold.py +60 -0
- reader_workbench/workbench/ontology.py +78 -0
- reader_workbench/workbench/paths.py +44 -0
- reader_workbench/workbench/ports/__init__.py +31 -0
- reader_workbench/workbench/ports/model.py +168 -0
- reader_workbench/workbench/records/__init__.py +44 -0
- reader_workbench/workbench/records/epoch.py +329 -0
- reader_workbench/workbench/records/evidence.py +247 -0
- reader_workbench/workbench/records/identity.py +87 -0
- reader_workbench/workbench/records/locking.py +185 -0
- reader_workbench/workbench/records/model.py +711 -0
- reader_workbench/workbench/records/sources.py +73 -0
- reader_workbench/workbench/records/store.py +1022 -0
- reader_workbench/workbench/records/verification.py +998 -0
- reader_workbench/workbench/registry.py +333 -0
- reader_workbench/workbench/spec_overrides.py +215 -0
- reader_workbench-1.0.0.dist-info/METADATA +91 -0
- reader_workbench-1.0.0.dist-info/RECORD +293 -0
- reader_workbench-1.0.0.dist-info/WHEEL +5 -0
- reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
- reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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)
|