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,170 @@
|
|
|
1
|
+
"""Merge tidy measurement table with a sample metadata map. Cleans the map by:
|
|
2
|
+
1) dropping all-empty columns
|
|
3
|
+
2) dropping positions that carry no metadata beyond 'position'
|
|
4
|
+
3) asserting remaining raw positions exist in the map
|
|
5
|
+
Then merges many:1 on 'position'."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from reader_workbench.domains.plate_reader.io.sample_map import parse_sample_map
|
|
15
|
+
from reader_workbench.errors import MergeError
|
|
16
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output, file_path_input
|
|
17
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SampleMapCfg(PluginConfig):
|
|
21
|
+
"""
|
|
22
|
+
Flexible plate-map merge.
|
|
23
|
+
- require_columns: metadata columns that MUST exist after merge (presence-only by default).
|
|
24
|
+
- require_non_null: if true, also assert these columns are non-null for all merged rows.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
require_columns: list[str] = Field(default_factory=list)
|
|
28
|
+
require_non_null: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SampleMapMerge(Plugin):
|
|
32
|
+
ConfigModel = SampleMapCfg
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def input_ports(cls):
|
|
36
|
+
return {
|
|
37
|
+
"df": dataframe_input("df", "tidy.v1"),
|
|
38
|
+
"sample_map": file_path_input("sample_map"),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def output_ports(cls):
|
|
43
|
+
return cls.promoted_output_ports(
|
|
44
|
+
outputs={"df": dataframe_output("df", "tidy.v1")},
|
|
45
|
+
promotions={"df": ("plate_reader.annotated.v1",)},
|
|
46
|
+
note="promotion requires mapped metadata columns that satisfy the richer contract",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def resolve_output_ports(self, *, inputs, outputs, cfg, where):
|
|
50
|
+
del inputs, cfg
|
|
51
|
+
resolved = dict(self.output_ports())
|
|
52
|
+
merged = outputs.get("df")
|
|
53
|
+
if not isinstance(merged, pd.DataFrame):
|
|
54
|
+
return resolved
|
|
55
|
+
try:
|
|
56
|
+
self.contracts.validate(merged, contract_id="plate_reader.annotated.v1", where=f"{where}:df")
|
|
57
|
+
except Exception:
|
|
58
|
+
return resolved
|
|
59
|
+
resolved["df"] = dataframe_output("df", "plate_reader.annotated.v1", surface=resolved["df"].surface)
|
|
60
|
+
return resolved
|
|
61
|
+
|
|
62
|
+
def _clean_plate_map(self, plate_map: pd.DataFrame) -> pd.DataFrame:
|
|
63
|
+
if "position" not in plate_map.columns:
|
|
64
|
+
raise MergeError("Plate map must contain a 'position' column")
|
|
65
|
+
|
|
66
|
+
# 1) drop all-empty columns (keeps 'position' even if empty by policy)
|
|
67
|
+
pm = plate_map.copy()
|
|
68
|
+
non_all_empty = [c for c in pm.columns if c == "position" or not pm[c].isna().all()]
|
|
69
|
+
pm = pm[non_all_empty]
|
|
70
|
+
|
|
71
|
+
# 2) drop rows with no metadata beyond 'position'
|
|
72
|
+
meta_cols = [c for c in pm.columns if c != "position"]
|
|
73
|
+
if not meta_cols:
|
|
74
|
+
# map has only 'position' with no metadata → nothing to merge
|
|
75
|
+
return pm.iloc[0:0].copy()
|
|
76
|
+
|
|
77
|
+
no_meta = pm[meta_cols].isna().all(axis=1)
|
|
78
|
+
pm = pm.loc[~no_meta].copy()
|
|
79
|
+
|
|
80
|
+
return pm
|
|
81
|
+
|
|
82
|
+
def run(self, ctx, inputs, cfg: SampleMapCfg):
|
|
83
|
+
df: pd.DataFrame = inputs["df"]
|
|
84
|
+
sm_path: Path = inputs["sample_map"]
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
sm_raw = parse_sample_map(sm_path)
|
|
88
|
+
sm = self._clean_plate_map(sm_raw)
|
|
89
|
+
if sm.empty:
|
|
90
|
+
raise MergeError("Plate map has no usable metadata rows after cleaning")
|
|
91
|
+
|
|
92
|
+
# A position row with no metadata explicitly excludes that well from annotated output.
|
|
93
|
+
removed_positions = sorted(set(sm_raw["position"].astype(str)) - set(sm["position"].astype(str)))
|
|
94
|
+
if removed_positions:
|
|
95
|
+
before = len(df)
|
|
96
|
+
df = df[~df["position"].astype(str).isin(removed_positions)].copy()
|
|
97
|
+
after = len(df)
|
|
98
|
+
try:
|
|
99
|
+
ctx.logger.info(
|
|
100
|
+
"[muted]sample_map: dropped %d raw rows (%d positions without metadata)[/muted]",
|
|
101
|
+
before - after,
|
|
102
|
+
len(removed_positions),
|
|
103
|
+
)
|
|
104
|
+
head = ", ".join(removed_positions[:20])
|
|
105
|
+
tail = " …" if len(removed_positions) > 20 else ""
|
|
106
|
+
ctx.logger.debug("sample_map: removed positions: %s%s", head, tail)
|
|
107
|
+
# Optional arithmetic trace (best-effort; relies on tidy schema)
|
|
108
|
+
try:
|
|
109
|
+
chans = df["channel"].astype(str).nunique()
|
|
110
|
+
avg_rows_per_pos = (before - after) / max(len(removed_positions), 1)
|
|
111
|
+
approx_time_slices = round(avg_rows_per_pos / max(chans, 1))
|
|
112
|
+
ctx.logger.debug(
|
|
113
|
+
"sample_map: consistency hint • removed_positions=%d • channels=%d • ~time_slices_per_channel=%d",
|
|
114
|
+
len(removed_positions),
|
|
115
|
+
chans,
|
|
116
|
+
approx_time_slices,
|
|
117
|
+
)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# 3) ensure all remaining raw positions exist in the (cleaned) map
|
|
124
|
+
raw_positions = set(df["position"].astype(str).unique())
|
|
125
|
+
map_positions = set(sm["position"].astype(str).unique())
|
|
126
|
+
missing = sorted(raw_positions - map_positions)
|
|
127
|
+
if missing:
|
|
128
|
+
raise MergeError(
|
|
129
|
+
f"Plate map missing entries for positions: {missing[:40]}{'…' if len(missing) > 40 else ''}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
merged = df.merge(sm, on="position", how="left", validate="m:1")
|
|
133
|
+
|
|
134
|
+
# Optional dtype normalization for 'batch' when present
|
|
135
|
+
if "batch" in merged.columns:
|
|
136
|
+
try:
|
|
137
|
+
merged["batch"] = pd.to_numeric(merged["batch"], errors="raise").astype("Int64")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise MergeError(f"'batch' must be integer-typed: {e}") from e
|
|
140
|
+
|
|
141
|
+
# Assert required metadata columns per-experiment (config-driven)
|
|
142
|
+
missing_cols = [c for c in cfg.require_columns if c not in merged.columns]
|
|
143
|
+
if missing_cols:
|
|
144
|
+
raise MergeError(f"Required metadata column(s) missing after merge: {missing_cols}")
|
|
145
|
+
if cfg.require_non_null and cfg.require_columns:
|
|
146
|
+
nulls = {c: int(merged[c].isna().sum()) for c in cfg.require_columns}
|
|
147
|
+
bad = {c: n for c, n in nulls.items() if n > 0}
|
|
148
|
+
if bad:
|
|
149
|
+
raise MergeError(f"Required metadata column(s) contain NaN: {bad}")
|
|
150
|
+
|
|
151
|
+
# Concise merge summary.
|
|
152
|
+
try:
|
|
153
|
+
added_cols = [c for c in merged.columns if c not in df.columns]
|
|
154
|
+
ctx.logger.info(
|
|
155
|
+
"sample_map • positions: raw=%d • map=%d • intersect=%d • added_cols=%d [%s]",
|
|
156
|
+
len(raw_positions),
|
|
157
|
+
len(map_positions),
|
|
158
|
+
len(raw_positions & map_positions),
|
|
159
|
+
len(added_cols),
|
|
160
|
+
", ".join(added_cols[:6]) + (" …" if len(added_cols) > 6 else ""),
|
|
161
|
+
)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
except MergeError:
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise MergeError(str(e)) from e
|
|
169
|
+
|
|
170
|
+
return {"df": merged}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Merge tidy measurements with a sample metadata table (keyed by sample_id by default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from reader_workbench.errors import MergeError
|
|
11
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output, file_path_input
|
|
12
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SampleMetadataCfg(PluginConfig):
|
|
16
|
+
key: str = "sample_id"
|
|
17
|
+
require_columns: list[str] = Field(default_factory=list)
|
|
18
|
+
require_non_null: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SampleMetadataMerge(Plugin):
|
|
22
|
+
ConfigModel = SampleMetadataCfg
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def input_ports(cls):
|
|
26
|
+
return {
|
|
27
|
+
"df": dataframe_input("df", "tidy.v1"),
|
|
28
|
+
"metadata": file_path_input("metadata"),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def output_ports(cls):
|
|
33
|
+
return {"df": dataframe_output("df", "tidy.v1")}
|
|
34
|
+
|
|
35
|
+
def _load_metadata(self, path: Path) -> pd.DataFrame:
|
|
36
|
+
if not path.exists():
|
|
37
|
+
raise MergeError(f"Metadata file does not exist: {path}")
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
raise MergeError(f"Metadata path must be a regular file: {path}")
|
|
40
|
+
suffix = path.suffix.lower()
|
|
41
|
+
if suffix == ".xlsx":
|
|
42
|
+
return pd.read_excel(path)
|
|
43
|
+
if suffix == ".csv":
|
|
44
|
+
return pd.read_csv(path)
|
|
45
|
+
raise MergeError(f"Unsupported metadata format {suffix or '<none>'!r}; expected .csv or .xlsx")
|
|
46
|
+
|
|
47
|
+
def run(self, ctx, inputs, cfg: SampleMetadataCfg):
|
|
48
|
+
df: pd.DataFrame = inputs["df"]
|
|
49
|
+
meta_path: Path = inputs["metadata"]
|
|
50
|
+
key = str(cfg.key)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
meta = self._load_metadata(meta_path)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise MergeError(f"Failed to read metadata file {meta_path}: {e}") from e
|
|
56
|
+
|
|
57
|
+
if key not in df.columns:
|
|
58
|
+
raise MergeError(f"Metadata merge key '{key}' missing from input dataframe")
|
|
59
|
+
if key not in meta.columns:
|
|
60
|
+
raise MergeError(f"Metadata merge key '{key}' missing from metadata file")
|
|
61
|
+
missing_keys = meta[key].isna() | meta[key].astype("string").str.strip().eq("")
|
|
62
|
+
if missing_keys.any():
|
|
63
|
+
rows = [int(index) + 2 for index in meta.index[missing_keys].tolist()]
|
|
64
|
+
raise MergeError(f"Metadata merge key '{key}' is blank at file rows: {rows}")
|
|
65
|
+
duplicate_keys = sorted(meta.loc[meta[key].duplicated(keep=False), key].astype(str).unique().tolist())
|
|
66
|
+
if duplicate_keys:
|
|
67
|
+
raise MergeError(f"Metadata merge key '{key}' must be unique; duplicates: {duplicate_keys}")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
merged = df.merge(meta, on=key, how="left", validate="m:1")
|
|
71
|
+
except (TypeError, ValueError, pd.errors.MergeError) as exc:
|
|
72
|
+
raise MergeError(f"Metadata merge failed for key '{key}': {exc}") from exc
|
|
73
|
+
|
|
74
|
+
missing_cols = [c for c in cfg.require_columns if c not in merged.columns]
|
|
75
|
+
if missing_cols:
|
|
76
|
+
raise MergeError(f"Required metadata column(s) missing after merge: {missing_cols}")
|
|
77
|
+
if cfg.require_non_null and cfg.require_columns:
|
|
78
|
+
nulls = {c: int(merged[c].isna().sum()) for c in cfg.require_columns}
|
|
79
|
+
bad = {c: n for c, n in nulls.items() if n > 0}
|
|
80
|
+
if bad:
|
|
81
|
+
raise MergeError(f"Required metadata column(s) contain NaN: {bad}")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
added_cols = [c for c in merged.columns if c not in df.columns]
|
|
85
|
+
ctx.logger.info(
|
|
86
|
+
"sample_metadata • rows=%d • added_cols=%d [%s]",
|
|
87
|
+
len(merged),
|
|
88
|
+
len(added_cols),
|
|
89
|
+
", ".join(added_cols[:6]) + (" …" if len(added_cols) > 6 else ""),
|
|
90
|
+
)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return {"df": merged}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in validators."""
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from reader_workbench.errors import ExecutionError
|
|
10
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
11
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PromoteCfg(PluginConfig):
|
|
15
|
+
require_columns: list[str] = Field(default_factory=lambda: ["treatment", "design_id"])
|
|
16
|
+
require_non_null: bool = True # be strict when promoting
|
|
17
|
+
trim_and_require_non_blank: list[str] = Field(default_factory=list)
|
|
18
|
+
require_finite: list[str] = Field(default_factory=list)
|
|
19
|
+
allow_instrument_overflow: list[str] = Field(default_factory=list)
|
|
20
|
+
# Only promote a subset of rows (e.g., samples). If provided, we require the column to exist.
|
|
21
|
+
type_column: str = "type"
|
|
22
|
+
include_types: list[str] = Field(default_factory=list) # e.g., ["SAMPLE"]
|
|
23
|
+
# Deterministically drop rows with NULL in these columns before assertions.
|
|
24
|
+
drop_where_null_in: list[str] = Field(default_factory=list)
|
|
25
|
+
synthesize_batch: bool = False
|
|
26
|
+
synthesized_batch_value: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PromoteToTidyPlusMap(Plugin):
|
|
30
|
+
ConfigModel = PromoteCfg
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def input_ports(cls):
|
|
34
|
+
return {"df": dataframe_input("df", "tidy.v1")}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def output_ports(cls):
|
|
38
|
+
return {"df": dataframe_output("df", "plate_reader.annotated.v1")}
|
|
39
|
+
|
|
40
|
+
def run(self, ctx, inputs, cfg: PromoteCfg):
|
|
41
|
+
df: pd.DataFrame = inputs["df"].copy()
|
|
42
|
+
|
|
43
|
+
# 0) Optional row filtering by "type" (case-insensitive exact match)
|
|
44
|
+
if cfg.include_types:
|
|
45
|
+
if cfg.type_column not in df.columns:
|
|
46
|
+
raise ExecutionError(
|
|
47
|
+
f"to_tidy_plus_map: include_types was provided but column {cfg.type_column!r} is missing"
|
|
48
|
+
)
|
|
49
|
+
keep = {str(x).casefold() for x in cfg.include_types}
|
|
50
|
+
before = len(df)
|
|
51
|
+
df = df[df[cfg.type_column].astype(str).str.casefold().isin(keep)].copy()
|
|
52
|
+
with suppress(Exception):
|
|
53
|
+
ctx.logger.info(
|
|
54
|
+
"to_tidy_plus_map: filtered rows by %r ∈ %s → kept %d/%d",
|
|
55
|
+
cfg.type_column,
|
|
56
|
+
sorted(cfg.include_types),
|
|
57
|
+
len(df),
|
|
58
|
+
before,
|
|
59
|
+
)
|
|
60
|
+
if df.empty:
|
|
61
|
+
raise ExecutionError(
|
|
62
|
+
f"to_tidy_plus_map: no rows remain after filtering by {cfg.type_column} ∈ {cfg.include_types}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# 0b) Optional: drop rows with NULL in specific columns
|
|
66
|
+
if cfg.drop_where_null_in:
|
|
67
|
+
missing_cols = [c for c in cfg.drop_where_null_in if c not in df.columns]
|
|
68
|
+
if missing_cols:
|
|
69
|
+
raise ExecutionError(f"to_tidy_plus_map: drop_where_null_in refers to missing columns: {missing_cols}")
|
|
70
|
+
before = len(df)
|
|
71
|
+
df = df.dropna(subset=list(cfg.drop_where_null_in)).copy()
|
|
72
|
+
with suppress(Exception):
|
|
73
|
+
ctx.logger.info(
|
|
74
|
+
"to_tidy_plus_map: dropped %d row(s) with NULL in %s",
|
|
75
|
+
before - len(df),
|
|
76
|
+
list(cfg.drop_where_null_in),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if "batch" not in df.columns or df["batch"].isna().all():
|
|
80
|
+
if cfg.synthesize_batch:
|
|
81
|
+
df = df.copy()
|
|
82
|
+
df["batch"] = int(cfg.synthesized_batch_value)
|
|
83
|
+
with suppress(Exception):
|
|
84
|
+
ctx.logger.info(
|
|
85
|
+
"to_tidy_plus_map: 'batch' missing → added constant %d for all rows (explicit config)",
|
|
86
|
+
int(cfg.synthesized_batch_value),
|
|
87
|
+
)
|
|
88
|
+
elif df["batch"].isna().any() and cfg.synthesize_batch:
|
|
89
|
+
df = df.copy()
|
|
90
|
+
df["batch"] = df["batch"].fillna(int(cfg.synthesized_batch_value))
|
|
91
|
+
missing = [c for c in cfg.require_columns if c not in df.columns]
|
|
92
|
+
if missing:
|
|
93
|
+
raise ExecutionError(f"Cannot promote to plate_reader.annotated.v1; missing columns: {missing}")
|
|
94
|
+
validation_columns = (
|
|
95
|
+
set(cfg.trim_and_require_non_blank) | set(cfg.require_finite) | set(cfg.allow_instrument_overflow)
|
|
96
|
+
)
|
|
97
|
+
missing_validation = sorted(validation_columns - set(df.columns))
|
|
98
|
+
if missing_validation:
|
|
99
|
+
raise ExecutionError(
|
|
100
|
+
"Cannot promote to plate_reader.annotated.v1; configured validation refers to "
|
|
101
|
+
f"missing columns: {missing_validation}"
|
|
102
|
+
)
|
|
103
|
+
unvalidated_overflow = sorted(set(cfg.allow_instrument_overflow) - set(cfg.require_finite))
|
|
104
|
+
if unvalidated_overflow:
|
|
105
|
+
raise ExecutionError(
|
|
106
|
+
f"Cannot promote; allow_instrument_overflow must name columns in require_finite: {unvalidated_overflow}"
|
|
107
|
+
)
|
|
108
|
+
if cfg.require_non_null:
|
|
109
|
+
bad = {c: int(df[c].isna().sum()) for c in cfg.require_columns if df[c].isna().any()}
|
|
110
|
+
if bad:
|
|
111
|
+
raise ExecutionError(f"Cannot promote; required columns contain NaN: {bad}")
|
|
112
|
+
blank: dict[str, int] = {}
|
|
113
|
+
for column in cfg.trim_and_require_non_blank:
|
|
114
|
+
normalized = df[column].astype("string").str.strip()
|
|
115
|
+
invalid = normalized.isna() | normalized.eq("")
|
|
116
|
+
if invalid.any():
|
|
117
|
+
blank[column] = int(invalid.sum())
|
|
118
|
+
df[column] = normalized
|
|
119
|
+
if blank:
|
|
120
|
+
raise ExecutionError(f"Cannot promote; configured identity columns contain blank values: {blank}")
|
|
121
|
+
nonfinite: dict[str, int] = {}
|
|
122
|
+
for column in cfg.require_finite:
|
|
123
|
+
numeric = pd.to_numeric(df[column], errors="coerce").to_numpy(dtype=float, na_value=np.nan)
|
|
124
|
+
invalid = ~np.isfinite(numeric)
|
|
125
|
+
if column in cfg.allow_instrument_overflow:
|
|
126
|
+
overflow_column = f"{column}_instrument_overflow"
|
|
127
|
+
bound_column = f"{column}_bound_kind"
|
|
128
|
+
missing_provenance = [name for name in (overflow_column, bound_column) if name not in df.columns]
|
|
129
|
+
if missing_provenance:
|
|
130
|
+
raise ExecutionError(
|
|
131
|
+
"Cannot promote; instrument-overflow validation requires provenance columns: "
|
|
132
|
+
f"{missing_provenance}"
|
|
133
|
+
)
|
|
134
|
+
raw_flags = df[overflow_column]
|
|
135
|
+
if raw_flags.isna().any() or not raw_flags.map(lambda value: isinstance(value, (bool, np.bool_))).all():
|
|
136
|
+
raise ExecutionError(
|
|
137
|
+
f"Cannot promote; {overflow_column!r} must contain booleans without missing values"
|
|
138
|
+
)
|
|
139
|
+
declared_overflow = (
|
|
140
|
+
np.isposinf(numeric)
|
|
141
|
+
& raw_flags.to_numpy(dtype=bool)
|
|
142
|
+
& df[bound_column].astype("string").eq("lower").to_numpy(dtype=bool)
|
|
143
|
+
)
|
|
144
|
+
invalid &= ~declared_overflow
|
|
145
|
+
invalid_count = int(invalid.sum())
|
|
146
|
+
if invalid_count:
|
|
147
|
+
nonfinite[column] = invalid_count
|
|
148
|
+
if nonfinite:
|
|
149
|
+
raise ExecutionError(
|
|
150
|
+
f"Cannot promote; configured measurement columns contain non-finite values: {nonfinite}"
|
|
151
|
+
)
|
|
152
|
+
# dtype normalization for 'batch' (if present)
|
|
153
|
+
if "batch" in df.columns:
|
|
154
|
+
df["batch"] = pd.to_numeric(df["batch"], errors="raise").astype("Int64")
|
|
155
|
+
return {"df": df}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from .builtins import BUILTIN_PROTOCOLS, builtin_protocol_catalog
|
|
2
|
+
from .compiler import (
|
|
3
|
+
compile_cytometry_flow_panel,
|
|
4
|
+
compile_generic_protocol,
|
|
5
|
+
compile_logic_four_state_vector_screen,
|
|
6
|
+
compile_plate_reader_dual_reporter_screen,
|
|
7
|
+
compile_plate_reader_growth_screen,
|
|
8
|
+
compile_plate_reader_single_reporter_screen,
|
|
9
|
+
)
|
|
10
|
+
from .model import (
|
|
11
|
+
BoundProtocol,
|
|
12
|
+
CompiledProtocolPlan,
|
|
13
|
+
FigureKind,
|
|
14
|
+
MetricStage,
|
|
15
|
+
ProtocolArtifactSpec,
|
|
16
|
+
ProtocolBinding,
|
|
17
|
+
ProtocolBindingValueRef,
|
|
18
|
+
ProtocolCatalog,
|
|
19
|
+
ProtocolCompiler,
|
|
20
|
+
ProtocolConfigFieldSpec,
|
|
21
|
+
ProtocolControlRule,
|
|
22
|
+
ProtocolDescriptor,
|
|
23
|
+
ProtocolEffectSignSpec,
|
|
24
|
+
ProtocolExecutionPlan,
|
|
25
|
+
ProtocolFactorSpec,
|
|
26
|
+
ProtocolFigureSpec,
|
|
27
|
+
ProtocolMetricSpec,
|
|
28
|
+
ProtocolPlotProfileSpec,
|
|
29
|
+
ProtocolPluginDefaultsSpec,
|
|
30
|
+
ProtocolRankingSpec,
|
|
31
|
+
ProtocolResourceSpec,
|
|
32
|
+
ProtocolSemanticExecution,
|
|
33
|
+
ProtocolSemanticNode,
|
|
34
|
+
ProtocolSemanticProfileOverride,
|
|
35
|
+
ProtocolSemanticProfileSpec,
|
|
36
|
+
ProtocolSemanticProgram,
|
|
37
|
+
ProtocolWindowSpec,
|
|
38
|
+
RankingDirection,
|
|
39
|
+
binding_value,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BUILTIN_PROTOCOLS",
|
|
44
|
+
"BoundProtocol",
|
|
45
|
+
"CompiledProtocolPlan",
|
|
46
|
+
"FigureKind",
|
|
47
|
+
"MetricStage",
|
|
48
|
+
"ProtocolArtifactSpec",
|
|
49
|
+
"ProtocolBinding",
|
|
50
|
+
"ProtocolBindingValueRef",
|
|
51
|
+
"ProtocolCatalog",
|
|
52
|
+
"ProtocolCompiler",
|
|
53
|
+
"ProtocolConfigFieldSpec",
|
|
54
|
+
"ProtocolControlRule",
|
|
55
|
+
"ProtocolDescriptor",
|
|
56
|
+
"ProtocolExecutionPlan",
|
|
57
|
+
"ProtocolEffectSignSpec",
|
|
58
|
+
"ProtocolFactorSpec",
|
|
59
|
+
"ProtocolFigureSpec",
|
|
60
|
+
"ProtocolMetricSpec",
|
|
61
|
+
"ProtocolPlotProfileSpec",
|
|
62
|
+
"ProtocolPluginDefaultsSpec",
|
|
63
|
+
"ProtocolRankingSpec",
|
|
64
|
+
"ProtocolResourceSpec",
|
|
65
|
+
"ProtocolSemanticExecution",
|
|
66
|
+
"ProtocolSemanticNode",
|
|
67
|
+
"ProtocolSemanticProfileOverride",
|
|
68
|
+
"ProtocolSemanticProfileSpec",
|
|
69
|
+
"ProtocolSemanticProgram",
|
|
70
|
+
"ProtocolWindowSpec",
|
|
71
|
+
"RankingDirection",
|
|
72
|
+
"binding_value",
|
|
73
|
+
"builtin_protocol_catalog",
|
|
74
|
+
"compile_cytometry_flow_panel",
|
|
75
|
+
"compile_generic_protocol",
|
|
76
|
+
"compile_logic_four_state_vector_screen",
|
|
77
|
+
"compile_plate_reader_dual_reporter_screen",
|
|
78
|
+
"compile_plate_reader_growth_screen",
|
|
79
|
+
"compile_plate_reader_single_reporter_screen",
|
|
80
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
|
|
6
|
+
from reader_workbench.workbench.input_discovery import (
|
|
7
|
+
DEFAULT_INPUT_EXCLUDE as DEFAULT_EXCLUDE,
|
|
8
|
+
)
|
|
9
|
+
from reader_workbench.workbench.input_discovery import (
|
|
10
|
+
DEFAULT_WORKBOOK_INCLUDE as DEFAULT_INCLUDE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .compiler import compile_plate_reader_growth_screen
|
|
14
|
+
from .model import (
|
|
15
|
+
ProtocolConfigFieldSpec,
|
|
16
|
+
ProtocolDescriptor,
|
|
17
|
+
ProtocolExecutionPlan,
|
|
18
|
+
ProtocolFigureSpec,
|
|
19
|
+
ProtocolMetricSpec,
|
|
20
|
+
ProtocolPlotProfileSpec,
|
|
21
|
+
ProtocolPluginDefaultsSpec,
|
|
22
|
+
ProtocolResourceSpec,
|
|
23
|
+
ProtocolSemanticProfileSpec,
|
|
24
|
+
binding_value,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_plate_reader_growth_protocol(
|
|
29
|
+
*,
|
|
30
|
+
dual_reporter_protocol: ProtocolDescriptor,
|
|
31
|
+
field_builder: Callable[..., ProtocolConfigFieldSpec],
|
|
32
|
+
) -> ProtocolDescriptor:
|
|
33
|
+
"""Build the one-channel growth protocol from shared plate-reader inputs."""
|
|
34
|
+
|
|
35
|
+
field = field_builder
|
|
36
|
+
ingest_field = next(item for item in dual_reporter_protocol.input_fields if item.key == "ingest")
|
|
37
|
+
channel_map_field = next(item for item in ingest_field.children if item.key == "channel_map")
|
|
38
|
+
growth_ingest = replace(
|
|
39
|
+
ingest_field,
|
|
40
|
+
children=tuple(
|
|
41
|
+
replace(
|
|
42
|
+
item,
|
|
43
|
+
summary=(
|
|
44
|
+
"Optional raw workbook label to canonical channel mapping. Leave null for "
|
|
45
|
+
"map-free kinetic discovery; snapshot and mixed parsing require a mapping."
|
|
46
|
+
),
|
|
47
|
+
default={"OD600:600": "OD600"},
|
|
48
|
+
)
|
|
49
|
+
if item is channel_map_field
|
|
50
|
+
else item
|
|
51
|
+
for item in ingest_field.children
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
inherited_preprocessing = next(
|
|
55
|
+
item for item in dual_reporter_protocol.analysis_fields if item.key == "preprocessing"
|
|
56
|
+
)
|
|
57
|
+
preprocessing_field = replace(
|
|
58
|
+
inherited_preprocessing,
|
|
59
|
+
children=tuple(
|
|
60
|
+
replace(
|
|
61
|
+
item,
|
|
62
|
+
children=tuple(
|
|
63
|
+
replace(child, default="none") if child.key == "action" else child for child in item.children
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
if item.key == "overflow"
|
|
67
|
+
else item
|
|
68
|
+
for item in inherited_preprocessing.children
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return ProtocolDescriptor(
|
|
73
|
+
protocol="plate_reader/growth_screen",
|
|
74
|
+
domain="plate_reader",
|
|
75
|
+
family="screen_analysis",
|
|
76
|
+
summary="One-channel plate-reader growth assay with explicit sample and treatment metadata.",
|
|
77
|
+
tags=("plate_reader", "growth", "screen", "single_channel"),
|
|
78
|
+
resources=(
|
|
79
|
+
ProtocolResourceSpec(
|
|
80
|
+
id="sample_map",
|
|
81
|
+
path="./inputs/metadata.xlsx",
|
|
82
|
+
summary="Well-to-sample metadata for the plate-reader workbook.",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
input_fields=(growth_ingest,),
|
|
86
|
+
analysis_fields=(
|
|
87
|
+
field(
|
|
88
|
+
"growth_channel",
|
|
89
|
+
"Measured channel used as the growth readout.",
|
|
90
|
+
kind="string",
|
|
91
|
+
default="OD600",
|
|
92
|
+
),
|
|
93
|
+
preprocessing_field,
|
|
94
|
+
),
|
|
95
|
+
factors=dual_reporter_protocol.factors,
|
|
96
|
+
semantic_profiles=(
|
|
97
|
+
ProtocolSemanticProfileSpec(
|
|
98
|
+
id="growth_raw",
|
|
99
|
+
family="growth_panel",
|
|
100
|
+
summary="Growth-screen semantics over one configured measurement channel.",
|
|
101
|
+
primary_metric="Growth",
|
|
102
|
+
primary_readout="configured growth channel",
|
|
103
|
+
tags=("growth", "plate_reader", "panel"),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
metrics=(
|
|
107
|
+
ProtocolMetricSpec(
|
|
108
|
+
id="Growth",
|
|
109
|
+
stage="raw",
|
|
110
|
+
summary="Raw configured growth trace.",
|
|
111
|
+
formula="configured_growth_channel",
|
|
112
|
+
profiles=("growth_raw",),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
figures=(
|
|
116
|
+
ProtocolFigureSpec(
|
|
117
|
+
id="raw_kinetics",
|
|
118
|
+
kind="qc",
|
|
119
|
+
summary="Raw growth trajectories by sample and treatment.",
|
|
120
|
+
primary=True,
|
|
121
|
+
),
|
|
122
|
+
ProtocolFigureSpec(
|
|
123
|
+
id="endpoint_by_condition",
|
|
124
|
+
kind="summary",
|
|
125
|
+
summary="Explicit-time growth comparison grouped by treatment or condition.",
|
|
126
|
+
),
|
|
127
|
+
ProtocolFigureSpec(
|
|
128
|
+
id="endpoint_by_design",
|
|
129
|
+
kind="summary",
|
|
130
|
+
summary="Explicit-time growth comparison grouped by sample or design.",
|
|
131
|
+
),
|
|
132
|
+
ProtocolFigureSpec(
|
|
133
|
+
id="growth_overview",
|
|
134
|
+
kind="kinetics",
|
|
135
|
+
summary="Growth trajectories and an explicitly timed endpoint in one view.",
|
|
136
|
+
),
|
|
137
|
+
ProtocolFigureSpec(
|
|
138
|
+
id="value_distributions",
|
|
139
|
+
kind="qc",
|
|
140
|
+
summary="Distribution view of the configured growth channel.",
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
plot_profiles=(
|
|
144
|
+
ProtocolPlotProfileSpec(
|
|
145
|
+
id="screen_overview",
|
|
146
|
+
summary="Explicit-time growth screen review.",
|
|
147
|
+
figures=("raw_kinetics", "endpoint_by_condition", "endpoint_by_design", "growth_overview"),
|
|
148
|
+
),
|
|
149
|
+
ProtocolPlotProfileSpec(
|
|
150
|
+
id="kinetics_qc",
|
|
151
|
+
summary="Growth trajectories and value distributions without an inferred endpoint.",
|
|
152
|
+
figures=("raw_kinetics", "value_distributions"),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
default_plot_profile="kinetics_qc",
|
|
156
|
+
execution=ProtocolExecutionPlan(
|
|
157
|
+
plugin_defaults=(
|
|
158
|
+
ProtocolPluginDefaultsSpec(
|
|
159
|
+
plugin="ingest/synergy_h1",
|
|
160
|
+
summary="Generic one-channel plate-reader ingest settings.",
|
|
161
|
+
with_={
|
|
162
|
+
"mode": binding_value("ingest.mode", "kinetic_only"),
|
|
163
|
+
"channel_map": binding_value("ingest.channel_map", {"OD600:600": "OD600"}),
|
|
164
|
+
"sheet_names": binding_value("ingest.sheet_names", None),
|
|
165
|
+
"time_round_decimals": binding_value("ingest.time_round_decimals", 12),
|
|
166
|
+
"time_step_h": binding_value("ingest.time_step_h", None),
|
|
167
|
+
"time_offset_h": binding_value("ingest.time_offset_h", 0.0),
|
|
168
|
+
"auto_roots": binding_value("ingest.auto_roots", None),
|
|
169
|
+
"auto_include": binding_value("ingest.auto_include", list(DEFAULT_INCLUDE)),
|
|
170
|
+
"auto_exclude": binding_value("ingest.auto_exclude", list(DEFAULT_EXCLUDE)),
|
|
171
|
+
"auto_pick": binding_value("ingest.auto_pick", "single"),
|
|
172
|
+
"auto_recursive": binding_value("ingest.auto_recursive", False),
|
|
173
|
+
"print_summary": binding_value("ingest.print_summary", True),
|
|
174
|
+
},
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
compiler=compile_plate_reader_growth_screen,
|
|
178
|
+
),
|
|
179
|
+
)
|