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,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ExecutionError
|
|
9
|
+
from reader_workbench.plotting.sinks import PlotFigure, normalize_plot_figures, save_plot_figures
|
|
10
|
+
from reader_workbench.workbench.ports import file_bundle_output
|
|
11
|
+
from reader_workbench.workbench.records import PathDescription
|
|
12
|
+
from reader_workbench.workbench.registry import Plugin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PlotPartitionCfg(BaseModel):
|
|
16
|
+
by: str | None = None
|
|
17
|
+
collection_ref: str | None = None
|
|
18
|
+
match: Literal["exact", "contains", "startswith", "endswith", "regex"] = "exact"
|
|
19
|
+
|
|
20
|
+
model_config = {"extra": "forbid"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FigurePlotPlugin(Plugin):
|
|
24
|
+
@classmethod
|
|
25
|
+
def output_ports(cls):
|
|
26
|
+
return {"artifacts": file_bundle_output("artifacts")}
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def render(self, ctx, inputs, cfg) -> list[PlotFigure]:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def run(self, ctx, inputs, cfg):
|
|
33
|
+
return save_rendered_figures(ctx=ctx, figures=self.render(ctx, inputs, cfg), plot_key=self.plugin_key)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_plot_partition_cfg(*, ctx, partition: PlotPartitionCfg):
|
|
37
|
+
if ctx.experiment is None:
|
|
38
|
+
raise ValueError("plot partition resolution requires experiment semantics in the run context")
|
|
39
|
+
return ctx.experiment.annotations.resolve_plot_partition(partition=partition)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def save_rendered_figures(*, ctx, figures: list[PlotFigure], plot_key: str) -> dict[str, list[str | PathDescription]]:
|
|
43
|
+
normalized = normalize_plot_figures(figures, where=f"plot/{plot_key}")
|
|
44
|
+
if not normalized:
|
|
45
|
+
raise ExecutionError(
|
|
46
|
+
f"plot/{plot_key}: renderer produced no figures; check data filters, time windows, channels, and partitions"
|
|
47
|
+
)
|
|
48
|
+
saved = save_plot_figures(normalized, ctx.plots_dir)
|
|
49
|
+
artifacts: list[str | PathDescription] = []
|
|
50
|
+
for figure, path in zip(normalized, saved, strict=True):
|
|
51
|
+
if figure.description is None:
|
|
52
|
+
artifacts.append(str(path))
|
|
53
|
+
else:
|
|
54
|
+
artifacts.append(PathDescription(path=path, description=figure.description))
|
|
55
|
+
return {"artifacts": artifacts}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Static cytometry gating diagnostic plot plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
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
|
+
class CytometryDiagnosticCfg(PluginConfig):
|
|
16
|
+
title: str | None = Field(default=None, min_length=1)
|
|
17
|
+
max_events: int = Field(default=50_000, gt=0)
|
|
18
|
+
filename: str = Field(default="cytometry_diagnostic", min_length=1)
|
|
19
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
20
|
+
dpi: int = Field(default=300, gt=0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CytometryDiagnosticPlot(FigurePlotPlugin):
|
|
24
|
+
ConfigModel = CytometryDiagnosticCfg
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def input_ports(cls):
|
|
28
|
+
return {
|
|
29
|
+
"original_events": dataframe_input("original_events", "tidy.v1"),
|
|
30
|
+
"gate_definition": dataframe_input("gate_definition", "cytometry.gate_definition.v1"),
|
|
31
|
+
"gated_events": dataframe_input("gated_events", "cytometry.gated_events.v1"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def render(self, ctx, inputs, cfg: CytometryDiagnosticCfg):
|
|
35
|
+
del ctx
|
|
36
|
+
from reader_workbench.domains.cytometry.plots import render_cytometry_diagnostic # noqa: PLC0415
|
|
37
|
+
|
|
38
|
+
figure = render_cytometry_diagnostic(
|
|
39
|
+
inputs["original_events"],
|
|
40
|
+
inputs["gate_definition"],
|
|
41
|
+
inputs["gated_events"],
|
|
42
|
+
max_events=cfg.max_events,
|
|
43
|
+
title=cfg.title,
|
|
44
|
+
)
|
|
45
|
+
return [
|
|
46
|
+
PlotFigure(
|
|
47
|
+
fig=figure,
|
|
48
|
+
filename=cfg.filename,
|
|
49
|
+
ext=extension,
|
|
50
|
+
dpi=cfg.dpi,
|
|
51
|
+
description="Configured cells, singlets, fluorescence, and final-retention diagnostics.",
|
|
52
|
+
)
|
|
53
|
+
for extension in cfg.format
|
|
54
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
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 DistributionsCfg(PluginConfig):
|
|
15
|
+
# what to draw
|
|
16
|
+
channels: list[str]
|
|
17
|
+
# modern grouping
|
|
18
|
+
partition: PlotPartitionCfg = Field(default_factory=lambda: PlotPartitionCfg(by="design_id"))
|
|
19
|
+
# layout
|
|
20
|
+
panel_by: Literal["channel", "group"] = "channel" # default: per-channel panels
|
|
21
|
+
hue: str | None = None
|
|
22
|
+
legend_loc: Literal["upper left", "upper right", "lower left", "lower right", "center", "best"] = "upper left"
|
|
23
|
+
# style/output
|
|
24
|
+
fig: dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
filename: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DistributionsPlot(FigurePlotPlugin):
|
|
29
|
+
ConfigModel = DistributionsCfg
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def input_ports(cls):
|
|
33
|
+
return {
|
|
34
|
+
"df": dataframe_input("df", "tidy.v1"),
|
|
35
|
+
"blanks": dataframe_input("blanks", "tidy.v1", optional=True),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def render(self, ctx, inputs, cfg: DistributionsCfg) -> list[PlotFigure]:
|
|
39
|
+
df: pd.DataFrame = inputs["df"]
|
|
40
|
+
blanks: pd.DataFrame = inputs.get("blanks", df.iloc[0:0])
|
|
41
|
+
from reader_workbench.domains.plate_reader.plots.distributions import plot_distributions # noqa: PLC0415
|
|
42
|
+
|
|
43
|
+
partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
|
|
44
|
+
|
|
45
|
+
return plot_distributions(
|
|
46
|
+
df=df,
|
|
47
|
+
blanks=blanks,
|
|
48
|
+
channels=cfg.channels,
|
|
49
|
+
group_on=partition.group_by,
|
|
50
|
+
pool_sets=partition.collection_items,
|
|
51
|
+
pool_match=partition.match, # type: ignore[arg-type]
|
|
52
|
+
panel_by=cfg.panel_by,
|
|
53
|
+
hue=cfg.hue,
|
|
54
|
+
legend_loc=cfg.legend_loc,
|
|
55
|
+
fig_kwargs=cfg.fig,
|
|
56
|
+
filename=cfg.filename,
|
|
57
|
+
palette_book=ctx.palette_book,
|
|
58
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Persisted dual-reporter kinetics and endpoint triptych."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pydantic import Field, model_validator
|
|
9
|
+
|
|
10
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
11
|
+
from reader_workbench.plotting.utils import slugify
|
|
12
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
13
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
14
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
15
|
+
|
|
16
|
+
_DESCRIPTION = (
|
|
17
|
+
"Growth and reporter-ratio kinetics with descriptive within-experiment resampling bands, "
|
|
18
|
+
"plus observed endpoint values and mean with sample standard deviation."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DualReporterTriptychCfg(PluginConfig):
|
|
23
|
+
design_column: str = Field(default="design_id", min_length=1)
|
|
24
|
+
treatment_column: str = Field(default="treatment", min_length=1)
|
|
25
|
+
time_column: str = Field(default="time", min_length=1)
|
|
26
|
+
growth_channel: str = Field(default="OD600", min_length=1)
|
|
27
|
+
ratio_channel: str = Field(default="YFP/CFP", min_length=1)
|
|
28
|
+
snapshot_channel: str | None = Field(default=None, min_length=1)
|
|
29
|
+
snapshot_time_h: float
|
|
30
|
+
snapshot_time_mode: Literal["nearest", "last_before", "first_after", "exact"] = "nearest"
|
|
31
|
+
snapshot_time_tolerance_h: float | None = Field(default=0.51, ge=0.0)
|
|
32
|
+
treatment_order: list[str] | None = None
|
|
33
|
+
treatment_order_ref: str | None = Field(default=None, min_length=1)
|
|
34
|
+
trajectory_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
|
|
35
|
+
trajectory_resamples: int = Field(default=300, ge=1)
|
|
36
|
+
filename_prefix: str = Field(default="dual_reporter_triptych", min_length=1)
|
|
37
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
38
|
+
dpi: int = Field(default=300, ge=1)
|
|
39
|
+
figsize: tuple[float, float] = (10.5, 3.5)
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="after")
|
|
42
|
+
def validate_semantic_options(self) -> DualReporterTriptychCfg:
|
|
43
|
+
filename_prefix = self.filename_prefix.strip()
|
|
44
|
+
if not filename_prefix:
|
|
45
|
+
raise ValueError("dual_reporter_triptych: filename_prefix must be a non-empty string")
|
|
46
|
+
self.filename_prefix = filename_prefix
|
|
47
|
+
if self.treatment_order is not None and self.treatment_order_ref is not None:
|
|
48
|
+
raise ValueError("dual_reporter_triptych: treatment_order and treatment_order_ref are mutually exclusive")
|
|
49
|
+
if self.treatment_order is not None:
|
|
50
|
+
normalized = [str(value).strip() for value in self.treatment_order]
|
|
51
|
+
if not normalized or any(not value for value in normalized):
|
|
52
|
+
raise ValueError("dual_reporter_triptych: treatment_order must contain non-empty strings")
|
|
53
|
+
if len(set(normalized)) != len(normalized):
|
|
54
|
+
raise ValueError("dual_reporter_triptych: treatment_order contains duplicate labels")
|
|
55
|
+
self.treatment_order = normalized
|
|
56
|
+
if any(not value > 0.0 for value in self.figsize):
|
|
57
|
+
raise ValueError("dual_reporter_triptych: figsize values must be positive")
|
|
58
|
+
if len(set(self.format)) != len(self.format):
|
|
59
|
+
raise ValueError("dual_reporter_triptych: format must not contain duplicates")
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DualReporterTriptychPlot(FigurePlotPlugin):
|
|
64
|
+
ConfigModel = DualReporterTriptychCfg
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def input_ports(cls):
|
|
68
|
+
return {"df": dataframe_input("df", "tidy.v1")}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def validate_semantic_references(cls, *, experiment, cfg: DualReporterTriptychCfg) -> None:
|
|
72
|
+
if cfg.treatment_order_ref is not None:
|
|
73
|
+
experiment.annotations.resolve_order_arg(
|
|
74
|
+
order=None,
|
|
75
|
+
order_ref=cfg.treatment_order_ref,
|
|
76
|
+
column=cfg.treatment_column,
|
|
77
|
+
arg_name="treatment_order",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def render(self, ctx, inputs, cfg: DualReporterTriptychCfg) -> list[PlotFigure]:
|
|
81
|
+
from reader_workbench.domains.plate_reader.analysis.timepoints import ( # noqa: PLC0415
|
|
82
|
+
infer_acquisition_transition_time_h,
|
|
83
|
+
)
|
|
84
|
+
from reader_workbench.domains.plate_reader.ordering import order_levels # noqa: PLC0415
|
|
85
|
+
from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych import ( # noqa: PLC0415
|
|
86
|
+
build_triptych_data,
|
|
87
|
+
choose_time,
|
|
88
|
+
)
|
|
89
|
+
from reader_workbench.domains.plate_reader.plots.dual_reporter_triptych_render import ( # noqa: PLC0415
|
|
90
|
+
render_dual_reporter_triptych,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
frame: pd.DataFrame = inputs["df"]
|
|
94
|
+
_require_columns(
|
|
95
|
+
frame,
|
|
96
|
+
(
|
|
97
|
+
cfg.design_column,
|
|
98
|
+
cfg.treatment_column,
|
|
99
|
+
cfg.time_column,
|
|
100
|
+
"channel",
|
|
101
|
+
"value",
|
|
102
|
+
"position",
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
design_values = order_levels(_nonempty_strings(frame[cfg.design_column]))
|
|
106
|
+
if not design_values:
|
|
107
|
+
raise ValueError(f"dual_reporter_triptych: no non-empty values in {cfg.design_column!r}")
|
|
108
|
+
design_series = frame[cfg.design_column].astype("string").str.strip()
|
|
109
|
+
filename_by_design = _artifact_names_by_design(
|
|
110
|
+
design_values,
|
|
111
|
+
filename_prefix=cfg.filename_prefix,
|
|
112
|
+
)
|
|
113
|
+
treatment_order = _resolve_treatment_order(ctx=ctx, cfg=cfg)
|
|
114
|
+
|
|
115
|
+
figures: list[PlotFigure] = []
|
|
116
|
+
for design_value in design_values:
|
|
117
|
+
selected = frame[design_series == design_value].copy()
|
|
118
|
+
time_values = pd.to_numeric(selected[cfg.time_column], errors="coerce").dropna().unique().tolist()
|
|
119
|
+
selected_time = choose_time(time_values, cfg.snapshot_time_h, cfg.snapshot_time_mode)
|
|
120
|
+
if selected_time is None:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"dual_reporter_triptych: "
|
|
123
|
+
f"design {design_value!r} has no time matching {cfg.snapshot_time_h:g} h "
|
|
124
|
+
f"with mode {cfg.snapshot_time_mode!r}"
|
|
125
|
+
)
|
|
126
|
+
delta = abs(float(selected_time) - float(cfg.snapshot_time_h))
|
|
127
|
+
if cfg.snapshot_time_tolerance_h is not None and delta > cfg.snapshot_time_tolerance_h:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"dual_reporter_triptych: "
|
|
130
|
+
f"design {design_value!r} selected {selected_time:g} h for target "
|
|
131
|
+
f"{cfg.snapshot_time_h:g} h, outside snapshot_time_tolerance_h="
|
|
132
|
+
f"{cfg.snapshot_time_tolerance_h:g}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
data = build_triptych_data(
|
|
136
|
+
selected,
|
|
137
|
+
time_col=cfg.time_column,
|
|
138
|
+
treatment_col=cfg.treatment_column,
|
|
139
|
+
growth_channel=cfg.growth_channel,
|
|
140
|
+
ratio_channel=cfg.ratio_channel,
|
|
141
|
+
snapshot_channel=cfg.snapshot_channel,
|
|
142
|
+
snapshot_time=float(selected_time),
|
|
143
|
+
treatment_order=treatment_order,
|
|
144
|
+
trajectory_interval_mass=cfg.trajectory_interval_mass,
|
|
145
|
+
trajectory_resamples=cfg.trajectory_resamples,
|
|
146
|
+
)
|
|
147
|
+
palette_book = getattr(ctx, "palette_book", None)
|
|
148
|
+
colors = palette_book.colors(len(data.treatment_order)) if palette_book is not None else None
|
|
149
|
+
figure = render_dual_reporter_triptych(
|
|
150
|
+
data,
|
|
151
|
+
time_col=cfg.time_column,
|
|
152
|
+
treatment_col=cfg.treatment_column,
|
|
153
|
+
acquisition_transition_time_h=infer_acquisition_transition_time_h(
|
|
154
|
+
selected,
|
|
155
|
+
time_col=cfg.time_column,
|
|
156
|
+
),
|
|
157
|
+
title=design_value,
|
|
158
|
+
colors=colors,
|
|
159
|
+
figsize=cfg.figsize,
|
|
160
|
+
)
|
|
161
|
+
filename = filename_by_design[design_value]
|
|
162
|
+
figures.extend(
|
|
163
|
+
PlotFigure(
|
|
164
|
+
fig=figure,
|
|
165
|
+
filename=filename,
|
|
166
|
+
ext=extension,
|
|
167
|
+
dpi=cfg.dpi,
|
|
168
|
+
description=_DESCRIPTION,
|
|
169
|
+
)
|
|
170
|
+
for extension in cfg.format
|
|
171
|
+
)
|
|
172
|
+
return figures
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _resolve_treatment_order(*, ctx, cfg: DualReporterTriptychCfg) -> list[str] | None:
|
|
176
|
+
if cfg.treatment_order_ref is None:
|
|
177
|
+
return cfg.treatment_order
|
|
178
|
+
experiment = getattr(ctx, "experiment", None)
|
|
179
|
+
if experiment is None:
|
|
180
|
+
raise ValueError("dual_reporter_triptych: treatment_order_ref requires experiment semantics")
|
|
181
|
+
return experiment.annotations.resolve_order_arg(
|
|
182
|
+
order=None,
|
|
183
|
+
order_ref=cfg.treatment_order_ref,
|
|
184
|
+
column=cfg.treatment_column,
|
|
185
|
+
arg_name="treatment_order",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _nonempty_strings(series: pd.Series) -> list[str]:
|
|
190
|
+
values: list[str] = []
|
|
191
|
+
seen: set[str] = set()
|
|
192
|
+
for value in series.dropna().tolist():
|
|
193
|
+
text = str(value).strip()
|
|
194
|
+
if not text or text.casefold() in {"nan", "none"} or text in seen:
|
|
195
|
+
continue
|
|
196
|
+
values.append(text)
|
|
197
|
+
seen.add(text)
|
|
198
|
+
return values
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _artifact_names_by_design(design_values: list[str], *, filename_prefix: str) -> dict[str, str]:
|
|
202
|
+
by_design: dict[str, str] = {}
|
|
203
|
+
owner_by_name: dict[str, str] = {}
|
|
204
|
+
for design_value in design_values:
|
|
205
|
+
name = f"{filename_prefix}__{design_value}"
|
|
206
|
+
normalized_name = slugify(name)
|
|
207
|
+
prior = owner_by_name.get(normalized_name)
|
|
208
|
+
if prior is not None:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"dual_reporter_triptych: "
|
|
211
|
+
f"designs {prior!r} and {design_value!r} resolve to the same artifact name {normalized_name!r}"
|
|
212
|
+
)
|
|
213
|
+
by_design[design_value] = name
|
|
214
|
+
owner_by_name[normalized_name] = design_value
|
|
215
|
+
return by_design
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _require_columns(frame: pd.DataFrame, columns: tuple[str, ...]) -> None:
|
|
219
|
+
missing = [column for column in dict.fromkeys(columns) if column not in frame.columns]
|
|
220
|
+
if missing:
|
|
221
|
+
raise ValueError("dual_reporter_triptych: missing column(s): " + ", ".join(missing))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = ["DualReporterTriptychCfg", "DualReporterTriptychPlot"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
7
|
+
|
|
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 FourStateEventWindowDiagnosticSubject(BaseModel):
|
|
16
|
+
model_config = ConfigDict(extra="forbid")
|
|
17
|
+
|
|
18
|
+
source_experiment_id: str = Field(min_length=1)
|
|
19
|
+
design_id: str = Field(min_length=1)
|
|
20
|
+
filename: str = Field(min_length=1)
|
|
21
|
+
title: str | None = Field(default=None, min_length=1)
|
|
22
|
+
|
|
23
|
+
@model_validator(mode="after")
|
|
24
|
+
def _reject_blank_values(self) -> FourStateEventWindowDiagnosticSubject:
|
|
25
|
+
required = {
|
|
26
|
+
"source_experiment_id": self.source_experiment_id,
|
|
27
|
+
"design_id": self.design_id,
|
|
28
|
+
"filename": self.filename,
|
|
29
|
+
}
|
|
30
|
+
blank = [name for name, value in required.items() if not value.strip()]
|
|
31
|
+
if self.title is not None and not self.title.strip():
|
|
32
|
+
blank.append("title")
|
|
33
|
+
if blank:
|
|
34
|
+
raise ValueError(f"subject values must not be blank: {', '.join(blank)}")
|
|
35
|
+
normalized_filename = slugify(self.filename)
|
|
36
|
+
if not normalized_filename or not any(character.isalnum() for character in normalized_filename):
|
|
37
|
+
raise ValueError("subject filename must contain a filesystem-safe character")
|
|
38
|
+
if _is_windows_reserved_filename(normalized_filename):
|
|
39
|
+
raise ValueError("subject filename must not use a Windows reserved device name")
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FourStateEventWindowAxisLabels(BaseModel):
|
|
44
|
+
"""Protocol-derived labels for the three source trace panels."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
growth: str = Field(min_length=1)
|
|
49
|
+
response: str = Field(min_length=1)
|
|
50
|
+
magnitude: str = Field(min_length=1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FourStateEventWindowDiagnosticCfg(PluginConfig):
|
|
54
|
+
subjects: list[FourStateEventWindowDiagnosticSubject] = Field(min_length=1)
|
|
55
|
+
primary_reduction_id: str = Field(min_length=1)
|
|
56
|
+
pre_window_duration_h: float | None = Field(default=None, gt=0.0)
|
|
57
|
+
axis_labels: FourStateEventWindowAxisLabels = Field(
|
|
58
|
+
default_factory=lambda: FourStateEventWindowAxisLabels(
|
|
59
|
+
growth="Signal",
|
|
60
|
+
response="log$_2$(response signal)",
|
|
61
|
+
magnitude="log$_2$(magnitude signal)",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
state_labels: dict[str, str] = Field(default_factory=lambda: {"00": "00", "10": "10", "01": "01", "11": "11"})
|
|
65
|
+
reference_label: str = Field(default="reference", min_length=1)
|
|
66
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
67
|
+
dpi: int = Field(default=300, ge=1)
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="after")
|
|
70
|
+
def _validate_subject_selection(self) -> FourStateEventWindowDiagnosticCfg:
|
|
71
|
+
identities = [(subject.source_experiment_id.strip(), subject.design_id.strip()) for subject in self.subjects]
|
|
72
|
+
if len(identities) != len(set(identities)):
|
|
73
|
+
raise ValueError("subjects must contain unique source/design identities")
|
|
74
|
+
filenames = [slugify(subject.filename).casefold() for subject in self.subjects]
|
|
75
|
+
if len(filenames) != len(set(filenames)):
|
|
76
|
+
raise ValueError("subjects must contain unique filenames")
|
|
77
|
+
if set(self.state_labels) != {"00", "10", "01", "11"}:
|
|
78
|
+
raise ValueError("state_labels must define exactly 00, 10, 01, and 11")
|
|
79
|
+
if any(not isinstance(value, str) or not value.strip() for value in self.state_labels.values()):
|
|
80
|
+
raise ValueError("state_labels values must be non-empty strings")
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FourStateEventWindowDiagnosticPlot(FigurePlotPlugin):
|
|
85
|
+
ConfigModel = FourStateEventWindowDiagnosticCfg
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def input_ports(cls):
|
|
89
|
+
return {
|
|
90
|
+
"traces": dataframe_input("traces", "plate_reader.four_state_event_window.traces.v3"),
|
|
91
|
+
"designs": dataframe_input("designs", "plate_reader.four_state_event_window.designs.v4"),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def render(self, ctx, inputs, cfg: FourStateEventWindowDiagnosticCfg):
|
|
95
|
+
from reader_workbench.domains.plate_reader.plots.four_state_event_window.diagnostic_render import ( # noqa: PLC0415
|
|
96
|
+
render_four_state_event_window_diagnostic,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
rendered: list[PlotFigure] = []
|
|
100
|
+
for subject in cfg.subjects:
|
|
101
|
+
figure = render_four_state_event_window_diagnostic(
|
|
102
|
+
inputs["traces"],
|
|
103
|
+
inputs["designs"],
|
|
104
|
+
source_experiment_id=subject.source_experiment_id,
|
|
105
|
+
design_id=subject.design_id,
|
|
106
|
+
reduction_id=cfg.primary_reduction_id,
|
|
107
|
+
pre_window_duration_h=cfg.pre_window_duration_h,
|
|
108
|
+
axis_labels=cfg.axis_labels.model_dump(),
|
|
109
|
+
state_labels=cfg.state_labels,
|
|
110
|
+
reference_label=cfg.reference_label,
|
|
111
|
+
title=subject.title,
|
|
112
|
+
)
|
|
113
|
+
rendered.extend(
|
|
114
|
+
PlotFigure(
|
|
115
|
+
fig=figure,
|
|
116
|
+
filename=subject.filename,
|
|
117
|
+
ext=extension,
|
|
118
|
+
dpi=cfg.dpi,
|
|
119
|
+
description=(
|
|
120
|
+
"Event-relative growth, response, magnitude, and reduced components "
|
|
121
|
+
"for one source experiment and design."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
for extension in cfg.format
|
|
125
|
+
)
|
|
126
|
+
return rendered
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _is_windows_reserved_filename(filename: str) -> bool:
|
|
130
|
+
"""Return whether a portable filename would address a Windows device."""
|
|
131
|
+
|
|
132
|
+
device_name = filename.split(".", maxsplit=1)[0]
|
|
133
|
+
return re.fullmatch(r"(?i:con|prn|aux|nul|com[1-9]|lpt[1-9])", device_name) is not None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import 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 FourStateEventWindowSummaryCfg(PluginConfig):
|
|
14
|
+
primary_reduction_id: str = Field(min_length=1)
|
|
15
|
+
experiment_ids: list[str] | None = Field(default=None, min_length=1)
|
|
16
|
+
design_ids: list[str] | None = Field(default=None, min_length=1)
|
|
17
|
+
maximum_rows: int = Field(default=64, ge=1)
|
|
18
|
+
title: str = Field(default="Four-state event-window summary", min_length=1)
|
|
19
|
+
filename: str = Field(default="four_state_event_window_summary", min_length=1)
|
|
20
|
+
format: list[Literal["png", "pdf", "svg"]] = Field(default_factory=lambda: ["png"], min_length=1)
|
|
21
|
+
dpi: int = Field(default=300, ge=1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FourStateEventWindowSummaryPlot(FigurePlotPlugin):
|
|
25
|
+
ConfigModel = FourStateEventWindowSummaryCfg
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def input_ports(cls):
|
|
29
|
+
return {"designs": dataframe_input("designs", "plate_reader.four_state_event_window.designs.v4")}
|
|
30
|
+
|
|
31
|
+
def render(self, ctx, inputs, cfg):
|
|
32
|
+
from reader_workbench.domains.plate_reader.plots.four_state_event_window.summary import ( # noqa: PLC0415
|
|
33
|
+
render_four_state_event_window_summary,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
figure = render_four_state_event_window_summary(
|
|
37
|
+
inputs["designs"],
|
|
38
|
+
primary_reduction_id=cfg.primary_reduction_id,
|
|
39
|
+
experiment_ids=cfg.experiment_ids,
|
|
40
|
+
design_ids=cfg.design_ids,
|
|
41
|
+
maximum_rows=cfg.maximum_rows,
|
|
42
|
+
title=cfg.title,
|
|
43
|
+
)
|
|
44
|
+
return [
|
|
45
|
+
PlotFigure(
|
|
46
|
+
fig=figure,
|
|
47
|
+
filename=cfg.filename,
|
|
48
|
+
ext=extension,
|
|
49
|
+
dpi=cfg.dpi,
|
|
50
|
+
description="Primary event-relative response and anchored-magnitude components by source and design.",
|
|
51
|
+
)
|
|
52
|
+
for extension in cfg.format
|
|
53
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from reader_workbench.plotting.sinks import PlotFigure
|
|
6
|
+
from reader_workbench.plugins.plot._shared import FigurePlotPlugin
|
|
7
|
+
from reader_workbench.workbench.ports import dataframe_input
|
|
8
|
+
from reader_workbench.workbench.registry import PluginConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FourStateVectorCollectionHeatmapCfg(PluginConfig):
|
|
12
|
+
title: str = "Four-state vector collection"
|
|
13
|
+
max_y_tick_labels: int = 80
|
|
14
|
+
filename: str = "four_state_vector_collection"
|
|
15
|
+
format: list[str] = Field(default_factory=lambda: ["png"])
|
|
16
|
+
dpi: int = 300
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FourStateVectorCollectionHeatmapPlot(FigurePlotPlugin):
|
|
20
|
+
ConfigModel = FourStateVectorCollectionHeatmapCfg
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def input_ports(cls):
|
|
24
|
+
return {"vectors": dataframe_input("vectors", "logic.four_state_vector_collection.v1")}
|
|
25
|
+
|
|
26
|
+
def render(self, ctx, inputs, cfg):
|
|
27
|
+
from reader_workbench.domains.logic.four_state_vector.collection import ( # noqa: PLC0415
|
|
28
|
+
render_four_state_vector_collection_heatmap,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
figure = render_four_state_vector_collection_heatmap(
|
|
32
|
+
inputs["vectors"],
|
|
33
|
+
title=cfg.title,
|
|
34
|
+
max_y_tick_labels=cfg.max_y_tick_labels,
|
|
35
|
+
)
|
|
36
|
+
return [
|
|
37
|
+
PlotFigure(
|
|
38
|
+
fig=figure,
|
|
39
|
+
filename=cfg.filename,
|
|
40
|
+
ext=extension,
|
|
41
|
+
dpi=cfg.dpi,
|
|
42
|
+
description="Cross-experiment four-state vector heatmap.",
|
|
43
|
+
)
|
|
44
|
+
for extension in cfg.format
|
|
45
|
+
]
|