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,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from reader_workbench.domains.plate_reader.ordering import order_levels
|
|
9
|
+
from reader_workbench.domains.plate_reader.plots.grouping import GroupMatch, resolve_groups
|
|
10
|
+
|
|
11
|
+
_PanelBy = Literal["channel", "x", "group"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_figure_groups(
|
|
15
|
+
*,
|
|
16
|
+
stats: pd.DataFrame,
|
|
17
|
+
group_col: str | None,
|
|
18
|
+
panel_by: _PanelBy,
|
|
19
|
+
pool_sets: list[dict[str, list[str]]] | None,
|
|
20
|
+
pool_match: GroupMatch,
|
|
21
|
+
) -> list[tuple[str, list[str | None]]]:
|
|
22
|
+
if not group_col:
|
|
23
|
+
return [("all", [None])]
|
|
24
|
+
|
|
25
|
+
universe = order_levels(stats[group_col].astype(str).unique().tolist())
|
|
26
|
+
if panel_by == "group":
|
|
27
|
+
if pool_sets:
|
|
28
|
+
resolved = resolve_groups(universe, pool_sets, match=pool_match)
|
|
29
|
+
ordered: list[str] = []
|
|
30
|
+
seen: set[str] = set()
|
|
31
|
+
for _, values in resolved:
|
|
32
|
+
for value in values:
|
|
33
|
+
if value in universe and value not in seen:
|
|
34
|
+
ordered.append(value)
|
|
35
|
+
seen.add(value)
|
|
36
|
+
return [("all", ordered or universe)]
|
|
37
|
+
return [("all", universe)]
|
|
38
|
+
|
|
39
|
+
if pool_sets:
|
|
40
|
+
resolved = resolve_groups(universe, pool_sets, match=pool_match)
|
|
41
|
+
union: list[str] = []
|
|
42
|
+
seen: set[str] = set()
|
|
43
|
+
for _, values in resolved:
|
|
44
|
+
for value in values:
|
|
45
|
+
if value in universe and value not in seen:
|
|
46
|
+
union.append(value)
|
|
47
|
+
seen.add(value)
|
|
48
|
+
return [(group, [group]) for group in (union or universe)]
|
|
49
|
+
return [(group, [group]) for group in universe]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_panel_configuration(
|
|
53
|
+
*,
|
|
54
|
+
panel_by: _PanelBy,
|
|
55
|
+
members: list[str | None],
|
|
56
|
+
y_list: list[str],
|
|
57
|
+
group_col: str | None,
|
|
58
|
+
channel_select: str | None,
|
|
59
|
+
selected_channel_default: str | None,
|
|
60
|
+
ch_for_file: str | None,
|
|
61
|
+
stats: pd.DataFrame,
|
|
62
|
+
x_col: str,
|
|
63
|
+
) -> tuple[list[str], str | None]:
|
|
64
|
+
if panel_by == "channel":
|
|
65
|
+
return y_list, None
|
|
66
|
+
if panel_by == "group":
|
|
67
|
+
if not group_col:
|
|
68
|
+
raise ValueError("panel_by='group' requires 'group_on'")
|
|
69
|
+
selected_channel = (
|
|
70
|
+
(str(ch_for_file) if ch_for_file is not None else None) or channel_select or selected_channel_default
|
|
71
|
+
)
|
|
72
|
+
if not selected_channel:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"panel_by='group' requires an explicit channel: set 'channel_select' or pass a single string to 'y'."
|
|
75
|
+
)
|
|
76
|
+
return [str(member) for member in members], str(selected_channel)
|
|
77
|
+
|
|
78
|
+
selected_channel = channel_select or selected_channel_default
|
|
79
|
+
if not selected_channel:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"panel_by='x' requires an explicit channel: set 'channel_select' or pass a single string to 'y'."
|
|
82
|
+
)
|
|
83
|
+
sub_stats = stats[stats["channel"].astype(str) == str(selected_channel)]
|
|
84
|
+
x_levels = sub_stats[x_col].astype(str).unique().tolist()
|
|
85
|
+
return order_levels(x_levels), str(selected_channel)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def compute_shared_ylim(
|
|
89
|
+
*,
|
|
90
|
+
stats: pd.DataFrame,
|
|
91
|
+
snapped: pd.DataFrame,
|
|
92
|
+
panels: list[str],
|
|
93
|
+
panel_by: _PanelBy,
|
|
94
|
+
selected_channel: str,
|
|
95
|
+
group_col: str | None,
|
|
96
|
+
x_col: str,
|
|
97
|
+
agg: str,
|
|
98
|
+
dispersion: str,
|
|
99
|
+
) -> tuple[float | None, float | None]:
|
|
100
|
+
if panel_by not in {"group", "x"} or len(panels) <= 1:
|
|
101
|
+
return None, None
|
|
102
|
+
|
|
103
|
+
def _collect_limits_for_subset(
|
|
104
|
+
sbar_sub: pd.DataFrame,
|
|
105
|
+
srep_sub: pd.DataFrame,
|
|
106
|
+
) -> tuple[float | None, float | None]:
|
|
107
|
+
values = (
|
|
108
|
+
pd.to_numeric(sbar_sub[agg], errors="coerce").dropna() if not sbar_sub.empty else pd.Series([], dtype=float)
|
|
109
|
+
)
|
|
110
|
+
value_min = float(values.min()) if not values.empty else None
|
|
111
|
+
value_max = float(values.max()) if not values.empty else None
|
|
112
|
+
if dispersion == "sd" and "std" in sbar_sub.columns:
|
|
113
|
+
center = pd.to_numeric(sbar_sub[agg], errors="coerce")
|
|
114
|
+
spread = pd.to_numeric(sbar_sub["std"], errors="coerce")
|
|
115
|
+
bottom = (center - spread).dropna()
|
|
116
|
+
top = (center + spread).dropna()
|
|
117
|
+
if not bottom.empty:
|
|
118
|
+
value_min = min(value_min if value_min is not None else np.inf, float(bottom.min()))
|
|
119
|
+
if not top.empty:
|
|
120
|
+
value_max = max(value_max or -np.inf, float(top.max()))
|
|
121
|
+
elif dispersion == "iqr" and {"q1", "q3"}.issubset(sbar_sub.columns):
|
|
122
|
+
if agg == "median":
|
|
123
|
+
top = pd.to_numeric(sbar_sub["q3"], errors="coerce").dropna()
|
|
124
|
+
if not top.empty:
|
|
125
|
+
value_max = max(value_max or -np.inf, float(top.max()))
|
|
126
|
+
else:
|
|
127
|
+
half = 0.5 * (
|
|
128
|
+
pd.to_numeric(sbar_sub["q3"], errors="coerce") - pd.to_numeric(sbar_sub["q1"], errors="coerce")
|
|
129
|
+
)
|
|
130
|
+
top = (pd.to_numeric(sbar_sub[agg], errors="coerce") + half).dropna()
|
|
131
|
+
if not top.empty:
|
|
132
|
+
value_max = max(value_max or -np.inf, float(top.max()))
|
|
133
|
+
if not srep_sub.empty and "value" in srep_sub.columns:
|
|
134
|
+
observation_values = pd.to_numeric(srep_sub["value"], errors="coerce").dropna()
|
|
135
|
+
if not observation_values.empty:
|
|
136
|
+
value_min = min(value_min if value_min is not None else float("inf"), float(observation_values.min()))
|
|
137
|
+
value_max = max(value_max if value_max is not None else float("-inf"), float(observation_values.max()))
|
|
138
|
+
return value_min, value_max
|
|
139
|
+
|
|
140
|
+
low_values: list[float] = []
|
|
141
|
+
high_values: list[float] = []
|
|
142
|
+
for panel in panels:
|
|
143
|
+
if panel_by == "group":
|
|
144
|
+
sbar_sub = stats[
|
|
145
|
+
(stats["channel"].astype(str) == str(selected_channel)) & (stats[group_col].astype(str) == str(panel))
|
|
146
|
+
]
|
|
147
|
+
srep_sub = snapped[
|
|
148
|
+
(snapped["channel"].astype(str) == str(selected_channel))
|
|
149
|
+
& (snapped[group_col].astype(str) == str(panel))
|
|
150
|
+
]
|
|
151
|
+
else:
|
|
152
|
+
sbar_sub = stats[
|
|
153
|
+
(stats["channel"].astype(str) == str(selected_channel)) & (stats[x_col].astype(str) == str(panel))
|
|
154
|
+
]
|
|
155
|
+
srep_sub = snapped[
|
|
156
|
+
(snapped["channel"].astype(str) == str(selected_channel)) & (snapped[x_col].astype(str) == str(panel))
|
|
157
|
+
]
|
|
158
|
+
value_min, value_max = _collect_limits_for_subset(sbar_sub, srep_sub)
|
|
159
|
+
if value_min is not None:
|
|
160
|
+
low_values.append(value_min)
|
|
161
|
+
if value_max is not None:
|
|
162
|
+
high_values.append(value_max)
|
|
163
|
+
if not high_values:
|
|
164
|
+
return None, None
|
|
165
|
+
y_lo = min(0.0, float(np.nanmin(low_values))) if low_values else 0.0
|
|
166
|
+
y_hi = float(np.nanmax(high_values))
|
|
167
|
+
pad = 0.05 * (y_hi - y_lo if y_hi > y_lo else max(1.0, y_hi or 1.0))
|
|
168
|
+
return (y_lo - pad if y_lo < 0.0 else 0.0), y_hi + pad
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Snapshot heatmap with tidy aggregation and square cells."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import matplotlib.colors as mcolors
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import polars as pl
|
|
15
|
+
from matplotlib.colors import Colormap, LinearSegmentedColormap
|
|
16
|
+
|
|
17
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
18
|
+
from reader_workbench.plotting.style import new_fig_ax, use_style
|
|
19
|
+
|
|
20
|
+
from ...analysis.timepoints import choose_nearest_time
|
|
21
|
+
from ..common import alias_column, plot_figure, pretty_name, warn_if_empty
|
|
22
|
+
from .inputs import (
|
|
23
|
+
SnapshotHeatmapInputs as SnapshotHeatmapInputs,
|
|
24
|
+
)
|
|
25
|
+
from .inputs import (
|
|
26
|
+
prepare_snapshot_heatmap_inputs as prepare_snapshot_heatmap_inputs,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = ["SnapshotHeatmapInputs", "plot_snapshot_heatmap", "prepare_snapshot_heatmap_inputs"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ensure(df: pd.DataFrame, cols: list[str]) -> None:
|
|
33
|
+
missing = [column for column in cols if column not in df.columns]
|
|
34
|
+
if missing:
|
|
35
|
+
raise ValueError(f"snapshot_heatmap: required columns missing: {missing}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _apply_explicit_axis_order(
|
|
39
|
+
pivot: pd.DataFrame,
|
|
40
|
+
*,
|
|
41
|
+
axis: str,
|
|
42
|
+
labels: list[str] | None,
|
|
43
|
+
) -> pd.DataFrame:
|
|
44
|
+
if labels is None:
|
|
45
|
+
return pivot
|
|
46
|
+
if not labels:
|
|
47
|
+
raise ValueError(f"snapshot_heatmap: {axis} order must not be empty")
|
|
48
|
+
|
|
49
|
+
available = list(map(str, pivot.columns if axis == "x" else pivot.index))
|
|
50
|
+
missing = [label for label in labels if label not in available]
|
|
51
|
+
if missing:
|
|
52
|
+
raise ValueError(f"snapshot_heatmap: {axis} order includes missing label(s): {missing}")
|
|
53
|
+
|
|
54
|
+
if axis == "x":
|
|
55
|
+
return pivot[labels]
|
|
56
|
+
return pivot.loc[labels]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_cmap(spec: Any) -> Colormap | None:
|
|
60
|
+
if spec is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(spec, mcolors.Colormap):
|
|
63
|
+
return spec
|
|
64
|
+
if isinstance(spec, list | tuple):
|
|
65
|
+
return LinearSegmentedColormap.from_list("custom", list(spec))
|
|
66
|
+
if isinstance(spec, str):
|
|
67
|
+
try:
|
|
68
|
+
return plt.get_cmap(spec)
|
|
69
|
+
except Exception:
|
|
70
|
+
return LinearSegmentedColormap.from_list("custom", ["#FFFFFF", spec])
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def plot_snapshot_heatmap(
|
|
75
|
+
*,
|
|
76
|
+
df: pd.DataFrame,
|
|
77
|
+
channel: str,
|
|
78
|
+
time: float,
|
|
79
|
+
x: str = "treatment",
|
|
80
|
+
y: str = "design_id",
|
|
81
|
+
order_x: list[str] | None = None,
|
|
82
|
+
order_y: list[str] | None = None,
|
|
83
|
+
square: bool = True,
|
|
84
|
+
vmin: float | None = None,
|
|
85
|
+
vmax: float | None = None,
|
|
86
|
+
fig_kwargs: dict[str, Any],
|
|
87
|
+
filename: str | None,
|
|
88
|
+
logger: logging.Logger | None = None,
|
|
89
|
+
) -> list[PlotFigure]:
|
|
90
|
+
x_col = alias_column(df, x)
|
|
91
|
+
y_col = alias_column(df, y)
|
|
92
|
+
_ensure(df, ["time", "channel", "value", x_col, y_col])
|
|
93
|
+
|
|
94
|
+
work_pl = pl.from_pandas(df)
|
|
95
|
+
time_expr = pl.col("time").cast(pl.Float64, strict=True)
|
|
96
|
+
value_expr = pl.col("value").cast(pl.Float64, strict=False)
|
|
97
|
+
value_expr = pl.when(value_expr.is_nan()).then(None).otherwise(value_expr)
|
|
98
|
+
work_pl = work_pl.with_columns(time_expr.alias("time"), value_expr.alias("value"))
|
|
99
|
+
|
|
100
|
+
rc = (fig_kwargs or {}).get("rc", {})
|
|
101
|
+
tol = (fig_kwargs or {}).get("time_tolerance", rc.get("time_tolerance", None))
|
|
102
|
+
times = (
|
|
103
|
+
work_pl.filter(pl.col("channel").cast(pl.Utf8) == str(channel))
|
|
104
|
+
.select(pl.col("time").fill_null(float("nan")).alias("time"))
|
|
105
|
+
.to_numpy()
|
|
106
|
+
.ravel()
|
|
107
|
+
)
|
|
108
|
+
tsel = choose_nearest_time(
|
|
109
|
+
times,
|
|
110
|
+
target_time=float(time),
|
|
111
|
+
tol=(None if tol is None else float(tol)),
|
|
112
|
+
where="snapshot_heatmap",
|
|
113
|
+
logger=logger,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
snap_pl = work_pl.filter(
|
|
117
|
+
(pl.col("channel").cast(pl.Utf8) == str(channel)) & ((pl.col("time") - float(tsel)).abs() <= 1e-9)
|
|
118
|
+
)
|
|
119
|
+
if snap_pl.is_empty():
|
|
120
|
+
raise ValueError("snapshot_heatmap: no rows after (channel,time) selection")
|
|
121
|
+
|
|
122
|
+
pivot_pl = (
|
|
123
|
+
snap_pl.group_by([y_col, x_col])
|
|
124
|
+
.agg(pl.col("value").median().alias("value"))
|
|
125
|
+
.sort([y_col, x_col])
|
|
126
|
+
.pivot(values="value", index=y_col, on=x_col, aggregate_function="first")
|
|
127
|
+
)
|
|
128
|
+
pivot = pivot_pl.to_pandas(use_pyarrow_extension_array=False)
|
|
129
|
+
if y_col in pivot.columns:
|
|
130
|
+
pivot = pivot.set_index(y_col)
|
|
131
|
+
|
|
132
|
+
pivot = _apply_explicit_axis_order(pivot, axis="x", labels=order_x)
|
|
133
|
+
pivot = _apply_explicit_axis_order(pivot, axis="y", labels=order_y)
|
|
134
|
+
|
|
135
|
+
if pivot.empty or pivot.shape[0] == 0 or pivot.shape[1] == 0:
|
|
136
|
+
warn_if_empty(pivot, where="snapshot_heatmap", detail="after pivot")
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
with use_style(rc=(fig_kwargs or {}).get("rc")):
|
|
140
|
+
fig, ax = new_fig_ax(fig_kwargs)
|
|
141
|
+
arr = pivot.to_numpy(dtype=float)
|
|
142
|
+
if not np.isfinite(arr).any():
|
|
143
|
+
raise ValueError("snapshot_heatmap: selected snapshot contains no finite values")
|
|
144
|
+
cmap = _resolve_cmap((fig_kwargs or {}).get("cmap"))
|
|
145
|
+
resolved_vmin = float(np.nanmin(arr)) if vmin is None else float(vmin)
|
|
146
|
+
resolved_vmax = float(np.nanmax(arr)) if vmax is None else float(vmax)
|
|
147
|
+
|
|
148
|
+
im = ax.imshow(
|
|
149
|
+
arr,
|
|
150
|
+
aspect=("equal" if square else "auto"),
|
|
151
|
+
vmin=resolved_vmin,
|
|
152
|
+
vmax=resolved_vmax,
|
|
153
|
+
cmap=cmap,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
ax.grid(False)
|
|
157
|
+
with suppress(Exception):
|
|
158
|
+
ax.set_facecolor("white")
|
|
159
|
+
|
|
160
|
+
ax.set_xticks(range(arr.shape[1]))
|
|
161
|
+
ax.set_xticklabels(list(map(str, pivot.columns)), rotation=45, ha="right")
|
|
162
|
+
ax.set_yticks(range(arr.shape[0]))
|
|
163
|
+
ax.set_yticklabels(list(map(str, pivot.index)))
|
|
164
|
+
ax.set_xlabel(pretty_name(str(x_col)).title())
|
|
165
|
+
ax.set_ylabel(pretty_name(str(y_col)).title())
|
|
166
|
+
ax.set_title(f"{channel} @ t={tsel:g} h")
|
|
167
|
+
|
|
168
|
+
ax.set_xticks(np.arange(-0.5, arr.shape[1], 1), minor=True)
|
|
169
|
+
ax.set_yticks(np.arange(-0.5, arr.shape[0], 1), minor=True)
|
|
170
|
+
ax.grid(which="minor", color="white", linestyle="-", linewidth=0.5)
|
|
171
|
+
ax.tick_params(which="minor", bottom=False, left=False)
|
|
172
|
+
|
|
173
|
+
shrink = float((fig_kwargs or {}).get("cbar_shrink", 1.0))
|
|
174
|
+
cbar_label: str | None = (fig_kwargs or {}).get("cbar_label")
|
|
175
|
+
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04, shrink=shrink, label=cbar_label)
|
|
176
|
+
|
|
177
|
+
base = filename or f"snapshot_heatmap__{channel}__t{tsel:g}h"
|
|
178
|
+
y_levels = list(map(str, pivot.index))
|
|
179
|
+
n_geno = len(y_levels)
|
|
180
|
+
|
|
181
|
+
def _short_id(value: str) -> str:
|
|
182
|
+
return hashlib.blake2b(value.encode("utf-8"), digest_size=4).hexdigest()
|
|
183
|
+
|
|
184
|
+
geno_id = _short_id("|".join(sorted(y_levels)))
|
|
185
|
+
x_levels = list(map(str, pivot.columns))
|
|
186
|
+
cmap_name = (
|
|
187
|
+
cmap.name if isinstance(cmap, Colormap) and hasattr(cmap, "name") else "custom" if cmap else "default"
|
|
188
|
+
)
|
|
189
|
+
tol = (fig_kwargs or {}).get("time_tolerance", None)
|
|
190
|
+
fp_payload = "|".join(
|
|
191
|
+
[
|
|
192
|
+
f"ch={channel}",
|
|
193
|
+
f"t={tsel:g}",
|
|
194
|
+
"x=" + ",".join(x_levels),
|
|
195
|
+
"y=" + ",".join(y_levels),
|
|
196
|
+
f"cmap={cmap_name}",
|
|
197
|
+
f"vmin={'nan' if vmin is None else float(vmin)}",
|
|
198
|
+
f"vmax={'nan' if vmax is None else float(vmax)}",
|
|
199
|
+
f"square={bool(square)}",
|
|
200
|
+
f"tol={'' if tol is None else float(tol)}",
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
fp_id = _short_id(fp_payload)
|
|
204
|
+
stub = f"{base}__gy{n_geno}-{geno_id}__fp{fp_id}"
|
|
205
|
+
return [plot_figure(fig=fig, filename=stub, fig_kwargs=fig_kwargs)]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from reader_workbench.domains.plate_reader.analysis.timepoints import choose_nearest_time
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SnapshotHeatmapInputs:
|
|
16
|
+
"""Prepared dataframe and rendering values for a snapshot heatmap."""
|
|
17
|
+
|
|
18
|
+
frame: pd.DataFrame
|
|
19
|
+
filename: str | None
|
|
20
|
+
fig_kwargs: dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def auto_cbar_label(channel: str, value_transform: str | None) -> str:
|
|
24
|
+
transform = (value_transform or "none").lower()
|
|
25
|
+
if channel.startswith("log2FC_"):
|
|
26
|
+
return f"log2FC ({channel.split('_', 1)[1]})"
|
|
27
|
+
if channel.startswith("FC_"):
|
|
28
|
+
return f"FC ({channel.split('_', 1)[1]})"
|
|
29
|
+
if transform == "log2":
|
|
30
|
+
return f"log2({channel})"
|
|
31
|
+
if transform == "log10":
|
|
32
|
+
return f"log10({channel})"
|
|
33
|
+
return channel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _transform_positive_values(values: pd.Series, *, transform: str) -> pd.Series:
|
|
37
|
+
numeric = pd.to_numeric(values, errors="coerce")
|
|
38
|
+
result = pd.Series(np.nan, index=numeric.index, dtype=float)
|
|
39
|
+
positive = numeric > 0
|
|
40
|
+
if transform == "log2":
|
|
41
|
+
result.loc[positive] = np.log2(numeric.loc[positive])
|
|
42
|
+
return result
|
|
43
|
+
result.loc[positive] = np.log10(numeric.loc[positive])
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prepare_snapshot_heatmap_inputs(
|
|
48
|
+
*,
|
|
49
|
+
df_in: pd.DataFrame | None,
|
|
50
|
+
fc_in: pd.DataFrame | None,
|
|
51
|
+
channel: str,
|
|
52
|
+
time: float,
|
|
53
|
+
time_tolerance: float,
|
|
54
|
+
value_transform: str | None,
|
|
55
|
+
fig: Mapping[str, Any] | None,
|
|
56
|
+
filename: str | None,
|
|
57
|
+
logger: logging.Logger | None = None,
|
|
58
|
+
) -> SnapshotHeatmapInputs:
|
|
59
|
+
channel = str(channel)
|
|
60
|
+
wants_fc = channel.startswith("FC_") or channel.startswith("log2FC_")
|
|
61
|
+
fig_kwargs = dict(fig or {})
|
|
62
|
+
fig_kwargs.setdefault("time_tolerance", time_tolerance)
|
|
63
|
+
|
|
64
|
+
if wants_fc:
|
|
65
|
+
if fc_in is None:
|
|
66
|
+
raise ValueError(f"snapshot_heatmap: channel={channel!r} requires a fold_change.v1 input (reads.fc)")
|
|
67
|
+
target = channel.split("_", 1)[1]
|
|
68
|
+
use_col = "log2FC" if channel.startswith("log2FC_") else "FC"
|
|
69
|
+
table = fc_in.copy()
|
|
70
|
+
table = table[table["target"].astype(str) == target].copy()
|
|
71
|
+
if table.empty:
|
|
72
|
+
raise ValueError(f"snapshot_heatmap: no fold_change rows found for target={target!r}")
|
|
73
|
+
chosen_time = choose_nearest_time(
|
|
74
|
+
table["time"],
|
|
75
|
+
target_time=float(time),
|
|
76
|
+
tol=float(time_tolerance),
|
|
77
|
+
where="snapshot_heatmap",
|
|
78
|
+
logger=logger,
|
|
79
|
+
)
|
|
80
|
+
subset = table[pd.to_numeric(table["time"], errors="coerce") == chosen_time].copy()
|
|
81
|
+
subset = subset.rename(columns={use_col: "value"})
|
|
82
|
+
subset["channel"] = channel
|
|
83
|
+
keep = ["time", "channel", "value", "treatment", "design_id", "design_id_alias"]
|
|
84
|
+
df = subset[[column for column in keep if column in subset.columns]].copy()
|
|
85
|
+
resolved_filename = filename or f"snapshot_heatmap__{channel}__t{chosen_time:g}h"
|
|
86
|
+
fig_kwargs.setdefault("cbar_label", auto_cbar_label(channel, None))
|
|
87
|
+
return SnapshotHeatmapInputs(frame=df, filename=resolved_filename, fig_kwargs=fig_kwargs)
|
|
88
|
+
|
|
89
|
+
if df_in is None:
|
|
90
|
+
raise ValueError("snapshot_heatmap: tidy df is required when channel is not FC_/log2FC_-prefixed")
|
|
91
|
+
|
|
92
|
+
df = df_in.copy()
|
|
93
|
+
if value_transform and str(value_transform).lower() in {"log2", "log10"}:
|
|
94
|
+
mask = df["channel"].astype(str) == channel
|
|
95
|
+
df.loc[mask, "value"] = _transform_positive_values(
|
|
96
|
+
df.loc[mask, "value"],
|
|
97
|
+
transform=str(value_transform).lower(),
|
|
98
|
+
)
|
|
99
|
+
fig_kwargs.setdefault("cbar_label", auto_cbar_label(channel, value_transform))
|
|
100
|
+
return SnapshotHeatmapInputs(frame=df, filename=filename, fig_kwargs=fig_kwargs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["SnapshotHeatmapInputs", "auto_cbar_label", "prepare_snapshot_heatmap_inputs"]
|