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,175 @@
|
|
|
1
|
+
"""Shared palette + subplot layout helpers for plotting modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import matplotlib as mpl
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
|
|
12
|
+
_PALETTES: dict[str, list[str]] = {
|
|
13
|
+
"colorblind": [
|
|
14
|
+
"#0072B2",
|
|
15
|
+
"#E69F00",
|
|
16
|
+
"#009E73",
|
|
17
|
+
"#CC79A7",
|
|
18
|
+
"#56B4E9",
|
|
19
|
+
"#D55E00",
|
|
20
|
+
"#F0E442",
|
|
21
|
+
"#000000",
|
|
22
|
+
],
|
|
23
|
+
"muted": [
|
|
24
|
+
"#4878CF",
|
|
25
|
+
"#6ACC65",
|
|
26
|
+
"#D65F5F",
|
|
27
|
+
"#B47CC7",
|
|
28
|
+
"#C4AD66",
|
|
29
|
+
"#77BEDB",
|
|
30
|
+
"#4D4D4D",
|
|
31
|
+
"#A0A0A0",
|
|
32
|
+
],
|
|
33
|
+
"tableau": [
|
|
34
|
+
"#4E79A7",
|
|
35
|
+
"#F28E2B",
|
|
36
|
+
"#E15759",
|
|
37
|
+
"#76B7B2",
|
|
38
|
+
"#59A14F",
|
|
39
|
+
"#EDC948",
|
|
40
|
+
"#B07AA1",
|
|
41
|
+
"#FF9DA7",
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class PaletteBook:
|
|
48
|
+
"""Small helper carried in RunContext (see workbench/context.py)."""
|
|
49
|
+
|
|
50
|
+
name: str = "colorblind"
|
|
51
|
+
|
|
52
|
+
def colors(self, n: int) -> list[str]:
|
|
53
|
+
if self.name not in _PALETTES:
|
|
54
|
+
opts = ", ".join(sorted(_PALETTES))
|
|
55
|
+
raise ValueError(f"Unknown palette '{self.name}'. Available: {opts}")
|
|
56
|
+
palette = _PALETTES[self.name]
|
|
57
|
+
if n <= len(palette):
|
|
58
|
+
return palette[:n]
|
|
59
|
+
out = []
|
|
60
|
+
while len(out) < n:
|
|
61
|
+
out.extend(palette)
|
|
62
|
+
return out[:n]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def available_palettes() -> list[str]:
|
|
66
|
+
return sorted(_PALETTES)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
DEFAULT_RC = {
|
|
70
|
+
"figure_figsize": (5, 5),
|
|
71
|
+
"savefig_dpi": 300,
|
|
72
|
+
"axes_spines_top": False,
|
|
73
|
+
"axes_spines_right": False,
|
|
74
|
+
"axes_titleweight": "bold",
|
|
75
|
+
"axes_labelweight": "regular",
|
|
76
|
+
"axes_grid": True,
|
|
77
|
+
"grid_alpha": 0.25,
|
|
78
|
+
"grid_linestyle": "-",
|
|
79
|
+
"grid_color": "#B0B0B0",
|
|
80
|
+
"axes_axisbelow": True,
|
|
81
|
+
"font_size": 13.0,
|
|
82
|
+
"axes_labelsize": 13.0,
|
|
83
|
+
"axes_titlesize": 14.0,
|
|
84
|
+
"xtick_labelsize": 12.0,
|
|
85
|
+
"ytick_labelsize": 12.0,
|
|
86
|
+
"legend_fontsize": 12.0,
|
|
87
|
+
"legend_title_fontsize": 12.0,
|
|
88
|
+
"xtick_direction": "out",
|
|
89
|
+
"ytick_direction": "out",
|
|
90
|
+
"legend_frameon": False,
|
|
91
|
+
"pdf_fonttype": 42,
|
|
92
|
+
"pdf_compression": 9,
|
|
93
|
+
"path_simplify": True,
|
|
94
|
+
"path_simplify_threshold": 0.0,
|
|
95
|
+
"agg_path_chunksize": 20000,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextlib.contextmanager
|
|
100
|
+
def use_style(rc: dict | None = None, color_cycle: Iterable[str] | None = None):
|
|
101
|
+
"""Context manager to push a small, opinionated Matplotlib style."""
|
|
102
|
+
rc = {**DEFAULT_RC, **(rc or {})}
|
|
103
|
+
scale = float(rc.pop("font_scale", 1.0))
|
|
104
|
+
|
|
105
|
+
def _scaled(key: str) -> float:
|
|
106
|
+
value = float(rc.get(key, DEFAULT_RC[key]))
|
|
107
|
+
return value * scale
|
|
108
|
+
|
|
109
|
+
with mpl.rc_context():
|
|
110
|
+
mpl.rcParams.update(
|
|
111
|
+
{
|
|
112
|
+
"figure.figsize": rc["figure_figsize"],
|
|
113
|
+
"savefig.dpi": rc["savefig_dpi"],
|
|
114
|
+
"axes.spines.top": rc["axes_spines_top"],
|
|
115
|
+
"axes.spines.right": rc["axes_spines_right"],
|
|
116
|
+
"axes.titleweight": rc["axes_titleweight"],
|
|
117
|
+
"axes.labelweight": rc["axes_labelweight"],
|
|
118
|
+
"axes.grid": rc["axes_grid"],
|
|
119
|
+
"grid.alpha": rc["grid_alpha"],
|
|
120
|
+
"grid.linestyle": rc["grid_linestyle"],
|
|
121
|
+
"grid.color": rc["grid_color"],
|
|
122
|
+
"axes.axisbelow": rc["axes_axisbelow"],
|
|
123
|
+
"font.size": _scaled("font_size"),
|
|
124
|
+
"axes.labelsize": _scaled("axes_labelsize"),
|
|
125
|
+
"axes.titlesize": _scaled("axes_titlesize"),
|
|
126
|
+
"xtick.labelsize": _scaled("xtick_labelsize"),
|
|
127
|
+
"ytick.labelsize": _scaled("ytick_labelsize"),
|
|
128
|
+
"legend.fontsize": _scaled("legend_fontsize"),
|
|
129
|
+
"legend.title_fontsize": _scaled("legend_title_fontsize"),
|
|
130
|
+
"xtick.direction": rc["xtick_direction"],
|
|
131
|
+
"ytick.direction": rc["ytick_direction"],
|
|
132
|
+
"legend.frameon": rc["legend_frameon"],
|
|
133
|
+
"pdf.fonttype": rc["pdf_fonttype"],
|
|
134
|
+
"pdf.compression": int(rc["pdf_compression"]),
|
|
135
|
+
"path.simplify": bool(rc["path_simplify"]),
|
|
136
|
+
"path.simplify_threshold": float(rc["path_simplify_threshold"]),
|
|
137
|
+
"agg.path.chunksize": int(rc["agg_path_chunksize"]),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
if color_cycle is not None:
|
|
141
|
+
mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=list(color_cycle))
|
|
142
|
+
yield
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def new_fig_ax(fig_kwargs: dict | None = None):
|
|
146
|
+
"""
|
|
147
|
+
Consistent figure construction.
|
|
148
|
+
Only pass kwargs that Matplotlib's Figure/subplots actually understand.
|
|
149
|
+
Plot- or style-level options must be consumed by the caller and not forwarded.
|
|
150
|
+
"""
|
|
151
|
+
fig_kwargs = dict(fig_kwargs or {})
|
|
152
|
+
allowed = {
|
|
153
|
+
"num",
|
|
154
|
+
"figsize",
|
|
155
|
+
"dpi",
|
|
156
|
+
"facecolor",
|
|
157
|
+
"edgecolor",
|
|
158
|
+
"frameon",
|
|
159
|
+
"clear",
|
|
160
|
+
"constrained_layout",
|
|
161
|
+
"layout",
|
|
162
|
+
"squeeze",
|
|
163
|
+
"subplot_kw",
|
|
164
|
+
"gridspec_kw",
|
|
165
|
+
"sharex",
|
|
166
|
+
"sharey",
|
|
167
|
+
}
|
|
168
|
+
fig_kwargs = {key: value for key, value in fig_kwargs.items() if key in allowed}
|
|
169
|
+
if "figsize" not in fig_kwargs:
|
|
170
|
+
fig_kwargs["figsize"] = DEFAULT_RC["figure_figsize"]
|
|
171
|
+
fig_kwargs.setdefault("constrained_layout", True)
|
|
172
|
+
return plt.subplots(**fig_kwargs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = ["DEFAULT_RC", "PaletteBook", "available_palettes", "new_fig_ax", "use_style"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Shared plotting utilities (filesystem + naming helpers)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ensure_dir(path: Path) -> None:
|
|
10
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def slugify(value: str) -> str:
|
|
14
|
+
value = str(value).strip()
|
|
15
|
+
value = re.sub(r"[^\w\-.]+", "_", value)
|
|
16
|
+
return re.sub(r"_{2,}", "_", value).strip("_")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_figure(fig, output_dir: Path, filename_stub: str, ext: str = "pdf", dpi: int | None = None) -> Path:
|
|
20
|
+
"""
|
|
21
|
+
Save figures as PDF by default (print-friendly, vector).
|
|
22
|
+
Use ext="png" if you explicitly need rasters.
|
|
23
|
+
"""
|
|
24
|
+
ensure_dir(output_dir)
|
|
25
|
+
out = output_dir / f"{slugify(filename_stub)}.{ext}"
|
|
26
|
+
fig.savefig(out, bbox_inches="tight", dpi=dpi)
|
|
27
|
+
return out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in workbench plugins shipped with Reader."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from reader_workbench.workbench.assets import AssetCatalog, AssetDescriptor
|
|
7
|
+
|
|
8
|
+
_CATEGORY_MODULES = {
|
|
9
|
+
"ingest": "reader_workbench.plugins.manifests.ingest",
|
|
10
|
+
"transform": "reader_workbench.plugins.manifests.transform",
|
|
11
|
+
"plot": "reader_workbench.plugins.manifests.plot",
|
|
12
|
+
"export": "reader_workbench.plugins.manifests.export",
|
|
13
|
+
"validator": "reader_workbench.plugins.manifests.validator",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _category_descriptors(category: str) -> tuple[AssetDescriptor, ...]:
|
|
18
|
+
module = importlib.import_module(_CATEGORY_MODULES[category])
|
|
19
|
+
return cast(tuple[AssetDescriptor, ...], module.BUILTIN_PLUGIN_DESCRIPTORS)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def builtin_plugin_catalog(*, categories: set[str] | None = None) -> AssetCatalog:
|
|
23
|
+
selected = (
|
|
24
|
+
tuple(_CATEGORY_MODULES)
|
|
25
|
+
if categories is None
|
|
26
|
+
else tuple(category for category in _CATEGORY_MODULES if category in categories)
|
|
27
|
+
)
|
|
28
|
+
descriptors = [descriptor for category in selected for descriptor in _category_descriptors(category)]
|
|
29
|
+
return AssetCatalog(descriptors)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def builtin_plugin_descriptors(*, categories: set[str] | None = None) -> tuple[AssetDescriptor, ...]:
|
|
33
|
+
return builtin_plugin_catalog(categories=categories).all()
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from reader_workbench.errors import ExecutionError
|
|
6
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_export_path(raw: str, *, exports_dir: Path) -> Path:
|
|
10
|
+
path = Path(raw).expanduser()
|
|
11
|
+
if path.is_absolute():
|
|
12
|
+
raise ExecutionError("Export paths must be relative to the experiment exports directory")
|
|
13
|
+
try:
|
|
14
|
+
resolved = resolve_path_within_root(path, root=exports_dir)
|
|
15
|
+
except ValueError as exc:
|
|
16
|
+
raise ExecutionError(
|
|
17
|
+
"Export paths must stay under the experiment exports directory after resolving symlinks"
|
|
18
|
+
) from exc
|
|
19
|
+
if resolved == exports_dir.resolve():
|
|
20
|
+
raise ExecutionError("Export paths must identify a file below the experiment exports directory")
|
|
21
|
+
return resolved
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ExecutionError
|
|
9
|
+
from reader_workbench.workbench.ports import dataframe_input, file_path_output
|
|
10
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
11
|
+
|
|
12
|
+
from ._paths import resolve_export_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExportCsvCfg(PluginConfig):
|
|
16
|
+
path: str = Field(..., description="Output CSV path relative to the experiment exports directory.")
|
|
17
|
+
index: bool = False
|
|
18
|
+
sep: str = ","
|
|
19
|
+
na_rep: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExportCsv(Plugin):
|
|
23
|
+
ConfigModel = ExportCsvCfg
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def input_ports(cls):
|
|
27
|
+
return {"df": dataframe_input("df")}
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def output_ports(cls):
|
|
31
|
+
return {"artifact": file_path_output("artifact")}
|
|
32
|
+
|
|
33
|
+
def run(self, ctx, inputs: dict[str, Any], cfg: ExportCsvCfg) -> dict[str, Any]:
|
|
34
|
+
df = inputs["df"]
|
|
35
|
+
if not isinstance(df, pd.DataFrame):
|
|
36
|
+
raise ExecutionError(f"export/csv expects a DataFrame input, got {type(df).__name__}")
|
|
37
|
+
out_path = resolve_export_path(cfg.path, exports_dir=ctx.exports_dir)
|
|
38
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
df.to_csv(out_path, index=cfg.index, sep=cfg.sep, na_rep=cfg.na_rep)
|
|
40
|
+
ctx.logger.info("export • csv → %s", out_path)
|
|
41
|
+
return {"artifact": out_path}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ExecutionError
|
|
9
|
+
from reader_workbench.workbench.ports import dataframe_input, file_path_output
|
|
10
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig
|
|
11
|
+
|
|
12
|
+
from ._paths import resolve_export_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExportXlsxCfg(PluginConfig):
|
|
16
|
+
path: str = Field(..., description="Output XLSX path relative to the experiment exports directory.")
|
|
17
|
+
sheet_name: str = "Sheet1"
|
|
18
|
+
index: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExportXlsx(Plugin):
|
|
22
|
+
ConfigModel = ExportXlsxCfg
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def input_ports(cls):
|
|
26
|
+
return {"df": dataframe_input("df")}
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def output_ports(cls):
|
|
30
|
+
return {"artifact": file_path_output("artifact")}
|
|
31
|
+
|
|
32
|
+
def run(self, ctx, inputs: dict[str, Any], cfg: ExportXlsxCfg) -> dict[str, Any]:
|
|
33
|
+
df = inputs["df"]
|
|
34
|
+
if not isinstance(df, pd.DataFrame):
|
|
35
|
+
raise ExecutionError(f"export/xlsx expects a DataFrame input, got {type(df).__name__}")
|
|
36
|
+
out_path = resolve_export_path(cfg.path, exports_dir=ctx.exports_dir)
|
|
37
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
try:
|
|
39
|
+
with pd.ExcelWriter(out_path, engine="openpyxl") as writer:
|
|
40
|
+
df.to_excel(writer, index=cfg.index, sheet_name=cfg.sheet_name)
|
|
41
|
+
except ImportError as e:
|
|
42
|
+
raise ExecutionError("export/xlsx requires the 'openpyxl' dependency to write .xlsx files.") from e
|
|
43
|
+
ctx.logger.info("export • xlsx → %s", out_path)
|
|
44
|
+
return {"artifact": out_path}
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from reader_workbench.errors import ParseError
|
|
7
|
+
|
|
8
|
+
from .discovery_policy import DEFAULT_ROOTS, discover_files
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def auto_pick_discovered_file(
|
|
12
|
+
files: Sequence[Path],
|
|
13
|
+
mode: str,
|
|
14
|
+
*,
|
|
15
|
+
singular_label: str,
|
|
16
|
+
) -> Path:
|
|
17
|
+
if mode == "single":
|
|
18
|
+
if len(files) != 1:
|
|
19
|
+
raise ParseError(
|
|
20
|
+
f"Auto-discovery expected exactly one {singular_label}, found "
|
|
21
|
+
f"{len(files)}:\n- " + "\n- ".join(str(path) for path in files)
|
|
22
|
+
)
|
|
23
|
+
return files[0]
|
|
24
|
+
if mode == "latest":
|
|
25
|
+
return max(files, key=lambda path: path.stat().st_mtime)
|
|
26
|
+
raise ParseError(f"Unknown auto_pick mode {mode!r} (expected: single|latest|merge)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def discover_auto_input_files(
|
|
30
|
+
*,
|
|
31
|
+
exp_dir: Path,
|
|
32
|
+
auto_roots: Sequence[str] | None,
|
|
33
|
+
auto_include: Sequence[str],
|
|
34
|
+
auto_exclude: Sequence[str],
|
|
35
|
+
auto_recursive: bool,
|
|
36
|
+
auto_pick: str,
|
|
37
|
+
discovery_label: str,
|
|
38
|
+
singular_label: str,
|
|
39
|
+
) -> list[Path]:
|
|
40
|
+
roots = list(auto_roots or DEFAULT_ROOTS)
|
|
41
|
+
files = discover_files(
|
|
42
|
+
exp_dir,
|
|
43
|
+
roots=roots,
|
|
44
|
+
include=list(auto_include),
|
|
45
|
+
exclude=list(auto_exclude),
|
|
46
|
+
recursive=auto_recursive,
|
|
47
|
+
)
|
|
48
|
+
if not files:
|
|
49
|
+
raise ParseError(
|
|
50
|
+
f"No {discovery_label} discovered under {roots} "
|
|
51
|
+
f"(include={list(auto_include)}, exclude={list(auto_exclude)}).\n"
|
|
52
|
+
"Hint: put raw files under ./inputs (default), or set auto_roots / reads.raw explicitly."
|
|
53
|
+
)
|
|
54
|
+
if auto_pick in {"single", "latest"}:
|
|
55
|
+
return [auto_pick_discovered_file(files, auto_pick, singular_label=singular_label)]
|
|
56
|
+
if auto_pick == "merge":
|
|
57
|
+
return files
|
|
58
|
+
raise ParseError(f"Unknown auto_pick mode {auto_pick!r} (expected: single|latest|merge)")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Raw-file autodiscovery policy for ingest plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from collections.abc import Iterable, Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from reader_workbench.errors import ParseError
|
|
10
|
+
from reader_workbench.workbench.input_discovery import (
|
|
11
|
+
DEFAULT_INPUT_EXCLUDE as DEFAULT_EXCLUDE,
|
|
12
|
+
)
|
|
13
|
+
from reader_workbench.workbench.input_discovery import (
|
|
14
|
+
DEFAULT_INPUT_ROOTS as DEFAULT_ROOTS,
|
|
15
|
+
)
|
|
16
|
+
from reader_workbench.workbench.input_discovery import (
|
|
17
|
+
DEFAULT_WORKBOOK_INCLUDE as DEFAULT_INCLUDE,
|
|
18
|
+
)
|
|
19
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _iter_candidates(root: Path, patterns: Sequence[str], recursive: bool) -> Iterable[Path]:
|
|
23
|
+
if not root.exists():
|
|
24
|
+
return []
|
|
25
|
+
if recursive:
|
|
26
|
+
for path in root.rglob("*"):
|
|
27
|
+
if path.is_file() and any(fnmatch.fnmatch(path.name, pat) for pat in patterns):
|
|
28
|
+
yield path
|
|
29
|
+
return
|
|
30
|
+
for path in root.glob("*"):
|
|
31
|
+
if path.is_file() and any(fnmatch.fnmatch(path.name, pat) for pat in patterns):
|
|
32
|
+
yield path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def discover_files(
|
|
36
|
+
base: Path,
|
|
37
|
+
*,
|
|
38
|
+
roots: Sequence[str] | None = None,
|
|
39
|
+
include: Sequence[str] = DEFAULT_INCLUDE,
|
|
40
|
+
exclude: Sequence[str] = DEFAULT_EXCLUDE,
|
|
41
|
+
recursive: bool = False,
|
|
42
|
+
) -> list[Path]:
|
|
43
|
+
roots = list(roots or DEFAULT_ROOTS)
|
|
44
|
+
out: list[Path] = []
|
|
45
|
+
seen: set[Path] = set()
|
|
46
|
+
for root in roots:
|
|
47
|
+
try:
|
|
48
|
+
root_path = resolve_path_within_root(root, root=base)
|
|
49
|
+
except ValueError as err:
|
|
50
|
+
raise ParseError(
|
|
51
|
+
f"auto_roots entry {root!r} must stay under the experiment root after resolving symlinks."
|
|
52
|
+
) from err
|
|
53
|
+
for path in _iter_candidates(root_path, include, recursive):
|
|
54
|
+
try:
|
|
55
|
+
confined_path = resolve_path_within_root(path, root=base)
|
|
56
|
+
except ValueError as err:
|
|
57
|
+
raise ParseError(
|
|
58
|
+
f"Discovered input {path!s} must stay under the experiment root after resolving symlinks."
|
|
59
|
+
) from err
|
|
60
|
+
if any(fnmatch.fnmatch(path.name, pat) for pat in exclude):
|
|
61
|
+
continue
|
|
62
|
+
if confined_path in seen:
|
|
63
|
+
continue
|
|
64
|
+
out.append(confined_path)
|
|
65
|
+
seen.add(confined_path)
|
|
66
|
+
return sorted(out)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Flow cytometer ingest for .fcs files (snapshot data)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from reader_workbench.domains.cytometry.io.fcs import parse_fcs_file
|
|
14
|
+
from reader_workbench.errors import ParseError
|
|
15
|
+
from reader_workbench.plugins.ingest._discovery import discover_auto_input_files
|
|
16
|
+
from reader_workbench.plugins.ingest.discovery_policy import DEFAULT_EXCLUDE
|
|
17
|
+
from reader_workbench.workbench.ports import dataframe_output, file_set_input
|
|
18
|
+
from reader_workbench.workbench.registry import Plugin, PluginConfig, PreflightIssue
|
|
19
|
+
|
|
20
|
+
DEFAULT_FCS_INCLUDE = ("*.fcs", "*.FCS")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlowCytometerCfg(PluginConfig):
|
|
24
|
+
# auto-discovery knobs
|
|
25
|
+
auto_roots: list[str] | None = None
|
|
26
|
+
auto_include: list[str] = Field(default_factory=lambda: list(DEFAULT_FCS_INCLUDE))
|
|
27
|
+
auto_exclude: list[str] = Field(default_factory=lambda: list(DEFAULT_EXCLUDE))
|
|
28
|
+
auto_pick: Literal["single", "latest", "merge"] = "merge"
|
|
29
|
+
auto_recursive: bool = False
|
|
30
|
+
|
|
31
|
+
# channel naming / output shaping
|
|
32
|
+
channel_name_field: str = "pns" # field in FCS channel metadata (e.g., pns or pnn)
|
|
33
|
+
channel_map: Mapping[str, str] | None = None
|
|
34
|
+
drop_channels: list[str] | None = None
|
|
35
|
+
sample_id_from: Literal["stem", "name"] = "stem"
|
|
36
|
+
time_value: float = 0.0
|
|
37
|
+
|
|
38
|
+
# logging
|
|
39
|
+
print_summary: bool = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FlowCytometerIngest(Plugin):
|
|
43
|
+
"""Ingest .fcs files into tidy.v1 (snapshot; time is constant)."""
|
|
44
|
+
|
|
45
|
+
ConfigModel = FlowCytometerCfg
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def input_ports(cls):
|
|
49
|
+
return {"raw": file_set_input("raw", optional=True)}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def output_ports(cls):
|
|
53
|
+
return {
|
|
54
|
+
"df": dataframe_output("df", "tidy.v1"),
|
|
55
|
+
"channels": dataframe_output("channels", "cytometer.channels.v1"),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def preflight_readiness(cls, *, exp_dir, cfg: FlowCytometerCfg, reads):
|
|
60
|
+
issues: list[PreflightIssue] = []
|
|
61
|
+
if importlib.util.find_spec("flowio") is None:
|
|
62
|
+
issues.append(
|
|
63
|
+
PreflightIssue(
|
|
64
|
+
kind="dependency",
|
|
65
|
+
message="flowio is required for ingest/flow_cytometer. Re-sync the core reader environment.",
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
if "raw" not in reads:
|
|
69
|
+
try:
|
|
70
|
+
discover_auto_input_files(
|
|
71
|
+
exp_dir=exp_dir,
|
|
72
|
+
auto_roots=cfg.auto_roots,
|
|
73
|
+
auto_include=cfg.auto_include,
|
|
74
|
+
auto_exclude=cfg.auto_exclude,
|
|
75
|
+
auto_recursive=cfg.auto_recursive,
|
|
76
|
+
auto_pick=cfg.auto_pick,
|
|
77
|
+
discovery_label=".fcs files",
|
|
78
|
+
singular_label=".fcs file",
|
|
79
|
+
)
|
|
80
|
+
except ParseError as err:
|
|
81
|
+
issues.append(PreflightIssue(kind="file", message=str(err)))
|
|
82
|
+
return tuple(issues)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def resolve_missing_file_inputs(cls, *, exp_dir, cfg: FlowCytometerCfg, inputs):
|
|
86
|
+
if "raw" in inputs:
|
|
87
|
+
return {}
|
|
88
|
+
files = discover_auto_input_files(
|
|
89
|
+
exp_dir=exp_dir,
|
|
90
|
+
auto_roots=cfg.auto_roots,
|
|
91
|
+
auto_include=cfg.auto_include,
|
|
92
|
+
auto_exclude=cfg.auto_exclude,
|
|
93
|
+
auto_recursive=cfg.auto_recursive,
|
|
94
|
+
auto_pick=cfg.auto_pick,
|
|
95
|
+
discovery_label=".fcs files",
|
|
96
|
+
singular_label=".fcs file",
|
|
97
|
+
)
|
|
98
|
+
return {"raw": tuple(files)}
|
|
99
|
+
|
|
100
|
+
def run(self, ctx, inputs, cfg: FlowCytometerCfg):
|
|
101
|
+
files = tuple(inputs["raw"])
|
|
102
|
+
channel_map = {str(k): str(v) for k, v in (cfg.channel_map or {}).items()}
|
|
103
|
+
drop_channels = {str(c) for c in (cfg.drop_channels or [])}
|
|
104
|
+
|
|
105
|
+
frames: list[pd.DataFrame] = []
|
|
106
|
+
channel_meta_frames: list[pd.DataFrame] = []
|
|
107
|
+
for f in files:
|
|
108
|
+
long, channels_meta = parse_fcs_file(
|
|
109
|
+
f,
|
|
110
|
+
channel_name_field=cfg.channel_name_field,
|
|
111
|
+
channel_map=channel_map,
|
|
112
|
+
drop_channels=drop_channels,
|
|
113
|
+
sample_id_from=cfg.sample_id_from,
|
|
114
|
+
time_value=cfg.time_value,
|
|
115
|
+
)
|
|
116
|
+
frames.append(long)
|
|
117
|
+
if not channels_meta.empty:
|
|
118
|
+
channel_meta_frames.append(channels_meta)
|
|
119
|
+
|
|
120
|
+
if not frames:
|
|
121
|
+
raise ParseError("No cytometer frames parsed from selected files")
|
|
122
|
+
out = pd.concat(frames, ignore_index=True)
|
|
123
|
+
|
|
124
|
+
if cfg.print_summary:
|
|
125
|
+
with suppress(Exception):
|
|
126
|
+
ctx.logger.info(
|
|
127
|
+
"flow_cytometer ingest • files=%d • rows=%d • channels=%d • samples=%d",
|
|
128
|
+
len(files),
|
|
129
|
+
len(out),
|
|
130
|
+
out["channel"].nunique(),
|
|
131
|
+
out["sample_id"].nunique(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if channel_meta_frames:
|
|
135
|
+
channels_meta = pd.concat(channel_meta_frames, ignore_index=True)
|
|
136
|
+
else:
|
|
137
|
+
channels_meta = pd.DataFrame(columns=["sample_id", "channel_index", "channel_name"])
|
|
138
|
+
|
|
139
|
+
return {"df": out, "channels": channels_meta}
|