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,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from ._shared import (
|
|
9
|
+
coerce_measurements,
|
|
10
|
+
drop_all_empty_rows,
|
|
11
|
+
is_blank_measurement,
|
|
12
|
+
require,
|
|
13
|
+
resolve_channel_from_map,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tidy_snapshot_block(
|
|
18
|
+
snapshot: pd.DataFrame,
|
|
19
|
+
*,
|
|
20
|
+
elapsed_h: float,
|
|
21
|
+
sheet_index: int,
|
|
22
|
+
sheet_name: str,
|
|
23
|
+
channels: Sequence[str] | None,
|
|
24
|
+
channel_map_ci: Mapping[str, str],
|
|
25
|
+
) -> pd.DataFrame:
|
|
26
|
+
require(channel_map_ci, "Snapshot parsing requires an explicit 'channel_map'")
|
|
27
|
+
require(not snapshot.empty, "Empty snapshot block")
|
|
28
|
+
|
|
29
|
+
header = snapshot.iloc[0]
|
|
30
|
+
well_columns = [index for index, value in enumerate(header) if str(value).strip().isdigit()]
|
|
31
|
+
require(well_columns, "Snapshot header did not contain numbered well columns")
|
|
32
|
+
well_numbers = [int(str(header[index]).strip()) for index in well_columns]
|
|
33
|
+
invalid_well_numbers = [number for number in well_numbers if number < 1 or number > 12]
|
|
34
|
+
require(
|
|
35
|
+
not invalid_well_numbers,
|
|
36
|
+
f"Snapshot header contains columns outside the 96-well range 1..12: {invalid_well_numbers}",
|
|
37
|
+
)
|
|
38
|
+
duplicate_well_numbers = sorted({number for number in well_numbers if well_numbers.count(number) > 1})
|
|
39
|
+
require(not duplicate_well_numbers, f"Snapshot header contains duplicate well columns: {duplicate_well_numbers}")
|
|
40
|
+
|
|
41
|
+
data_rows = drop_all_empty_rows(snapshot.iloc[1:].copy())
|
|
42
|
+
require(not data_rows.empty, "Snapshot block contains a header but no data rows")
|
|
43
|
+
|
|
44
|
+
row_column_candidates = [
|
|
45
|
+
column
|
|
46
|
+
for column in range(data_rows.shape[1])
|
|
47
|
+
if data_rows.iloc[:, column].map(lambda value: bool(re.fullmatch(r"[A-H]", str(value).strip()))).any()
|
|
48
|
+
]
|
|
49
|
+
require(
|
|
50
|
+
len(row_column_candidates) == 1,
|
|
51
|
+
f"Could not identify one row-letter column in snapshot block on sheet {sheet_name!r}",
|
|
52
|
+
)
|
|
53
|
+
row_column = row_column_candidates[0]
|
|
54
|
+
label_columns = [
|
|
55
|
+
column for column in range(data_rows.shape[1]) if column != row_column and column not in well_columns
|
|
56
|
+
]
|
|
57
|
+
expected_channels = set(channels or channel_map_ci.values())
|
|
58
|
+
|
|
59
|
+
def channel_for_data_row(row: pd.Series, *, plate_row: str) -> str:
|
|
60
|
+
declarations: list[tuple[str, str]] = []
|
|
61
|
+
for column in label_columns:
|
|
62
|
+
value = row.iat[column]
|
|
63
|
+
if is_blank_measurement(value):
|
|
64
|
+
continue
|
|
65
|
+
raw_label = str(value).strip()
|
|
66
|
+
try:
|
|
67
|
+
resolved = resolve_channel_from_map(raw_label, channel_map_ci=channel_map_ci)
|
|
68
|
+
except ValueError as error:
|
|
69
|
+
raise ValueError(f"{error} in snapshot sheet {sheet_name!r}, plate row {plate_row!r}") from error
|
|
70
|
+
if resolved is not None:
|
|
71
|
+
declarations.append((raw_label, resolved))
|
|
72
|
+
if not declarations:
|
|
73
|
+
raise ValueError(f"Missing snapshot channel label in sheet {sheet_name!r}, plate row {plate_row!r}")
|
|
74
|
+
if len(declarations) > 1:
|
|
75
|
+
labels = [raw for raw, _ in declarations]
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Multiple snapshot channel labels {labels} in sheet {sheet_name!r}, plate row {plate_row!r}"
|
|
78
|
+
)
|
|
79
|
+
return declarations[0][1]
|
|
80
|
+
|
|
81
|
+
rows: list[dict[str, object]] = []
|
|
82
|
+
current_plate_row: str | None = None
|
|
83
|
+
current_channels: set[str] = set()
|
|
84
|
+
|
|
85
|
+
def finish_plate_row() -> None:
|
|
86
|
+
if current_plate_row is None:
|
|
87
|
+
return
|
|
88
|
+
missing = expected_channels - current_channels
|
|
89
|
+
require(
|
|
90
|
+
not missing,
|
|
91
|
+
f"Missing snapshot channel labels {sorted(missing)} in sheet {sheet_name!r}, "
|
|
92
|
+
f"plate row {current_plate_row!r}",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for _, data_row in data_rows.iterrows():
|
|
96
|
+
row_marker = data_row.iat[row_column]
|
|
97
|
+
if not is_blank_measurement(row_marker):
|
|
98
|
+
plate_row = str(row_marker).strip()
|
|
99
|
+
require(
|
|
100
|
+
bool(re.fullmatch(r"[A-H]", plate_row)),
|
|
101
|
+
f"Invalid snapshot row label {plate_row!r} in sheet {sheet_name!r}",
|
|
102
|
+
)
|
|
103
|
+
finish_plate_row()
|
|
104
|
+
current_plate_row = plate_row
|
|
105
|
+
current_channels = set()
|
|
106
|
+
require(
|
|
107
|
+
current_plate_row is not None,
|
|
108
|
+
f"Snapshot measurement row precedes a plate-row label in sheet {sheet_name!r}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
channel = channel_for_data_row(data_row, plate_row=current_plate_row)
|
|
112
|
+
require(
|
|
113
|
+
channel not in current_channels,
|
|
114
|
+
f"Duplicate snapshot channel {channel!r} in sheet {sheet_name!r}, plate row {current_plate_row!r}",
|
|
115
|
+
)
|
|
116
|
+
current_channels.add(channel)
|
|
117
|
+
for well_number, value in zip(well_numbers, data_row.iloc[well_columns].tolist(), strict=True):
|
|
118
|
+
rows.append(
|
|
119
|
+
{
|
|
120
|
+
"position": f"{current_plate_row}{well_number}",
|
|
121
|
+
"time": float(elapsed_h),
|
|
122
|
+
"channel": channel,
|
|
123
|
+
"value": value,
|
|
124
|
+
"sheet_index": sheet_index,
|
|
125
|
+
"sheet_name": sheet_name,
|
|
126
|
+
"source": "snapshot",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
finish_plate_row()
|
|
130
|
+
return coerce_measurements(pd.DataFrame(rows), source="snapshot")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Plate-reader ordering semantics for treatments, doses, and labels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _extract_first_number(s: str) -> float | None:
|
|
11
|
+
match = _NUM_RE.search(str(s))
|
|
12
|
+
return float(match.group(0)) if match else None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _unit_scale_to_uM(s: str) -> float:
|
|
16
|
+
text = str(s).lower()
|
|
17
|
+
if "nm" in text:
|
|
18
|
+
return 1e-3
|
|
19
|
+
if "µm" in text or "um" in text or "μm" in text:
|
|
20
|
+
return 1.0
|
|
21
|
+
if "mm" in text:
|
|
22
|
+
return 1e3
|
|
23
|
+
if re.search(r"\b(?<![a-z])m\b", text):
|
|
24
|
+
return 1e6
|
|
25
|
+
return 1.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def smart_string_numeric_key(s: str) -> tuple[int, float, str]:
|
|
29
|
+
value = str(s)
|
|
30
|
+
num = _extract_first_number(value)
|
|
31
|
+
if num is None:
|
|
32
|
+
return (1, float("inf"), value.lower())
|
|
33
|
+
return (0, float(num) * _unit_scale_to_uM(value), value.lower())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prefix_before_number(s: str) -> str:
|
|
37
|
+
value = str(s)
|
|
38
|
+
match = _NUM_RE.search(value)
|
|
39
|
+
prefix = value[: match.start()].strip() if match else value.strip()
|
|
40
|
+
return re.sub(r"\s+", " ", prefix).strip().lower()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def smart_grouped_dose_key(s: str) -> tuple[str, int, float, str]:
|
|
44
|
+
value = str(s)
|
|
45
|
+
prefix = _prefix_before_number(value)
|
|
46
|
+
num = _extract_first_number(value)
|
|
47
|
+
has_num = 0 if num is not None else 1
|
|
48
|
+
scaled = float(num) * _unit_scale_to_uM(value) if num is not None else float("inf")
|
|
49
|
+
return (prefix, has_num, scaled, value.lower())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def order_levels(levels: list[str]) -> list[str]:
|
|
53
|
+
try:
|
|
54
|
+
return sorted(levels, key=smart_grouped_dose_key)
|
|
55
|
+
except Exception:
|
|
56
|
+
return sorted(levels, key=smart_string_numeric_key)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["order_levels", "smart_grouped_dose_key", "smart_string_numeric_key"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Plate-reader plotting primitives and figure builders."""
|
|
2
|
+
|
|
3
|
+
from .distributions import plot_distributions
|
|
4
|
+
from .snapshot_barplot import plot_snapshot_barplot
|
|
5
|
+
from .snapshot_heatmap import plot_snapshot_heatmap
|
|
6
|
+
from .time_series import plot_time_series
|
|
7
|
+
from .ts_and_snap import plot_ts_and_snap
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"plot_time_series",
|
|
11
|
+
"plot_snapshot_heatmap",
|
|
12
|
+
"plot_snapshot_barplot",
|
|
13
|
+
"plot_distributions",
|
|
14
|
+
"plot_ts_and_snap",
|
|
15
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def require_columns(df: pd.DataFrame, cols: Iterable[str], *, where: str) -> None:
|
|
10
|
+
missing = [column for column in cols if column not in df.columns]
|
|
11
|
+
if missing:
|
|
12
|
+
raise ValueError(f"{where}: missing required columns: {missing}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def warn_if_empty(df: pd.DataFrame, *, where: str, detail: str | None = None) -> bool:
|
|
16
|
+
if not df.empty:
|
|
17
|
+
return False
|
|
18
|
+
message = f"[warn]{where}[/warn] • no rows to plot"
|
|
19
|
+
if detail:
|
|
20
|
+
message += f" ({detail})"
|
|
21
|
+
logging.getLogger("reader").info(message)
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def alias_column(df: pd.DataFrame, name: str | None, suffix: str = "_alias") -> str | None:
|
|
26
|
+
if name is None:
|
|
27
|
+
return None
|
|
28
|
+
candidate = f"{str(name)}{suffix}"
|
|
29
|
+
return candidate if candidate in df.columns else name
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Shared plotting helpers for plate-reader renderers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Iterable, Mapping
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
15
|
+
|
|
16
|
+
from ._data import alias_column, require_columns, warn_if_empty
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from reader_workbench.plotting.style import PaletteBook
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pretty_name(name: str, suffix: str = "_alias") -> str:
|
|
23
|
+
if name.endswith(suffix):
|
|
24
|
+
return name[: -len(suffix)]
|
|
25
|
+
return name
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def best_subplot_grid(n: int) -> tuple[int, int]:
|
|
29
|
+
n = max(1, int(n))
|
|
30
|
+
rows = int(math.floor(math.sqrt(n)))
|
|
31
|
+
cols = int(math.ceil(n / rows))
|
|
32
|
+
return rows, cols
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def colors_for(n: int, palette_book: PaletteBook | None) -> list[str]:
|
|
36
|
+
if palette_book:
|
|
37
|
+
if n == 1:
|
|
38
|
+
palette = palette_book.colors(2)
|
|
39
|
+
return [palette[1]] if (palette and str(palette[0]).lower() in {"#000000", "black"}) else [palette[0]]
|
|
40
|
+
return palette_book.colors(n)
|
|
41
|
+
from reader_workbench.plotting.style import PaletteBook as _PaletteBook # noqa: PLC0415
|
|
42
|
+
|
|
43
|
+
cycle = _PaletteBook(name="colorblind").colors(max(2, n))
|
|
44
|
+
if n == 1 and str(cycle[0]).lower() in {"#000000", "black"} and len(cycle) > 1:
|
|
45
|
+
return [cycle[1]]
|
|
46
|
+
return cycle[:n]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def plot_figure(
|
|
50
|
+
*,
|
|
51
|
+
fig: Any,
|
|
52
|
+
filename: str,
|
|
53
|
+
fig_kwargs: Mapping[str, Any] | None,
|
|
54
|
+
description: str | None = None,
|
|
55
|
+
) -> PlotFigure:
|
|
56
|
+
ext = str((fig_kwargs or {}).get("ext", "pdf")).lower()
|
|
57
|
+
dpi = (fig_kwargs or {}).get("dpi", None)
|
|
58
|
+
return PlotFigure(fig=fig, filename=filename, ext=ext, dpi=dpi, description=description)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def exact_observation_counts(frame: pd.DataFrame, *, value_col: str = "value") -> tuple[int, int]:
|
|
62
|
+
"""Return total and bounded-or-non-finite counts without modifying source rows."""
|
|
63
|
+
|
|
64
|
+
exact = _exact_observation_mask(frame, value_col=value_col)
|
|
65
|
+
return len(frame), int((~exact).sum())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def select_exact_observations(
|
|
69
|
+
frame: pd.DataFrame,
|
|
70
|
+
*,
|
|
71
|
+
where: str,
|
|
72
|
+
value_col: str = "value",
|
|
73
|
+
) -> pd.DataFrame:
|
|
74
|
+
"""Withhold observations that cannot support exact descriptive summaries."""
|
|
75
|
+
|
|
76
|
+
exact = _exact_observation_mask(frame, value_col=value_col)
|
|
77
|
+
omitted_count = int((~exact).sum())
|
|
78
|
+
if omitted_count:
|
|
79
|
+
logging.getLogger("reader").warning(
|
|
80
|
+
"%s: withheld %d bounded or non-finite observation(s)",
|
|
81
|
+
where,
|
|
82
|
+
omitted_count,
|
|
83
|
+
)
|
|
84
|
+
selected = frame.loc[exact].copy()
|
|
85
|
+
selected[value_col] = pd.to_numeric(selected[value_col], errors="coerce")
|
|
86
|
+
return selected
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _exact_observation_mask(frame: pd.DataFrame, *, value_col: str) -> pd.Series:
|
|
90
|
+
values = pd.to_numeric(frame[value_col], errors="coerce").to_numpy(dtype=float, copy=False)
|
|
91
|
+
exact = pd.Series(np.isfinite(values), index=frame.index)
|
|
92
|
+
bound_column = f"{value_col}_bound_kind"
|
|
93
|
+
if bound_column in frame.columns:
|
|
94
|
+
exact &= frame[bound_column].eq("exact")
|
|
95
|
+
for suffix in ("policy_clipped", "instrument_overflow"):
|
|
96
|
+
flag_column = f"{value_col}_{suffix}"
|
|
97
|
+
if flag_column in frame.columns:
|
|
98
|
+
exact &= frame[flag_column].eq(False)
|
|
99
|
+
return exact
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def descriptive_mean_resampling_interval(
|
|
103
|
+
values: np.ndarray,
|
|
104
|
+
*,
|
|
105
|
+
interval_mass: float,
|
|
106
|
+
resamples: int,
|
|
107
|
+
rng: np.random.Generator,
|
|
108
|
+
) -> tuple[float, float, float]:
|
|
109
|
+
"""Summarize observed rows without claiming population coverage."""
|
|
110
|
+
|
|
111
|
+
mass = _interval_mass(interval_mass)
|
|
112
|
+
draws = _resample_count(resamples)
|
|
113
|
+
observed = np.asarray(values, dtype=float)
|
|
114
|
+
finite_mask = np.isfinite(observed)
|
|
115
|
+
censored_count = int((~finite_mask).sum())
|
|
116
|
+
if censored_count:
|
|
117
|
+
_warn_censored_summary(censored_count=censored_count, observed_count=int(observed.size))
|
|
118
|
+
return (math.nan, math.nan, math.nan)
|
|
119
|
+
finite = observed
|
|
120
|
+
if finite.size == 0:
|
|
121
|
+
return (math.nan, math.nan, math.nan)
|
|
122
|
+
mean = float(finite.mean())
|
|
123
|
+
if finite.size <= 1:
|
|
124
|
+
return mean, mean, mean
|
|
125
|
+
alpha = (1.0 - mass) / 2.0
|
|
126
|
+
if finite.size <= 3:
|
|
127
|
+
resampled_means = finite[_ordered_resampling_indices(finite.size)].mean(axis=1)
|
|
128
|
+
else:
|
|
129
|
+
samples = rng.integers(0, finite.size, size=(draws, finite.size))
|
|
130
|
+
resampled_means = finite[samples].mean(axis=1)
|
|
131
|
+
lower, upper = np.quantile(resampled_means, [alpha, 1.0 - alpha])
|
|
132
|
+
return mean, float(lower), float(upper)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def descriptive_linear_resampling_interval(
|
|
136
|
+
groups: Iterable[np.ndarray | list[float] | tuple[float, ...]],
|
|
137
|
+
*,
|
|
138
|
+
coefficients: Iterable[float],
|
|
139
|
+
interval_mass: float,
|
|
140
|
+
resamples: int,
|
|
141
|
+
rng: np.random.Generator,
|
|
142
|
+
) -> tuple[float, float, float]:
|
|
143
|
+
mass = _interval_mass(interval_mass)
|
|
144
|
+
draws = _resample_count(resamples)
|
|
145
|
+
observed_groups = [np.asarray(group, dtype=float) for group in groups]
|
|
146
|
+
coeffs = [float(value) for value in coefficients]
|
|
147
|
+
if len(observed_groups) != len(coeffs):
|
|
148
|
+
raise ValueError("descriptive_linear_resampling_interval: groups and coefficients must have the same length")
|
|
149
|
+
|
|
150
|
+
finite_groups: list[np.ndarray] = []
|
|
151
|
+
for values in observed_groups:
|
|
152
|
+
finite_mask = np.isfinite(values)
|
|
153
|
+
censored_count = int((~finite_mask).sum())
|
|
154
|
+
if censored_count:
|
|
155
|
+
_warn_censored_summary(censored_count=censored_count, observed_count=int(values.size))
|
|
156
|
+
return (math.nan, math.nan, math.nan)
|
|
157
|
+
if values.size == 0:
|
|
158
|
+
return (math.nan, math.nan, math.nan)
|
|
159
|
+
finite_groups.append(values)
|
|
160
|
+
|
|
161
|
+
mean = float(sum(coeff * values.mean() for coeff, values in zip(coeffs, finite_groups, strict=True)))
|
|
162
|
+
if all(values.size <= 1 for values in finite_groups):
|
|
163
|
+
return mean, mean, mean
|
|
164
|
+
alpha = (1.0 - mass) / 2.0
|
|
165
|
+
|
|
166
|
+
boot_values = np.zeros(draws, dtype=float)
|
|
167
|
+
for coeff, values in zip(coeffs, finite_groups, strict=True):
|
|
168
|
+
if values.size == 1:
|
|
169
|
+
boot_values += coeff * float(values[0])
|
|
170
|
+
continue
|
|
171
|
+
sample_index = rng.integers(0, values.size, size=(draws, values.size))
|
|
172
|
+
boot_values += coeff * values[sample_index].mean(axis=1)
|
|
173
|
+
lower, upper = np.quantile(boot_values, [alpha, 1.0 - alpha])
|
|
174
|
+
return mean, float(lower), float(upper)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _interval_mass(value: float) -> float:
|
|
178
|
+
mass = float(value)
|
|
179
|
+
if not math.isfinite(mass) or not 0.0 < mass < 1.0:
|
|
180
|
+
raise ValueError("descriptive interval mass must lie strictly between 0 and 1")
|
|
181
|
+
return mass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resample_count(value: int) -> int:
|
|
185
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
186
|
+
raise ValueError("descriptive resample count must be a positive integer")
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _warn_censored_summary(*, censored_count: int, observed_count: int) -> None:
|
|
191
|
+
logging.getLogger("reader").warning(
|
|
192
|
+
"plot summary: omitted censored summary; %d non-finite value(s) among %d observed value(s)",
|
|
193
|
+
censored_count,
|
|
194
|
+
observed_count,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def shared_numeric_limits(
|
|
199
|
+
values: Iterable[object],
|
|
200
|
+
*,
|
|
201
|
+
center: float | None = None,
|
|
202
|
+
pad_fraction: float = 0.08,
|
|
203
|
+
min_span: float = 1e-6,
|
|
204
|
+
) -> tuple[float, float]:
|
|
205
|
+
finite = np.asarray(list(values), dtype=float)
|
|
206
|
+
finite = finite[np.isfinite(finite)]
|
|
207
|
+
if finite.size == 0:
|
|
208
|
+
raise ValueError("shared_numeric_limits: expected at least one finite value")
|
|
209
|
+
|
|
210
|
+
lower = float(finite.min())
|
|
211
|
+
upper = float(finite.max())
|
|
212
|
+
span_floor = max(float(min_span), 1e-12)
|
|
213
|
+
pad_fraction = max(0.0, float(pad_fraction))
|
|
214
|
+
|
|
215
|
+
if center is not None:
|
|
216
|
+
midpoint = float(center)
|
|
217
|
+
half_span = max(abs(lower - midpoint), abs(upper - midpoint), span_floor / 2.0)
|
|
218
|
+
half_span *= 1.0 + pad_fraction
|
|
219
|
+
return midpoint - half_span, midpoint + half_span
|
|
220
|
+
|
|
221
|
+
span = max(upper - lower, span_floor)
|
|
222
|
+
pad = span * pad_fraction
|
|
223
|
+
midpoint = (lower + upper) / 2.0
|
|
224
|
+
half_span = max(span / 2.0 + pad, span_floor / 2.0)
|
|
225
|
+
return midpoint - half_span, midpoint + half_span
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@lru_cache(maxsize=4)
|
|
229
|
+
def _ordered_resampling_indices(size: int) -> np.ndarray:
|
|
230
|
+
if size <= 0:
|
|
231
|
+
raise ValueError("resampling size must be positive")
|
|
232
|
+
grids = np.indices((size,) * size, dtype=np.intp)
|
|
233
|
+
return grids.reshape(size, -1).T
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def annotate_points_smart(
|
|
237
|
+
*,
|
|
238
|
+
ax: Any,
|
|
239
|
+
points: Iterable[tuple[float, float]],
|
|
240
|
+
labels: Iterable[str],
|
|
241
|
+
text_kwargs: Mapping[str, Any] | None = None,
|
|
242
|
+
) -> list[Any]:
|
|
243
|
+
fig = ax.figure
|
|
244
|
+
fig.canvas.draw()
|
|
245
|
+
renderer = fig.canvas.get_renderer()
|
|
246
|
+
axes_bbox = ax.get_window_extent(renderer=renderer)
|
|
247
|
+
point_array = np.asarray(list(points), dtype=float)
|
|
248
|
+
label_list = [str(label) for label in labels]
|
|
249
|
+
if point_array.size == 0 or not label_list:
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
candidate_offsets = [
|
|
253
|
+
(6, 6),
|
|
254
|
+
(6, -6),
|
|
255
|
+
(-6, 6),
|
|
256
|
+
(-6, -6),
|
|
257
|
+
(10, 0),
|
|
258
|
+
(-10, 0),
|
|
259
|
+
(0, 10),
|
|
260
|
+
(0, -10),
|
|
261
|
+
(12, 12),
|
|
262
|
+
(12, -12),
|
|
263
|
+
(-12, 12),
|
|
264
|
+
(-12, -12),
|
|
265
|
+
(18, 0),
|
|
266
|
+
(-18, 0),
|
|
267
|
+
(0, 18),
|
|
268
|
+
(0, -18),
|
|
269
|
+
]
|
|
270
|
+
display_points = ax.transData.transform(point_array)
|
|
271
|
+
placed_bboxes: list[Any] = []
|
|
272
|
+
annotations: list[Any] = []
|
|
273
|
+
defaults = {
|
|
274
|
+
"fontsize": 8,
|
|
275
|
+
"annotation_clip": False,
|
|
276
|
+
"clip_on": False,
|
|
277
|
+
"zorder": 6,
|
|
278
|
+
}
|
|
279
|
+
defaults.update(dict(text_kwargs or {}))
|
|
280
|
+
|
|
281
|
+
for idx, ((x_val, y_val), label) in enumerate(zip(point_array, label_list, strict=False)):
|
|
282
|
+
other_points = np.delete(display_points, idx, axis=0) if len(display_points) > 1 else np.empty((0, 2))
|
|
283
|
+
best_offset = candidate_offsets[0]
|
|
284
|
+
best_score = float("inf")
|
|
285
|
+
for offset in candidate_offsets:
|
|
286
|
+
annotation = ax.annotate(
|
|
287
|
+
label,
|
|
288
|
+
(float(x_val), float(y_val)),
|
|
289
|
+
xytext=offset,
|
|
290
|
+
textcoords="offset points",
|
|
291
|
+
**defaults,
|
|
292
|
+
)
|
|
293
|
+
bbox = annotation.get_window_extent(renderer=renderer).expanded(1.04, 1.10)
|
|
294
|
+
overlap_penalty = sum(_bbox_overlap_area(bbox, existing) for existing in placed_bboxes)
|
|
295
|
+
point_penalty = sum(1.0 for px, py in other_points if bbox.contains(px, py))
|
|
296
|
+
out_penalty = _bbox_outside_axes_penalty(bbox, axes_bbox)
|
|
297
|
+
distance_penalty = abs(offset[0]) + abs(offset[1])
|
|
298
|
+
score = overlap_penalty * 1000.0 + point_penalty * 25000.0 + out_penalty * 25.0 + distance_penalty
|
|
299
|
+
annotation.remove()
|
|
300
|
+
if score < best_score:
|
|
301
|
+
best_score = score
|
|
302
|
+
best_offset = offset
|
|
303
|
+
final_annotation = ax.annotate(
|
|
304
|
+
label,
|
|
305
|
+
(float(x_val), float(y_val)),
|
|
306
|
+
xytext=best_offset,
|
|
307
|
+
textcoords="offset points",
|
|
308
|
+
**defaults,
|
|
309
|
+
)
|
|
310
|
+
placed_bboxes.append(final_annotation.get_window_extent(renderer=renderer).expanded(1.04, 1.10))
|
|
311
|
+
annotations.append(final_annotation)
|
|
312
|
+
return annotations
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _bbox_overlap_area(left: Any, right: Any) -> float:
|
|
316
|
+
if not left.overlaps(right):
|
|
317
|
+
return 0.0
|
|
318
|
+
x_overlap = max(0.0, min(left.x1, right.x1) - max(left.x0, right.x0))
|
|
319
|
+
y_overlap = max(0.0, min(left.y1, right.y1) - max(left.y0, right.y0))
|
|
320
|
+
return float(x_overlap * y_overlap)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _bbox_outside_axes_penalty(bbox: Any, axes_bbox: Any) -> float:
|
|
324
|
+
penalty = 0.0
|
|
325
|
+
penalty += max(0.0, axes_bbox.x0 - bbox.x0)
|
|
326
|
+
penalty += max(0.0, bbox.x1 - axes_bbox.x1)
|
|
327
|
+
penalty += max(0.0, axes_bbox.y0 - bbox.y0)
|
|
328
|
+
penalty += max(0.0, bbox.y1 - axes_bbox.y1)
|
|
329
|
+
return float(penalty)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
__all__ = [
|
|
333
|
+
"alias_column",
|
|
334
|
+
"annotate_points_smart",
|
|
335
|
+
"best_subplot_grid",
|
|
336
|
+
"descriptive_linear_resampling_interval",
|
|
337
|
+
"descriptive_mean_resampling_interval",
|
|
338
|
+
"colors_for",
|
|
339
|
+
"exact_observation_counts",
|
|
340
|
+
"plot_figure",
|
|
341
|
+
"pretty_name",
|
|
342
|
+
"require_columns",
|
|
343
|
+
"select_exact_observations",
|
|
344
|
+
"shared_numeric_limits",
|
|
345
|
+
"warn_if_empty",
|
|
346
|
+
]
|