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,214 @@
|
|
|
1
|
+
"""Strict configuration model for the four-state vector transform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
_CONFIG_KEYS = frozenset(
|
|
10
|
+
{
|
|
11
|
+
"carry_metadata",
|
|
12
|
+
"design_by",
|
|
13
|
+
"eps_abs",
|
|
14
|
+
"eps_range",
|
|
15
|
+
"eps_ratio",
|
|
16
|
+
"eps_ref",
|
|
17
|
+
"exclude_reference_from_output",
|
|
18
|
+
"log2_offset_delta",
|
|
19
|
+
"ref_add_alpha",
|
|
20
|
+
"reference",
|
|
21
|
+
"require_all_corners_per_design",
|
|
22
|
+
"response",
|
|
23
|
+
"target_time_h",
|
|
24
|
+
"time_column",
|
|
25
|
+
"time_mode",
|
|
26
|
+
"time_tolerance_h",
|
|
27
|
+
"treatment_case_sensitive",
|
|
28
|
+
"treatment_column",
|
|
29
|
+
"treatment_map",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sub(obj: Mapping[str, Any], key: str, *, allowed: frozenset[str]) -> dict[str, Any]:
|
|
35
|
+
value = obj.get(key)
|
|
36
|
+
if value is None:
|
|
37
|
+
return {}
|
|
38
|
+
if isinstance(value, Mapping):
|
|
39
|
+
payload = dict(value)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"four_state_vector.{key} must be a mapping.")
|
|
42
|
+
unsupported = sorted(str(item) for item in payload if item not in allowed)
|
|
43
|
+
if unsupported:
|
|
44
|
+
noun = "setting" if len(unsupported) == 1 else "settings"
|
|
45
|
+
raise ValueError(f"four_state_vector.{key} has unsupported {noun}: {', '.join(unsupported)}.")
|
|
46
|
+
return payload
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _nonempty_text(value: object, *, field_name: str) -> str:
|
|
50
|
+
if not isinstance(value, str) or not value.strip():
|
|
51
|
+
raise ValueError(f"four_state_vector.{field_name} must be a non-empty string.")
|
|
52
|
+
return value.strip()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _string_list(value: object, *, field_name: str) -> list[str]:
|
|
56
|
+
if not isinstance(value, list) or any(not isinstance(item, str) or not item.strip() for item in value):
|
|
57
|
+
raise ValueError(f"four_state_vector.{field_name} must be a list of non-empty strings.")
|
|
58
|
+
return [item.strip() for item in value]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class FourStateVectorResponseCfg:
|
|
63
|
+
"""Channels used for logic shape and reference-anchored intensity."""
|
|
64
|
+
|
|
65
|
+
logic_channel: str
|
|
66
|
+
intensity_channel: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class FourStateVectorReferenceCfg:
|
|
71
|
+
design_id: str
|
|
72
|
+
observation_stat: Literal["mean", "median"] = "mean"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class FourStateVectorConfig:
|
|
77
|
+
# experiment structure
|
|
78
|
+
design_by: list[str]
|
|
79
|
+
time_column: str
|
|
80
|
+
|
|
81
|
+
# channels
|
|
82
|
+
response: FourStateVectorResponseCfg
|
|
83
|
+
|
|
84
|
+
# mapping
|
|
85
|
+
treatment_map: dict[str, str] # keys = {"00","10","01","11"}
|
|
86
|
+
reference: FourStateVectorReferenceCfg
|
|
87
|
+
treatment_case_sensitive: bool = True
|
|
88
|
+
treatment_column: str | None = None
|
|
89
|
+
|
|
90
|
+
# snapshot/time picking
|
|
91
|
+
target_time_h: float | None = None
|
|
92
|
+
time_mode: Literal["nearest", "last_before", "first_after", "exact"] = "nearest"
|
|
93
|
+
time_tolerance_h: float | None = 0.5
|
|
94
|
+
# misc rules
|
|
95
|
+
require_all_corners_per_design: bool = True
|
|
96
|
+
|
|
97
|
+
# intensity math knobs (spec §1.1b)
|
|
98
|
+
ref_add_alpha: float = 0.0 # α: additive to A_i in denominator
|
|
99
|
+
log2_offset_delta: float = 0.0 # δ: additive inside log2(y_linear + δ)
|
|
100
|
+
|
|
101
|
+
# numerical guards
|
|
102
|
+
eps_ratio: float = 1e-9 # ratio/log guard for logic & intensity
|
|
103
|
+
eps_range: float = 1e-12 # min range for min-max (logic)
|
|
104
|
+
eps_ref: float = 1e-9 # reference denom guard
|
|
105
|
+
eps_abs: float = 0.0 # tiny add in numerator for intensity
|
|
106
|
+
|
|
107
|
+
# output table policy
|
|
108
|
+
exclude_reference_from_output: bool = True
|
|
109
|
+
carry_metadata: list[str] = field(default_factory=lambda: ["sequence", "id"])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_four_state_vector_config(xform_cfg: Mapping[str, Any]) -> FourStateVectorConfig:
|
|
113
|
+
"""
|
|
114
|
+
Validate an explicit transform mapping and build an FourStateVectorConfig.
|
|
115
|
+
Requires explicit 'response.logic_channel' and 'response.intensity_channel'.
|
|
116
|
+
"""
|
|
117
|
+
if not isinstance(xform_cfg, Mapping):
|
|
118
|
+
raise ValueError("four-state vector settings must be a mapping.")
|
|
119
|
+
unsupported = sorted(str(key) for key in xform_cfg if key not in _CONFIG_KEYS)
|
|
120
|
+
if unsupported:
|
|
121
|
+
raise ValueError(f"Unsupported four-state vector settings: {', '.join(unsupported)}.")
|
|
122
|
+
|
|
123
|
+
design_by = _string_list(xform_cfg.get("design_by", ["design_id"]), field_name="design_by")
|
|
124
|
+
if not design_by or design_by[0] != "design_id":
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"four_state_vector.design_by must start with 'design_id' to align with the four-state vector spec."
|
|
127
|
+
)
|
|
128
|
+
time_column = _nonempty_text(xform_cfg.get("time_column", "time"), field_name="time_column")
|
|
129
|
+
|
|
130
|
+
resp_in = _sub(xform_cfg, "response", allowed=frozenset({"logic_channel", "intensity_channel"}))
|
|
131
|
+
logic_channel = resp_in.get("logic_channel", None)
|
|
132
|
+
intensity_channel = resp_in.get("intensity_channel", None)
|
|
133
|
+
response = FourStateVectorResponseCfg(
|
|
134
|
+
logic_channel=_nonempty_text(logic_channel, field_name="response.logic_channel"),
|
|
135
|
+
intensity_channel=_nonempty_text(intensity_channel, field_name="response.intensity_channel"),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# mapping
|
|
139
|
+
raw_treatment_map = xform_cfg.get("treatment_map", {})
|
|
140
|
+
if not isinstance(raw_treatment_map, Mapping):
|
|
141
|
+
raise ValueError("four_state_vector.treatment_map must be a mapping.")
|
|
142
|
+
tmap = dict(raw_treatment_map)
|
|
143
|
+
if set(tmap.keys()) != {"00", "10", "01", "11"}:
|
|
144
|
+
raise ValueError("four_state_vector.treatment_map must have exactly the keys {'00','10','01','11'}.")
|
|
145
|
+
if any(not isinstance(value, str) or not value.strip() for value in tmap.values()):
|
|
146
|
+
raise ValueError("four_state_vector.treatment_map values must be non-empty strings.")
|
|
147
|
+
tmap = {key: value.strip() for key, value in tmap.items()}
|
|
148
|
+
tcase = bool(xform_cfg.get("treatment_case_sensitive", True))
|
|
149
|
+
treatment_column_raw = xform_cfg.get("treatment_column")
|
|
150
|
+
if treatment_column_raw is not None and (
|
|
151
|
+
not isinstance(treatment_column_raw, str) or not treatment_column_raw.strip()
|
|
152
|
+
):
|
|
153
|
+
raise ValueError("four_state_vector.treatment_column must be a non-empty string when provided.")
|
|
154
|
+
treatment_column = str(treatment_column_raw).strip() if isinstance(treatment_column_raw, str) else None
|
|
155
|
+
|
|
156
|
+
# time picking
|
|
157
|
+
target_time_h = xform_cfg.get("target_time_h")
|
|
158
|
+
time_mode = str(xform_cfg.get("time_mode", "nearest")).lower()
|
|
159
|
+
tol = xform_cfg.get("time_tolerance_h", 0.5)
|
|
160
|
+
if time_mode not in {"nearest", "last_before", "first_after", "exact"}:
|
|
161
|
+
raise ValueError(f"Invalid four_state_vector.time_mode='{time_mode}'")
|
|
162
|
+
require_all = bool(xform_cfg.get("require_all_corners_per_design", True))
|
|
163
|
+
|
|
164
|
+
ref = _sub(xform_cfg, "reference", allowed=frozenset({"design_id", "observation_stat"}))
|
|
165
|
+
ref_label = _nonempty_text(ref.get("design_id"), field_name="reference.design_id")
|
|
166
|
+
reference = FourStateVectorReferenceCfg(
|
|
167
|
+
design_id=ref_label,
|
|
168
|
+
observation_stat=(str(ref.get("observation_stat", "mean")).lower() if ref.get("observation_stat") else "mean"),
|
|
169
|
+
)
|
|
170
|
+
if reference.observation_stat not in {"mean", "median"}:
|
|
171
|
+
raise ValueError(f"Invalid four_state_vector.reference.observation_stat='{reference.observation_stat}'")
|
|
172
|
+
# eps / table policy
|
|
173
|
+
eps_ratio = float(xform_cfg.get("eps_ratio", 1e-9))
|
|
174
|
+
eps_range = float(xform_cfg.get("eps_range", 1e-12))
|
|
175
|
+
eps_ref = float(xform_cfg.get("eps_ref", 1e-9))
|
|
176
|
+
eps_abs = float(xform_cfg.get("eps_abs", 0.0))
|
|
177
|
+
|
|
178
|
+
ref_add_alpha = float(xform_cfg.get("ref_add_alpha", 0.0))
|
|
179
|
+
log2_offset_delta = float(xform_cfg.get("log2_offset_delta", 0.0))
|
|
180
|
+
exclude_ref = bool(xform_cfg.get("exclude_reference_from_output", True))
|
|
181
|
+
carry_metadata = _string_list(
|
|
182
|
+
xform_cfg.get("carry_metadata", ["sequence", "id"]),
|
|
183
|
+
field_name="carry_metadata",
|
|
184
|
+
)
|
|
185
|
+
cfg = FourStateVectorConfig(
|
|
186
|
+
design_by=design_by,
|
|
187
|
+
time_column=time_column,
|
|
188
|
+
response=response,
|
|
189
|
+
treatment_map=tmap,
|
|
190
|
+
treatment_case_sensitive=tcase,
|
|
191
|
+
treatment_column=treatment_column,
|
|
192
|
+
target_time_h=(float(target_time_h) if target_time_h is not None else None),
|
|
193
|
+
time_mode=time_mode,
|
|
194
|
+
time_tolerance_h=(float(tol) if tol is not None else None),
|
|
195
|
+
require_all_corners_per_design=require_all,
|
|
196
|
+
reference=reference,
|
|
197
|
+
eps_ratio=eps_ratio,
|
|
198
|
+
eps_range=eps_range,
|
|
199
|
+
eps_ref=eps_ref,
|
|
200
|
+
eps_abs=eps_abs,
|
|
201
|
+
ref_add_alpha=ref_add_alpha,
|
|
202
|
+
log2_offset_delta=log2_offset_delta,
|
|
203
|
+
exclude_reference_from_output=exclude_ref,
|
|
204
|
+
carry_metadata=carry_metadata,
|
|
205
|
+
)
|
|
206
|
+
return cfg
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = [
|
|
210
|
+
"FourStateVectorConfig",
|
|
211
|
+
"FourStateVectorResponseCfg",
|
|
212
|
+
"FourStateVectorReferenceCfg",
|
|
213
|
+
"load_four_state_vector_config",
|
|
214
|
+
]
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Pure preparation and rendering for persisted four-state vector diagnostics.
|
|
2
|
+
|
|
3
|
+
The diagnostic joins an annotated measurement trace to the already-persisted
|
|
4
|
+
vector summary. It does not select a new acquisition time or recompute vector.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych import (
|
|
18
|
+
DualReporterTriptychData,
|
|
19
|
+
build_triptych_data,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
STATE_ORDER = ("00", "10", "01", "11")
|
|
23
|
+
_LOGIC_COLUMNS = {state: f"v{state}" for state in STATE_ORDER}
|
|
24
|
+
_INTENSITY_COLUMNS = {state: f"y{state}_star" for state in STATE_ORDER}
|
|
25
|
+
_STATE_COLORS = {
|
|
26
|
+
"00": "#364152",
|
|
27
|
+
"10": "#237A70",
|
|
28
|
+
"01": "#2F65D9",
|
|
29
|
+
"11": "#B82E3F",
|
|
30
|
+
}
|
|
31
|
+
_STATE_MARKERS = {"00": "o", "10": "s", "01": "^", "11": "D"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FourStateVectorDiagnosticData:
|
|
36
|
+
design_id: str
|
|
37
|
+
selected_time_h: float
|
|
38
|
+
reference_design_id: str
|
|
39
|
+
time_column: str
|
|
40
|
+
state_column: str
|
|
41
|
+
triptych: DualReporterTriptychData
|
|
42
|
+
logic_components: dict[str, float]
|
|
43
|
+
intensity_components: dict[str, float]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def prepare_four_state_vector_diagnostics(
|
|
47
|
+
annotated: pd.DataFrame,
|
|
48
|
+
vector: pd.DataFrame,
|
|
49
|
+
*,
|
|
50
|
+
treatment_column: str,
|
|
51
|
+
treatment_map: Mapping[str, str],
|
|
52
|
+
treatment_case_sensitive: bool,
|
|
53
|
+
time_column: str,
|
|
54
|
+
growth_channel: str,
|
|
55
|
+
response_channel: str,
|
|
56
|
+
design_ids: Sequence[str] | None = None,
|
|
57
|
+
time_atol: float = 1e-9,
|
|
58
|
+
trajectory_interval_mass: float = 0.95,
|
|
59
|
+
trajectory_resamples: int = 300,
|
|
60
|
+
) -> tuple[FourStateVectorDiagnosticData, ...]:
|
|
61
|
+
"""Prepare one diagnostic per persisted vector design.
|
|
62
|
+
|
|
63
|
+
Four-state vector row order is the default output order. An explicit ``design_ids``
|
|
64
|
+
selection preserves caller order and fails when any id is unavailable.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_require_columns(
|
|
68
|
+
annotated,
|
|
69
|
+
("design_id", treatment_column, time_column, "channel", "value", "position"),
|
|
70
|
+
where="four-state vector diagnostic annotated data",
|
|
71
|
+
)
|
|
72
|
+
_require_columns(
|
|
73
|
+
vector,
|
|
74
|
+
(
|
|
75
|
+
"design_id",
|
|
76
|
+
"time_selected_h",
|
|
77
|
+
"reference_design_id",
|
|
78
|
+
*_LOGIC_COLUMNS.values(),
|
|
79
|
+
*_INTENSITY_COLUMNS.values(),
|
|
80
|
+
),
|
|
81
|
+
where="four-state vector diagnostic vector data",
|
|
82
|
+
)
|
|
83
|
+
if annotated.empty:
|
|
84
|
+
raise ValueError("four-state vector diagnostic annotated data must not be empty")
|
|
85
|
+
if vector.empty:
|
|
86
|
+
raise ValueError("four-state vector diagnostic vector data must not be empty")
|
|
87
|
+
if not math.isfinite(float(time_atol)) or float(time_atol) < 0.0:
|
|
88
|
+
raise ValueError("four-state vector diagnostic time_atol must be finite and non-negative")
|
|
89
|
+
|
|
90
|
+
source_to_state = _source_to_state(
|
|
91
|
+
treatment_map,
|
|
92
|
+
case_sensitive=treatment_case_sensitive,
|
|
93
|
+
)
|
|
94
|
+
vector_work = vector.copy().reset_index(drop=True)
|
|
95
|
+
vector_work["design_id"] = _non_empty_text_series(vector_work["design_id"], field="vector.design_id")
|
|
96
|
+
duplicate_designs = vector_work.loc[vector_work["design_id"].duplicated(keep=False), "design_id"].unique().tolist()
|
|
97
|
+
if duplicate_designs:
|
|
98
|
+
raise ValueError(f"four-state vector diagnostic vector has duplicate design_id values: {duplicate_designs}")
|
|
99
|
+
|
|
100
|
+
selected_designs = _selected_design_ids(vector_work, design_ids=design_ids)
|
|
101
|
+
annotated_work = annotated.copy()
|
|
102
|
+
annotated_work["design_id"] = _non_empty_text_series(
|
|
103
|
+
annotated_work["design_id"],
|
|
104
|
+
field="annotated.design_id",
|
|
105
|
+
)
|
|
106
|
+
source_values = annotated_work[treatment_column].astype(str)
|
|
107
|
+
if not treatment_case_sensitive:
|
|
108
|
+
source_values = source_values.str.strip().str.casefold()
|
|
109
|
+
annotated_work["__four_state_vector_state"] = source_values.map(source_to_state)
|
|
110
|
+
annotated_work = annotated_work[annotated_work["__four_state_vector_state"].isin(STATE_ORDER)].copy()
|
|
111
|
+
|
|
112
|
+
prepared: list[FourStateVectorDiagnosticData] = []
|
|
113
|
+
by_design = vector_work.set_index("design_id", drop=False)
|
|
114
|
+
for design_id in selected_designs:
|
|
115
|
+
vector_row = by_design.loc[design_id]
|
|
116
|
+
selected_time_h = _finite_scalar(vector_row["time_selected_h"], field=f"{design_id}.time_selected_h")
|
|
117
|
+
reference_design_id = _non_empty_text(
|
|
118
|
+
vector_row["reference_design_id"],
|
|
119
|
+
field=f"{design_id}.reference_design_id",
|
|
120
|
+
)
|
|
121
|
+
design_traces = annotated_work[annotated_work["design_id"].eq(design_id)].copy()
|
|
122
|
+
if design_traces.empty:
|
|
123
|
+
raise ValueError(f"four-state vector diagnostic has no annotated rows for vector design_id {design_id!r}")
|
|
124
|
+
_require_state_channel_coverage(
|
|
125
|
+
design_traces,
|
|
126
|
+
design_id=design_id,
|
|
127
|
+
channels=(growth_channel, response_channel),
|
|
128
|
+
)
|
|
129
|
+
_require_persisted_time_in_traces(
|
|
130
|
+
design_traces,
|
|
131
|
+
design_id=design_id,
|
|
132
|
+
selected_time_h=selected_time_h,
|
|
133
|
+
time_column=time_column,
|
|
134
|
+
channels=(growth_channel, response_channel),
|
|
135
|
+
time_atol=float(time_atol),
|
|
136
|
+
)
|
|
137
|
+
triptych = build_triptych_data(
|
|
138
|
+
design_traces,
|
|
139
|
+
time_col=time_column,
|
|
140
|
+
treatment_col="__four_state_vector_state",
|
|
141
|
+
growth_channel=growth_channel,
|
|
142
|
+
ratio_channel=response_channel,
|
|
143
|
+
snapshot_channel=response_channel,
|
|
144
|
+
snapshot_time=selected_time_h,
|
|
145
|
+
treatment_order=STATE_ORDER,
|
|
146
|
+
time_atol=float(time_atol),
|
|
147
|
+
trajectory_interval_mass=float(trajectory_interval_mass),
|
|
148
|
+
trajectory_resamples=int(trajectory_resamples),
|
|
149
|
+
)
|
|
150
|
+
logic_components = {
|
|
151
|
+
state: _finite_scalar(vector_row[column], field=f"{design_id}.{column}")
|
|
152
|
+
for state, column in _LOGIC_COLUMNS.items()
|
|
153
|
+
}
|
|
154
|
+
invalid_logic = {state: value for state, value in logic_components.items() if value < 0.0 or value > 1.0}
|
|
155
|
+
if invalid_logic:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"four-state vector diagnostic logic-shape components must lie in [0, 1] for {design_id!r}: {invalid_logic}"
|
|
158
|
+
)
|
|
159
|
+
intensity_components = {
|
|
160
|
+
state: _finite_scalar(vector_row[column], field=f"{design_id}.{column}")
|
|
161
|
+
for state, column in _INTENSITY_COLUMNS.items()
|
|
162
|
+
}
|
|
163
|
+
prepared.append(
|
|
164
|
+
FourStateVectorDiagnosticData(
|
|
165
|
+
design_id=design_id,
|
|
166
|
+
selected_time_h=selected_time_h,
|
|
167
|
+
reference_design_id=reference_design_id,
|
|
168
|
+
time_column=time_column,
|
|
169
|
+
state_column="__four_state_vector_state",
|
|
170
|
+
triptych=triptych,
|
|
171
|
+
logic_components=logic_components,
|
|
172
|
+
intensity_components=intensity_components,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return tuple(prepared)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_four_state_vector_diagnostic(
|
|
179
|
+
data: FourStateVectorDiagnosticData,
|
|
180
|
+
*,
|
|
181
|
+
title: str | None = None,
|
|
182
|
+
figsize: tuple[float, float] = (15.0, 5.5),
|
|
183
|
+
dpi: int = 300,
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""Render trajectories beside the persisted vector components."""
|
|
186
|
+
|
|
187
|
+
if len(figsize) != 2 or any(not math.isfinite(float(value)) or float(value) <= 0.0 for value in figsize):
|
|
188
|
+
raise ValueError("four-state vector diagnostic figsize must contain two positive finite values")
|
|
189
|
+
if int(dpi) < 1:
|
|
190
|
+
raise ValueError("four-state vector diagnostic dpi must be positive")
|
|
191
|
+
|
|
192
|
+
from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych_render import ( # noqa: PLC0415
|
|
193
|
+
render_dual_reporter_triptych,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
figure = render_dual_reporter_triptych(
|
|
197
|
+
data.triptych,
|
|
198
|
+
time_col=data.time_column,
|
|
199
|
+
treatment_col=data.state_column,
|
|
200
|
+
title=None,
|
|
201
|
+
figsize=figsize,
|
|
202
|
+
)
|
|
203
|
+
figure.set_dpi(int(dpi))
|
|
204
|
+
figure.axes[0].set_title("Growth trajectory")
|
|
205
|
+
figure.axes[1].set_title("Response trajectory")
|
|
206
|
+
snapshot_spec = figure.axes[2].get_subplotspec()
|
|
207
|
+
figure.delaxes(figure.axes[2])
|
|
208
|
+
component_grid = snapshot_spec.subgridspec(2, 1, hspace=0.7)
|
|
209
|
+
logic_axis = figure.add_subplot(component_grid[0, 0])
|
|
210
|
+
intensity_axis = figure.add_subplot(component_grid[1, 0])
|
|
211
|
+
_render_components(
|
|
212
|
+
logic_axis,
|
|
213
|
+
data.logic_components,
|
|
214
|
+
title="Logic shape",
|
|
215
|
+
x_label="Persisted value (unit interval)",
|
|
216
|
+
x_limits=(-0.05, 1.05),
|
|
217
|
+
)
|
|
218
|
+
_render_components(
|
|
219
|
+
intensity_axis,
|
|
220
|
+
data.intensity_components,
|
|
221
|
+
title="Relative intensity",
|
|
222
|
+
x_label="Persisted value (log2)",
|
|
223
|
+
draw_zero=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
heading = title.strip() if isinstance(title, str) and title.strip() else "four-state vector diagnostic"
|
|
227
|
+
figure.suptitle(
|
|
228
|
+
f"{heading} · {data.design_id}\n"
|
|
229
|
+
f"selected time {data.selected_time_h:g} h · reference {data.reference_design_id}",
|
|
230
|
+
fontsize=13,
|
|
231
|
+
)
|
|
232
|
+
return figure
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _render_components(
|
|
236
|
+
axis: Any,
|
|
237
|
+
values: Mapping[str, float],
|
|
238
|
+
*,
|
|
239
|
+
title: str,
|
|
240
|
+
x_label: str,
|
|
241
|
+
x_limits: tuple[float, float] | None = None,
|
|
242
|
+
draw_zero: bool = False,
|
|
243
|
+
) -> None:
|
|
244
|
+
positions = np.arange(len(STATE_ORDER), dtype=float)
|
|
245
|
+
component_values = np.array([values[state] for state in STATE_ORDER], dtype=float)
|
|
246
|
+
if draw_zero:
|
|
247
|
+
axis.axvline(0.0, color="#8A94A6", linewidth=0.9)
|
|
248
|
+
for position, state, value in zip(positions, STATE_ORDER, component_values, strict=True):
|
|
249
|
+
axis.hlines(position, min(0.0, value), max(0.0, value), color=_STATE_COLORS[state], linewidth=1.5)
|
|
250
|
+
axis.scatter(
|
|
251
|
+
value,
|
|
252
|
+
position,
|
|
253
|
+
color=_STATE_COLORS[state],
|
|
254
|
+
marker=_STATE_MARKERS[state],
|
|
255
|
+
s=34,
|
|
256
|
+
zorder=3,
|
|
257
|
+
)
|
|
258
|
+
axis.set_yticks(positions, labels=STATE_ORDER)
|
|
259
|
+
axis.invert_yaxis()
|
|
260
|
+
axis.set_title(title)
|
|
261
|
+
axis.set_xlabel(x_label)
|
|
262
|
+
axis.set_ylabel("State")
|
|
263
|
+
if x_limits is not None:
|
|
264
|
+
axis.set_xlim(*x_limits)
|
|
265
|
+
axis.grid(axis="x", color="#D8DEE8", linewidth=0.6, alpha=0.7)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _selected_design_ids(vector: pd.DataFrame, *, design_ids: Sequence[str] | None) -> tuple[str, ...]:
|
|
269
|
+
available = tuple(vector["design_id"].astype(str).tolist())
|
|
270
|
+
if design_ids is None:
|
|
271
|
+
return available
|
|
272
|
+
selected = tuple(_non_empty_text(value, field="design_ids") for value in design_ids)
|
|
273
|
+
if not selected:
|
|
274
|
+
raise ValueError("four-state vector diagnostic design_ids must not be empty when provided")
|
|
275
|
+
if len(set(selected)) != len(selected):
|
|
276
|
+
raise ValueError("four-state vector diagnostic design_ids must be unique")
|
|
277
|
+
missing = [design_id for design_id in selected if design_id not in set(available)]
|
|
278
|
+
if missing:
|
|
279
|
+
raise ValueError(f"four-state vector diagnostic design_ids are absent from vector: {missing}")
|
|
280
|
+
return selected
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _source_to_state(treatment_map: Mapping[str, str], *, case_sensitive: bool) -> dict[str, str]:
|
|
284
|
+
if set(treatment_map) != set(STATE_ORDER):
|
|
285
|
+
raise ValueError("four-state vector diagnostic treatment_map must contain exactly 00, 10, 01, and 11")
|
|
286
|
+
reverse: dict[str, str] = {}
|
|
287
|
+
for state in STATE_ORDER:
|
|
288
|
+
source_value = _non_empty_text(treatment_map[state], field=f"treatment_map.{state}")
|
|
289
|
+
key = source_value if case_sensitive else source_value.strip().casefold()
|
|
290
|
+
if key in reverse:
|
|
291
|
+
raise ValueError("four-state vector diagnostic treatment_map source values must be unique")
|
|
292
|
+
reverse[key] = state
|
|
293
|
+
return reverse
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _require_state_channel_coverage(frame: pd.DataFrame, *, design_id: str, channels: Sequence[str]) -> None:
|
|
297
|
+
for channel in channels:
|
|
298
|
+
observed = set(
|
|
299
|
+
frame.loc[frame["channel"].astype(str).eq(str(channel)), "__four_state_vector_state"].astype(str)
|
|
300
|
+
)
|
|
301
|
+
missing = [state for state in STATE_ORDER if state not in observed]
|
|
302
|
+
if missing:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"four-state vector diagnostic design {design_id!r} channel {channel!r} is missing state traces: {missing}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _require_persisted_time_in_traces(
|
|
309
|
+
frame: pd.DataFrame,
|
|
310
|
+
*,
|
|
311
|
+
design_id: str,
|
|
312
|
+
selected_time_h: float,
|
|
313
|
+
time_column: str,
|
|
314
|
+
channels: Sequence[str],
|
|
315
|
+
time_atol: float,
|
|
316
|
+
) -> None:
|
|
317
|
+
times = pd.to_numeric(frame[time_column], errors="coerce")
|
|
318
|
+
for channel in channels:
|
|
319
|
+
channel_times = times[frame["channel"].astype(str).eq(str(channel))].dropna().to_numpy(dtype=float)
|
|
320
|
+
if not np.isclose(channel_times, selected_time_h, rtol=0.0, atol=time_atol).any():
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"four-state vector diagnostic persisted selection time {selected_time_h:g} h for {design_id!r} "
|
|
323
|
+
f"is absent from channel {channel!r} traces"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _require_columns(frame: pd.DataFrame, columns: Sequence[str], *, where: str) -> None:
|
|
328
|
+
missing = [column for column in columns if column not in frame.columns]
|
|
329
|
+
if missing:
|
|
330
|
+
raise ValueError(f"{where} is missing columns: {missing}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _finite_scalar(value: object, *, field: str) -> float:
|
|
334
|
+
try:
|
|
335
|
+
number = float(value)
|
|
336
|
+
except (TypeError, ValueError):
|
|
337
|
+
raise ValueError(f"four-state vector diagnostic {field} must be a finite number") from None
|
|
338
|
+
if not math.isfinite(number):
|
|
339
|
+
raise ValueError(f"four-state vector diagnostic {field} must be a finite number")
|
|
340
|
+
return number
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _non_empty_text_series(series: pd.Series, *, field: str) -> pd.Series:
|
|
344
|
+
values = series.astype(str).str.strip()
|
|
345
|
+
if values.eq("").any() or values.str.casefold().eq("nan").any():
|
|
346
|
+
raise ValueError(f"four-state vector diagnostic {field} must contain non-empty values")
|
|
347
|
+
return values
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _non_empty_text(value: object, *, field: str) -> str:
|
|
351
|
+
text = str(value).strip()
|
|
352
|
+
if not text or text.casefold() in {"nan", "<na>"}:
|
|
353
|
+
raise ValueError(f"four-state vector diagnostic {field} must be a non-empty string")
|
|
354
|
+
return text
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
__all__ = [
|
|
358
|
+
"FourStateVectorDiagnosticData",
|
|
359
|
+
"prepare_four_state_vector_diagnostics",
|
|
360
|
+
"render_four_state_vector_diagnostic",
|
|
361
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import FourStateVectorError
|
|
9
|
+
|
|
10
|
+
from .collection.checks import require_vector_columns
|
|
11
|
+
from .collection.constants import VECTOR_CHANNELS
|
|
12
|
+
from .collection.render import render_four_state_vector_collection_heatmap
|
|
13
|
+
|
|
14
|
+
_NUMERIC_COLUMNS = (
|
|
15
|
+
"intensity_log2_offset_delta",
|
|
16
|
+
"r_logic",
|
|
17
|
+
*VECTOR_CHANNELS,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_experiment_heatmap_frame(vector: pd.DataFrame, *, experiment_id: str) -> pd.DataFrame:
|
|
22
|
+
"""Adapt one experiment's logic.four_state_vector.v1 table to the heatmap renderer contract."""
|
|
23
|
+
|
|
24
|
+
require_vector_columns(vector)
|
|
25
|
+
if vector.empty:
|
|
26
|
+
raise FourStateVectorError("four-state vector heatmap has no rows to plot.")
|
|
27
|
+
normalized_experiment_id = _non_empty_text(experiment_id, field="experiment_id")
|
|
28
|
+
|
|
29
|
+
frame = vector.copy().reset_index(drop=True)
|
|
30
|
+
_require_non_empty_text_columns(frame, columns=("design_id", "reference_design_id"))
|
|
31
|
+
for column in _NUMERIC_COLUMNS:
|
|
32
|
+
frame[column] = _finite_numeric(frame[column], column=column)
|
|
33
|
+
if "time_selected_h" in frame.columns:
|
|
34
|
+
frame["time_selected_h"] = _finite_numeric(
|
|
35
|
+
frame["time_selected_h"],
|
|
36
|
+
column="time_selected_h",
|
|
37
|
+
allow_nan=True,
|
|
38
|
+
)
|
|
39
|
+
frame["flat_logic"] = frame["flat_logic"].astype(bool)
|
|
40
|
+
|
|
41
|
+
frame["source_index"] = 0
|
|
42
|
+
frame["source_experiment_id"] = normalized_experiment_id
|
|
43
|
+
frame["source_row_index"] = range(len(frame))
|
|
44
|
+
frame["row_label"] = _row_labels(frame, experiment_id=normalized_experiment_id)
|
|
45
|
+
return frame
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_experiment_four_state_vector_heatmap(
|
|
49
|
+
vector: pd.DataFrame,
|
|
50
|
+
*,
|
|
51
|
+
experiment_id: str,
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
max_y_tick_labels: int = 80,
|
|
54
|
+
):
|
|
55
|
+
frame = normalize_experiment_heatmap_frame(vector, experiment_id=experiment_id)
|
|
56
|
+
return render_four_state_vector_collection_heatmap(frame, title=title, max_y_tick_labels=max_y_tick_labels)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _row_labels(frame: pd.DataFrame, *, experiment_id: str) -> list[str]:
|
|
60
|
+
return [f"{experiment_id}::{design_id}" for design_id in frame["design_id"].astype(str).tolist()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _finite_numeric(series: pd.Series, *, column: str, allow_nan: bool = False) -> pd.Series:
|
|
64
|
+
values = pd.to_numeric(series, errors="coerce")
|
|
65
|
+
coerced_to_nan = series.notna() & values.isna()
|
|
66
|
+
nonfinite = values.notna() & ~values.fillna(0.0).map(lambda value: math.isfinite(float(value)))
|
|
67
|
+
invalid = coerced_to_nan | nonfinite
|
|
68
|
+
if not allow_nan:
|
|
69
|
+
invalid |= values.isna()
|
|
70
|
+
if invalid.any():
|
|
71
|
+
raise FourStateVectorError(f"four-state vector heatmap column {column!r} must contain finite numeric values.")
|
|
72
|
+
return values.astype(float)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _require_non_empty_text_columns(frame: pd.DataFrame, *, columns: Sequence[str]) -> None:
|
|
76
|
+
for column in columns:
|
|
77
|
+
values = frame[column].astype(str).str.strip()
|
|
78
|
+
if values.eq("").any():
|
|
79
|
+
raise FourStateVectorError(f"four-state vector heatmap column {column!r} must contain non-empty values.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _non_empty_text(value: str, *, field: str) -> str:
|
|
83
|
+
text = str(value).strip()
|
|
84
|
+
if not text:
|
|
85
|
+
raise FourStateVectorError(f"four-state vector heatmap {field} must be a non-empty string.")
|
|
86
|
+
return text
|