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,70 @@
|
|
|
1
|
+
"""Alias mappings for categorical columns. Either replace in-place or create
|
|
2
|
+
<column>_alias columns. Prints a succinct per-column summary of applied aliases."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from reader_workbench.plugins.transform._labeling import apply_label_mappings
|
|
9
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
10
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AliasCfg(PluginConfig):
|
|
14
|
+
"""
|
|
15
|
+
mappings:
|
|
16
|
+
<column_name>:
|
|
17
|
+
<raw_value>: <alias_value>
|
|
18
|
+
...
|
|
19
|
+
in_place: if true, mutate <column_name> directly; else create <column_name>_alias
|
|
20
|
+
case_insensitive: map using casefold() on incoming values (keys in 'aliases' are matched case-insensitively)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
mappings: Mapping[str, Mapping[str, str]] | None = None
|
|
24
|
+
in_place: bool = False
|
|
25
|
+
case_insensitive: bool = True
|
|
26
|
+
suffix: str = "_alias"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AliasTransform(Plugin):
|
|
30
|
+
ConfigModel = AliasCfg
|
|
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 cls.passthrough_output_ports(
|
|
39
|
+
outputs={"df": dataframe_output("df", "tidy.v1")},
|
|
40
|
+
passthrough={"df": "df"},
|
|
41
|
+
promoted_examples={"df": ("plate_reader.annotated.v1",)},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def resolve_output_ports(self, *, inputs, outputs, cfg, where):
|
|
45
|
+
del cfg
|
|
46
|
+
return self.inherit_dataframe_output_ports(
|
|
47
|
+
inputs=inputs,
|
|
48
|
+
outputs=outputs,
|
|
49
|
+
passthrough={"df": "df"},
|
|
50
|
+
where=where,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def run(self, ctx, inputs, cfg: AliasCfg):
|
|
54
|
+
if cfg.mappings is None:
|
|
55
|
+
raise ValueError("alias: provide with.mappings")
|
|
56
|
+
if not isinstance(cfg.mappings, Mapping):
|
|
57
|
+
raise ValueError("alias: mappings must be a mapping of column -> {raw: alias}")
|
|
58
|
+
mappings = {str(col): mapping for col, mapping in cfg.mappings.items()}
|
|
59
|
+
output_names = {str(col): f"{col}{cfg.suffix}" for col in mappings}
|
|
60
|
+
return {
|
|
61
|
+
"df": apply_label_mappings(
|
|
62
|
+
ctx=ctx,
|
|
63
|
+
df=inputs["df"],
|
|
64
|
+
mappings=mappings,
|
|
65
|
+
output_names=output_names,
|
|
66
|
+
in_place=cfg.in_place,
|
|
67
|
+
case_insensitive=cfg.case_insensitive,
|
|
68
|
+
label="alias",
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from reader_workbench.plugins.transform._labeling import apply_label_mappings
|
|
4
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
5
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AnnotationLabelsCfg(PluginConfig):
|
|
9
|
+
refs: list[str] | None = None
|
|
10
|
+
in_place: bool = False
|
|
11
|
+
case_insensitive: bool = True
|
|
12
|
+
suffix: str = "_alias"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnnotationLabelsTransform(Plugin):
|
|
16
|
+
ConfigModel = AnnotationLabelsCfg
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def input_ports(cls):
|
|
20
|
+
return {"df": dataframe_input("df", "tidy.v1")}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def output_ports(cls):
|
|
24
|
+
return cls.passthrough_output_ports(
|
|
25
|
+
outputs={"df": dataframe_output("df", "tidy.v1")},
|
|
26
|
+
passthrough={"df": "df"},
|
|
27
|
+
promoted_examples={"df": ("plate_reader.annotated.v1",)},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def resolve_output_ports(self, *, inputs, outputs, cfg, where):
|
|
31
|
+
del cfg
|
|
32
|
+
return self.inherit_dataframe_output_ports(
|
|
33
|
+
inputs=inputs,
|
|
34
|
+
outputs=outputs,
|
|
35
|
+
passthrough={"df": "df"},
|
|
36
|
+
where=where,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def run(self, ctx, inputs, cfg: AnnotationLabelsCfg):
|
|
40
|
+
if ctx.experiment is None:
|
|
41
|
+
raise ValueError("assay_labels requires experiment annotations in the run context")
|
|
42
|
+
label_specs = ctx.experiment.annotations.resolve_label_specs(cfg.refs)
|
|
43
|
+
if not label_specs:
|
|
44
|
+
raise ValueError("assay_labels: no annotations.labels are configured")
|
|
45
|
+
|
|
46
|
+
mappings: dict[str, dict[str, str]] = {}
|
|
47
|
+
output_names: dict[str, str] = {}
|
|
48
|
+
for spec in label_specs:
|
|
49
|
+
mappings[spec.source] = dict(spec.values)
|
|
50
|
+
output_names[spec.source] = spec.output or f"{spec.source}{cfg.suffix}"
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"df": apply_label_mappings(
|
|
54
|
+
ctx=ctx,
|
|
55
|
+
df=inputs["df"],
|
|
56
|
+
mappings=mappings,
|
|
57
|
+
output_names=output_names,
|
|
58
|
+
in_place=cfg.in_place,
|
|
59
|
+
case_insensitive=cfg.case_insensitive,
|
|
60
|
+
label="assay_labels",
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
6
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BlankCfg(PluginConfig):
|
|
10
|
+
method: str = "disregard" # disregard | subtract
|
|
11
|
+
capture_blanks: bool = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detect_blanks(df: pd.DataFrame) -> pd.DataFrame:
|
|
15
|
+
out = df.copy()
|
|
16
|
+
mask = False
|
|
17
|
+
for col in ["is_blank", "blank", "isBlank"]:
|
|
18
|
+
if col in out.columns:
|
|
19
|
+
try:
|
|
20
|
+
mask = mask | out[col].astype(bool)
|
|
21
|
+
except Exception:
|
|
22
|
+
mask = mask | out[col].astype(str).str.lower().isin({"true", "1", "t", "yes"})
|
|
23
|
+
for col in ["treatment", "genotype", "sample_type"]:
|
|
24
|
+
if col in out.columns:
|
|
25
|
+
mask = mask | out[col].astype(str).str.contains("blank", case=False, na=False)
|
|
26
|
+
return out[mask].copy() if isinstance(mask, pd.Series) else out.iloc[0:0].copy()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BlankCorrection(Plugin):
|
|
30
|
+
ConfigModel = BlankCfg
|
|
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 cls.passthrough_output_ports(
|
|
39
|
+
outputs={
|
|
40
|
+
"df": dataframe_output("df", "tidy.v1"),
|
|
41
|
+
"blanks": dataframe_output("blanks", "tidy.v1"),
|
|
42
|
+
},
|
|
43
|
+
passthrough={"df": "df", "blanks": "df"},
|
|
44
|
+
promoted_examples={
|
|
45
|
+
"df": ("plate_reader.annotated.v1",),
|
|
46
|
+
"blanks": ("plate_reader.annotated.v1",),
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def resolve_output_ports(self, *, inputs, outputs, cfg, where):
|
|
51
|
+
del cfg
|
|
52
|
+
return self.inherit_dataframe_output_ports(
|
|
53
|
+
inputs=inputs,
|
|
54
|
+
outputs=outputs,
|
|
55
|
+
passthrough={"df": "df", "blanks": "df"},
|
|
56
|
+
where=where,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def run(self, ctx, inputs, cfg: BlankCfg):
|
|
60
|
+
df: pd.DataFrame = inputs["df"].copy()
|
|
61
|
+
blanks = _detect_blanks(df)
|
|
62
|
+
if cfg.method == "disregard":
|
|
63
|
+
return {"df": df, "blanks": blanks}
|
|
64
|
+
if cfg.method == "subtract":
|
|
65
|
+
if blanks.empty:
|
|
66
|
+
return {"df": df, "blanks": blanks}
|
|
67
|
+
corr = (
|
|
68
|
+
blanks.assign(value=pd.to_numeric(blanks["value"], errors="coerce"))
|
|
69
|
+
.groupby("channel")["value"]
|
|
70
|
+
.median()
|
|
71
|
+
.rename("__blank__")
|
|
72
|
+
)
|
|
73
|
+
out = df.copy()
|
|
74
|
+
out["value"] = pd.to_numeric(out["value"], errors="coerce")
|
|
75
|
+
out = out.join(corr, on="channel")
|
|
76
|
+
out["value"] = out["value"] - out["__blank__"].fillna(0.0)
|
|
77
|
+
out = out.drop(columns="__blank__")
|
|
78
|
+
return {"df": out, "blanks": blanks}
|
|
79
|
+
raise ValueError(f"unknown method {cfg.method}")
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Crosstalk pairing transform over fold_change.v1 tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from reader_workbench.domains.logic.crosstalk import compute_crosstalk_pairs
|
|
11
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
12
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _pick_alias(df: pd.DataFrame, base: str | None) -> str | None:
|
|
16
|
+
if not base:
|
|
17
|
+
return None
|
|
18
|
+
alias = f"{base}_alias"
|
|
19
|
+
if base in df.columns:
|
|
20
|
+
return base
|
|
21
|
+
if alias in df.columns:
|
|
22
|
+
return alias
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CrosstalkPairsCfg(PluginConfig):
|
|
27
|
+
value_column: str = "log2FC"
|
|
28
|
+
value_scale: Literal["log2", "linear"] = Field(..., description="Scale of value_column.")
|
|
29
|
+
|
|
30
|
+
target: str | None = None
|
|
31
|
+
time_mode: Literal["single", "exact", "nearest", "latest", "all"] = Field(
|
|
32
|
+
..., description="How to select time(s) from the fold_change table."
|
|
33
|
+
)
|
|
34
|
+
time_policy: Literal["per_time", "all"] = Field(
|
|
35
|
+
"per_time", description="How to handle multiple times after selection."
|
|
36
|
+
)
|
|
37
|
+
time: float | None = None
|
|
38
|
+
times: list[float] | None = None
|
|
39
|
+
time_tolerance: float = 0.51
|
|
40
|
+
time_column: str = "time"
|
|
41
|
+
|
|
42
|
+
mapping_mode: Literal["explicit", "column", "top1"] = Field(
|
|
43
|
+
..., description="How to map design_id -> cognate treatment."
|
|
44
|
+
)
|
|
45
|
+
design_column: str = "design_id"
|
|
46
|
+
treatment_column: str = "treatment"
|
|
47
|
+
design_treatment_column: str | None = None
|
|
48
|
+
design_treatment_map: dict[str, str] = Field(default_factory=dict)
|
|
49
|
+
design_treatment_overrides: dict[str, str] = Field(default_factory=dict)
|
|
50
|
+
top1_tie_policy: Literal["error", "alphabetical"] = "error"
|
|
51
|
+
top1_tie_tolerance: float = 0.0
|
|
52
|
+
|
|
53
|
+
agg: Literal["median", "mean"] = "median"
|
|
54
|
+
|
|
55
|
+
require_self_treatment: bool = True
|
|
56
|
+
require_self_is_top1: bool = False
|
|
57
|
+
|
|
58
|
+
min_self: float | None = None
|
|
59
|
+
max_cross: float | None = None
|
|
60
|
+
max_other: float | None = None
|
|
61
|
+
min_self_minus_best_other: float | None = None
|
|
62
|
+
min_self_ratio_best_other: float | None = None
|
|
63
|
+
min_selectivity_delta: float | None = None
|
|
64
|
+
min_selectivity_ratio: float | None = None
|
|
65
|
+
|
|
66
|
+
only_passing: bool = True
|
|
67
|
+
top_n: int = 10
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CrosstalkPairs(Plugin):
|
|
71
|
+
"""Compute crosstalk-safe design pairings from a fold_change.v1 table."""
|
|
72
|
+
|
|
73
|
+
ConfigModel = CrosstalkPairsCfg
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def input_ports(cls):
|
|
77
|
+
return {"table": dataframe_input("table", "fold_change.v1")}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def output_ports(cls):
|
|
81
|
+
return {"table": dataframe_output("table", "crosstalk_pairs.v1")}
|
|
82
|
+
|
|
83
|
+
def run(self, ctx, inputs: dict[str, Any], cfg: CrosstalkPairsCfg) -> dict[str, Any]:
|
|
84
|
+
df = inputs["table"].copy()
|
|
85
|
+
|
|
86
|
+
dcol = _pick_alias(df, cfg.design_column)
|
|
87
|
+
tcol = _pick_alias(df, cfg.treatment_column)
|
|
88
|
+
if dcol is None:
|
|
89
|
+
raise ValueError(f"crosstalk_pairs: design column '{cfg.design_column}' (or its alias) is missing")
|
|
90
|
+
if tcol is None:
|
|
91
|
+
raise ValueError(f"crosstalk_pairs: treatment column '{cfg.treatment_column}' (or its alias) is missing")
|
|
92
|
+
|
|
93
|
+
map_col = _pick_alias(df, cfg.design_treatment_column) if cfg.design_treatment_column else None
|
|
94
|
+
|
|
95
|
+
result = compute_crosstalk_pairs(
|
|
96
|
+
df,
|
|
97
|
+
design_col=dcol,
|
|
98
|
+
treatment_col=tcol,
|
|
99
|
+
value_col=cfg.value_column,
|
|
100
|
+
value_scale=cfg.value_scale,
|
|
101
|
+
target=cfg.target,
|
|
102
|
+
time_mode=cfg.time_mode,
|
|
103
|
+
time_policy=cfg.time_policy,
|
|
104
|
+
time=cfg.time,
|
|
105
|
+
times=cfg.times,
|
|
106
|
+
time_column=cfg.time_column,
|
|
107
|
+
time_tolerance=cfg.time_tolerance,
|
|
108
|
+
mapping_mode=cfg.mapping_mode,
|
|
109
|
+
design_treatment_column=map_col,
|
|
110
|
+
design_treatment_map=cfg.design_treatment_map,
|
|
111
|
+
design_treatment_overrides=cfg.design_treatment_overrides,
|
|
112
|
+
top1_tie_policy=cfg.top1_tie_policy,
|
|
113
|
+
top1_tie_tolerance=cfg.top1_tie_tolerance,
|
|
114
|
+
require_self_treatment=cfg.require_self_treatment,
|
|
115
|
+
require_self_is_top1=cfg.require_self_is_top1,
|
|
116
|
+
agg=cfg.agg,
|
|
117
|
+
min_self=cfg.min_self,
|
|
118
|
+
max_cross=cfg.max_cross,
|
|
119
|
+
max_other=cfg.max_other,
|
|
120
|
+
min_self_minus_best_other=cfg.min_self_minus_best_other,
|
|
121
|
+
min_self_ratio_best_other=cfg.min_self_ratio_best_other,
|
|
122
|
+
min_selectivity_delta=cfg.min_selectivity_delta,
|
|
123
|
+
min_selectivity_ratio=cfg.min_selectivity_ratio,
|
|
124
|
+
only_passing=cfg.only_passing,
|
|
125
|
+
logger=ctx.logger,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
pairs = result.pairs
|
|
129
|
+
designs = result.designs
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
total_designs = int(designs["design_id"].nunique()) if "design_id" in designs.columns else 0
|
|
133
|
+
time_count = len(result.times_used)
|
|
134
|
+
total_pairs = total_designs * (total_designs - 1) // 2 * max(1, time_count)
|
|
135
|
+
passing = int(pairs.shape[0])
|
|
136
|
+
ctx.logger.info(
|
|
137
|
+
"crosstalk_pairs - designs=%d - times=%d - pairs=%d - passing=%d - metric=%s",
|
|
138
|
+
total_designs,
|
|
139
|
+
time_count,
|
|
140
|
+
total_pairs,
|
|
141
|
+
passing,
|
|
142
|
+
cfg.value_column,
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
if not pairs.empty:
|
|
149
|
+
preview = pairs.copy()
|
|
150
|
+
preview["pair_score"] = pd.to_numeric(preview["pair_score"], errors="coerce")
|
|
151
|
+
preview = preview[preview["pair_score"].notna()].sort_values("pair_score", ascending=False)
|
|
152
|
+
if not preview.empty:
|
|
153
|
+
n_show = max(1, int(cfg.top_n))
|
|
154
|
+
ctx.logger.info("crosstalk_pairs - top %d pairs by pair_score:", n_show)
|
|
155
|
+
for _, row in preview.head(n_show).iterrows():
|
|
156
|
+
ctx.logger.info(
|
|
157
|
+
" - t=%s | %s <-> %s | self=%s/%s | cross=%s/%s | score=%.3g",
|
|
158
|
+
_fmt(row.get("time")),
|
|
159
|
+
row.get("design_a"),
|
|
160
|
+
row.get("design_b"),
|
|
161
|
+
_fmt(row.get("a_self_value")),
|
|
162
|
+
_fmt(row.get("b_self_value")),
|
|
163
|
+
_fmt(row.get("a_cross_to_b")),
|
|
164
|
+
_fmt(row.get("b_cross_to_a")),
|
|
165
|
+
float(row.get("pair_score")) if pd.notna(row.get("pair_score")) else float("nan"),
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return {"table": pairs}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _fmt(val: object) -> str:
|
|
174
|
+
try:
|
|
175
|
+
f = float(val)
|
|
176
|
+
if pd.isna(f):
|
|
177
|
+
return "nan"
|
|
178
|
+
return f"{f:.3g}"
|
|
179
|
+
except Exception:
|
|
180
|
+
return str(val)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Explicit cytometry gating transform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
|
|
9
|
+
from reader_workbench.domains.cytometry.analysis import GateSpec, ThresholdSpec
|
|
10
|
+
from reader_workbench.domains.cytometry.analysis.workflow import (
|
|
11
|
+
CytometryGatingRequest,
|
|
12
|
+
CytometryQCSpec,
|
|
13
|
+
run_cytometry_gating,
|
|
14
|
+
)
|
|
15
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
16
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CytometryGatingCfg(PluginConfig):
|
|
20
|
+
cells_x_channel: str = Field(min_length=1)
|
|
21
|
+
cells_y_channel: str = Field(min_length=1)
|
|
22
|
+
cells_x_range: tuple[float, float]
|
|
23
|
+
cells_y_range: tuple[float, float]
|
|
24
|
+
singlet_x_channel: str = Field(min_length=1)
|
|
25
|
+
singlet_y_channel: str = Field(min_length=1)
|
|
26
|
+
singlet_ratio_range: tuple[float, float]
|
|
27
|
+
cells_enabled: bool
|
|
28
|
+
singlets_enabled: bool
|
|
29
|
+
fluorescence_channel: str = Field(min_length=1)
|
|
30
|
+
threshold_mode: Literal["manual", "from_control_quantile"]
|
|
31
|
+
threshold_value: float | None
|
|
32
|
+
threshold_group_column: str | None
|
|
33
|
+
threshold_control_value: str | None
|
|
34
|
+
threshold_quantile: float | None
|
|
35
|
+
group_column: str | None
|
|
36
|
+
minimum_final_events: int = Field(ge=0)
|
|
37
|
+
minimum_final_percent: float = Field(ge=0.0, le=100.0)
|
|
38
|
+
maximum_nonpositive_percent: float = Field(ge=0.0, le=100.0)
|
|
39
|
+
nonpositive_scope: Literal["all_events", "gated_events"]
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="after")
|
|
42
|
+
def _validate_threshold_policy(self):
|
|
43
|
+
if self.threshold_mode == "manual":
|
|
44
|
+
if self.threshold_value is None:
|
|
45
|
+
raise ValueError("threshold_value is required for manual thresholding")
|
|
46
|
+
if any(
|
|
47
|
+
value is not None
|
|
48
|
+
for value in (self.threshold_group_column, self.threshold_control_value, self.threshold_quantile)
|
|
49
|
+
):
|
|
50
|
+
raise ValueError("manual thresholding may not declare control threshold fields")
|
|
51
|
+
return self
|
|
52
|
+
if self.threshold_value is not None:
|
|
53
|
+
raise ValueError("threshold_value must be null for control-quantile thresholding")
|
|
54
|
+
if not self.threshold_group_column:
|
|
55
|
+
raise ValueError("threshold_group_column is required for control-quantile thresholding")
|
|
56
|
+
if not self.threshold_control_value:
|
|
57
|
+
raise ValueError("threshold_control_value is required for control-quantile thresholding")
|
|
58
|
+
if self.threshold_quantile is None or not 0.0 <= self.threshold_quantile <= 1.0:
|
|
59
|
+
raise ValueError("threshold_quantile must be between 0 and 1 for control-quantile thresholding")
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CytometryGatingTransform(Plugin):
|
|
64
|
+
ConfigModel = CytometryGatingCfg
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def input_ports(cls):
|
|
68
|
+
return {"events": dataframe_input("events", "tidy.v1")}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def output_ports(cls):
|
|
72
|
+
return {
|
|
73
|
+
"gate_definition": dataframe_output("gate_definition", "cytometry.gate_definition.v1"),
|
|
74
|
+
"gated_events": dataframe_output("gated_events", "cytometry.gated_events.v1"),
|
|
75
|
+
"sample_stats": dataframe_output("sample_stats", "cytometry.sample_stats.v1"),
|
|
76
|
+
"group_stats": dataframe_output("group_stats", "cytometry.group_stats.v1"),
|
|
77
|
+
"qc": dataframe_output("qc", "cytometry.qc.v1"),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def run(self, ctx, inputs, cfg: CytometryGatingCfg):
|
|
81
|
+
del ctx
|
|
82
|
+
threshold = ThresholdSpec(
|
|
83
|
+
channel=cfg.fluorescence_channel,
|
|
84
|
+
value=float(cfg.threshold_value or 0.0),
|
|
85
|
+
mode=cfg.threshold_mode,
|
|
86
|
+
group_column=cfg.threshold_group_column,
|
|
87
|
+
control_value=cfg.threshold_control_value,
|
|
88
|
+
quantile=float(cfg.threshold_quantile or 0.0),
|
|
89
|
+
)
|
|
90
|
+
result = run_cytometry_gating(
|
|
91
|
+
inputs["events"],
|
|
92
|
+
CytometryGatingRequest(
|
|
93
|
+
gate=GateSpec(
|
|
94
|
+
cells_x_channel=cfg.cells_x_channel,
|
|
95
|
+
cells_y_channel=cfg.cells_y_channel,
|
|
96
|
+
cells_x_range=cfg.cells_x_range,
|
|
97
|
+
cells_y_range=cfg.cells_y_range,
|
|
98
|
+
singlet_x_channel=cfg.singlet_x_channel,
|
|
99
|
+
singlet_y_channel=cfg.singlet_y_channel,
|
|
100
|
+
singlet_ratio_range=cfg.singlet_ratio_range,
|
|
101
|
+
cells_enabled=cfg.cells_enabled,
|
|
102
|
+
singlets_enabled=cfg.singlets_enabled,
|
|
103
|
+
),
|
|
104
|
+
threshold=threshold,
|
|
105
|
+
group_column=cfg.group_column,
|
|
106
|
+
qc=CytometryQCSpec(
|
|
107
|
+
minimum_final_events=cfg.minimum_final_events,
|
|
108
|
+
minimum_final_percent=cfg.minimum_final_percent,
|
|
109
|
+
maximum_nonpositive_percent=cfg.maximum_nonpositive_percent,
|
|
110
|
+
nonpositive_scope=cfg.nonpositive_scope,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
return {
|
|
115
|
+
"gate_definition": result.gate_definition.to_pandas(),
|
|
116
|
+
"gated_events": result.gated_events.to_pandas(),
|
|
117
|
+
"sample_stats": result.sample_stats.to_pandas(),
|
|
118
|
+
"group_stats": result.group_stats.to_pandas(),
|
|
119
|
+
"qc": result.qc.to_pandas(),
|
|
120
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Fold-change report:
|
|
2
|
+
• Selects the nearest snapshot time(s) per group
|
|
3
|
+
• Computes FC against explicit baselines (global or per-group overrides)
|
|
4
|
+
• Emits a validated artifact (fold_change.v1) — no mutation of the main tidy df
|
|
5
|
+
• Prints a concise, rich stdout summary (via logger) for quick inspection"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from reader_workbench.domains.plate_reader.analysis import FoldChangeAnalysisSpec, compute_fold_change_table
|
|
14
|
+
from reader_workbench.workbench.ports import dataframe_input, dataframe_output
|
|
15
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
16
|
+
|
|
17
|
+
# ----------------------------- config model -----------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FoldChangeCfg(PluginConfig):
|
|
21
|
+
# What to compute
|
|
22
|
+
target: str # e.g., "YFP/CFP" or "YFP/OD600"
|
|
23
|
+
report_times: list[float] # e.g., [8.0, 14.0]
|
|
24
|
+
time_tolerance: float = 0.51 # nearest-time selection tolerance (h)
|
|
25
|
+
observation_stat: Literal["median", "mean"] = "median"
|
|
26
|
+
|
|
27
|
+
# Grouping and labels
|
|
28
|
+
treatment_column: str = "treatment" # we will prefer '<col>_alias' when present
|
|
29
|
+
group_by: list[str] = Field(default_factory=lambda: ["design_id"])
|
|
30
|
+
expected_treatments: list[str] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
# Baseline policy
|
|
33
|
+
use_global_baseline: bool = False
|
|
34
|
+
global_baseline_value: str | None = None # used when use_global_baseline==True
|
|
35
|
+
# overrides: list of maps; any keys matching group_by columns define a match; each must
|
|
36
|
+
# include 'baseline_value'. Example:
|
|
37
|
+
# - { design_id: "araBADp", baseline_value: "0 uM arabinose" }
|
|
38
|
+
overrides: list[dict[str, Any]] = Field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
# Output columns (names)
|
|
41
|
+
fc_column: str = "FC"
|
|
42
|
+
log2fc_column: str = "log2FC"
|
|
43
|
+
|
|
44
|
+
# Attach extra metadata columns if present (won't be required by contract, just carried through)
|
|
45
|
+
attach_metadata: list[str] = Field(default_factory=lambda: ["batch"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FoldChange(Plugin):
|
|
49
|
+
"""Contract-driven transform that emits a fold_change.v1 table."""
|
|
50
|
+
|
|
51
|
+
ConfigModel = FoldChangeCfg
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def input_ports(cls):
|
|
55
|
+
return {"df": dataframe_input("df", "tidy.v1")}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def output_ports(cls):
|
|
59
|
+
return {"table": dataframe_output("table", "fold_change.v1")}
|
|
60
|
+
|
|
61
|
+
# ------------------------------- run --------------------------------
|
|
62
|
+
|
|
63
|
+
def run(self, ctx, inputs, cfg: FoldChangeCfg):
|
|
64
|
+
spec = FoldChangeAnalysisSpec(
|
|
65
|
+
target=cfg.target,
|
|
66
|
+
report_times=tuple(cfg.report_times),
|
|
67
|
+
time_tolerance=cfg.time_tolerance,
|
|
68
|
+
observation_stat=cfg.observation_stat,
|
|
69
|
+
treatment_column=cfg.treatment_column,
|
|
70
|
+
group_by=tuple(cfg.group_by),
|
|
71
|
+
expected_treatments=tuple(cfg.expected_treatments),
|
|
72
|
+
use_global_baseline=cfg.use_global_baseline,
|
|
73
|
+
global_baseline_value=cfg.global_baseline_value,
|
|
74
|
+
overrides=tuple(dict(rule) for rule in cfg.overrides),
|
|
75
|
+
fc_column=cfg.fc_column,
|
|
76
|
+
log2fc_column=cfg.log2fc_column,
|
|
77
|
+
attach_metadata=tuple(cfg.attach_metadata),
|
|
78
|
+
)
|
|
79
|
+
return {"table": compute_fold_change_table(inputs["df"], spec=spec, logger=ctx.logger)}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from reader_workbench.domains.plate_reader.analysis.four_state_event_window.contracts import (
|
|
8
|
+
FourStateEventWindowAnalysisSpec,
|
|
9
|
+
)
|
|
10
|
+
from reader_workbench.domains.plate_reader.analysis.four_state_event_window.materialize import materialize_experiment
|
|
11
|
+
from reader_workbench.domains.plate_reader.analysis.four_state_event_window.sources import build_experiment_source
|
|
12
|
+
from reader_workbench.workbench.ports import dataframe_output, record_collection_input
|
|
13
|
+
from reader_workbench.workbench.records import SourceRecordCollection
|
|
14
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FourStateEventWindowCfg(PluginConfig):
|
|
18
|
+
source: dict[str, Any]
|
|
19
|
+
event: dict[str, Any]
|
|
20
|
+
reductions: list[dict[str, Any]]
|
|
21
|
+
aggregation: dict[str, Any]
|
|
22
|
+
quality: dict[str, Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FourStateEventWindowTransform(Plugin):
|
|
26
|
+
"""Materialize event-relative summaries from aligned source record collections."""
|
|
27
|
+
|
|
28
|
+
ConfigModel = FourStateEventWindowCfg
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def input_ports(cls):
|
|
32
|
+
source = "plate_reader.annotated.v1"
|
|
33
|
+
return {
|
|
34
|
+
"response_records": record_collection_input("response_records", source),
|
|
35
|
+
"magnitude_records": record_collection_input("magnitude_records", source),
|
|
36
|
+
"trajectory_records": record_collection_input("trajectory_records", source),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def output_ports(cls):
|
|
41
|
+
return {
|
|
42
|
+
"wells": dataframe_output("wells", "plate_reader.four_state_event_window.wells.v3"),
|
|
43
|
+
"designs": dataframe_output("designs", "plate_reader.four_state_event_window.designs.v4"),
|
|
44
|
+
"descriptive_resampling_draws": dataframe_output(
|
|
45
|
+
"descriptive_resampling_draws",
|
|
46
|
+
"plate_reader.four_state_event_window.descriptive_resampling_draws.v3",
|
|
47
|
+
),
|
|
48
|
+
"traces": dataframe_output("traces", "plate_reader.four_state_event_window.traces.v3"),
|
|
49
|
+
"events": dataframe_output("events", "plate_reader.four_state_event_window.events.v2"),
|
|
50
|
+
"dispositions": dataframe_output(
|
|
51
|
+
"dispositions",
|
|
52
|
+
"plate_reader.four_state_event_window.dispositions.v1",
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def run(self, ctx, inputs, cfg):
|
|
57
|
+
spec = FourStateEventWindowAnalysisSpec.from_mapping(cfg.model_dump())
|
|
58
|
+
response: SourceRecordCollection = inputs["response_records"]
|
|
59
|
+
magnitude: SourceRecordCollection = inputs["magnitude_records"]
|
|
60
|
+
trajectory: SourceRecordCollection = inputs["trajectory_records"]
|
|
61
|
+
experiment_ids = tuple(item.ref.experiment_id for item in response)
|
|
62
|
+
if len(set(experiment_ids)) != len(experiment_ids):
|
|
63
|
+
raise ValueError("four-state event-window source experiments must be unique")
|
|
64
|
+
spec.source.require_known_experiment_ids(experiment_ids)
|
|
65
|
+
for label, collection in (("magnitude", magnitude), ("trajectory", trajectory)):
|
|
66
|
+
observed = tuple(item.ref.experiment_id for item in collection)
|
|
67
|
+
if observed != experiment_ids:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"four-state event-window {label} source order must match response source experiments: "
|
|
70
|
+
f"expected {experiment_ids}, got {observed}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
materialized = []
|
|
74
|
+
for response_item, magnitude_item, trajectory_item in zip(
|
|
75
|
+
response,
|
|
76
|
+
magnitude,
|
|
77
|
+
trajectory,
|
|
78
|
+
strict=True,
|
|
79
|
+
):
|
|
80
|
+
source = build_experiment_source(
|
|
81
|
+
experiment_id=response_item.ref.experiment_id,
|
|
82
|
+
response_frame=response_item.load_dataframe(),
|
|
83
|
+
magnitude_frame=magnitude_item.load_dataframe(),
|
|
84
|
+
trajectory_frame=trajectory_item.load_dataframe(),
|
|
85
|
+
source_spec=spec.source,
|
|
86
|
+
event_spec=spec.event,
|
|
87
|
+
)
|
|
88
|
+
materialized.append(materialize_experiment(source, request=spec))
|
|
89
|
+
names = ("wells", "designs", "descriptive_resampling_draws", "traces", "events", "dispositions")
|
|
90
|
+
return {
|
|
91
|
+
name: pd.concat([frames[index] for frames in materialized], ignore_index=True)
|
|
92
|
+
for index, name in enumerate(names)
|
|
93
|
+
}
|