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,308 @@
|
|
|
1
|
+
"""Manifest-backed source loading for four-state event-window analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from .contracts import EventSpec, FourStateEventWindowSourceSpec
|
|
12
|
+
from .well_exclusions import WellExclusion
|
|
13
|
+
|
|
14
|
+
STATE_ORDER = ("00", "10", "01", "11")
|
|
15
|
+
ANNOTATED_CONTRACT = "plate_reader.annotated.v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class EventInterval:
|
|
20
|
+
experiment_id: str
|
|
21
|
+
event_id: str
|
|
22
|
+
event_kind: str
|
|
23
|
+
interval_start_assay_h: float
|
|
24
|
+
interval_end_assay_h: float
|
|
25
|
+
estimate_assay_h: float
|
|
26
|
+
estimate_method: str
|
|
27
|
+
uncertainty_h: float
|
|
28
|
+
post_event_coverage_h: float
|
|
29
|
+
declaration: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ExperimentSource:
|
|
34
|
+
experiment_id: str
|
|
35
|
+
response: pd.DataFrame
|
|
36
|
+
magnitude: pd.DataFrame
|
|
37
|
+
trajectory: pd.DataFrame
|
|
38
|
+
event: EventInterval
|
|
39
|
+
well_exclusions: tuple[WellExclusion, ...] = ()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def event_record(event: EventInterval) -> pd.DataFrame:
|
|
43
|
+
return pd.DataFrame.from_records(
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
"experiment_id": event.experiment_id,
|
|
47
|
+
"event_id": event.event_id,
|
|
48
|
+
"event_kind": event.event_kind,
|
|
49
|
+
"event_interval_start_assay_h": event.interval_start_assay_h,
|
|
50
|
+
"event_interval_end_assay_h": event.interval_end_assay_h,
|
|
51
|
+
"event_time_estimate_assay_h": event.estimate_assay_h,
|
|
52
|
+
"event_time_estimate_method": event.estimate_method,
|
|
53
|
+
"event_time_uncertainty_h": event.uncertainty_h,
|
|
54
|
+
"post_event_coverage_h": event.post_event_coverage_h,
|
|
55
|
+
"declaration": event.declaration,
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_experiment_source(
|
|
62
|
+
*,
|
|
63
|
+
experiment_id: str,
|
|
64
|
+
response_frame: pd.DataFrame,
|
|
65
|
+
magnitude_frame: pd.DataFrame,
|
|
66
|
+
trajectory_frame: pd.DataFrame,
|
|
67
|
+
source_spec: FourStateEventWindowSourceSpec,
|
|
68
|
+
event_spec: EventSpec,
|
|
69
|
+
) -> ExperimentSource:
|
|
70
|
+
"""Normalize three already-resolved records into one analysis source."""
|
|
71
|
+
|
|
72
|
+
state_column = source_spec.state_column
|
|
73
|
+
treatment_map = source_spec.state_values
|
|
74
|
+
case_sensitive = source_spec.state_values_case_sensitive
|
|
75
|
+
if set(treatment_map) != set(STATE_ORDER) or len(set(treatment_map.values())) != len(STATE_ORDER):
|
|
76
|
+
raise ValueError(f"{experiment_id}: resolved state map must define four distinct 00, 10, 01, and 11 values.")
|
|
77
|
+
response = _load_signal(
|
|
78
|
+
response_frame,
|
|
79
|
+
channel=source_spec.response_channel,
|
|
80
|
+
state_column=state_column,
|
|
81
|
+
treatment_map=treatment_map,
|
|
82
|
+
case_sensitive=case_sensitive,
|
|
83
|
+
event_spec=event_spec,
|
|
84
|
+
context=f"{experiment_id}:response",
|
|
85
|
+
)
|
|
86
|
+
magnitude = _load_signal(
|
|
87
|
+
magnitude_frame,
|
|
88
|
+
channel=source_spec.magnitude_channel,
|
|
89
|
+
state_column=state_column,
|
|
90
|
+
treatment_map=treatment_map,
|
|
91
|
+
case_sensitive=case_sensitive,
|
|
92
|
+
event_spec=event_spec,
|
|
93
|
+
context=f"{experiment_id}:magnitude",
|
|
94
|
+
)
|
|
95
|
+
trajectory = _load_signal(
|
|
96
|
+
trajectory_frame,
|
|
97
|
+
channel=source_spec.growth_channel,
|
|
98
|
+
state_column=state_column,
|
|
99
|
+
treatment_map=treatment_map,
|
|
100
|
+
case_sensitive=case_sensitive,
|
|
101
|
+
event_spec=event_spec,
|
|
102
|
+
context=f"{experiment_id}:growth",
|
|
103
|
+
require_positive=False,
|
|
104
|
+
)
|
|
105
|
+
well_exclusions = tuple(item for item in source_spec.well_exclusions if item.experiment_id == experiment_id)
|
|
106
|
+
_validate_well_exclusions(
|
|
107
|
+
well_exclusions,
|
|
108
|
+
response=response,
|
|
109
|
+
magnitude=magnitude,
|
|
110
|
+
trajectory=trajectory,
|
|
111
|
+
experiment_id=experiment_id,
|
|
112
|
+
)
|
|
113
|
+
event = resolve_event_interval(response, experiment_id=experiment_id, event_spec=event_spec)
|
|
114
|
+
magnitude_event = resolve_event_interval(magnitude, experiment_id=experiment_id, event_spec=event_spec)
|
|
115
|
+
growth_event = resolve_event_interval(trajectory, experiment_id=experiment_id, event_spec=event_spec)
|
|
116
|
+
_require_event_parity(event, magnitude_event, context=f"{experiment_id}:response/magnitude")
|
|
117
|
+
_require_event_parity(event, growth_event, context=f"{experiment_id}:response/growth")
|
|
118
|
+
|
|
119
|
+
for frame in (response, magnitude, trajectory):
|
|
120
|
+
frame["experiment_id"] = experiment_id
|
|
121
|
+
frame["time_from_event_h"] = frame["time"].to_numpy(dtype=float) - event.estimate_assay_h
|
|
122
|
+
reference_id = source_spec.reference_design_id
|
|
123
|
+
if reference_id not in set(magnitude["design_id"].astype(str)):
|
|
124
|
+
raise ValueError(f"{experiment_id}: reference design {reference_id!r} is absent from magnitude records.")
|
|
125
|
+
|
|
126
|
+
return ExperimentSource(
|
|
127
|
+
experiment_id=experiment_id,
|
|
128
|
+
response=response,
|
|
129
|
+
magnitude=magnitude,
|
|
130
|
+
trajectory=trajectory,
|
|
131
|
+
event=event,
|
|
132
|
+
well_exclusions=well_exclusions,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _validate_well_exclusions(
|
|
137
|
+
exclusions: tuple[WellExclusion, ...],
|
|
138
|
+
*,
|
|
139
|
+
response: pd.DataFrame,
|
|
140
|
+
magnitude: pd.DataFrame,
|
|
141
|
+
trajectory: pd.DataFrame,
|
|
142
|
+
experiment_id: str,
|
|
143
|
+
) -> None:
|
|
144
|
+
for exclusion in exclusions:
|
|
145
|
+
expected = {(exclusion.design_id, exclusion.state)}
|
|
146
|
+
for signal_kind, frame in (
|
|
147
|
+
("response", response),
|
|
148
|
+
("magnitude", magnitude),
|
|
149
|
+
("growth", trajectory),
|
|
150
|
+
):
|
|
151
|
+
selected = frame.loc[frame["position"].astype(str).eq(exclusion.position)]
|
|
152
|
+
observed = set(zip(selected["design_id"].astype(str), selected["state"].astype(str), strict=True))
|
|
153
|
+
if observed != expected:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"{experiment_id}:{signal_kind} well exclusion {exclusion.position!r} "
|
|
156
|
+
f"does not match design/state {exclusion.design_id!r}/{exclusion.state!r}."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def resolve_event_interval(
|
|
161
|
+
frame: pd.DataFrame,
|
|
162
|
+
*,
|
|
163
|
+
experiment_id: str,
|
|
164
|
+
event_spec: EventSpec,
|
|
165
|
+
) -> EventInterval:
|
|
166
|
+
segments = pd.to_numeric(frame[event_spec.segment_column], errors="coerce")
|
|
167
|
+
segment_values = segments.to_numpy(dtype=float, na_value=np.nan)
|
|
168
|
+
if not np.isfinite(segment_values).all() or not np.equal(segment_values, np.trunc(segment_values)).all():
|
|
169
|
+
raise ValueError(f"{experiment_id}: event segment indexes must be finite integers.")
|
|
170
|
+
segment_indexes = segments.astype(int)
|
|
171
|
+
indexes = set(segment_indexes)
|
|
172
|
+
expected = {event_spec.pre_segment_index, event_spec.post_segment_index}
|
|
173
|
+
if indexes != expected:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"{experiment_id}: event requires segment indexes {sorted(expected)}; found {sorted(indexes)}."
|
|
176
|
+
)
|
|
177
|
+
times = pd.to_numeric(frame["time"], errors="coerce")
|
|
178
|
+
if not np.isfinite(times.to_numpy(dtype=float)).all():
|
|
179
|
+
raise ValueError(f"{experiment_id}: event alignment requires finite acquisition times.")
|
|
180
|
+
pre = times.loc[segment_indexes.eq(event_spec.pre_segment_index)]
|
|
181
|
+
post = times.loc[segment_indexes.eq(event_spec.post_segment_index)]
|
|
182
|
+
last_pre = float(pre.max())
|
|
183
|
+
first_post = float(post.min())
|
|
184
|
+
assay_end = float(post.max())
|
|
185
|
+
if not last_pre < first_post <= assay_end:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"{experiment_id}: event segments are not chronological: "
|
|
188
|
+
f"last_pre={last_pre}, first_post={first_post}, assay_end={assay_end}."
|
|
189
|
+
)
|
|
190
|
+
estimate = (last_pre + first_post) / 2.0
|
|
191
|
+
return EventInterval(
|
|
192
|
+
experiment_id=experiment_id,
|
|
193
|
+
event_id=event_spec.event_id,
|
|
194
|
+
event_kind=event_spec.event_kind,
|
|
195
|
+
interval_start_assay_h=last_pre,
|
|
196
|
+
interval_end_assay_h=first_post,
|
|
197
|
+
estimate_assay_h=estimate,
|
|
198
|
+
estimate_method=event_spec.estimate_method,
|
|
199
|
+
uncertainty_h=(first_post - last_pre) / 2.0,
|
|
200
|
+
post_event_coverage_h=assay_end - first_post,
|
|
201
|
+
declaration=event_spec.declaration,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _load_signal(
|
|
206
|
+
frame: pd.DataFrame,
|
|
207
|
+
*,
|
|
208
|
+
channel: str,
|
|
209
|
+
state_column: str,
|
|
210
|
+
treatment_map: Mapping[str, str],
|
|
211
|
+
case_sensitive: bool,
|
|
212
|
+
event_spec: EventSpec,
|
|
213
|
+
context: str,
|
|
214
|
+
require_positive: bool = True,
|
|
215
|
+
) -> pd.DataFrame:
|
|
216
|
+
frame = frame.copy()
|
|
217
|
+
required = {"design_id", "position", "time", "channel", "value", state_column, event_spec.segment_column}
|
|
218
|
+
missing = sorted(required - set(frame.columns))
|
|
219
|
+
if missing:
|
|
220
|
+
raise ValueError(f"{context} record is missing columns: {missing}.")
|
|
221
|
+
work = frame.loc[frame["channel"].astype(str).eq(channel)].copy()
|
|
222
|
+
if work.empty:
|
|
223
|
+
raise ValueError(f"{context} record has no rows for channel {channel!r}.")
|
|
224
|
+
reverse = {(value if case_sensitive else value.strip().casefold()): state for state, value in treatment_map.items()}
|
|
225
|
+
values = work[state_column].astype(str)
|
|
226
|
+
if not case_sensitive:
|
|
227
|
+
values = values.str.strip().str.casefold()
|
|
228
|
+
work["state"] = values.map(reverse)
|
|
229
|
+
unknown = sorted(work.loc[work["state"].isna(), state_column].astype(str).unique().tolist())
|
|
230
|
+
if unknown:
|
|
231
|
+
raise ValueError(f"{context} record contains unmapped state values: {unknown}.")
|
|
232
|
+
work["design_id"] = work["design_id"].astype(str)
|
|
233
|
+
work["position"] = work["position"].astype(str)
|
|
234
|
+
work["state"] = work["state"].astype(str)
|
|
235
|
+
work["time"] = pd.to_numeric(work["time"], errors="coerce")
|
|
236
|
+
work["value"] = pd.to_numeric(work["value"], errors="coerce")
|
|
237
|
+
if not np.isfinite(work[["time", "value"]].to_numpy(dtype=float)).all():
|
|
238
|
+
raise ValueError(f"{context} record contains non-finite time or values.")
|
|
239
|
+
if require_positive and (work["value"].to_numpy(dtype=float) <= 0.0).any():
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"{context} record contains non-positive values; "
|
|
242
|
+
"four-state event-window log-space reduction requires strictly positive source values."
|
|
243
|
+
)
|
|
244
|
+
_normalize_value_provenance(work, context=context)
|
|
245
|
+
return work.loc[
|
|
246
|
+
:,
|
|
247
|
+
[
|
|
248
|
+
"design_id",
|
|
249
|
+
"position",
|
|
250
|
+
"state",
|
|
251
|
+
"time",
|
|
252
|
+
"channel",
|
|
253
|
+
"value",
|
|
254
|
+
"value_policy_clipped",
|
|
255
|
+
"value_instrument_overflow",
|
|
256
|
+
"value_bound_kind",
|
|
257
|
+
event_spec.segment_column,
|
|
258
|
+
],
|
|
259
|
+
].reset_index(drop=True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _normalize_value_provenance(frame: pd.DataFrame, *, context: str) -> None:
|
|
263
|
+
required = {"value_policy_clipped", "value_instrument_overflow", "value_bound_kind"}
|
|
264
|
+
missing = sorted(required - set(frame.columns))
|
|
265
|
+
if missing:
|
|
266
|
+
raise ValueError(f"{context} record is missing required value provenance columns: {missing}.")
|
|
267
|
+
policy = _boolean_provenance(frame["value_policy_clipped"], context=f"{context}:value_policy_clipped")
|
|
268
|
+
overflow = _boolean_provenance(frame["value_instrument_overflow"], context=f"{context}:value_instrument_overflow")
|
|
269
|
+
frame["value_policy_clipped"] = policy
|
|
270
|
+
frame["value_instrument_overflow"] = overflow
|
|
271
|
+
bounds = frame["value_bound_kind"].astype(str)
|
|
272
|
+
allowed = {"exact", "lower", "upper", "indeterminate"}
|
|
273
|
+
unknown = sorted(set(bounds) - allowed)
|
|
274
|
+
if unknown:
|
|
275
|
+
raise ValueError(f"{context} record contains unsupported value_bound_kind values: {unknown}.")
|
|
276
|
+
if ((policy | overflow) & bounds.eq("exact")).any():
|
|
277
|
+
raise ValueError(f"{context} record marks clipped or overflowed values as exact.")
|
|
278
|
+
if (bounds.ne("exact") & ~(policy | overflow)).any():
|
|
279
|
+
raise ValueError(f"{context} record contains a value bound without clipping or overflow provenance.")
|
|
280
|
+
frame["value_bound_kind"] = bounds
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _boolean_provenance(values: pd.Series, *, context: str) -> pd.Series:
|
|
284
|
+
if values.isna().any() or not values.map(lambda value: isinstance(value, (bool, np.bool_))).all():
|
|
285
|
+
raise ValueError(f"{context} must contain booleans without missing values.")
|
|
286
|
+
return values.astype(bool)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _require_event_parity(left: EventInterval, right: EventInterval, *, context: str) -> None:
|
|
290
|
+
fields = (
|
|
291
|
+
"interval_start_assay_h",
|
|
292
|
+
"interval_end_assay_h",
|
|
293
|
+
"estimate_assay_h",
|
|
294
|
+
"post_event_coverage_h",
|
|
295
|
+
)
|
|
296
|
+
if any(not np.isclose(getattr(left, field), getattr(right, field), rtol=0.0, atol=1.0e-12) for field in fields):
|
|
297
|
+
raise ValueError(f"{context} event bounds disagree across source records.")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__all__ = [
|
|
301
|
+
"ANNOTATED_CONTRACT",
|
|
302
|
+
"STATE_ORDER",
|
|
303
|
+
"EventInterval",
|
|
304
|
+
"ExperimentSource",
|
|
305
|
+
"build_experiment_source",
|
|
306
|
+
"event_record",
|
|
307
|
+
"resolve_event_interval",
|
|
308
|
+
]
|
reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Verify that declared well exclusions correspond to missing temporal support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .contracts import QualitySpec, ReductionSpec
|
|
10
|
+
from .reduction import four_state_event_window_temporal_spec, reduce_temporal_trace
|
|
11
|
+
from .well_exclusions import WellExclusion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def require_well_exclusion_support_failure(
|
|
15
|
+
exclusions: Sequence[WellExclusion],
|
|
16
|
+
*,
|
|
17
|
+
reductions: Mapping[str, ReductionSpec],
|
|
18
|
+
quality: QualitySpec,
|
|
19
|
+
response: pd.DataFrame,
|
|
20
|
+
magnitude: pd.DataFrame,
|
|
21
|
+
event_estimates_h: Sequence[float],
|
|
22
|
+
pre_window_end_h: float,
|
|
23
|
+
experiment_id: str,
|
|
24
|
+
) -> None:
|
|
25
|
+
for exclusion in exclusions:
|
|
26
|
+
try:
|
|
27
|
+
reduction = reductions[exclusion.reduction_id]
|
|
28
|
+
except KeyError as exc:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"{experiment_id}: well exclusion names unknown reduction {exclusion.reduction_id!r}."
|
|
31
|
+
) from exc
|
|
32
|
+
support_failures: list[str] = []
|
|
33
|
+
for frame in (response, magnitude):
|
|
34
|
+
trace = frame.loc[frame["position"].astype(str).eq(exclusion.position)]
|
|
35
|
+
for event_estimate_h in event_estimates_h:
|
|
36
|
+
try:
|
|
37
|
+
reduce_temporal_trace(
|
|
38
|
+
trace["time"].to_numpy(dtype=float),
|
|
39
|
+
trace["value"].to_numpy(dtype=float),
|
|
40
|
+
spec=four_state_event_window_temporal_spec(reduction, quality),
|
|
41
|
+
trace_id=f"{experiment_id}:{exclusion.position}:{reduction.id}:support",
|
|
42
|
+
origin_h=event_estimate_h,
|
|
43
|
+
policy_clipped=trace["value_policy_clipped"].to_numpy(dtype=bool),
|
|
44
|
+
instrument_overflow=trace["value_instrument_overflow"].to_numpy(dtype=bool),
|
|
45
|
+
bound_kinds=trace["value_bound_kind"].to_numpy(dtype=object),
|
|
46
|
+
)
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
if _is_support_failure(str(exc)):
|
|
49
|
+
support_failures.append(str(exc))
|
|
50
|
+
if reduction.response_basis == "post_minus_pre":
|
|
51
|
+
if reduction.pre_window_duration_h is None:
|
|
52
|
+
raise ValueError(f"{experiment_id}:{reduction.id}: delta response lacks an explicit pre-event window.")
|
|
53
|
+
trace = response.loc[response["position"].astype(str).eq(exclusion.position)]
|
|
54
|
+
try:
|
|
55
|
+
reduce_temporal_trace(
|
|
56
|
+
trace["time"].to_numpy(dtype=float),
|
|
57
|
+
trace["value"].to_numpy(dtype=float),
|
|
58
|
+
spec=four_state_event_window_temporal_spec(
|
|
59
|
+
reduction,
|
|
60
|
+
quality,
|
|
61
|
+
absolute_window_h=(
|
|
62
|
+
pre_window_end_h - reduction.pre_window_duration_h,
|
|
63
|
+
pre_window_end_h,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
trace_id=f"{experiment_id}:{exclusion.position}:{reduction.id}:pre-support",
|
|
67
|
+
policy_clipped=trace["value_policy_clipped"].to_numpy(dtype=bool),
|
|
68
|
+
instrument_overflow=trace["value_instrument_overflow"].to_numpy(dtype=bool),
|
|
69
|
+
bound_kinds=trace["value_bound_kind"].to_numpy(dtype=object),
|
|
70
|
+
)
|
|
71
|
+
except ValueError as exc:
|
|
72
|
+
if _is_support_failure(str(exc)):
|
|
73
|
+
support_failures.append(str(exc))
|
|
74
|
+
if not support_failures:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"{experiment_id}:{exclusion.position}:{exclusion.reduction_id} "
|
|
77
|
+
"does not have insufficient event-window coverage."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_support_failure(message: str) -> bool:
|
|
82
|
+
return any(
|
|
83
|
+
token in message
|
|
84
|
+
for token in (
|
|
85
|
+
"does not cover",
|
|
86
|
+
"interior gap",
|
|
87
|
+
"observations in the selected interval",
|
|
88
|
+
"requires observed interval boundaries",
|
|
89
|
+
"selected interval contains no observations",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = ["require_well_exclusion_support_failure"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Define explicit, reduction-bound exclusions for unsupported assay wells."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .contract_fields import exact_fields, nonempty
|
|
9
|
+
|
|
10
|
+
_COVERAGE_REASON = "insufficient_event_window_coverage"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class WellExclusion:
|
|
15
|
+
experiment_id: str
|
|
16
|
+
design_id: str
|
|
17
|
+
state: str
|
|
18
|
+
position: str
|
|
19
|
+
reduction_id: str
|
|
20
|
+
reason: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_well_exclusions(value: object) -> tuple[WellExclusion, ...]:
|
|
24
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
25
|
+
raise ValueError("source.well_exclusions must be a sequence.")
|
|
26
|
+
exclusions: list[WellExclusion] = []
|
|
27
|
+
for index, item in enumerate(value):
|
|
28
|
+
fields = {"experiment_id", "design_id", "state", "position", "reduction_id", "reason"}
|
|
29
|
+
payload = exact_fields(item, context=f"source.well_exclusions[{index}]", required=fields)
|
|
30
|
+
state = nonempty(payload["state"], context=f"source.well_exclusions[{index}].state")
|
|
31
|
+
if state not in {"00", "10", "01", "11"}:
|
|
32
|
+
raise ValueError(f"source.well_exclusions[{index}].state must be 00, 10, 01, or 11.")
|
|
33
|
+
reason = nonempty(payload["reason"], context=f"source.well_exclusions[{index}].reason")
|
|
34
|
+
if reason != _COVERAGE_REASON:
|
|
35
|
+
raise ValueError(f"source.well_exclusions[{index}].reason must be {_COVERAGE_REASON!r}.")
|
|
36
|
+
exclusions.append(
|
|
37
|
+
WellExclusion(
|
|
38
|
+
experiment_id=nonempty(
|
|
39
|
+
payload["experiment_id"], context=f"source.well_exclusions[{index}].experiment_id"
|
|
40
|
+
),
|
|
41
|
+
design_id=nonempty(payload["design_id"], context=f"source.well_exclusions[{index}].design_id"),
|
|
42
|
+
state=state,
|
|
43
|
+
position=nonempty(payload["position"], context=f"source.well_exclusions[{index}].position"),
|
|
44
|
+
reduction_id=nonempty(payload["reduction_id"], context=f"source.well_exclusions[{index}].reduction_id"),
|
|
45
|
+
reason=reason,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
keys = [(item.experiment_id, item.position, item.reduction_id) for item in exclusions]
|
|
49
|
+
if len(keys) != len(set(keys)):
|
|
50
|
+
raise ValueError("source.well_exclusions must not repeat an experiment, position, and reduction.")
|
|
51
|
+
return tuple(exclusions)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["WellExclusion", "parse_well_exclusions"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Plate-reader timepoint selection and nearest-snapshot helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def nearest_time_per_key(df: pd.DataFrame, *, target_time: float, keys: Sequence[str], tol: float) -> pd.DataFrame:
|
|
14
|
+
work = pl.from_pandas(df.reset_index(drop=True)).with_row_index("__row__")
|
|
15
|
+
time_col = "time"
|
|
16
|
+
time_expr = pl.col(time_col).cast(pl.Float64, strict=False)
|
|
17
|
+
time_expr = pl.when(time_expr.is_nan()).then(None).otherwise(time_expr)
|
|
18
|
+
work = work.with_columns(time_expr.alias(time_col))
|
|
19
|
+
work = work.filter(pl.col(time_col).is_not_null())
|
|
20
|
+
work = work.with_columns((pl.col(time_col) - float(target_time)).abs().alias("__dt__"))
|
|
21
|
+
work = work.with_columns(pl.col("__dt__").min().over(list(keys)).alias("__dt_min__"))
|
|
22
|
+
work = work.filter(pl.col("__dt__") == pl.col("__dt_min__"))
|
|
23
|
+
work = work.sort("__row__").unique(subset=list(keys), keep="first")
|
|
24
|
+
work = work.filter(pl.col("__dt__") <= float(tol))
|
|
25
|
+
work = work.drop(["__dt__", "__dt_min__", "__row__"])
|
|
26
|
+
return work.to_pandas(use_pyarrow_extension_array=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def choose_nearest_time(
|
|
30
|
+
times: Sequence[object] | np.ndarray,
|
|
31
|
+
*,
|
|
32
|
+
target_time: float,
|
|
33
|
+
tol: float | None,
|
|
34
|
+
where: str,
|
|
35
|
+
logger: logging.Logger | None = None,
|
|
36
|
+
) -> float:
|
|
37
|
+
cleaned = pd.to_numeric(pd.Series(times), errors="coerce").dropna().to_numpy(dtype=float)
|
|
38
|
+
if cleaned.size == 0:
|
|
39
|
+
raise ValueError(f"{where}: no valid time values")
|
|
40
|
+
unique_times = np.asarray(sorted(np.unique(cleaned)), dtype=float)
|
|
41
|
+
diffs = np.abs(unique_times - float(target_time))
|
|
42
|
+
chosen_index = int(np.argmin(diffs))
|
|
43
|
+
chosen_time = float(unique_times[chosen_index])
|
|
44
|
+
chosen_delta = float(diffs[chosen_index])
|
|
45
|
+
if tol is not None and chosen_delta > float(tol) and logger is not None:
|
|
46
|
+
logger.info(
|
|
47
|
+
"[warn]%s[/warn] • requested t=%.2f h; nearest available t=%.2f h (Δ=%.2f h) — using nearest",
|
|
48
|
+
where,
|
|
49
|
+
float(target_time),
|
|
50
|
+
chosen_time,
|
|
51
|
+
chosen_delta,
|
|
52
|
+
)
|
|
53
|
+
return chosen_time
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def infer_acquisition_transition_time_h(df: pd.DataFrame, *, time_col: str) -> float | None:
|
|
57
|
+
"""Return the first time in a later workbook acquisition segment.
|
|
58
|
+
|
|
59
|
+
A sheet transition is acquisition provenance. It does not identify a
|
|
60
|
+
biological intervention unless a separate, explicit event contract says
|
|
61
|
+
that it does.
|
|
62
|
+
"""
|
|
63
|
+
if "sheet_index" not in df.columns:
|
|
64
|
+
return None
|
|
65
|
+
sheet_values = pd.to_numeric(df["sheet_index"], errors="coerce").dropna()
|
|
66
|
+
if sheet_values.empty:
|
|
67
|
+
return None
|
|
68
|
+
min_sheet = float(sheet_values.min())
|
|
69
|
+
sheet_series = pd.to_numeric(df["sheet_index"], errors="coerce")
|
|
70
|
+
times = pd.to_numeric(df.loc[sheet_series > min_sheet, time_col], errors="coerce").dropna()
|
|
71
|
+
if times.empty:
|
|
72
|
+
return None
|
|
73
|
+
return float(times.min())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = ["choose_nearest_time", "infer_acquisition_transition_time_h", "nearest_time_per_key"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Plate-reader sample-map parsing."""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_sample_map(path: str | Path) -> pd.DataFrame:
|
|
10
|
+
"""
|
|
11
|
+
Load a sample metadata map with a 'position' key.
|
|
12
|
+
|
|
13
|
+
Accepted inputs:
|
|
14
|
+
1) A table with an explicit 'position' column (preferred).
|
|
15
|
+
2) A table with 'row' and 'col' columns (will be combined into 'position').
|
|
16
|
+
|
|
17
|
+
This parser is plate-reader oriented: the join key is a well position.
|
|
18
|
+
"""
|
|
19
|
+
p = Path(path)
|
|
20
|
+
if not p.exists():
|
|
21
|
+
raise ValueError(f"Sample map does not exist: {p}")
|
|
22
|
+
if not p.is_file():
|
|
23
|
+
raise ValueError(f"Sample map must be a regular file: {p}")
|
|
24
|
+
suffix = p.suffix.lower()
|
|
25
|
+
if suffix == ".xlsx":
|
|
26
|
+
df = pd.read_excel(p)
|
|
27
|
+
elif suffix == ".csv":
|
|
28
|
+
df = pd.read_csv(p)
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f"Unsupported sample-map format {suffix or '<none>'!r}; expected .csv or .xlsx")
|
|
31
|
+
|
|
32
|
+
normalized_columns = [str(column).strip().lower() for column in df.columns]
|
|
33
|
+
duplicate_columns = sorted(name for name, count in Counter(normalized_columns).items() if count > 1)
|
|
34
|
+
if duplicate_columns:
|
|
35
|
+
raise ValueError(f"Sample map has duplicate column names after normalization: {duplicate_columns}")
|
|
36
|
+
cols = dict(zip(normalized_columns, df.columns, strict=True))
|
|
37
|
+
if "position" in cols:
|
|
38
|
+
if cols["position"] != "position":
|
|
39
|
+
df = df.rename(columns={cols["position"]: "position"})
|
|
40
|
+
elif {"row", "col"}.issubset(cols):
|
|
41
|
+
row_col = cols["row"]
|
|
42
|
+
col_col = cols["col"]
|
|
43
|
+
out = df.copy()
|
|
44
|
+
row_values = out[row_col].astype("string").str.strip()
|
|
45
|
+
col_values = out[col_col].astype("string").str.strip()
|
|
46
|
+
invalid_parts = row_values.isna() | row_values.eq("") | col_values.isna() | col_values.eq("")
|
|
47
|
+
if invalid_parts.any():
|
|
48
|
+
rows = [int(index) + 2 for index in out.index[invalid_parts].tolist()]
|
|
49
|
+
raise ValueError(f"Sample map has blank row/col values at file rows: {rows}")
|
|
50
|
+
out["position"] = row_values + col_values
|
|
51
|
+
df = out.drop(columns=[row_col, col_col])
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError("Sample map must contain either a 'position' column or a ('row','col') pair.")
|
|
54
|
+
|
|
55
|
+
positions = df["position"].astype("string").str.strip().str.upper()
|
|
56
|
+
invalid = positions.isna() | positions.eq("") | positions.eq("NAN") | positions.eq("<NA>")
|
|
57
|
+
if invalid.any():
|
|
58
|
+
rows = [int(index) + 2 for index in df.index[invalid].tolist()]
|
|
59
|
+
raise ValueError(f"Sample map has blank position values at file rows: {rows}")
|
|
60
|
+
duplicates = sorted(positions[positions.duplicated(keep=False)].unique().tolist())
|
|
61
|
+
if duplicates:
|
|
62
|
+
raise ValueError(f"Sample map positions must be unique; duplicates: {duplicates}")
|
|
63
|
+
df = df.copy()
|
|
64
|
+
df["position"] = positions
|
|
65
|
+
return df
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Public parser surface for BioTek Synergy H1 workbooks."""
|
|
2
|
+
|
|
3
|
+
from ._parser import parse_kinetic_only, parse_snapshot_and_timeseries
|
|
4
|
+
from ._shared import probe_synergy_workbook
|
|
5
|
+
|
|
6
|
+
__all__ = ["parse_kinetic_only", "parse_snapshot_and_timeseries", "probe_synergy_workbook"]
|