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,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from reader_workbench.errors import ExecutionError
|
|
8
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
9
|
+
from reader_workbench.plotting.utils import slugify
|
|
10
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
11
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
12
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FourStateVectorDiagnosticCfg(PluginConfig):
|
|
16
|
+
state_map_ref: str = Field(min_length=1)
|
|
17
|
+
time_column: str = Field(default="time", min_length=1)
|
|
18
|
+
growth_channel: str = Field(default="OD600", min_length=1)
|
|
19
|
+
response_channel: str = Field(default="YFP/CFP", min_length=1)
|
|
20
|
+
design_ids: list[str] | None = None
|
|
21
|
+
trajectory_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
|
|
22
|
+
trajectory_resamples: int = Field(default=300, ge=1)
|
|
23
|
+
time_atol: float = Field(default=1e-9, ge=0.0)
|
|
24
|
+
title: str | None = Field(default=None, min_length=1)
|
|
25
|
+
filename: str = Field(default="four_state_vector_diagnostic", min_length=1)
|
|
26
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
27
|
+
dpi: int = Field(default=300, ge=1)
|
|
28
|
+
figsize: tuple[float, float] = (15.0, 5.5)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FourStateVectorDiagnosticPlot(FigurePlotPlugin):
|
|
32
|
+
"""Render a record-driven diagnostic without recomputing vector."""
|
|
33
|
+
|
|
34
|
+
ConfigModel = FourStateVectorDiagnosticCfg
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def input_ports(cls):
|
|
38
|
+
return {
|
|
39
|
+
"df": dataframe_input("df", "plate_reader.annotated.v1"),
|
|
40
|
+
"vector": dataframe_input("vector", "logic.four_state_vector.v1"),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def render(self, ctx, inputs, cfg: FourStateVectorDiagnosticCfg) -> list[PlotFigure]:
|
|
44
|
+
if ctx is None or getattr(ctx, "experiment", None) is None:
|
|
45
|
+
raise ExecutionError("plot/four_state_vector_diagnostic requires experiment annotation semantics")
|
|
46
|
+
state_space = ctx.experiment.annotations.resolve_ordered_state_space(ref=cfg.state_map_ref)
|
|
47
|
+
if tuple(state_space.state_ids) != ("00", "10", "01", "11"):
|
|
48
|
+
raise ExecutionError("plot/four_state_vector_diagnostic requires ordered states 00, 10, 01, 11")
|
|
49
|
+
|
|
50
|
+
from reader_workbench.domains.logic.four_state_vector.diagnostic import ( # noqa: PLC0415
|
|
51
|
+
prepare_four_state_vector_diagnostics,
|
|
52
|
+
render_four_state_vector_diagnostic,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
prepared = prepare_four_state_vector_diagnostics(
|
|
56
|
+
inputs["df"],
|
|
57
|
+
inputs["vector"],
|
|
58
|
+
treatment_column=state_space.column,
|
|
59
|
+
treatment_map=state_space.source_values,
|
|
60
|
+
treatment_case_sensitive=state_space.case_sensitive,
|
|
61
|
+
time_column=cfg.time_column,
|
|
62
|
+
growth_channel=cfg.growth_channel,
|
|
63
|
+
response_channel=cfg.response_channel,
|
|
64
|
+
design_ids=cfg.design_ids,
|
|
65
|
+
time_atol=cfg.time_atol,
|
|
66
|
+
trajectory_interval_mass=cfg.trajectory_interval_mass,
|
|
67
|
+
trajectory_resamples=cfg.trajectory_resamples,
|
|
68
|
+
)
|
|
69
|
+
filenames = [_design_filename(cfg.filename, item.design_id) for item in prepared]
|
|
70
|
+
if len(set(filenames)) != len(filenames):
|
|
71
|
+
raise ExecutionError("plot/four_state_vector_diagnostic design ids resolve to duplicate artifact filenames")
|
|
72
|
+
|
|
73
|
+
figures: list[PlotFigure] = []
|
|
74
|
+
for item, filename in zip(prepared, filenames, strict=True):
|
|
75
|
+
figure = render_four_state_vector_diagnostic(
|
|
76
|
+
item,
|
|
77
|
+
title=cfg.title,
|
|
78
|
+
figsize=cfg.figsize,
|
|
79
|
+
dpi=cfg.dpi,
|
|
80
|
+
)
|
|
81
|
+
description = (
|
|
82
|
+
f"Growth and response trajectories with persisted vector components for design {item.design_id} "
|
|
83
|
+
f"at {item.selected_time_h:g} hours."
|
|
84
|
+
)
|
|
85
|
+
figures.extend(
|
|
86
|
+
PlotFigure(
|
|
87
|
+
fig=figure,
|
|
88
|
+
filename=filename,
|
|
89
|
+
ext=extension,
|
|
90
|
+
dpi=cfg.dpi,
|
|
91
|
+
description=description,
|
|
92
|
+
)
|
|
93
|
+
for extension in cfg.format
|
|
94
|
+
)
|
|
95
|
+
return figures
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _design_filename(base: str, design_id: str) -> str:
|
|
99
|
+
base_slug = slugify(base)
|
|
100
|
+
if not base_slug:
|
|
101
|
+
raise ExecutionError(f"plot/four_state_vector_diagnostic cannot derive a filename from base {base!r}")
|
|
102
|
+
design_slug = slugify(design_id)
|
|
103
|
+
if not design_slug:
|
|
104
|
+
raise ExecutionError(f"plot/four_state_vector_diagnostic cannot derive a filename from design id {design_id!r}")
|
|
105
|
+
return f"{base_slug}--{design_slug}"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
four-state vector heatmap plot plugin.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from reader_workbench.errors import FourStateVectorError
|
|
13
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
14
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
15
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
16
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FourStateVectorHeatmapCfg(PluginConfig):
|
|
20
|
+
experiment_id: str | None = None
|
|
21
|
+
title: str | None = None
|
|
22
|
+
max_y_tick_labels: int = 80
|
|
23
|
+
filename: str | None = None
|
|
24
|
+
format: list[str] = Field(default_factory=lambda: ["pdf"])
|
|
25
|
+
dpi: int = 300
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FourStateVectorHeatmapPlot(FigurePlotPlugin):
|
|
29
|
+
ConfigModel = FourStateVectorHeatmapCfg
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def input_ports(cls):
|
|
33
|
+
return {"vector": dataframe_input("vector", "logic.four_state_vector.v1")}
|
|
34
|
+
|
|
35
|
+
def render(self, ctx, inputs, cfg: FourStateVectorHeatmapCfg) -> list[PlotFigure]:
|
|
36
|
+
vector: pd.DataFrame = inputs["vector"]
|
|
37
|
+
from reader_workbench.domains.logic.four_state_vector.heatmap import ( # noqa: PLC0415
|
|
38
|
+
render_experiment_four_state_vector_heatmap,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
experiment_id = cfg.experiment_id or _experiment_id_from_context(ctx)
|
|
42
|
+
title = cfg.title or f"{experiment_id} four-state vector heatmap"
|
|
43
|
+
fig = render_experiment_four_state_vector_heatmap(
|
|
44
|
+
vector,
|
|
45
|
+
experiment_id=experiment_id,
|
|
46
|
+
title=title,
|
|
47
|
+
max_y_tick_labels=cfg.max_y_tick_labels,
|
|
48
|
+
)
|
|
49
|
+
filename = cfg.filename or "four_state_vector_heatmap"
|
|
50
|
+
return [PlotFigure(fig=fig, filename=filename, ext=ext, dpi=cfg.dpi) for ext in cfg.format]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _experiment_id_from_context(ctx) -> str:
|
|
54
|
+
exp_dir = getattr(ctx, "exp_dir", None)
|
|
55
|
+
if isinstance(exp_dir, Path):
|
|
56
|
+
return exp_dir.name
|
|
57
|
+
if exp_dir is not None:
|
|
58
|
+
name = Path(str(exp_dir)).name
|
|
59
|
+
if name:
|
|
60
|
+
return name
|
|
61
|
+
raise FourStateVectorError(
|
|
62
|
+
"plot/four_state_vector_heatmap requires experiment_id when no experiment directory is available."
|
|
63
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
8
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
9
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
10
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogicSymmetryPlotCfg(PluginConfig):
|
|
14
|
+
title: str = "Logic symmetry"
|
|
15
|
+
dispersion: Literal["none", "bars", "halo"] = "halo"
|
|
16
|
+
encodings: dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
ideals_overlay: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
visuals: dict[str, Any] = Field(default_factory=dict)
|
|
19
|
+
filename: str = "logic_symmetry"
|
|
20
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["pdf"], min_length=1)
|
|
21
|
+
dpi: int = Field(300, gt=0)
|
|
22
|
+
figsize: tuple[float, float] = (7, 6)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LogicSymmetryPlot(FigurePlotPlugin):
|
|
26
|
+
"""Render a persisted logic-symmetry table without owning its computation."""
|
|
27
|
+
|
|
28
|
+
ConfigModel = LogicSymmetryPlotCfg
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def input_ports(cls):
|
|
32
|
+
return {"table": dataframe_input("table", "logic_symmetry.v1")}
|
|
33
|
+
|
|
34
|
+
def render(self, ctx, inputs, cfg: LogicSymmetryPlotCfg) -> list[PlotFigure]:
|
|
35
|
+
from reader_workbench.domains.logic.logic_symmetry import render_logic_symmetry # noqa: PLC0415
|
|
36
|
+
|
|
37
|
+
figure = render_logic_symmetry(
|
|
38
|
+
inputs["table"],
|
|
39
|
+
title=cfg.title,
|
|
40
|
+
dispersion=cfg.dispersion,
|
|
41
|
+
encodings=cfg.encodings,
|
|
42
|
+
ideals_overlay=cfg.ideals_overlay,
|
|
43
|
+
visuals=cfg.visuals,
|
|
44
|
+
figsize=cfg.figsize,
|
|
45
|
+
dpi=cfg.dpi,
|
|
46
|
+
)
|
|
47
|
+
return [
|
|
48
|
+
PlotFigure(
|
|
49
|
+
fig=figure,
|
|
50
|
+
filename=cfg.filename,
|
|
51
|
+
ext=extension,
|
|
52
|
+
dpi=cfg.dpi,
|
|
53
|
+
description="Logic and asymmetry geometry over the configured four-state summary.",
|
|
54
|
+
)
|
|
55
|
+
for extension in cfg.format
|
|
56
|
+
]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Four-panel diagnostic over a persisted single-reporter ratio record."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from pydantic import BaseModel, Field, model_validator
|
|
10
|
+
|
|
11
|
+
from reader_workbench.domains.time_series import ObservationAggregationSpec, TemporalReductionSpec
|
|
12
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
13
|
+
from reader_workbench.plotting.utils import slugify
|
|
14
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
|
|
15
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
16
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
17
|
+
|
|
18
|
+
_UNIT_DESCRIPTIONS = {
|
|
19
|
+
"declared_replicate": "temporally reduced declared-replicate values by condition",
|
|
20
|
+
"observation_only": "temporally reduced observation-only values by condition",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SingleReporterDiagnosticFigureCfg(BaseModel):
|
|
25
|
+
figsize: tuple[float, float] = (14.0, 3.7)
|
|
26
|
+
axis_label_size: float = Field(default=9.5, gt=0.0)
|
|
27
|
+
tick_label_size: float = Field(default=8.5, gt=0.0)
|
|
28
|
+
legend_fontsize: float = Field(default=8.5, gt=0.0)
|
|
29
|
+
line_width: float = Field(default=1.7, gt=0.0)
|
|
30
|
+
point_size: float = Field(default=28.0, gt=0.0)
|
|
31
|
+
condition_tick_rotation: float = Field(default=20.0, ge=-90.0, le=90.0)
|
|
32
|
+
|
|
33
|
+
model_config = {"extra": "forbid"}
|
|
34
|
+
|
|
35
|
+
@model_validator(mode="after")
|
|
36
|
+
def validate_figure(self) -> SingleReporterDiagnosticFigureCfg:
|
|
37
|
+
if len(self.figsize) != 2 or any(
|
|
38
|
+
not math.isfinite(float(value)) or float(value) <= 0.0 for value in self.figsize
|
|
39
|
+
):
|
|
40
|
+
raise ValueError("single_reporter_diagnostic: fig.figsize must contain two positive finite values")
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SingleReporterObservationUnitCfg(BaseModel):
|
|
45
|
+
"""Identify descriptive source observations without declaring replication."""
|
|
46
|
+
|
|
47
|
+
role: Literal["observation_only"]
|
|
48
|
+
column: str = Field(min_length=1)
|
|
49
|
+
|
|
50
|
+
model_config = {"extra": "forbid"}
|
|
51
|
+
|
|
52
|
+
@model_validator(mode="after")
|
|
53
|
+
def validate_observation_unit(self) -> SingleReporterObservationUnitCfg:
|
|
54
|
+
column = self.column.strip()
|
|
55
|
+
if not column:
|
|
56
|
+
raise ValueError("single_reporter_diagnostic: observation_unit.column must be a non-empty string")
|
|
57
|
+
self.column = column
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SingleReporterIdentityScopeCfg(BaseModel):
|
|
62
|
+
"""Bind unit identities to stable assay entities, independent of presentation."""
|
|
63
|
+
|
|
64
|
+
entity_columns: list[str] = Field(min_length=1)
|
|
65
|
+
|
|
66
|
+
model_config = {"extra": "forbid"}
|
|
67
|
+
|
|
68
|
+
@model_validator(mode="after")
|
|
69
|
+
def validate_identity_scope(self) -> SingleReporterIdentityScopeCfg:
|
|
70
|
+
columns = [str(column).strip() for column in self.entity_columns]
|
|
71
|
+
if not columns or any(not column for column in columns):
|
|
72
|
+
raise ValueError("single_reporter_diagnostic: identity_scope.entity_columns must be non-empty strings")
|
|
73
|
+
if len(set(columns)) != len(columns):
|
|
74
|
+
raise ValueError("single_reporter_diagnostic: identity_scope.entity_columns must not contain duplicates")
|
|
75
|
+
self.entity_columns = columns
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SingleReporterDiagnosticCfg(PluginConfig):
|
|
80
|
+
partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
|
|
81
|
+
identity_scope: SingleReporterIdentityScopeCfg
|
|
82
|
+
condition_column: str = Field(default="treatment", min_length=1)
|
|
83
|
+
condition_order: list[str] | None = None
|
|
84
|
+
condition_order_ref: str | None = Field(default=None, min_length=1)
|
|
85
|
+
temporal_reduction: dict[str, Any]
|
|
86
|
+
observation_aggregation: dict[str, Any]
|
|
87
|
+
observation_unit: SingleReporterObservationUnitCfg | None = None
|
|
88
|
+
time_column: str = Field(default="time", min_length=1)
|
|
89
|
+
normalizer_channel: str = Field(min_length=1)
|
|
90
|
+
reporter_channel: str = Field(min_length=1)
|
|
91
|
+
ratio_channel: str = Field(min_length=1)
|
|
92
|
+
filename_prefix: str = Field(default="single_reporter_diagnostic", min_length=1)
|
|
93
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
94
|
+
dpi: int = Field(default=300, ge=1)
|
|
95
|
+
fig: SingleReporterDiagnosticFigureCfg = Field(default_factory=SingleReporterDiagnosticFigureCfg)
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def validate_semantic_options(self) -> SingleReporterDiagnosticCfg:
|
|
99
|
+
for field_name in (
|
|
100
|
+
"condition_column",
|
|
101
|
+
"time_column",
|
|
102
|
+
"normalizer_channel",
|
|
103
|
+
"reporter_channel",
|
|
104
|
+
"ratio_channel",
|
|
105
|
+
"filename_prefix",
|
|
106
|
+
):
|
|
107
|
+
value = str(getattr(self, field_name)).strip()
|
|
108
|
+
if not value:
|
|
109
|
+
raise ValueError(f"single_reporter_diagnostic: {field_name} must be a non-empty string")
|
|
110
|
+
setattr(self, field_name, value)
|
|
111
|
+
if len({self.normalizer_channel, self.reporter_channel, self.ratio_channel}) != 3:
|
|
112
|
+
raise ValueError("single_reporter_diagnostic: normalizer, reporter, and ratio channels must be distinct")
|
|
113
|
+
if self.condition_column in self.identity_scope.entity_columns:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"single_reporter_diagnostic: condition_column is already part of unit identity and must not be "
|
|
116
|
+
"repeated in identity_scope.entity_columns"
|
|
117
|
+
)
|
|
118
|
+
temporal = TemporalReductionSpec.from_mapping(self.temporal_reduction)
|
|
119
|
+
aggregation = ObservationAggregationSpec.from_mapping(self.observation_aggregation)
|
|
120
|
+
self.temporal_reduction = temporal.to_mapping()
|
|
121
|
+
self.observation_aggregation = aggregation.to_mapping()
|
|
122
|
+
if self.condition_order is not None and self.condition_order_ref is not None:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"single_reporter_diagnostic: condition_order and condition_order_ref are mutually exclusive"
|
|
125
|
+
)
|
|
126
|
+
if self.condition_order is not None:
|
|
127
|
+
values = [str(value).strip() for value in self.condition_order]
|
|
128
|
+
if not values or any(not value for value in values):
|
|
129
|
+
raise ValueError("single_reporter_diagnostic: condition_order must contain non-empty labels")
|
|
130
|
+
if len(set(values)) != len(values):
|
|
131
|
+
raise ValueError("single_reporter_diagnostic: condition_order contains duplicate labels")
|
|
132
|
+
self.condition_order = values
|
|
133
|
+
if len(set(self.format)) != len(self.format):
|
|
134
|
+
raise ValueError("single_reporter_diagnostic: format must not contain duplicates")
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SingleReporterDiagnosticPlot(FigurePlotPlugin):
|
|
139
|
+
ConfigModel = SingleReporterDiagnosticCfg
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def input_ports(cls):
|
|
143
|
+
return {"df": dataframe_input("df", "plate_reader.annotated.v1")}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _resolve_condition_order(cls, *, experiment, cfg: SingleReporterDiagnosticCfg):
|
|
147
|
+
return experiment.annotations.resolve_order_arg(
|
|
148
|
+
order=cfg.condition_order,
|
|
149
|
+
order_ref=cfg.condition_order_ref,
|
|
150
|
+
column=cfg.condition_column,
|
|
151
|
+
arg_name="condition_order",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def validate_semantic_references(cls, *, experiment, cfg: SingleReporterDiagnosticCfg) -> None:
|
|
156
|
+
experiment.annotations.resolve_plot_partition(partition=cfg.partition)
|
|
157
|
+
cls._resolve_condition_order(experiment=experiment, cfg=cfg)
|
|
158
|
+
|
|
159
|
+
def render(self, ctx, inputs, cfg: SingleReporterDiagnosticCfg) -> list[PlotFigure]:
|
|
160
|
+
from reader_workbench.domains.plate_reader.plots.single_reporter_diagnostic import ( # noqa: PLC0415
|
|
161
|
+
prepare_single_reporter_diagnostics,
|
|
162
|
+
)
|
|
163
|
+
from reader_workbench.domains.plate_reader.plots.single_reporter_diagnostic_render import ( # noqa: PLC0415
|
|
164
|
+
render_single_reporter_diagnostic,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if ctx.experiment is None:
|
|
168
|
+
raise ValueError("single_reporter_diagnostic requires experiment semantics in the run context")
|
|
169
|
+
frame: pd.DataFrame = inputs["df"]
|
|
170
|
+
partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
|
|
171
|
+
condition_order = type(self)._resolve_condition_order(experiment=ctx.experiment, cfg=cfg)
|
|
172
|
+
unit_column, observation_column, unit_role = _reduction_columns(
|
|
173
|
+
ctx=ctx,
|
|
174
|
+
frame=frame,
|
|
175
|
+
observation_unit=cfg.observation_unit,
|
|
176
|
+
)
|
|
177
|
+
diagnostics = prepare_single_reporter_diagnostics(
|
|
178
|
+
frame,
|
|
179
|
+
group_on=partition.group_by,
|
|
180
|
+
collection_items=partition.collection_items,
|
|
181
|
+
group_match=partition.match,
|
|
182
|
+
condition_column=cfg.condition_column,
|
|
183
|
+
condition_order=condition_order,
|
|
184
|
+
entity_columns=cfg.identity_scope.entity_columns,
|
|
185
|
+
unit_column=unit_column,
|
|
186
|
+
observation_column=observation_column,
|
|
187
|
+
unit_role=unit_role,
|
|
188
|
+
time_column=cfg.time_column,
|
|
189
|
+
normalizer_channel=cfg.normalizer_channel,
|
|
190
|
+
reporter_channel=cfg.reporter_channel,
|
|
191
|
+
ratio_channel=cfg.ratio_channel,
|
|
192
|
+
temporal_reduction=TemporalReductionSpec.from_mapping(cfg.temporal_reduction),
|
|
193
|
+
observation_aggregation=ObservationAggregationSpec.from_mapping(cfg.observation_aggregation),
|
|
194
|
+
)
|
|
195
|
+
artifact_names = _artifact_names(diagnostics, filename_prefix=cfg.filename_prefix)
|
|
196
|
+
figures: list[PlotFigure] = []
|
|
197
|
+
palette_book = getattr(ctx, "palette_book", None)
|
|
198
|
+
for diagnostic in diagnostics:
|
|
199
|
+
colors = palette_book.colors(len(diagnostic.condition_order)) if palette_book is not None else None
|
|
200
|
+
figure = render_single_reporter_diagnostic(
|
|
201
|
+
diagnostic,
|
|
202
|
+
colors=colors,
|
|
203
|
+
**cfg.fig.model_dump(),
|
|
204
|
+
)
|
|
205
|
+
figures.extend(
|
|
206
|
+
PlotFigure(
|
|
207
|
+
fig=figure,
|
|
208
|
+
filename=artifact_names[diagnostic.group_label],
|
|
209
|
+
ext=extension,
|
|
210
|
+
dpi=cfg.dpi,
|
|
211
|
+
description=(
|
|
212
|
+
"Normalizer, reporter, and reporter-normalizer kinetics with an explicit endpoint or "
|
|
213
|
+
f"interval reduction, {_UNIT_DESCRIPTIONS[diagnostic.unit_role]}, and visible normalizer QC."
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
for extension in cfg.format
|
|
217
|
+
)
|
|
218
|
+
return figures
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _reduction_columns(
|
|
222
|
+
*,
|
|
223
|
+
ctx,
|
|
224
|
+
frame: pd.DataFrame,
|
|
225
|
+
observation_unit: SingleReporterObservationUnitCfg | None,
|
|
226
|
+
) -> tuple[str, str, Literal["declared_replicate", "observation_only"]]:
|
|
227
|
+
evidence = getattr(ctx.experiment, "evidence", None)
|
|
228
|
+
declared = getattr(evidence, "replicate_identity_field", None)
|
|
229
|
+
if declared is not None:
|
|
230
|
+
unit_column = str(declared).strip()
|
|
231
|
+
if unit_column not in frame.columns:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
"single_reporter_diagnostic: declared evidence.replicate_identity_field "
|
|
234
|
+
f"{unit_column!r} is absent from the persisted ratio record"
|
|
235
|
+
)
|
|
236
|
+
observation_column = observation_unit.column if observation_unit is not None else unit_column
|
|
237
|
+
_require_observation_column(frame=frame, column=observation_column)
|
|
238
|
+
return unit_column, observation_column, "declared_replicate"
|
|
239
|
+
if observation_unit is None:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"single_reporter_diagnostic: no replicate identity is declared; configure "
|
|
242
|
+
"observation_unit with role='observation_only' to plot descriptive record observations"
|
|
243
|
+
)
|
|
244
|
+
_require_observation_column(frame=frame, column=observation_unit.column)
|
|
245
|
+
return observation_unit.column, observation_unit.column, "observation_only"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _require_observation_column(*, frame: pd.DataFrame, column: str) -> None:
|
|
249
|
+
if column not in frame.columns:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"single_reporter_diagnostic: observation_unit column {column!r} is absent from the persisted ratio record"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _artifact_names(diagnostics, *, filename_prefix: str) -> dict[str, str]:
|
|
256
|
+
names: dict[str, str] = {}
|
|
257
|
+
owner_by_slug: dict[str, str] = {}
|
|
258
|
+
for diagnostic in diagnostics:
|
|
259
|
+
label = str(diagnostic.group_label)
|
|
260
|
+
filename = filename_prefix if label == "all" else f"{filename_prefix}__{label}"
|
|
261
|
+
normalized = slugify(filename)
|
|
262
|
+
prior = owner_by_slug.get(normalized)
|
|
263
|
+
if prior is not None:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
"single_reporter_diagnostic: "
|
|
266
|
+
f"partitions {prior!r} and {label!r} resolve to the same artifact name {normalized!r}"
|
|
267
|
+
)
|
|
268
|
+
names[label] = filename
|
|
269
|
+
owner_by_slug[normalized] = label
|
|
270
|
+
return names
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = [
|
|
274
|
+
"SingleReporterDiagnosticCfg",
|
|
275
|
+
"SingleReporterDiagnosticFigureCfg",
|
|
276
|
+
"SingleReporterIdentityScopeCfg",
|
|
277
|
+
"SingleReporterObservationUnitCfg",
|
|
278
|
+
"SingleReporterDiagnosticPlot",
|
|
279
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
9
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
|
|
10
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
11
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SnapshotBarCfg(PluginConfig):
|
|
15
|
+
x: str
|
|
16
|
+
y: list[str] | str
|
|
17
|
+
hue: str | None = None
|
|
18
|
+
partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
|
|
19
|
+
time: float = Field(..., description="Snapshot time (hours); required for deterministic plotting.")
|
|
20
|
+
fig: dict[str, Any] = Field(default_factory=dict)
|
|
21
|
+
filename: str | None = None
|
|
22
|
+
agg: str = "mean" # median|mean
|
|
23
|
+
dispersion: Literal["sd", "iqr", "none"] = "sd"
|
|
24
|
+
time_tolerance: float = 0.51
|
|
25
|
+
panel_by: Literal["channel", "x", "group"] = "channel"
|
|
26
|
+
channel_select: str | None = None
|
|
27
|
+
file_by: Literal["auto", "channel"] = "auto"
|
|
28
|
+
show_legend: bool = False
|
|
29
|
+
legend_loc: str = "upper right"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SnapshotBarplot(FigurePlotPlugin):
|
|
33
|
+
ConfigModel = SnapshotBarCfg
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def input_ports(cls):
|
|
37
|
+
return {"df": dataframe_input("df", "tidy.v1")}
|
|
38
|
+
|
|
39
|
+
def render(self, ctx, inputs, cfg: SnapshotBarCfg) -> list[PlotFigure]:
|
|
40
|
+
df: pd.DataFrame = inputs["df"]
|
|
41
|
+
from reader_workbench.domains.plate_reader.plots.snapshot_barplot import plot_snapshot_barplot # noqa: PLC0415
|
|
42
|
+
|
|
43
|
+
partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
|
|
44
|
+
|
|
45
|
+
return plot_snapshot_barplot(
|
|
46
|
+
df=df,
|
|
47
|
+
x=cfg.x,
|
|
48
|
+
y=cfg.y,
|
|
49
|
+
hue=cfg.hue,
|
|
50
|
+
group_on=partition.group_by,
|
|
51
|
+
pool_sets=partition.collection_items,
|
|
52
|
+
time=cfg.time,
|
|
53
|
+
pool_match=partition.match, # type: ignore
|
|
54
|
+
fig_kwargs=cfg.fig,
|
|
55
|
+
filename=cfg.filename,
|
|
56
|
+
palette_book=ctx.palette_book,
|
|
57
|
+
agg=cfg.agg,
|
|
58
|
+
dispersion=cfg.dispersion,
|
|
59
|
+
time_tolerance=cfg.time_tolerance,
|
|
60
|
+
panel_by=cfg.panel_by,
|
|
61
|
+
channel_select=cfg.channel_select,
|
|
62
|
+
file_by=cfg.file_by,
|
|
63
|
+
show_legend=cfg.show_legend,
|
|
64
|
+
legend_loc=cfg.legend_loc,
|
|
65
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
|
|
9
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
10
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
11
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
12
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_snapshot_heatmap_impl():
|
|
16
|
+
module = import_module("reader_workbench.domains.plate_reader.plots.snapshot_heatmap")
|
|
17
|
+
return module.plot_snapshot_heatmap, module.prepare_snapshot_heatmap_inputs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HeatmapCfg(PluginConfig):
|
|
21
|
+
channel: str
|
|
22
|
+
time: float
|
|
23
|
+
x: str = "treatment"
|
|
24
|
+
y: str = "design_id"
|
|
25
|
+
order_x: list[str] | None = None
|
|
26
|
+
order_y: list[str] | None = None
|
|
27
|
+
order_x_ref: str | None = None
|
|
28
|
+
order_y_ref: str | None = None
|
|
29
|
+
square: bool = True
|
|
30
|
+
vmin: float | None = None
|
|
31
|
+
vmax: float | None = None
|
|
32
|
+
value_transform: str | None = "none" # "none" | "log2" | "log10"
|
|
33
|
+
time_tolerance: float = 0.51
|
|
34
|
+
fig: dict[str, Any] = Field(default_factory=dict)
|
|
35
|
+
filename: str | None = None
|
|
36
|
+
|
|
37
|
+
@model_validator(mode="after")
|
|
38
|
+
def validate_order_sources(self) -> HeatmapCfg:
|
|
39
|
+
if self.order_x is not None and self.order_x_ref is not None:
|
|
40
|
+
raise ValueError("snapshot_heatmap: order_x and order_x_ref are mutually exclusive")
|
|
41
|
+
if self.order_y is not None and self.order_y_ref is not None:
|
|
42
|
+
raise ValueError("snapshot_heatmap: order_y and order_y_ref are mutually exclusive")
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SnapshotHeatmapPlot(FigurePlotPlugin):
|
|
47
|
+
ConfigModel = HeatmapCfg
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def input_ports(cls):
|
|
51
|
+
return {
|
|
52
|
+
"df": dataframe_input("df", "tidy.v1", optional=True),
|
|
53
|
+
"fc": dataframe_input("fc", "fold_change.v1", optional=True),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def render(self, ctx, inputs, cfg: HeatmapCfg) -> list[PlotFigure]:
|
|
57
|
+
if ctx.experiment is None:
|
|
58
|
+
raise ValueError("snapshot_heatmap requires experiment semantics in the run context")
|
|
59
|
+
df_in: pd.DataFrame | None = inputs.get("df")
|
|
60
|
+
fc_in: pd.DataFrame | None = inputs.get("fc")
|
|
61
|
+
plot_snapshot_heatmap, prepare_snapshot_heatmap_inputs = _load_snapshot_heatmap_impl()
|
|
62
|
+
|
|
63
|
+
prepared = prepare_snapshot_heatmap_inputs(
|
|
64
|
+
df_in=df_in,
|
|
65
|
+
fc_in=fc_in,
|
|
66
|
+
channel=cfg.channel,
|
|
67
|
+
time=cfg.time,
|
|
68
|
+
time_tolerance=cfg.time_tolerance,
|
|
69
|
+
value_transform=cfg.value_transform,
|
|
70
|
+
fig=cfg.fig,
|
|
71
|
+
filename=cfg.filename,
|
|
72
|
+
logger=ctx.logger,
|
|
73
|
+
)
|
|
74
|
+
channel = str(cfg.channel)
|
|
75
|
+
df = prepared.frame
|
|
76
|
+
filename = prepared.filename
|
|
77
|
+
fig_kwargs = prepared.fig_kwargs
|
|
78
|
+
resolved_order_x = ctx.experiment.annotations.resolve_order_arg(
|
|
79
|
+
order=cfg.order_x,
|
|
80
|
+
order_ref=cfg.order_x_ref,
|
|
81
|
+
column=cfg.x,
|
|
82
|
+
arg_name="order_x",
|
|
83
|
+
)
|
|
84
|
+
resolved_order_y = ctx.experiment.annotations.resolve_order_arg(
|
|
85
|
+
order=cfg.order_y,
|
|
86
|
+
order_ref=cfg.order_y_ref,
|
|
87
|
+
column=cfg.y,
|
|
88
|
+
arg_name="order_y",
|
|
89
|
+
)
|
|
90
|
+
return plot_snapshot_heatmap(
|
|
91
|
+
df=df,
|
|
92
|
+
channel=channel,
|
|
93
|
+
time=cfg.time,
|
|
94
|
+
x=cfg.x,
|
|
95
|
+
y=cfg.y,
|
|
96
|
+
order_x=resolved_order_x,
|
|
97
|
+
order_y=resolved_order_y,
|
|
98
|
+
square=cfg.square,
|
|
99
|
+
vmin=cfg.vmin,
|
|
100
|
+
vmax=cfg.vmax,
|
|
101
|
+
fig_kwargs=fig_kwargs,
|
|
102
|
+
filename=filename,
|
|
103
|
+
logger=ctx.logger,
|
|
104
|
+
)
|