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,191 @@
|
|
|
1
|
+
"""Aggregate four-state event-window well summaries into design-level records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .contracts import FourStateEventWindowAnalysisSpec, ReductionSpec
|
|
9
|
+
from .event_sensitivity import symmetric_event_sensitivity_half_width
|
|
10
|
+
from .observation_resampling import joint_state_descriptive_resampling_draws
|
|
11
|
+
from .reduction import combine_bound_kinds, invert_bound_kind
|
|
12
|
+
from .seeds import stable_seed
|
|
13
|
+
from .sources import STATE_ORDER, ExperimentSource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_design_records(
|
|
17
|
+
midpoint: pd.DataFrame,
|
|
18
|
+
*,
|
|
19
|
+
lower: pd.DataFrame,
|
|
20
|
+
upper: pd.DataFrame,
|
|
21
|
+
source: ExperimentSource,
|
|
22
|
+
request: FourStateEventWindowAnalysisSpec,
|
|
23
|
+
reduction: ReductionSpec,
|
|
24
|
+
) -> pd.DataFrame:
|
|
25
|
+
"""Aggregate one reduction and its event-bound sensitivity records."""
|
|
26
|
+
|
|
27
|
+
midpoint_values = _aggregate_state_values(midpoint, request=request)
|
|
28
|
+
lower_values = _aggregate_state_values(lower, request=request)
|
|
29
|
+
upper_values = _aggregate_state_values(upper, request=request)
|
|
30
|
+
records: list[dict[str, object]] = []
|
|
31
|
+
for design_id, design in midpoint_values.groupby("design_id", sort=True):
|
|
32
|
+
if set(design["state"].astype(str)) != set(STATE_ORDER):
|
|
33
|
+
raise ValueError(f"{source.experiment_id}:{design_id}:{reduction.id} lacks complete four-state support.")
|
|
34
|
+
lower_design = lower_values.loc[lower_values["design_id"].astype(str).eq(str(design_id))].set_index("state")
|
|
35
|
+
upper_design = upper_values.loc[upper_values["design_id"].astype(str).eq(str(design_id))].set_index("state")
|
|
36
|
+
design_by_state = design.set_index("state")
|
|
37
|
+
record: dict[str, object] = {
|
|
38
|
+
"experiment_id": source.experiment_id,
|
|
39
|
+
"design_id": str(design_id),
|
|
40
|
+
"reference_design_id": request.source.reference_design_id,
|
|
41
|
+
"reduction_id": reduction.id,
|
|
42
|
+
"reduction_method": reduction.method,
|
|
43
|
+
"response_basis": reduction.response_basis,
|
|
44
|
+
"reduction_role": reduction.role,
|
|
45
|
+
"observation_stat": request.aggregation.observation_stat,
|
|
46
|
+
"descriptive_resampling_draws": request.aggregation.descriptive_resampling_draws,
|
|
47
|
+
"descriptive_interval_mass": request.aggregation.descriptive_interval_mass,
|
|
48
|
+
"event_id": source.event.event_id,
|
|
49
|
+
"event_time_estimate_assay_h": source.event.estimate_assay_h,
|
|
50
|
+
"event_time_uncertainty_h": source.event.uncertainty_h,
|
|
51
|
+
"window_start_event_h": reduction.window_start_event_h,
|
|
52
|
+
"window_end_event_h": reduction.window_end_event_h,
|
|
53
|
+
"is_reference": str(design_id) == request.source.reference_design_id,
|
|
54
|
+
"positive_floor": request.quality.positive_floor,
|
|
55
|
+
"allowed_max_interior_gap_h": request.quality.max_interior_gap_h,
|
|
56
|
+
"required_min_observations_per_state": request.quality.min_observations_per_state,
|
|
57
|
+
"min_observation_count_per_state": int(design["observation_count"].min()),
|
|
58
|
+
"min_observed_points_per_trace": int(design["min_observed_points"].min()),
|
|
59
|
+
"max_observed_interior_gap_h": float(design["max_interior_gap_h"].max()),
|
|
60
|
+
"min_pre_observed_points_per_trace": int(design["min_pre_observed_points"].min()),
|
|
61
|
+
"max_pre_observed_interior_gap_h": float(design["max_pre_interior_gap_h"].max()),
|
|
62
|
+
}
|
|
63
|
+
for state in STATE_ORDER:
|
|
64
|
+
row = design_by_state.loc[state]
|
|
65
|
+
lower_row = lower_design.loc[state]
|
|
66
|
+
upper_row = upper_design.loc[state]
|
|
67
|
+
record[f"r{state}"] = float(row["response"])
|
|
68
|
+
record[f"b{state}"] = float(row["anchored_magnitude"])
|
|
69
|
+
record[f"r{state}_descriptive_resampling_sd"] = float(row["response_descriptive_resampling_sd"])
|
|
70
|
+
record[f"b{state}_descriptive_resampling_sd"] = float(row["anchored_magnitude_descriptive_resampling_sd"])
|
|
71
|
+
record[f"r{state}_descriptive_interval_low"] = float(row["response_descriptive_interval_low"])
|
|
72
|
+
record[f"r{state}_descriptive_interval_high"] = float(row["response_descriptive_interval_high"])
|
|
73
|
+
record[f"b{state}_descriptive_interval_low"] = float(row["anchored_magnitude_descriptive_interval_low"])
|
|
74
|
+
record[f"b{state}_descriptive_interval_high"] = float(row["anchored_magnitude_descriptive_interval_high"])
|
|
75
|
+
record[f"r{state}_event_half_range"] = symmetric_event_sensitivity_half_width(
|
|
76
|
+
float(row["response"]),
|
|
77
|
+
float(lower_row["response"]),
|
|
78
|
+
float(upper_row["response"]),
|
|
79
|
+
)
|
|
80
|
+
record[f"b{state}_event_half_range"] = symmetric_event_sensitivity_half_width(
|
|
81
|
+
float(row["anchored_magnitude"]),
|
|
82
|
+
float(lower_row["anchored_magnitude"]),
|
|
83
|
+
float(upper_row["anchored_magnitude"]),
|
|
84
|
+
)
|
|
85
|
+
for prefix, field in (("r", "response"), ("b", "anchored_magnitude")):
|
|
86
|
+
record[f"{prefix}{state}_event_sensitivity_has_policy_clipping"] = any(
|
|
87
|
+
bool(candidate[f"{field}_has_policy_clipping"]) for candidate in (row, lower_row, upper_row)
|
|
88
|
+
)
|
|
89
|
+
record[f"{prefix}{state}_event_sensitivity_has_instrument_overflow"] = any(
|
|
90
|
+
bool(candidate[f"{field}_has_instrument_overflow"]) for candidate in (row, lower_row, upper_row)
|
|
91
|
+
)
|
|
92
|
+
record[f"r{state}_has_policy_clipping"] = bool(row["response_has_policy_clipping"])
|
|
93
|
+
record[f"r{state}_has_instrument_overflow"] = bool(row["response_has_instrument_overflow"])
|
|
94
|
+
record[f"r{state}_bound_kind"] = str(row["response_bound_kind"])
|
|
95
|
+
record[f"b{state}_has_policy_clipping"] = bool(row["anchored_magnitude_has_policy_clipping"])
|
|
96
|
+
record[f"b{state}_has_instrument_overflow"] = bool(row["anchored_magnitude_has_instrument_overflow"])
|
|
97
|
+
record[f"b{state}_bound_kind"] = str(row["anchored_magnitude_bound_kind"])
|
|
98
|
+
record[f"n{state}"] = int(row["observation_count"])
|
|
99
|
+
records.append(record)
|
|
100
|
+
return pd.DataFrame.from_records(records)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _aggregate_state_values(wells: pd.DataFrame, *, request: FourStateEventWindowAnalysisSpec) -> pd.DataFrame:
|
|
104
|
+
stat = request.aggregation.observation_stat
|
|
105
|
+
aggregate = np.median if stat == "median" else np.mean
|
|
106
|
+
anchor_id = request.source.reference_design_id
|
|
107
|
+
anchor = wells.loc[wells["design_id"].astype(str).eq(anchor_id)]
|
|
108
|
+
if set(anchor["state"].astype(str)) != set(STATE_ORDER):
|
|
109
|
+
raise ValueError(f"reference design {anchor_id!r} lacks complete four-state support.")
|
|
110
|
+
rows: list[dict[str, object]] = []
|
|
111
|
+
alpha = (1.0 - request.aggregation.descriptive_interval_mass) / 2.0
|
|
112
|
+
for (design_id, state), frame in wells.groupby(["design_id", "state"], sort=True):
|
|
113
|
+
response_values = frame["response_well"].to_numpy(dtype=float)
|
|
114
|
+
magnitude_values = frame["magnitude_well"].to_numpy(dtype=float)
|
|
115
|
+
state_anchor = anchor.loc[anchor["state"].astype(str).eq(str(state))]
|
|
116
|
+
anchor_values = state_anchor["magnitude_well"].to_numpy(dtype=float)
|
|
117
|
+
minimum = request.quality.min_observations_per_state
|
|
118
|
+
if min(len(response_values), len(magnitude_values), len(anchor_values)) < minimum:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"{design_id}:{state} requires at least {minimum} within-experiment design and reference observations."
|
|
121
|
+
)
|
|
122
|
+
rng = np.random.default_rng(
|
|
123
|
+
stable_seed(
|
|
124
|
+
request.aggregation.random_seed,
|
|
125
|
+
str(frame["experiment_id"].iloc[0]),
|
|
126
|
+
str(design_id),
|
|
127
|
+
str(state),
|
|
128
|
+
str(frame["reduction_id"].iloc[0]),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
response_draws, anchored_magnitude_draws = joint_state_descriptive_resampling_draws(
|
|
132
|
+
response_values,
|
|
133
|
+
magnitude_values,
|
|
134
|
+
anchor_values,
|
|
135
|
+
samples=request.aggregation.descriptive_resampling_draws,
|
|
136
|
+
stat=stat,
|
|
137
|
+
rng=rng,
|
|
138
|
+
paired_anchor=str(design_id) == anchor_id,
|
|
139
|
+
)
|
|
140
|
+
response_bound = combine_bound_kinds(*frame["response_bound_kind"])
|
|
141
|
+
design_magnitude_bound = combine_bound_kinds(*frame["magnitude_bound_kind"])
|
|
142
|
+
anchor_magnitude_bound = combine_bound_kinds(*state_anchor["magnitude_bound_kind"])
|
|
143
|
+
anchored_bound = (
|
|
144
|
+
"exact"
|
|
145
|
+
if str(design_id) == anchor_id
|
|
146
|
+
else combine_bound_kinds(
|
|
147
|
+
design_magnitude_bound,
|
|
148
|
+
invert_bound_kind(anchor_magnitude_bound),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
rows.append(
|
|
152
|
+
{
|
|
153
|
+
"design_id": str(design_id),
|
|
154
|
+
"state": str(state),
|
|
155
|
+
"response": float(aggregate(response_values)),
|
|
156
|
+
"anchored_magnitude": float(aggregate(magnitude_values) - aggregate(anchor_values)),
|
|
157
|
+
"response_descriptive_resampling_sd": float(np.std(response_draws, ddof=1)),
|
|
158
|
+
"anchored_magnitude_descriptive_resampling_sd": float(np.std(anchored_magnitude_draws, ddof=1)),
|
|
159
|
+
"response_descriptive_interval_low": float(np.quantile(response_draws, alpha)),
|
|
160
|
+
"response_descriptive_interval_high": float(np.quantile(response_draws, 1.0 - alpha)),
|
|
161
|
+
"anchored_magnitude_descriptive_interval_low": float(np.quantile(anchored_magnitude_draws, alpha)),
|
|
162
|
+
"anchored_magnitude_descriptive_interval_high": float(
|
|
163
|
+
np.quantile(anchored_magnitude_draws, 1.0 - alpha)
|
|
164
|
+
),
|
|
165
|
+
"observation_count": int(min(len(response_values), len(magnitude_values))),
|
|
166
|
+
"min_observed_points": int(
|
|
167
|
+
frame[["response_observed_point_count", "magnitude_observed_point_count"]].min().min()
|
|
168
|
+
),
|
|
169
|
+
"max_interior_gap_h": float(
|
|
170
|
+
frame[["response_max_interior_gap_h", "magnitude_max_interior_gap_h"]].max().max()
|
|
171
|
+
),
|
|
172
|
+
"min_pre_observed_points": int(frame["response_pre_observed_point_count"].min()),
|
|
173
|
+
"max_pre_interior_gap_h": float(frame["response_pre_max_interior_gap_h"].max()),
|
|
174
|
+
"response_has_policy_clipping": bool(frame["response_policy_clipped_point_count"].gt(0).any()),
|
|
175
|
+
"response_has_instrument_overflow": bool(frame["response_instrument_overflow_point_count"].gt(0).any()),
|
|
176
|
+
"response_bound_kind": response_bound,
|
|
177
|
+
"anchored_magnitude_has_policy_clipping": bool(
|
|
178
|
+
frame["magnitude_policy_clipped_point_count"].gt(0).any()
|
|
179
|
+
or state_anchor["magnitude_policy_clipped_point_count"].gt(0).any()
|
|
180
|
+
),
|
|
181
|
+
"anchored_magnitude_has_instrument_overflow": bool(
|
|
182
|
+
frame["magnitude_instrument_overflow_point_count"].gt(0).any()
|
|
183
|
+
or state_anchor["magnitude_instrument_overflow_point_count"].gt(0).any()
|
|
184
|
+
),
|
|
185
|
+
"anchored_magnitude_bound_kind": anchored_bound,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
return pd.DataFrame.from_records(rows)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["build_design_records"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Validate scalar and mapping fields used by four-state event-window contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def mapping(value: object, *, context: str) -> dict[str, Any]:
|
|
12
|
+
if not isinstance(value, Mapping):
|
|
13
|
+
raise ValueError(f"{context} must be a mapping.")
|
|
14
|
+
return {str(key): item for key, item in value.items()}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def exact_fields(
|
|
18
|
+
value: object,
|
|
19
|
+
*,
|
|
20
|
+
context: str,
|
|
21
|
+
required: set[str],
|
|
22
|
+
optional: set[str] | None = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
payload = mapping(value, context=context)
|
|
25
|
+
allowed = required | (optional or set())
|
|
26
|
+
unknown = sorted(set(payload) - allowed)
|
|
27
|
+
if unknown:
|
|
28
|
+
raise ValueError(f"{context} has unknown fields: {unknown}.")
|
|
29
|
+
missing = sorted(required - set(payload))
|
|
30
|
+
if missing:
|
|
31
|
+
raise ValueError(f"{context} is missing required fields: {missing}.")
|
|
32
|
+
return payload
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def nonempty(value: object, *, context: str) -> str:
|
|
36
|
+
if not isinstance(value, str) or not value.strip():
|
|
37
|
+
raise ValueError(f"{context} must be a non-empty string.")
|
|
38
|
+
return value.strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def finite(value: object, *, context: str) -> float:
|
|
42
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
43
|
+
raise ValueError(f"{context} must be a finite number.")
|
|
44
|
+
result = float(value)
|
|
45
|
+
if not np.isfinite(result):
|
|
46
|
+
raise ValueError(f"{context} must be a finite number.")
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["exact_fields", "finite", "mapping", "nonempty"]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Strict contracts for event-relative plate-reader response summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from .contract_fields import exact_fields as _exact_fields
|
|
10
|
+
from .contract_fields import finite as _finite
|
|
11
|
+
from .contract_fields import mapping as _mapping
|
|
12
|
+
from .contract_fields import nonempty as _nonempty
|
|
13
|
+
from .design_dispositions import DesignDisposition, parse_design_dispositions
|
|
14
|
+
from .well_exclusions import WellExclusion, parse_well_exclusions
|
|
15
|
+
|
|
16
|
+
ReductionMethod = Literal["geometric_time_mean", "integrated_linear_mean"]
|
|
17
|
+
ResponseBasis = Literal["post_window", "post_minus_pre"]
|
|
18
|
+
ReductionRole = Literal["primary", "sensitivity"]
|
|
19
|
+
ObservationStat = Literal["mean", "median"]
|
|
20
|
+
EventEstimateMethod = Literal["segment_gap_midpoint"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class FourStateEventWindowSourceSpec:
|
|
25
|
+
response_channel: str
|
|
26
|
+
magnitude_channel: str
|
|
27
|
+
growth_channel: str
|
|
28
|
+
reference_design_id: str
|
|
29
|
+
state_column: str
|
|
30
|
+
state_values: Mapping[str, str]
|
|
31
|
+
state_labels: Mapping[str, str]
|
|
32
|
+
state_values_case_sensitive: bool = True
|
|
33
|
+
well_exclusions: tuple[WellExclusion, ...] = ()
|
|
34
|
+
design_dispositions: tuple[DesignDisposition, ...] = ()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_mapping(cls, value: object) -> FourStateEventWindowSourceSpec:
|
|
38
|
+
fields = {
|
|
39
|
+
"response_channel",
|
|
40
|
+
"magnitude_channel",
|
|
41
|
+
"growth_channel",
|
|
42
|
+
"reference_design_id",
|
|
43
|
+
"state_column",
|
|
44
|
+
"state_values",
|
|
45
|
+
}
|
|
46
|
+
payload = _exact_fields(
|
|
47
|
+
value,
|
|
48
|
+
context="source",
|
|
49
|
+
required=fields,
|
|
50
|
+
optional={
|
|
51
|
+
"design_dispositions",
|
|
52
|
+
"state_labels",
|
|
53
|
+
"state_values_case_sensitive",
|
|
54
|
+
"well_exclusions",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
state_values = _mapping(payload["state_values"], context="source.state_values")
|
|
58
|
+
expected_states = {"00", "10", "01", "11"}
|
|
59
|
+
if set(state_values) != expected_states:
|
|
60
|
+
raise ValueError("source.state_values must define exactly 00, 10, 01, and 11.")
|
|
61
|
+
normalized_values = {
|
|
62
|
+
state: _nonempty(state_values[state], context=f"source.state_values.{state}")
|
|
63
|
+
for state in sorted(expected_states)
|
|
64
|
+
}
|
|
65
|
+
if len(set(normalized_values.values())) != 4:
|
|
66
|
+
raise ValueError("source.state_values must map to four distinct source values.")
|
|
67
|
+
state_labels = _mapping(payload.get("state_labels", normalized_values), context="source.state_labels")
|
|
68
|
+
if set(state_labels) != expected_states:
|
|
69
|
+
raise ValueError("source.state_labels must define exactly 00, 10, 01, and 11.")
|
|
70
|
+
normalized_labels = {
|
|
71
|
+
state: _nonempty(state_labels[state], context=f"source.state_labels.{state}")
|
|
72
|
+
for state in sorted(expected_states)
|
|
73
|
+
}
|
|
74
|
+
case_sensitive = payload.get("state_values_case_sensitive", True)
|
|
75
|
+
if not isinstance(case_sensitive, bool):
|
|
76
|
+
raise ValueError("source.state_values_case_sensitive must be true or false.")
|
|
77
|
+
reference_design_id = _nonempty(payload["reference_design_id"], context="source.reference_design_id")
|
|
78
|
+
return cls(
|
|
79
|
+
response_channel=_nonempty(payload["response_channel"], context="source.response_channel"),
|
|
80
|
+
magnitude_channel=_nonempty(payload["magnitude_channel"], context="source.magnitude_channel"),
|
|
81
|
+
growth_channel=_nonempty(payload["growth_channel"], context="source.growth_channel"),
|
|
82
|
+
reference_design_id=reference_design_id,
|
|
83
|
+
state_column=_nonempty(payload["state_column"], context="source.state_column"),
|
|
84
|
+
state_values=normalized_values,
|
|
85
|
+
state_labels=normalized_labels,
|
|
86
|
+
state_values_case_sensitive=case_sensitive,
|
|
87
|
+
well_exclusions=parse_well_exclusions(payload.get("well_exclusions", [])),
|
|
88
|
+
design_dispositions=parse_design_dispositions(payload.get("design_dispositions", [])),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def require_known_experiment_ids(self, experiment_ids: Sequence[str]) -> None:
|
|
92
|
+
"""Reject design selections that do not name a resolved source experiment."""
|
|
93
|
+
|
|
94
|
+
known = {str(value) for value in experiment_ids}
|
|
95
|
+
referenced = {item.experiment_id for item in (*self.well_exclusions, *self.design_dispositions)}
|
|
96
|
+
unknown = sorted(referenced - known)
|
|
97
|
+
if unknown:
|
|
98
|
+
raise ValueError(f"source experiment selections name unknown experiments: {unknown}.")
|
|
99
|
+
|
|
100
|
+
def require_known_reduction_ids(self, reduction_ids: Sequence[str]) -> None:
|
|
101
|
+
"""Reject design dispositions that do not name a declared reduction."""
|
|
102
|
+
|
|
103
|
+
known = {str(value) for value in reduction_ids}
|
|
104
|
+
for disposition in self.design_dispositions:
|
|
105
|
+
if disposition.reduction_id not in known:
|
|
106
|
+
raise ValueError(f"design disposition names unknown reduction {disposition.reduction_id!r}.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class EventSpec:
|
|
111
|
+
event_id: str
|
|
112
|
+
event_kind: str
|
|
113
|
+
segment_column: str
|
|
114
|
+
pre_segment_index: int
|
|
115
|
+
post_segment_index: int
|
|
116
|
+
estimate_method: EventEstimateMethod
|
|
117
|
+
declaration: str
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_mapping(cls, value: object) -> EventSpec:
|
|
121
|
+
fields = {
|
|
122
|
+
"event_id",
|
|
123
|
+
"event_kind",
|
|
124
|
+
"segment_column",
|
|
125
|
+
"pre_segment_index",
|
|
126
|
+
"post_segment_index",
|
|
127
|
+
"estimate_method",
|
|
128
|
+
"declaration",
|
|
129
|
+
}
|
|
130
|
+
payload = _exact_fields(value, context="event", required=fields)
|
|
131
|
+
pre = payload["pre_segment_index"]
|
|
132
|
+
post = payload["post_segment_index"]
|
|
133
|
+
if isinstance(pre, bool) or not isinstance(pre, int):
|
|
134
|
+
raise ValueError("event.pre_segment_index must be an integer.")
|
|
135
|
+
if isinstance(post, bool) or not isinstance(post, int):
|
|
136
|
+
raise ValueError("event.post_segment_index must be an integer.")
|
|
137
|
+
if pre == post:
|
|
138
|
+
raise ValueError("event segment indexes must differ.")
|
|
139
|
+
estimate = _nonempty(payload["estimate_method"], context="event.estimate_method")
|
|
140
|
+
if estimate != "segment_gap_midpoint":
|
|
141
|
+
raise ValueError("event.estimate_method must be 'segment_gap_midpoint' for symmetric event uncertainty.")
|
|
142
|
+
return cls(
|
|
143
|
+
event_id=_nonempty(payload["event_id"], context="event.event_id"),
|
|
144
|
+
event_kind=_nonempty(payload["event_kind"], context="event.event_kind"),
|
|
145
|
+
segment_column=_nonempty(payload["segment_column"], context="event.segment_column"),
|
|
146
|
+
pre_segment_index=pre,
|
|
147
|
+
post_segment_index=post,
|
|
148
|
+
estimate_method=estimate,
|
|
149
|
+
declaration=_nonempty(payload["declaration"], context="event.declaration"),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class ReductionSpec:
|
|
155
|
+
id: str
|
|
156
|
+
window_start_event_h: float
|
|
157
|
+
window_end_event_h: float
|
|
158
|
+
method: ReductionMethod
|
|
159
|
+
response_basis: ResponseBasis
|
|
160
|
+
role: ReductionRole
|
|
161
|
+
pre_window_duration_h: float | None = None
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_mapping(cls, value: object, *, index: int) -> ReductionSpec:
|
|
165
|
+
required = {
|
|
166
|
+
"id",
|
|
167
|
+
"window_start_event_h",
|
|
168
|
+
"window_end_event_h",
|
|
169
|
+
"method",
|
|
170
|
+
"response_basis",
|
|
171
|
+
"role",
|
|
172
|
+
}
|
|
173
|
+
payload = _exact_fields(
|
|
174
|
+
value,
|
|
175
|
+
context=f"reductions[{index}]",
|
|
176
|
+
required=required,
|
|
177
|
+
optional={"pre_window_duration_h"},
|
|
178
|
+
)
|
|
179
|
+
start = _finite(payload["window_start_event_h"], context=f"reductions[{index}].window_start_event_h")
|
|
180
|
+
end = _finite(payload["window_end_event_h"], context=f"reductions[{index}].window_end_event_h")
|
|
181
|
+
if start < 0.0 or end <= start:
|
|
182
|
+
raise ValueError(f"reductions[{index}] requires 0 <= window start < window end.")
|
|
183
|
+
method = _nonempty(payload["method"], context=f"reductions[{index}].method")
|
|
184
|
+
if method not in {"geometric_time_mean", "integrated_linear_mean"}:
|
|
185
|
+
raise ValueError(f"reductions[{index}].method is unsupported: {method!r}.")
|
|
186
|
+
basis = _nonempty(payload["response_basis"], context=f"reductions[{index}].response_basis")
|
|
187
|
+
if basis not in {"post_window", "post_minus_pre"}:
|
|
188
|
+
raise ValueError(f"reductions[{index}].response_basis is unsupported: {basis!r}.")
|
|
189
|
+
role = _nonempty(payload["role"], context=f"reductions[{index}].role")
|
|
190
|
+
if role not in {"primary", "sensitivity"}:
|
|
191
|
+
raise ValueError(f"reductions[{index}].role is unsupported: {role!r}.")
|
|
192
|
+
raw_pre = payload.get("pre_window_duration_h")
|
|
193
|
+
pre = None if raw_pre is None else _finite(raw_pre, context=f"reductions[{index}].pre_window_duration_h")
|
|
194
|
+
if basis == "post_minus_pre" and (pre is None or pre <= 0.0):
|
|
195
|
+
raise ValueError(f"reductions[{index}] post_minus_pre requires a positive pre_window_duration_h.")
|
|
196
|
+
if basis == "post_window" and pre is not None:
|
|
197
|
+
raise ValueError(f"reductions[{index}] pre_window_duration_h is only valid for post_minus_pre.")
|
|
198
|
+
return cls(
|
|
199
|
+
id=_nonempty(payload["id"], context=f"reductions[{index}].id"),
|
|
200
|
+
window_start_event_h=start,
|
|
201
|
+
window_end_event_h=end,
|
|
202
|
+
method=method,
|
|
203
|
+
response_basis=basis,
|
|
204
|
+
role=role,
|
|
205
|
+
pre_window_duration_h=pre,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True)
|
|
210
|
+
class AggregationSpec:
|
|
211
|
+
observation_stat: ObservationStat
|
|
212
|
+
descriptive_resampling_draws: int
|
|
213
|
+
descriptive_interval_mass: float
|
|
214
|
+
random_seed: int
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_mapping(cls, value: object) -> AggregationSpec:
|
|
218
|
+
fields = {
|
|
219
|
+
"observation_stat",
|
|
220
|
+
"descriptive_resampling_draws",
|
|
221
|
+
"descriptive_interval_mass",
|
|
222
|
+
"random_seed",
|
|
223
|
+
}
|
|
224
|
+
payload = _exact_fields(value, context="aggregation", required=fields)
|
|
225
|
+
stat = _nonempty(payload["observation_stat"], context="aggregation.observation_stat")
|
|
226
|
+
if stat not in {"mean", "median"}:
|
|
227
|
+
raise ValueError(f"aggregation.observation_stat is unsupported: {stat!r}.")
|
|
228
|
+
samples = payload["descriptive_resampling_draws"]
|
|
229
|
+
seed = payload["random_seed"]
|
|
230
|
+
if isinstance(samples, bool) or not isinstance(samples, int) or samples < 100:
|
|
231
|
+
raise ValueError("aggregation.descriptive_resampling_draws must be an integer of at least 100.")
|
|
232
|
+
if isinstance(seed, bool) or not isinstance(seed, int) or seed < 0:
|
|
233
|
+
raise ValueError("aggregation.random_seed must be a non-negative integer.")
|
|
234
|
+
level = _finite(payload["descriptive_interval_mass"], context="aggregation.descriptive_interval_mass")
|
|
235
|
+
if not 0.5 < level < 1.0:
|
|
236
|
+
raise ValueError("aggregation.descriptive_interval_mass must be between 0.5 and 1.0.")
|
|
237
|
+
return cls(
|
|
238
|
+
observation_stat=stat,
|
|
239
|
+
descriptive_resampling_draws=samples,
|
|
240
|
+
descriptive_interval_mass=level,
|
|
241
|
+
random_seed=seed,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass(frozen=True)
|
|
246
|
+
class QualitySpec:
|
|
247
|
+
positive_floor: float
|
|
248
|
+
max_interior_gap_h: float
|
|
249
|
+
min_observations_per_state: int
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def from_mapping(cls, value: object) -> QualitySpec:
|
|
253
|
+
fields = {"positive_floor", "max_interior_gap_h", "min_observations_per_state"}
|
|
254
|
+
payload = _exact_fields(value, context="quality", required=fields)
|
|
255
|
+
floor = _finite(payload["positive_floor"], context="quality.positive_floor")
|
|
256
|
+
gap = _finite(payload["max_interior_gap_h"], context="quality.max_interior_gap_h")
|
|
257
|
+
observations = payload["min_observations_per_state"]
|
|
258
|
+
if floor <= 0.0:
|
|
259
|
+
raise ValueError("quality.positive_floor must be positive.")
|
|
260
|
+
if gap <= 0.0:
|
|
261
|
+
raise ValueError("quality.max_interior_gap_h must be positive.")
|
|
262
|
+
if isinstance(observations, bool) or not isinstance(observations, int) or observations < 2:
|
|
263
|
+
raise ValueError("quality.min_observations_per_state must be an integer of at least 2.")
|
|
264
|
+
return cls(
|
|
265
|
+
positive_floor=floor,
|
|
266
|
+
max_interior_gap_h=gap,
|
|
267
|
+
min_observations_per_state=observations,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class FourStateEventWindowAnalysisSpec:
|
|
273
|
+
source: FourStateEventWindowSourceSpec
|
|
274
|
+
event: EventSpec
|
|
275
|
+
reductions: tuple[ReductionSpec, ...]
|
|
276
|
+
aggregation: AggregationSpec
|
|
277
|
+
quality: QualitySpec
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def primary_reduction(self) -> ReductionSpec:
|
|
281
|
+
return next(spec for spec in self.reductions if spec.role == "primary")
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def from_mapping(cls, value: object) -> FourStateEventWindowAnalysisSpec:
|
|
285
|
+
fields = {
|
|
286
|
+
"source",
|
|
287
|
+
"event",
|
|
288
|
+
"reductions",
|
|
289
|
+
"aggregation",
|
|
290
|
+
"quality",
|
|
291
|
+
}
|
|
292
|
+
payload = _exact_fields(value, context="analysis", required=fields)
|
|
293
|
+
raw_reductions = payload["reductions"]
|
|
294
|
+
if isinstance(raw_reductions, (str, bytes)) or not isinstance(raw_reductions, Sequence):
|
|
295
|
+
raise ValueError("reductions must be a non-empty sequence.")
|
|
296
|
+
reductions = tuple(ReductionSpec.from_mapping(item, index=index) for index, item in enumerate(raw_reductions))
|
|
297
|
+
ids = [spec.id for spec in reductions]
|
|
298
|
+
if not reductions or len(ids) != len(set(ids)):
|
|
299
|
+
raise ValueError("reduction ids must be non-empty and unique.")
|
|
300
|
+
if sum(spec.role == "primary" for spec in reductions) != 1:
|
|
301
|
+
raise ValueError("analysis must declare exactly one primary reduction.")
|
|
302
|
+
source = FourStateEventWindowSourceSpec.from_mapping(payload["source"])
|
|
303
|
+
source.require_known_reduction_ids(ids)
|
|
304
|
+
return cls(
|
|
305
|
+
source=source,
|
|
306
|
+
event=EventSpec.from_mapping(payload["event"]),
|
|
307
|
+
reductions=reductions,
|
|
308
|
+
aggregation=AggregationSpec.from_mapping(payload["aggregation"]),
|
|
309
|
+
quality=QualitySpec.from_mapping(payload["quality"]),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
__all__ = [
|
|
314
|
+
"AggregationSpec",
|
|
315
|
+
"EventSpec",
|
|
316
|
+
"QualitySpec",
|
|
317
|
+
"ReductionSpec",
|
|
318
|
+
"FourStateEventWindowAnalysisSpec",
|
|
319
|
+
"FourStateEventWindowSourceSpec",
|
|
320
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Define plate- and reduction-scoped design eligibility dispositions."""
|
|
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
|
+
_OBSERVATION_REASON = "insufficient_state_observations"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class DesignDisposition:
|
|
15
|
+
experiment_id: str
|
|
16
|
+
design_id: str
|
|
17
|
+
state: str
|
|
18
|
+
reduction_id: str
|
|
19
|
+
reason: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_design_dispositions(value: object) -> tuple[DesignDisposition, ...]:
|
|
23
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
24
|
+
raise ValueError("source.design_dispositions must be a sequence.")
|
|
25
|
+
dispositions: list[DesignDisposition] = []
|
|
26
|
+
for index, item in enumerate(value):
|
|
27
|
+
context = f"source.design_dispositions[{index}]"
|
|
28
|
+
payload = exact_fields(
|
|
29
|
+
item,
|
|
30
|
+
context=context,
|
|
31
|
+
required={"experiment_id", "design_id", "state", "reduction_id", "reason"},
|
|
32
|
+
)
|
|
33
|
+
state = nonempty(payload["state"], context=f"{context}.state")
|
|
34
|
+
if state not in {"00", "10", "01", "11"}:
|
|
35
|
+
raise ValueError(f"{context}.state must be 00, 10, 01, or 11.")
|
|
36
|
+
reason = nonempty(payload["reason"], context=f"{context}.reason")
|
|
37
|
+
if reason != _OBSERVATION_REASON:
|
|
38
|
+
raise ValueError(f"{context}.reason must be {_OBSERVATION_REASON!r}.")
|
|
39
|
+
dispositions.append(
|
|
40
|
+
DesignDisposition(
|
|
41
|
+
experiment_id=nonempty(payload["experiment_id"], context=f"{context}.experiment_id"),
|
|
42
|
+
design_id=nonempty(payload["design_id"], context=f"{context}.design_id"),
|
|
43
|
+
state=state,
|
|
44
|
+
reduction_id=nonempty(payload["reduction_id"], context=f"{context}.reduction_id"),
|
|
45
|
+
reason=reason,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
keys = [(item.experiment_id, item.design_id, item.reduction_id) for item in dispositions]
|
|
49
|
+
if len(keys) != len(set(keys)):
|
|
50
|
+
raise ValueError("source.design_dispositions must not repeat an experiment, design, and reduction.")
|
|
51
|
+
return tuple(dispositions)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["DesignDisposition", "parse_design_dispositions"]
|