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,195 @@
|
|
|
1
|
+
"""Static Matplotlib rendering for the dual-reporter triptych contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
|
|
11
|
+
from reader_workbench.plotting.style import use_style
|
|
12
|
+
|
|
13
|
+
from .dual_reporter_triptych import DualReporterTriptychData
|
|
14
|
+
|
|
15
|
+
_DEFAULT_COLORS = ("#334155", "#0f766e", "#2563eb", "#be123c")
|
|
16
|
+
_LINESTYLES = ("-", "--", "-.", ":")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_dual_reporter_triptych(
|
|
20
|
+
data: DualReporterTriptychData,
|
|
21
|
+
*,
|
|
22
|
+
time_col: str,
|
|
23
|
+
treatment_col: str,
|
|
24
|
+
acquisition_transition_time_h: float | None = None,
|
|
25
|
+
title: str | None = None,
|
|
26
|
+
colors: Sequence[str] | None = None,
|
|
27
|
+
figsize: tuple[float, float] = (10.5, 3.5),
|
|
28
|
+
) -> Figure:
|
|
29
|
+
"""Render two kinetic panels and one observed-value snapshot panel."""
|
|
30
|
+
|
|
31
|
+
if data.od600_time.empty:
|
|
32
|
+
raise ValueError("dual_reporter_triptych: no growth time-series data available")
|
|
33
|
+
if data.ratio_time.empty:
|
|
34
|
+
raise ValueError("dual_reporter_triptych: no ratio time-series data available")
|
|
35
|
+
if data.snapshot_stats.empty:
|
|
36
|
+
raise ValueError("dual_reporter_triptych: no snapshot data available")
|
|
37
|
+
|
|
38
|
+
order = list(data.treatment_order)
|
|
39
|
+
palette = list(colors or _DEFAULT_COLORS)
|
|
40
|
+
if not palette:
|
|
41
|
+
raise ValueError("dual_reporter_triptych: colors must contain at least one color")
|
|
42
|
+
color_map = {value: palette[index % len(palette)] for index, value in enumerate(order)}
|
|
43
|
+
|
|
44
|
+
with use_style(
|
|
45
|
+
rc={
|
|
46
|
+
"axes_titleweight": "regular",
|
|
47
|
+
"axes_titlesize": 11.0,
|
|
48
|
+
"axes_labelsize": 10.0,
|
|
49
|
+
"font_size": 10.0,
|
|
50
|
+
"legend_fontsize": 9.0,
|
|
51
|
+
"legend_title_fontsize": 9.0,
|
|
52
|
+
"xtick_labelsize": 9.0,
|
|
53
|
+
"ytick_labelsize": 9.0,
|
|
54
|
+
},
|
|
55
|
+
color_cycle=palette,
|
|
56
|
+
):
|
|
57
|
+
figure, axes = plt.subplots(1, 3, figsize=figsize, constrained_layout=True)
|
|
58
|
+
_draw_time_panel(
|
|
59
|
+
axes[0],
|
|
60
|
+
data.od600_time,
|
|
61
|
+
time_col=time_col,
|
|
62
|
+
treatment_col=treatment_col,
|
|
63
|
+
order=order,
|
|
64
|
+
color_map=color_map,
|
|
65
|
+
snapshot_time=data.snapshot_time,
|
|
66
|
+
acquisition_transition_time_h=acquisition_transition_time_h,
|
|
67
|
+
title=f"{data.growth_channel} kinetics",
|
|
68
|
+
ylabel=data.growth_channel,
|
|
69
|
+
)
|
|
70
|
+
_draw_time_panel(
|
|
71
|
+
axes[1],
|
|
72
|
+
data.ratio_time,
|
|
73
|
+
time_col=time_col,
|
|
74
|
+
treatment_col=treatment_col,
|
|
75
|
+
order=order,
|
|
76
|
+
color_map=color_map,
|
|
77
|
+
snapshot_time=data.snapshot_time,
|
|
78
|
+
acquisition_transition_time_h=acquisition_transition_time_h,
|
|
79
|
+
title=f"{data.ratio_channel} kinetics",
|
|
80
|
+
ylabel=data.ratio_channel,
|
|
81
|
+
)
|
|
82
|
+
_draw_snapshot_panel(
|
|
83
|
+
axes[2],
|
|
84
|
+
data,
|
|
85
|
+
treatment_col=treatment_col,
|
|
86
|
+
color_map=color_map,
|
|
87
|
+
title=f"{data.snapshot_channel} snapshot at {data.snapshot_time:g} h",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
handles, labels = axes[0].get_legend_handles_labels()
|
|
91
|
+
if handles:
|
|
92
|
+
figure.legend(
|
|
93
|
+
handles,
|
|
94
|
+
labels,
|
|
95
|
+
loc="outside lower center",
|
|
96
|
+
ncols=max(1, min(len(handles), 4)),
|
|
97
|
+
title=str(treatment_col).replace("_", " ").capitalize(),
|
|
98
|
+
)
|
|
99
|
+
if title:
|
|
100
|
+
figure.suptitle(str(title), fontweight="normal")
|
|
101
|
+
return figure
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _draw_time_panel(
|
|
105
|
+
axis,
|
|
106
|
+
frame,
|
|
107
|
+
*,
|
|
108
|
+
time_col: str,
|
|
109
|
+
treatment_col: str,
|
|
110
|
+
order: list[str],
|
|
111
|
+
color_map: dict[str, str],
|
|
112
|
+
snapshot_time: float,
|
|
113
|
+
acquisition_transition_time_h: float | None,
|
|
114
|
+
title: str,
|
|
115
|
+
ylabel: str,
|
|
116
|
+
) -> None:
|
|
117
|
+
for index, treatment in enumerate(order):
|
|
118
|
+
subset = frame[frame[treatment_col].astype(str) == str(treatment)].sort_values(time_col)
|
|
119
|
+
if subset.empty:
|
|
120
|
+
continue
|
|
121
|
+
x = subset[time_col].to_numpy(dtype=float)
|
|
122
|
+
mean = subset["y_mean"].to_numpy(dtype=float)
|
|
123
|
+
lower = subset["y_lo"].to_numpy(dtype=float)
|
|
124
|
+
upper = subset["y_hi"].to_numpy(dtype=float)
|
|
125
|
+
color = color_map[treatment]
|
|
126
|
+
axis.fill_between(x, lower, upper, color=color, alpha=0.16, linewidth=0)
|
|
127
|
+
axis.plot(
|
|
128
|
+
x,
|
|
129
|
+
mean,
|
|
130
|
+
color=color,
|
|
131
|
+
linestyle=_LINESTYLES[index % len(_LINESTYLES)],
|
|
132
|
+
linewidth=1.7,
|
|
133
|
+
label=treatment,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
axis.axvline(float(snapshot_time), color="#111827", linewidth=1.0)
|
|
137
|
+
if acquisition_transition_time_h is not None:
|
|
138
|
+
transition = float(acquisition_transition_time_h)
|
|
139
|
+
if np.isfinite(transition):
|
|
140
|
+
axis.axvline(transition, color="#64748b", linewidth=1.0, linestyle=(0, (5, 3)))
|
|
141
|
+
axis.set_title(title)
|
|
142
|
+
axis.set_xlabel("Time (h)")
|
|
143
|
+
axis.set_ylabel(ylabel)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _draw_snapshot_panel(
|
|
147
|
+
axis,
|
|
148
|
+
data: DualReporterTriptychData,
|
|
149
|
+
*,
|
|
150
|
+
treatment_col: str,
|
|
151
|
+
color_map: dict[str, str],
|
|
152
|
+
title: str,
|
|
153
|
+
) -> None:
|
|
154
|
+
order = list(data.treatment_order)
|
|
155
|
+
x_positions = {value: float(index) for index, value in enumerate(order)}
|
|
156
|
+
|
|
157
|
+
for treatment in order:
|
|
158
|
+
subset = data.snapshot_points[data.snapshot_points[treatment_col].astype(str) == str(treatment)].sort_values(
|
|
159
|
+
"observation_index"
|
|
160
|
+
)
|
|
161
|
+
offsets = np.linspace(-0.12, 0.12, len(subset)) if len(subset) > 1 else np.zeros(len(subset))
|
|
162
|
+
if not subset.empty:
|
|
163
|
+
axis.scatter(
|
|
164
|
+
x_positions[treatment] + offsets,
|
|
165
|
+
subset["value"].to_numpy(dtype=float),
|
|
166
|
+
s=28,
|
|
167
|
+
facecolors="white",
|
|
168
|
+
edgecolors=color_map[treatment],
|
|
169
|
+
linewidths=1.0,
|
|
170
|
+
zorder=3,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for _, row in data.snapshot_stats.iterrows():
|
|
174
|
+
treatment = str(row[treatment_col])
|
|
175
|
+
if treatment not in x_positions:
|
|
176
|
+
continue
|
|
177
|
+
center = x_positions[treatment]
|
|
178
|
+
mean = float(row["y_mean"])
|
|
179
|
+
lower = float(row["y_lo"])
|
|
180
|
+
upper = float(row["y_hi"])
|
|
181
|
+
if not all(np.isfinite(value) for value in (mean, lower, upper)):
|
|
182
|
+
continue
|
|
183
|
+
color = color_map[treatment]
|
|
184
|
+
axis.plot([center, center], [lower, upper], color=color, linewidth=1.0, zorder=4)
|
|
185
|
+
axis.plot([center - 0.05, center + 0.05], [lower, lower], color=color, linewidth=1.0, zorder=4)
|
|
186
|
+
axis.plot([center - 0.05, center + 0.05], [upper, upper], color=color, linewidth=1.0, zorder=4)
|
|
187
|
+
axis.plot([center - 0.16, center + 0.16], [mean, mean], color=color, linewidth=2.2, zorder=5)
|
|
188
|
+
|
|
189
|
+
axis.set_title(title)
|
|
190
|
+
axis.set_xlabel(str(treatment_col).replace("_", " ").capitalize())
|
|
191
|
+
axis.set_ylabel(data.snapshot_channel)
|
|
192
|
+
axis.set_xticks(range(len(order)), order, rotation=0)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = ["render_dual_reporter_triptych"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Canonical four-state event-window plot selection and rendering."""
|
|
2
|
+
|
|
3
|
+
from .diagnostic import (
|
|
4
|
+
FourStateEventWindowDiagnostic,
|
|
5
|
+
prepare_four_state_event_window_diagnostic,
|
|
6
|
+
)
|
|
7
|
+
from .diagnostic_render import render_four_state_event_window_diagnostic
|
|
8
|
+
from .schema import COMPONENT_COLUMNS, MAGNITUDE_COLUMNS, RESPONSE_COLUMNS, STATE_ORDER
|
|
9
|
+
from .summary import (
|
|
10
|
+
FourStateEventWindowSummaryMatrix,
|
|
11
|
+
four_state_event_window_summary_matrix,
|
|
12
|
+
render_four_state_event_window_summary,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"COMPONENT_COLUMNS",
|
|
17
|
+
"MAGNITUDE_COLUMNS",
|
|
18
|
+
"RESPONSE_COLUMNS",
|
|
19
|
+
"FourStateEventWindowDiagnostic",
|
|
20
|
+
"FourStateEventWindowSummaryMatrix",
|
|
21
|
+
"STATE_ORDER",
|
|
22
|
+
"prepare_four_state_event_window_diagnostic",
|
|
23
|
+
"render_four_state_event_window_diagnostic",
|
|
24
|
+
"render_four_state_event_window_summary",
|
|
25
|
+
"four_state_event_window_summary_matrix",
|
|
26
|
+
]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Select one diagnostic from canonical four-state event-window records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .schema import COMPONENT_COLUMNS, STATE_ORDER
|
|
11
|
+
|
|
12
|
+
BOUND_KINDS = frozenset({"exact", "lower", "upper", "indeterminate"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class FourStateEventWindowDiagnostic:
|
|
17
|
+
"""One source design and primary reduction ready for rendering."""
|
|
18
|
+
|
|
19
|
+
source_experiment_id: str
|
|
20
|
+
design_id: str
|
|
21
|
+
reference_design_id: str
|
|
22
|
+
reduction_id: str
|
|
23
|
+
reduction_method: str
|
|
24
|
+
response_basis: str
|
|
25
|
+
observation_stat: str
|
|
26
|
+
descriptive_resampling_draws: int
|
|
27
|
+
descriptive_interval_mass: float
|
|
28
|
+
event_id: str
|
|
29
|
+
event_time_uncertainty_h: float
|
|
30
|
+
window: tuple[float, float]
|
|
31
|
+
pre_window: tuple[float, float] | None
|
|
32
|
+
component_values: tuple[float, ...]
|
|
33
|
+
component_descriptive_interval_low: tuple[float, ...]
|
|
34
|
+
component_descriptive_interval_high: tuple[float, ...]
|
|
35
|
+
component_event_half_range: tuple[float, ...]
|
|
36
|
+
component_bound_kinds: tuple[str, ...]
|
|
37
|
+
component_has_policy_clipping: tuple[bool, ...]
|
|
38
|
+
component_has_instrument_overflow: tuple[bool, ...]
|
|
39
|
+
component_event_has_policy_clipping: tuple[bool, ...]
|
|
40
|
+
component_event_has_instrument_overflow: tuple[bool, ...]
|
|
41
|
+
traces: pd.DataFrame
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prepare_four_state_event_window_diagnostic(
|
|
45
|
+
traces: pd.DataFrame,
|
|
46
|
+
designs: pd.DataFrame,
|
|
47
|
+
*,
|
|
48
|
+
source_experiment_id: str,
|
|
49
|
+
design_id: str,
|
|
50
|
+
reduction_id: str,
|
|
51
|
+
pre_window_duration_h: float | None,
|
|
52
|
+
) -> FourStateEventWindowDiagnostic:
|
|
53
|
+
"""Select and validate one diagnostic subject from canonical records."""
|
|
54
|
+
|
|
55
|
+
source_experiment_id = _identifier(source_experiment_id, field="source_experiment_id")
|
|
56
|
+
design_id = _identifier(design_id, field="design_id")
|
|
57
|
+
reduction_id = _identifier(reduction_id, field="reduction_id")
|
|
58
|
+
_require_columns(designs, _design_columns(), record="designs")
|
|
59
|
+
selected = designs.loc[
|
|
60
|
+
designs["experiment_id"].astype(str).eq(source_experiment_id)
|
|
61
|
+
& designs["design_id"].astype(str).eq(design_id)
|
|
62
|
+
& designs["reduction_id"].astype(str).eq(reduction_id)
|
|
63
|
+
]
|
|
64
|
+
if len(selected) != 1:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"four-state event-window diagnostic requires exactly one designs row for "
|
|
67
|
+
f"source experiment {source_experiment_id!r}, design {design_id!r}, and reduction {reduction_id!r}; "
|
|
68
|
+
f"found {len(selected)}"
|
|
69
|
+
)
|
|
70
|
+
row = selected.iloc[0]
|
|
71
|
+
response_basis = _choice(row["response_basis"], field="response_basis", choices={"post_window", "post_minus_pre"})
|
|
72
|
+
pre_window = _pre_window(
|
|
73
|
+
response_basis=response_basis,
|
|
74
|
+
pre_window_duration_h=pre_window_duration_h,
|
|
75
|
+
event_time_uncertainty_h=float(row["event_time_uncertainty_h"]),
|
|
76
|
+
)
|
|
77
|
+
window = (float(row["window_start_event_h"]), float(row["window_end_event_h"]))
|
|
78
|
+
if not np.isfinite(window).all() or window[1] <= window[0]:
|
|
79
|
+
raise ValueError("four-state event-window diagnostic requires a finite window start before its end")
|
|
80
|
+
|
|
81
|
+
values = tuple(float(row[column]) for column in COMPONENT_COLUMNS)
|
|
82
|
+
interval_low = tuple(float(row[f"{column}_descriptive_interval_low"]) for column in COMPONENT_COLUMNS)
|
|
83
|
+
interval_high = tuple(float(row[f"{column}_descriptive_interval_high"]) for column in COMPONENT_COLUMNS)
|
|
84
|
+
event_half_range = tuple(float(row[f"{column}_event_half_range"]) for column in COMPONENT_COLUMNS)
|
|
85
|
+
if not np.isfinite((*values, *interval_low, *interval_high, *event_half_range)).all():
|
|
86
|
+
raise ValueError("four-state event-window diagnostic requires finite component dispersion values")
|
|
87
|
+
if any(low > high for low, high in zip(interval_low, interval_high, strict=True)):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"four-state event-window diagnostic descriptive-interval lower bounds must not exceed upper bounds"
|
|
90
|
+
)
|
|
91
|
+
if any(value < 0.0 for value in event_half_range):
|
|
92
|
+
raise ValueError("four-state event-window diagnostic event-sensitivity half-ranges must be non-negative")
|
|
93
|
+
|
|
94
|
+
trace_set = _select_traces(
|
|
95
|
+
traces,
|
|
96
|
+
source_experiment_id=source_experiment_id,
|
|
97
|
+
design_id=design_id,
|
|
98
|
+
reference_design_id=_identifier(row["reference_design_id"], field="reference_design_id"),
|
|
99
|
+
)
|
|
100
|
+
return FourStateEventWindowDiagnostic(
|
|
101
|
+
source_experiment_id=source_experiment_id,
|
|
102
|
+
design_id=design_id,
|
|
103
|
+
reference_design_id=_identifier(row["reference_design_id"], field="reference_design_id"),
|
|
104
|
+
reduction_id=reduction_id,
|
|
105
|
+
reduction_method=_identifier(row["reduction_method"], field="reduction_method"),
|
|
106
|
+
response_basis=response_basis,
|
|
107
|
+
observation_stat=_choice(row["observation_stat"], field="observation_stat", choices={"mean", "median"}),
|
|
108
|
+
descriptive_resampling_draws=_positive_int(
|
|
109
|
+
row["descriptive_resampling_draws"], field="descriptive_resampling_draws"
|
|
110
|
+
),
|
|
111
|
+
descriptive_interval_mass=_descriptive_interval_mass(row["descriptive_interval_mass"]),
|
|
112
|
+
event_id=_identifier(row["event_id"], field="event_id"),
|
|
113
|
+
event_time_uncertainty_h=_nonnegative_float(row["event_time_uncertainty_h"], field="event_time_uncertainty_h"),
|
|
114
|
+
window=window,
|
|
115
|
+
pre_window=pre_window,
|
|
116
|
+
component_values=values,
|
|
117
|
+
component_descriptive_interval_low=interval_low,
|
|
118
|
+
component_descriptive_interval_high=interval_high,
|
|
119
|
+
component_event_half_range=event_half_range,
|
|
120
|
+
component_bound_kinds=tuple(
|
|
121
|
+
_choice(row[f"{column}_bound_kind"], field=f"{column}_bound_kind", choices=BOUND_KINDS)
|
|
122
|
+
for column in COMPONENT_COLUMNS
|
|
123
|
+
),
|
|
124
|
+
component_has_policy_clipping=_component_bools(row, suffix="has_policy_clipping"),
|
|
125
|
+
component_has_instrument_overflow=_component_bools(row, suffix="has_instrument_overflow"),
|
|
126
|
+
component_event_has_policy_clipping=_component_bools(row, suffix="event_sensitivity_has_policy_clipping"),
|
|
127
|
+
component_event_has_instrument_overflow=_component_bools(
|
|
128
|
+
row, suffix="event_sensitivity_has_instrument_overflow"
|
|
129
|
+
),
|
|
130
|
+
traces=trace_set,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _design_columns() -> set[str]:
|
|
135
|
+
component_columns = {
|
|
136
|
+
f"{component}_{suffix}"
|
|
137
|
+
for component in COMPONENT_COLUMNS
|
|
138
|
+
for suffix in (
|
|
139
|
+
"descriptive_interval_low",
|
|
140
|
+
"descriptive_interval_high",
|
|
141
|
+
"event_half_range",
|
|
142
|
+
"bound_kind",
|
|
143
|
+
"has_policy_clipping",
|
|
144
|
+
"has_instrument_overflow",
|
|
145
|
+
"event_sensitivity_has_policy_clipping",
|
|
146
|
+
"event_sensitivity_has_instrument_overflow",
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
"experiment_id",
|
|
151
|
+
"design_id",
|
|
152
|
+
"reference_design_id",
|
|
153
|
+
"reduction_id",
|
|
154
|
+
"reduction_method",
|
|
155
|
+
"response_basis",
|
|
156
|
+
"observation_stat",
|
|
157
|
+
"descriptive_resampling_draws",
|
|
158
|
+
"descriptive_interval_mass",
|
|
159
|
+
"event_id",
|
|
160
|
+
"event_time_uncertainty_h",
|
|
161
|
+
"window_start_event_h",
|
|
162
|
+
"window_end_event_h",
|
|
163
|
+
*COMPONENT_COLUMNS,
|
|
164
|
+
*component_columns,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _select_traces(
|
|
169
|
+
traces: pd.DataFrame,
|
|
170
|
+
*,
|
|
171
|
+
source_experiment_id: str,
|
|
172
|
+
design_id: str,
|
|
173
|
+
reference_design_id: str,
|
|
174
|
+
) -> pd.DataFrame:
|
|
175
|
+
_require_columns(
|
|
176
|
+
traces,
|
|
177
|
+
{
|
|
178
|
+
"experiment_id",
|
|
179
|
+
"design_id",
|
|
180
|
+
"position",
|
|
181
|
+
"state",
|
|
182
|
+
"time_from_event_h",
|
|
183
|
+
"value",
|
|
184
|
+
"value_policy_clipped",
|
|
185
|
+
"value_instrument_overflow",
|
|
186
|
+
"value_bound_kind",
|
|
187
|
+
"signal_kind",
|
|
188
|
+
},
|
|
189
|
+
record="traces",
|
|
190
|
+
)
|
|
191
|
+
source = traces.loc[traces["experiment_id"].astype(str).eq(source_experiment_id)]
|
|
192
|
+
selected = source.loc[source["design_id"].astype(str).eq(design_id)]
|
|
193
|
+
_validate_trace_support(selected, design_id=design_id, signal_kinds=("growth", "response", "magnitude"))
|
|
194
|
+
frames = [selected]
|
|
195
|
+
if reference_design_id != design_id:
|
|
196
|
+
reference = source.loc[
|
|
197
|
+
source["design_id"].astype(str).eq(reference_design_id) & source["signal_kind"].astype(str).eq("magnitude")
|
|
198
|
+
]
|
|
199
|
+
_validate_trace_support(reference, design_id=reference_design_id, signal_kinds=("magnitude",))
|
|
200
|
+
frames.append(reference)
|
|
201
|
+
result = pd.concat(frames, ignore_index=True).sort_values(
|
|
202
|
+
["signal_kind", "design_id", "state", "position", "time_from_event_h"], kind="stable"
|
|
203
|
+
)
|
|
204
|
+
numeric = result.loc[:, ["time_from_event_h", "value"]].to_numpy(dtype=float)
|
|
205
|
+
if not np.isfinite(numeric).all():
|
|
206
|
+
raise ValueError("four-state event-window diagnostic traces require finite event-relative times and values")
|
|
207
|
+
log_rows = result["signal_kind"].astype(str).isin({"response", "magnitude"})
|
|
208
|
+
if result.loc[log_rows, "value"].astype(float).le(0.0).any():
|
|
209
|
+
raise ValueError("four-state event-window diagnostic response and magnitude traces must be positive")
|
|
210
|
+
unknown_bounds = sorted(set(result["value_bound_kind"].astype(str)) - BOUND_KINDS)
|
|
211
|
+
if unknown_bounds:
|
|
212
|
+
raise ValueError(f"four-state event-window diagnostic traces contain unsupported bounds: {unknown_bounds}")
|
|
213
|
+
return result.reset_index(drop=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _pre_window(
|
|
217
|
+
*,
|
|
218
|
+
response_basis: str,
|
|
219
|
+
pre_window_duration_h: float | None,
|
|
220
|
+
event_time_uncertainty_h: float,
|
|
221
|
+
) -> tuple[float, float] | None:
|
|
222
|
+
uncertainty = _nonnegative_float(event_time_uncertainty_h, field="event_time_uncertainty_h")
|
|
223
|
+
if response_basis == "post_window":
|
|
224
|
+
if pre_window_duration_h is not None:
|
|
225
|
+
raise ValueError("post_window diagnostics must not declare pre_window_duration_h")
|
|
226
|
+
return None
|
|
227
|
+
duration = _nonnegative_float(pre_window_duration_h, field="pre_window_duration_h")
|
|
228
|
+
if duration <= 0.0:
|
|
229
|
+
raise ValueError("post_minus_pre diagnostics require positive pre_window_duration_h")
|
|
230
|
+
end = -uncertainty
|
|
231
|
+
return (end - duration, end)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _component_bools(row: pd.Series, *, suffix: str) -> tuple[bool, ...]:
|
|
235
|
+
values: list[bool] = []
|
|
236
|
+
for component in COMPONENT_COLUMNS:
|
|
237
|
+
value = row[f"{component}_{suffix}"]
|
|
238
|
+
if not isinstance(value, (bool, np.bool_)):
|
|
239
|
+
raise ValueError(f"four-state event-window diagnostic {component}_{suffix} must be boolean")
|
|
240
|
+
values.append(bool(value))
|
|
241
|
+
return tuple(values)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _validate_trace_support(rows: pd.DataFrame, *, design_id: str, signal_kinds: tuple[str, ...]) -> None:
|
|
245
|
+
for signal_kind in signal_kinds:
|
|
246
|
+
states = set(rows.loc[rows["signal_kind"].astype(str).eq(signal_kind), "state"].astype(str))
|
|
247
|
+
if states != set(STATE_ORDER):
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"four-state event-window diagnostic requires {signal_kind!r} traces for design {design_id!r} "
|
|
250
|
+
f"in states {list(STATE_ORDER)}; found {sorted(states)}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _require_columns(frame: pd.DataFrame, required: set[str], *, record: str) -> None:
|
|
255
|
+
missing = sorted(required - set(frame.columns))
|
|
256
|
+
if missing:
|
|
257
|
+
raise ValueError(f"four-state event-window diagnostic {record} record is missing columns: {missing}")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _identifier(value: object, *, field: str) -> str:
|
|
261
|
+
if not isinstance(value, str) or not value.strip():
|
|
262
|
+
raise ValueError(f"four-state event-window diagnostic {field} must be a non-empty string")
|
|
263
|
+
return value.strip()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _choice(value: object, *, field: str, choices: set[str] | frozenset[str]) -> str:
|
|
267
|
+
result = _identifier(value, field=field)
|
|
268
|
+
if result not in choices:
|
|
269
|
+
raise ValueError(f"four-state event-window diagnostic {field} must be one of {sorted(choices)}")
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _positive_int(value: object, *, field: str) -> int:
|
|
274
|
+
if isinstance(value, bool) or not isinstance(value, (int, np.integer)) or int(value) <= 0:
|
|
275
|
+
raise ValueError(f"four-state event-window diagnostic {field} must be a positive integer")
|
|
276
|
+
return int(value)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _nonnegative_float(value: object, *, field: str) -> float:
|
|
280
|
+
if value is None or isinstance(value, bool):
|
|
281
|
+
raise ValueError(f"four-state event-window diagnostic {field} must be a non-negative finite number")
|
|
282
|
+
result = float(value)
|
|
283
|
+
if not np.isfinite(result) or result < 0.0:
|
|
284
|
+
raise ValueError(f"four-state event-window diagnostic {field} must be a non-negative finite number")
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _descriptive_interval_mass(value: object) -> float:
|
|
289
|
+
result = _nonnegative_float(value, field="descriptive_interval_mass")
|
|
290
|
+
if not 0.5 < result < 1.0:
|
|
291
|
+
raise ValueError("four-state event-window diagnostic descriptive_interval_mass must be between 0.5 and 1.0")
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
__all__ = ["FourStateEventWindowDiagnostic", "prepare_four_state_event_window_diagnostic"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Render the reduced components of a four-state event-window diagnostic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .diagnostic import FourStateEventWindowDiagnostic
|
|
11
|
+
from .diagnostic_style import BOUND_MARKERS, STATE_COLORS
|
|
12
|
+
from .schema import COMPONENT_COLUMNS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def draw_component_panel(
|
|
16
|
+
axis: Any,
|
|
17
|
+
*,
|
|
18
|
+
diagnostic: FourStateEventWindowDiagnostic,
|
|
19
|
+
axis_labels: Mapping[str, str],
|
|
20
|
+
reference_label: str,
|
|
21
|
+
) -> None:
|
|
22
|
+
y = np.arange(len(COMPONENT_COLUMNS))
|
|
23
|
+
values = np.asarray(diagnostic.component_values)
|
|
24
|
+
interval_low = np.asarray(diagnostic.component_descriptive_interval_low)
|
|
25
|
+
interval_high = np.asarray(diagnostic.component_descriptive_interval_high)
|
|
26
|
+
event_range = np.asarray(diagnostic.component_event_half_range)
|
|
27
|
+
for index, (component, value) in enumerate(zip(COMPONENT_COLUMNS, values, strict=True)):
|
|
28
|
+
state = component[1:]
|
|
29
|
+
color = STATE_COLORS[state]
|
|
30
|
+
sensitivity = axis.hlines(
|
|
31
|
+
y[index],
|
|
32
|
+
value - event_range[index],
|
|
33
|
+
value + event_range[index],
|
|
34
|
+
color=color,
|
|
35
|
+
linewidth=5.0,
|
|
36
|
+
alpha=0.18,
|
|
37
|
+
zorder=1,
|
|
38
|
+
)
|
|
39
|
+
sensitivity.set_gid("four-state-event-window-component-event-range")
|
|
40
|
+
interval = axis.hlines(
|
|
41
|
+
y[index],
|
|
42
|
+
interval_low[index],
|
|
43
|
+
interval_high[index],
|
|
44
|
+
color=color,
|
|
45
|
+
linewidth=1.4,
|
|
46
|
+
zorder=2,
|
|
47
|
+
)
|
|
48
|
+
interval.set_gid("four-state-event-window-component-descriptive-interval")
|
|
49
|
+
caps = axis.vlines(
|
|
50
|
+
[interval_low[index], interval_high[index]],
|
|
51
|
+
y[index] - 0.09,
|
|
52
|
+
y[index] + 0.09,
|
|
53
|
+
color=color,
|
|
54
|
+
linewidth=1.1,
|
|
55
|
+
zorder=2,
|
|
56
|
+
)
|
|
57
|
+
caps.set_gid("four-state-event-window-component-descriptive-caps")
|
|
58
|
+
axis.scatter(
|
|
59
|
+
value,
|
|
60
|
+
y[index],
|
|
61
|
+
color=color,
|
|
62
|
+
marker=BOUND_MARKERS[diagnostic.component_bound_kinds[index]],
|
|
63
|
+
s=30.0,
|
|
64
|
+
edgecolor="white",
|
|
65
|
+
linewidth=0.5,
|
|
66
|
+
zorder=3,
|
|
67
|
+
)
|
|
68
|
+
axis.axvline(0.0, color="#64748b", linewidth=0.9)
|
|
69
|
+
axis.axhline(3.5, color="#cbd5e1", linewidth=0.8)
|
|
70
|
+
axis.set_yticks(y, labels=COMPONENT_COLUMNS)
|
|
71
|
+
axis.invert_yaxis()
|
|
72
|
+
axis.set_xlabel("Reduced value (log$_2$ units)", labelpad=8)
|
|
73
|
+
_draw_group_label(axis, start=-0.35, end=3.35, label=f"Response $r_i$\n{axis_labels['response']}")
|
|
74
|
+
_draw_group_label(
|
|
75
|
+
axis,
|
|
76
|
+
start=3.65,
|
|
77
|
+
end=7.35,
|
|
78
|
+
label=f"Signal $b_i$\n{axis_labels['magnitude']}\nrelative to {reference_label}",
|
|
79
|
+
)
|
|
80
|
+
notes = quality_notes(diagnostic)
|
|
81
|
+
if notes:
|
|
82
|
+
axis.text(
|
|
83
|
+
0.0,
|
|
84
|
+
-0.16,
|
|
85
|
+
"Quality flags — " + "; ".join(notes),
|
|
86
|
+
transform=axis.transAxes,
|
|
87
|
+
ha="left",
|
|
88
|
+
va="top",
|
|
89
|
+
fontsize=10.5,
|
|
90
|
+
color="#7c2d12",
|
|
91
|
+
wrap=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _draw_group_label(axis: Any, *, start: float, end: float, label: str) -> None:
|
|
96
|
+
transform = axis.get_yaxis_transform()
|
|
97
|
+
(bracket,) = axis.plot(
|
|
98
|
+
[1.025, 1.055, 1.055, 1.025],
|
|
99
|
+
[start, start, end, end],
|
|
100
|
+
color="#8793A1",
|
|
101
|
+
linewidth=0.9,
|
|
102
|
+
transform=transform,
|
|
103
|
+
clip_on=False,
|
|
104
|
+
)
|
|
105
|
+
bracket.set_gid("four-state-event-window-component-group")
|
|
106
|
+
axis.text(
|
|
107
|
+
1.075,
|
|
108
|
+
(start + end) / 2.0,
|
|
109
|
+
label,
|
|
110
|
+
transform=transform,
|
|
111
|
+
ha="left",
|
|
112
|
+
va="center",
|
|
113
|
+
fontsize=11.5,
|
|
114
|
+
color="#536273",
|
|
115
|
+
linespacing=1.15,
|
|
116
|
+
clip_on=False,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def quality_notes(diagnostic: FourStateEventWindowDiagnostic) -> list[str]:
|
|
121
|
+
notes: list[str] = []
|
|
122
|
+
for index, component in enumerate(COMPONENT_COLUMNS):
|
|
123
|
+
flags: list[str] = []
|
|
124
|
+
bound = diagnostic.component_bound_kinds[index]
|
|
125
|
+
if bound != "exact":
|
|
126
|
+
flags.append(f"{bound} bound")
|
|
127
|
+
if diagnostic.component_has_policy_clipping[index]:
|
|
128
|
+
flags.append("policy clipping")
|
|
129
|
+
if diagnostic.component_has_instrument_overflow[index]:
|
|
130
|
+
flags.append("instrument overflow")
|
|
131
|
+
if diagnostic.component_event_has_policy_clipping[index]:
|
|
132
|
+
flags.append("event-range clipping")
|
|
133
|
+
if diagnostic.component_event_has_instrument_overflow[index]:
|
|
134
|
+
flags.append("event-range overflow")
|
|
135
|
+
if flags:
|
|
136
|
+
notes.append(f"{component}: {', '.join(flags)}")
|
|
137
|
+
return notes
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def has_quality_flags(diagnostic: FourStateEventWindowDiagnostic) -> bool:
|
|
141
|
+
trace_flags = (
|
|
142
|
+
diagnostic.traces["value_policy_clipped"].astype(bool)
|
|
143
|
+
| diagnostic.traces["value_instrument_overflow"].astype(bool)
|
|
144
|
+
| diagnostic.traces["value_bound_kind"].astype(str).ne("exact")
|
|
145
|
+
).any()
|
|
146
|
+
return bool(trace_flags or quality_notes(diagnostic))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["draw_component_panel", "has_quality_flags", "quality_notes"]
|