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.
Files changed (293) hide show
  1. reader_workbench/__init__.py +22 -0
  2. reader_workbench/__main__.py +4 -0
  3. reader_workbench/_version.py +17 -0
  4. reader_workbench/api/__init__.py +74 -0
  5. reader_workbench/api/_record_reads.py +75 -0
  6. reader_workbench/api/artifacts.py +79 -0
  7. reader_workbench/api/facade.py +538 -0
  8. reader_workbench/api/models.py +285 -0
  9. reader_workbench/api/notebooks.py +63 -0
  10. reader_workbench/contracts/__init__.py +18 -0
  11. reader_workbench/contracts/builtins/__init__.py +36 -0
  12. reader_workbench/contracts/builtins/cytometry.py +140 -0
  13. reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
  14. reader_workbench/contracts/builtins/generic.py +21 -0
  15. reader_workbench/contracts/builtins/logic.py +149 -0
  16. reader_workbench/contracts/builtins/plate_reader.py +47 -0
  17. reader_workbench/contracts/catalog.py +257 -0
  18. reader_workbench/contracts/model.py +109 -0
  19. reader_workbench/domains/__init__.py +1 -0
  20. reader_workbench/domains/cytometry/__init__.py +3 -0
  21. reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
  22. reader_workbench/domains/cytometry/analysis/events.py +182 -0
  23. reader_workbench/domains/cytometry/analysis/gating.py +175 -0
  24. reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
  25. reader_workbench/domains/cytometry/io/__init__.py +3 -0
  26. reader_workbench/domains/cytometry/io/fcs.py +135 -0
  27. reader_workbench/domains/cytometry/plots/__init__.py +5 -0
  28. reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
  29. reader_workbench/domains/logic/__init__.py +3 -0
  30. reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
  31. reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
  32. reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
  33. reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
  34. reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
  35. reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
  36. reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
  37. reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
  38. reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
  39. reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
  40. reader_workbench/domains/logic/four_state_vector/config.py +214 -0
  41. reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
  42. reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
  43. reader_workbench/domains/logic/four_state_vector/math.py +191 -0
  44. reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
  45. reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
  46. reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
  47. reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
  48. reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
  49. reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
  50. reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
  51. reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
  52. reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
  53. reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
  54. reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
  55. reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
  56. reader_workbench/domains/logic/treatment_columns.py +42 -0
  57. reader_workbench/domains/plate_reader/__init__.py +1 -0
  58. reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
  59. reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
  60. reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
  61. reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
  62. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
  63. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
  64. reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
  65. reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
  66. reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
  67. reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
  68. reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
  69. reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
  70. reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
  71. reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
  72. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
  73. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
  74. reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
  75. reader_workbench/domains/plate_reader/io/__init__.py +6 -0
  76. reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
  77. reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
  78. reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
  79. reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
  80. reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
  81. reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
  82. reader_workbench/domains/plate_reader/ordering.py +59 -0
  83. reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
  84. reader_workbench/domains/plate_reader/plots/_data.py +29 -0
  85. reader_workbench/domains/plate_reader/plots/common.py +346 -0
  86. reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
  87. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
  88. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
  89. reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
  90. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
  91. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
  92. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
  93. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
  94. reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
  95. reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
  96. reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
  97. reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
  98. reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
  99. reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
  100. reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
  101. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
  102. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
  103. reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
  104. reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
  105. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
  106. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
  107. reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
  108. reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
  109. reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
  110. reader_workbench/domains/time_series/__init__.py +29 -0
  111. reader_workbench/domains/time_series/aggregation.py +60 -0
  112. reader_workbench/domains/time_series/contracts.py +368 -0
  113. reader_workbench/domains/time_series/reduction.py +395 -0
  114. reader_workbench/errors.py +55 -0
  115. reader_workbench/maintenance/__init__.py +6 -0
  116. reader_workbench/maintenance/docs.py +335 -0
  117. reader_workbench/maintenance/model.py +28 -0
  118. reader_workbench/maintenance/release.py +39 -0
  119. reader_workbench/maintenance/skills.py +124 -0
  120. reader_workbench/plotting/__init__.py +20 -0
  121. reader_workbench/plotting/mpl.py +56 -0
  122. reader_workbench/plotting/sinks.py +69 -0
  123. reader_workbench/plotting/style.py +175 -0
  124. reader_workbench/plotting/utils.py +27 -0
  125. reader_workbench/plugins/__init__.py +1 -0
  126. reader_workbench/plugins/catalog.py +33 -0
  127. reader_workbench/plugins/export/__init__.py +0 -0
  128. reader_workbench/plugins/export/_paths.py +21 -0
  129. reader_workbench/plugins/export/csv.py +41 -0
  130. reader_workbench/plugins/export/xlsx.py +44 -0
  131. reader_workbench/plugins/ingest/__init__.py +0 -0
  132. reader_workbench/plugins/ingest/_discovery.py +58 -0
  133. reader_workbench/plugins/ingest/discovery_policy.py +66 -0
  134. reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
  135. reader_workbench/plugins/ingest/synergy_h1.py +234 -0
  136. reader_workbench/plugins/manifests/__init__.py +1 -0
  137. reader_workbench/plugins/manifests/export.py +29 -0
  138. reader_workbench/plugins/manifests/ingest.py +29 -0
  139. reader_workbench/plugins/manifests/plot.py +161 -0
  140. reader_workbench/plugins/manifests/transform.py +172 -0
  141. reader_workbench/plugins/manifests/validator.py +18 -0
  142. reader_workbench/plugins/plot/__init__.py +0 -0
  143. reader_workbench/plugins/plot/_shared.py +55 -0
  144. reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
  145. reader_workbench/plugins/plot/distributions.py +58 -0
  146. reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
  147. reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
  148. reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
  149. reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
  150. reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
  151. reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
  152. reader_workbench/plugins/plot/logic_symmetry.py +56 -0
  153. reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
  154. reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
  155. reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
  156. reader_workbench/plugins/plot/time_series.py +114 -0
  157. reader_workbench/plugins/plot/ts_and_snap.py +210 -0
  158. reader_workbench/plugins/transform/__init__.py +0 -0
  159. reader_workbench/plugins/transform/_four_state_vector.py +204 -0
  160. reader_workbench/plugins/transform/_labeling.py +109 -0
  161. reader_workbench/plugins/transform/alias.py +70 -0
  162. reader_workbench/plugins/transform/assay_labels.py +62 -0
  163. reader_workbench/plugins/transform/blank.py +79 -0
  164. reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
  165. reader_workbench/plugins/transform/cytometry_gating.py +120 -0
  166. reader_workbench/plugins/transform/fold_change.py +79 -0
  167. reader_workbench/plugins/transform/four_state_event_window.py +93 -0
  168. reader_workbench/plugins/transform/four_state_vector.py +62 -0
  169. reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
  170. reader_workbench/plugins/transform/logic_symmetry.py +67 -0
  171. reader_workbench/plugins/transform/outlier_filter.py +60 -0
  172. reader_workbench/plugins/transform/overflow.py +197 -0
  173. reader_workbench/plugins/transform/ratio.py +237 -0
  174. reader_workbench/plugins/transform/sample_map.py +170 -0
  175. reader_workbench/plugins/transform/sample_metadata.py +94 -0
  176. reader_workbench/plugins/validator/__init__.py +1 -0
  177. reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
  178. reader_workbench/protocols/__init__.py +80 -0
  179. reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
  180. reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
  181. reader_workbench/protocols/builtins.py +1656 -0
  182. reader_workbench/protocols/compiler.py +22 -0
  183. reader_workbench/protocols/compilers/__init__.py +1 -0
  184. reader_workbench/protocols/compilers/common.py +100 -0
  185. reader_workbench/protocols/compilers/cytometry.py +87 -0
  186. reader_workbench/protocols/compilers/generic.py +14 -0
  187. reader_workbench/protocols/compilers/logic.py +245 -0
  188. reader_workbench/protocols/compilers/plate_reader.py +937 -0
  189. reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
  190. reader_workbench/protocols/model.py +1486 -0
  191. reader_workbench/protocols/semantic_coverage.py +234 -0
  192. reader_workbench/runtime/__init__.py +12 -0
  193. reader_workbench/runtime/builtin.py +23 -0
  194. reader_workbench/runtime/model.py +42 -0
  195. reader_workbench/workbench/__init__.py +60 -0
  196. reader_workbench/workbench/assets/__init__.py +22 -0
  197. reader_workbench/workbench/assets/types.py +118 -0
  198. reader_workbench/workbench/audit/__init__.py +5 -0
  199. reader_workbench/workbench/audit/experiments.py +307 -0
  200. reader_workbench/workbench/audit/staging.py +187 -0
  201. reader_workbench/workbench/cli/__init__.py +51 -0
  202. reader_workbench/workbench/cli/_lazy.py +9 -0
  203. reader_workbench/workbench/cli/_records_view.py +150 -0
  204. reader_workbench/workbench/cli/_surface_execution.py +443 -0
  205. reader_workbench/workbench/cli/audit.py +95 -0
  206. reader_workbench/workbench/cli/automation.py +229 -0
  207. reader_workbench/workbench/cli/demo.py +46 -0
  208. reader_workbench/workbench/cli/dop.py +91 -0
  209. reader_workbench/workbench/cli/experiments.py +635 -0
  210. reader_workbench/workbench/cli/helpers.py +232 -0
  211. reader_workbench/workbench/cli/main.py +59 -0
  212. reader_workbench/workbench/cli/maintenance.py +82 -0
  213. reader_workbench/workbench/cli/notebooks.py +260 -0
  214. reader_workbench/workbench/cli/pagination.py +117 -0
  215. reader_workbench/workbench/cli/protocols.py +336 -0
  216. reader_workbench/workbench/cli/shared.py +309 -0
  217. reader_workbench/workbench/cli/surfaces.py +534 -0
  218. reader_workbench/workbench/cli/verification.py +128 -0
  219. reader_workbench/workbench/commands.py +10 -0
  220. reader_workbench/workbench/config/__init__.py +47 -0
  221. reader_workbench/workbench/config/identity.py +13 -0
  222. reader_workbench/workbench/config/load.py +405 -0
  223. reader_workbench/workbench/config/model.py +274 -0
  224. reader_workbench/workbench/context.py +26 -0
  225. reader_workbench/workbench/decl/__init__.py +31 -0
  226. reader_workbench/workbench/decl/build.py +190 -0
  227. reader_workbench/workbench/decl/model.py +81 -0
  228. reader_workbench/workbench/dop/__init__.py +12 -0
  229. reader_workbench/workbench/dop/builtins.py +261 -0
  230. reader_workbench/workbench/dop/model.py +209 -0
  231. reader_workbench/workbench/engine/__init__.py +42 -0
  232. reader_workbench/workbench/engine/_shared.py +76 -0
  233. reader_workbench/workbench/engine/contracts.py +283 -0
  234. reader_workbench/workbench/engine/execution.py +326 -0
  235. reader_workbench/workbench/engine/file_outputs.py +260 -0
  236. reader_workbench/workbench/engine/inputs.py +161 -0
  237. reader_workbench/workbench/engine/invocations.py +507 -0
  238. reader_workbench/workbench/engine/planning.py +72 -0
  239. reader_workbench/workbench/engine/runtime.py +464 -0
  240. reader_workbench/workbench/engine/setup.py +149 -0
  241. reader_workbench/workbench/engine/validation.py +684 -0
  242. reader_workbench/workbench/experiment/__init__.py +47 -0
  243. reader_workbench/workbench/experiment/model.py +381 -0
  244. reader_workbench/workbench/experiments.py +133 -0
  245. reader_workbench/workbench/graph/__init__.py +47 -0
  246. reader_workbench/workbench/graph/nodes.py +102 -0
  247. reader_workbench/workbench/graph/normalize.py +177 -0
  248. reader_workbench/workbench/graph/refs.py +148 -0
  249. reader_workbench/workbench/input_discovery.py +19 -0
  250. reader_workbench/workbench/inspection/__init__.py +3 -0
  251. reader_workbench/workbench/inspection/catalogs.py +128 -0
  252. reader_workbench/workbench/inspection/common.py +92 -0
  253. reader_workbench/workbench/inspection/dop.py +64 -0
  254. reader_workbench/workbench/inspection/experiments.py +449 -0
  255. reader_workbench/workbench/inspection/inventory.py +68 -0
  256. reader_workbench/workbench/inspection/protocols.py +368 -0
  257. reader_workbench/workbench/inspection/readiness.py +333 -0
  258. reader_workbench/workbench/inspection/reports.py +367 -0
  259. reader_workbench/workbench/inspection/results.py +166 -0
  260. reader_workbench/workbench/inspection/runtime.py +287 -0
  261. reader_workbench/workbench/inspection/semantics.py +192 -0
  262. reader_workbench/workbench/inspection/validation.py +30 -0
  263. reader_workbench/workbench/notebooks/__init__.py +17 -0
  264. reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
  265. reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
  266. reader_workbench/workbench/notebooks/components/__init__.py +21 -0
  267. reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
  268. reader_workbench/workbench/notebooks/components/overview.py +119 -0
  269. reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
  270. reader_workbench/workbench/notebooks/launch.py +274 -0
  271. reader_workbench/workbench/notebooks/presentation.py +136 -0
  272. reader_workbench/workbench/notebooks/scaffold.py +60 -0
  273. reader_workbench/workbench/ontology.py +78 -0
  274. reader_workbench/workbench/paths.py +44 -0
  275. reader_workbench/workbench/ports/__init__.py +31 -0
  276. reader_workbench/workbench/ports/model.py +168 -0
  277. reader_workbench/workbench/records/__init__.py +44 -0
  278. reader_workbench/workbench/records/epoch.py +329 -0
  279. reader_workbench/workbench/records/evidence.py +247 -0
  280. reader_workbench/workbench/records/identity.py +87 -0
  281. reader_workbench/workbench/records/locking.py +185 -0
  282. reader_workbench/workbench/records/model.py +711 -0
  283. reader_workbench/workbench/records/sources.py +73 -0
  284. reader_workbench/workbench/records/store.py +1022 -0
  285. reader_workbench/workbench/records/verification.py +998 -0
  286. reader_workbench/workbench/registry.py +333 -0
  287. reader_workbench/workbench/spec_overrides.py +215 -0
  288. reader_workbench-1.0.0.dist-info/METADATA +91 -0
  289. reader_workbench-1.0.0.dist-info/RECORD +293 -0
  290. reader_workbench-1.0.0.dist-info/WHEEL +5 -0
  291. reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
  292. reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  293. reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,140 @@
1
+ """Materialize explicit well and design eligibility dispositions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ import pandas as pd
8
+
9
+ from .design_dispositions import DesignDisposition
10
+ from .well_exclusions import WellExclusion
11
+
12
+ _COLUMNS = (
13
+ "experiment_id",
14
+ "design_id",
15
+ "scope",
16
+ "state",
17
+ "position",
18
+ "reduction_id",
19
+ "reason",
20
+ "observed_count",
21
+ "required_count",
22
+ )
23
+
24
+
25
+ def partition_eligible_wells(
26
+ wells: pd.DataFrame,
27
+ *,
28
+ dispositions: Sequence[DesignDisposition],
29
+ experiment_id: str,
30
+ reduction_id: str,
31
+ reference_design_id: str,
32
+ available_design_ids: set[str],
33
+ required_count: int,
34
+ ) -> tuple[pd.DataFrame, list[dict[str, object]]]:
35
+ selected = [
36
+ item for item in dispositions if item.experiment_id == experiment_id and item.reduction_id == reduction_id
37
+ ]
38
+ disposed_ids: set[str] = set()
39
+ records: list[dict[str, object]] = []
40
+ for item in selected:
41
+ if item.design_id == reference_design_id:
42
+ raise ValueError(f"{experiment_id}:{reduction_id} cannot dispose the reference design.")
43
+ if item.design_id not in available_design_ids:
44
+ raise ValueError(f"{experiment_id}:{reduction_id} disposition names absent design {item.design_id!r}.")
45
+ observed_count = int(
46
+ wells.loc[
47
+ wells["design_id"].astype(str).eq(item.design_id) & wells["state"].astype(str).eq(item.state)
48
+ ].shape[0]
49
+ )
50
+ if observed_count >= required_count:
51
+ raise ValueError(
52
+ f"{experiment_id}:{item.design_id}:{item.state}:{reduction_id} has {observed_count} observations; "
53
+ f"at least {required_count} are required, so the design disposition is not justified."
54
+ )
55
+ disposed_ids.add(item.design_id)
56
+ records.append(
57
+ _record(
58
+ experiment_id=item.experiment_id,
59
+ design_id=item.design_id,
60
+ scope="design",
61
+ state=item.state,
62
+ position=None,
63
+ reduction_id=item.reduction_id,
64
+ reason=item.reason,
65
+ observed_count=observed_count,
66
+ required_count=required_count,
67
+ )
68
+ )
69
+ eligible = wells.loc[~wells["design_id"].astype(str).isin(disposed_ids)].copy()
70
+ return eligible, records
71
+
72
+
73
+ def partition_reduction_wells(
74
+ midpoint: pd.DataFrame,
75
+ lower: pd.DataFrame,
76
+ upper: pd.DataFrame,
77
+ *,
78
+ exclusions: Sequence[WellExclusion],
79
+ dispositions: Sequence[DesignDisposition],
80
+ experiment_id: str,
81
+ reduction_id: str,
82
+ reference_design_id: str,
83
+ available_design_ids: set[str],
84
+ required_count: int,
85
+ ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, list[dict[str, object]]]:
86
+ eligible, records = partition_eligible_wells(
87
+ midpoint,
88
+ dispositions=dispositions,
89
+ experiment_id=experiment_id,
90
+ reduction_id=reduction_id,
91
+ reference_design_id=reference_design_id,
92
+ available_design_ids=available_design_ids,
93
+ required_count=required_count,
94
+ )
95
+ eligible_ids = set(eligible["design_id"].astype(str))
96
+ eligible_lower = lower.loc[lower["design_id"].astype(str).isin(eligible_ids)].copy()
97
+ eligible_upper = upper.loc[upper["design_id"].astype(str).isin(eligible_ids)].copy()
98
+ records[:0] = well_disposition_records(
99
+ exclusions,
100
+ experiment_id=experiment_id,
101
+ reduction_id=reduction_id,
102
+ )
103
+ return eligible, eligible_lower, eligible_upper, records
104
+
105
+
106
+ def well_disposition_records(
107
+ exclusions: Sequence[WellExclusion],
108
+ *,
109
+ experiment_id: str,
110
+ reduction_id: str,
111
+ ) -> list[dict[str, object]]:
112
+ return [
113
+ _record(
114
+ experiment_id=item.experiment_id,
115
+ design_id=item.design_id,
116
+ scope="well",
117
+ state=item.state,
118
+ position=item.position,
119
+ reduction_id=item.reduction_id,
120
+ reason=item.reason,
121
+ observed_count=None,
122
+ required_count=None,
123
+ )
124
+ for item in exclusions
125
+ if item.experiment_id == experiment_id and item.reduction_id == reduction_id
126
+ ]
127
+
128
+
129
+ def disposition_frame(records: Sequence[dict[str, object]]) -> pd.DataFrame:
130
+ frame = pd.DataFrame.from_records(records, columns=_COLUMNS)
131
+ frame["observed_count"] = frame["observed_count"].astype("Int64")
132
+ frame["required_count"] = frame["required_count"].astype("Int64")
133
+ return frame
134
+
135
+
136
+ def _record(**values: object) -> dict[str, object]:
137
+ return {column: values[column] for column in _COLUMNS}
138
+
139
+
140
+ __all__ = ["disposition_frame", "partition_reduction_wells"]
@@ -0,0 +1,27 @@
1
+ """Event-time sensitivity primitives for four-state event-window reductions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+
8
+ def symmetric_event_sensitivity_half_width(
9
+ midpoint: float,
10
+ lower_bound: float,
11
+ upper_bound: float,
12
+ ) -> float:
13
+ """Return a midpoint-centered envelope covering both event-bound reductions.
14
+
15
+ Four-state event-window reduction is not necessarily linear in event time. The
16
+ midpoint estimate therefore need not bisect the values obtained at the two
17
+ event bounds. A symmetric display interval uses the larger absolute
18
+ deviation from the midpoint.
19
+ """
20
+
21
+ values = np.asarray([midpoint, lower_bound, upper_bound], dtype=float)
22
+ if not np.all(np.isfinite(values)):
23
+ raise ValueError("event-time sensitivity values must be finite.")
24
+ return float(max(abs(values[0] - values[1]), abs(values[2] - values[0])))
25
+
26
+
27
+ __all__ = ["symmetric_event_sensitivity_half_width"]
@@ -0,0 +1,274 @@
1
+ """Materialize well and design four-state event-window records from verified sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pandas as pd
6
+
7
+ from .aggregation import build_design_records
8
+ from .contracts import FourStateEventWindowAnalysisSpec, ReductionSpec
9
+ from .disposition_records import disposition_frame, partition_reduction_wells
10
+ from .observation_resampling import descriptive_resampling_records
11
+ from .reduction import (
12
+ combine_bound_kinds,
13
+ four_state_event_window_temporal_spec,
14
+ invert_bound_kind,
15
+ reduce_temporal_trace,
16
+ )
17
+ from .sources import ExperimentSource, event_record
18
+ from .well_exclusion_validation import require_well_exclusion_support_failure
19
+
20
+
21
+ def materialize_experiment(
22
+ source: ExperimentSource,
23
+ *,
24
+ request: FourStateEventWindowAnalysisSpec,
25
+ ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
26
+ """Build midpoint well/design records plus event-bound sensitivity."""
27
+
28
+ require_well_exclusion_support_failure(
29
+ source.well_exclusions,
30
+ reductions={item.id: item for item in request.reductions},
31
+ quality=request.quality,
32
+ response=source.response,
33
+ magnitude=source.magnitude,
34
+ event_estimates_h=(
35
+ source.event.interval_start_assay_h,
36
+ source.event.estimate_assay_h,
37
+ source.event.interval_end_assay_h,
38
+ ),
39
+ pre_window_end_h=source.event.interval_start_assay_h,
40
+ experiment_id=source.experiment_id,
41
+ )
42
+ well_frames: list[pd.DataFrame] = []
43
+ design_frames: list[pd.DataFrame] = []
44
+ draw_frames: list[pd.DataFrame] = []
45
+ disposition_records: list[dict[str, object]] = []
46
+ for reduction in request.reductions:
47
+ midpoint = _reduce_wells(
48
+ source,
49
+ request=request,
50
+ reduction=reduction,
51
+ event_estimate_h=source.event.estimate_assay_h,
52
+ )
53
+ lower = _reduce_wells(
54
+ source,
55
+ request=request,
56
+ reduction=reduction,
57
+ event_estimate_h=source.event.interval_start_assay_h,
58
+ )
59
+ upper = _reduce_wells(
60
+ source,
61
+ request=request,
62
+ reduction=reduction,
63
+ event_estimate_h=source.event.interval_end_assay_h,
64
+ )
65
+ eligible_midpoint, eligible_lower, eligible_upper, reduction_dispositions = partition_reduction_wells(
66
+ midpoint,
67
+ lower,
68
+ upper,
69
+ exclusions=request.source.well_exclusions,
70
+ dispositions=request.source.design_dispositions,
71
+ experiment_id=source.experiment_id,
72
+ reduction_id=reduction.id,
73
+ reference_design_id=request.source.reference_design_id,
74
+ available_design_ids=set(source.response["design_id"].astype(str)),
75
+ required_count=request.quality.min_observations_per_state,
76
+ )
77
+ well_frames.append(midpoint)
78
+ draw_frames.append(descriptive_resampling_records(eligible_midpoint, request=request))
79
+ design_frames.append(
80
+ build_design_records(
81
+ eligible_midpoint,
82
+ lower=eligible_lower,
83
+ upper=eligible_upper,
84
+ source=source,
85
+ request=request,
86
+ reduction=reduction,
87
+ )
88
+ )
89
+ disposition_records.extend(reduction_dispositions)
90
+
91
+ wells = pd.concat(well_frames, ignore_index=True)
92
+ designs = pd.concat(design_frames, ignore_index=True)
93
+ draws = pd.concat(draw_frames, ignore_index=True)
94
+ well_key = ["experiment_id", "design_id", "state", "position", "reduction_id"]
95
+ design_key = ["experiment_id", "design_id", "reduction_id"]
96
+ if wells.duplicated(subset=well_key).any():
97
+ raise ValueError(f"{source.experiment_id}: well record identity is not unique.")
98
+ if designs.duplicated(subset=design_key).any():
99
+ raise ValueError(f"{source.experiment_id}: design record identity is not unique.")
100
+ draw_key = ["experiment_id", "design_id", "reduction_id", "draw_index"]
101
+ if draws.duplicated(subset=draw_key).any():
102
+ raise ValueError(f"{source.experiment_id}: descriptive-resampling draw identity is not unique.")
103
+ traces = _trace_record(source, request=request)
104
+ return wells, designs, draws, traces, event_record(source.event), disposition_frame(disposition_records)
105
+
106
+
107
+ def _reduce_wells(
108
+ source: ExperimentSource,
109
+ *,
110
+ request: FourStateEventWindowAnalysisSpec,
111
+ reduction: ReductionSpec,
112
+ event_estimate_h: float,
113
+ ) -> pd.DataFrame:
114
+ response = _summaries_by_trace(
115
+ source.response,
116
+ request=request,
117
+ reduction=reduction,
118
+ event_estimate_h=event_estimate_h,
119
+ signal_kind="response",
120
+ pre_window_end_h=source.event.interval_start_assay_h,
121
+ )
122
+ magnitude = _summaries_by_trace(
123
+ source.magnitude,
124
+ request=request,
125
+ reduction=reduction,
126
+ event_estimate_h=event_estimate_h,
127
+ signal_kind="magnitude",
128
+ pre_window_end_h=None,
129
+ )
130
+ keys = ["design_id", "state", "position"]
131
+ merged = response.merge(
132
+ magnitude, on=keys, how="outer", validate="one_to_one", suffixes=("_response", "_magnitude")
133
+ )
134
+ if merged.isna().any().any():
135
+ missing = merged.columns[merged.isna().any()].tolist()
136
+ raise ValueError(f"{source.experiment_id}:{reduction.id}: response/magnitude wells do not align: {missing}.")
137
+ merged.insert(0, "experiment_id", source.experiment_id)
138
+ merged["reduction_id"] = reduction.id
139
+ merged["reduction_method"] = reduction.method
140
+ merged["response_basis"] = reduction.response_basis
141
+ merged["reduction_role"] = reduction.role
142
+ merged["event_time_estimate_assay_h"] = float(event_estimate_h)
143
+ merged["window_start_event_h"] = reduction.window_start_event_h
144
+ merged["window_end_event_h"] = reduction.window_end_event_h
145
+ merged["window_start_assay_h"] = event_estimate_h + reduction.window_start_event_h
146
+ merged["window_end_assay_h"] = event_estimate_h + reduction.window_end_event_h
147
+ merged["is_reference"] = merged["design_id"].astype(str).eq(request.source.reference_design_id)
148
+ return merged.sort_values(keys, kind="mergesort").reset_index(drop=True)
149
+
150
+
151
+ def _summaries_by_trace(
152
+ frame: pd.DataFrame,
153
+ *,
154
+ request: FourStateEventWindowAnalysisSpec,
155
+ reduction: ReductionSpec,
156
+ event_estimate_h: float,
157
+ signal_kind: str,
158
+ pre_window_end_h: float | None,
159
+ ) -> pd.DataFrame:
160
+ rows: list[dict[str, object]] = []
161
+ excluded = {
162
+ (item.design_id, item.state, item.position)
163
+ for item in request.source.well_exclusions
164
+ if item.experiment_id == source_experiment_id(frame) and item.reduction_id == reduction.id
165
+ }
166
+ for (design_id, state, position), trace in frame.groupby(
167
+ ["design_id", "state", "position"], sort=True, dropna=False
168
+ ):
169
+ if (str(design_id), str(state), str(position)) in excluded:
170
+ continue
171
+ trace_id = f"{frame['experiment_id'].iloc[0]}:{signal_kind}:{design_id}:{state}:{position}:{reduction.id}"
172
+ post = reduce_temporal_trace(
173
+ trace["time"].to_numpy(dtype=float),
174
+ trace["value"].to_numpy(dtype=float),
175
+ spec=four_state_event_window_temporal_spec(reduction, request.quality),
176
+ origin_h=event_estimate_h,
177
+ trace_id=trace_id,
178
+ policy_clipped=trace["value_policy_clipped"].to_numpy(dtype=bool),
179
+ instrument_overflow=trace["value_instrument_overflow"].to_numpy(dtype=bool),
180
+ bound_kinds=trace["value_bound_kind"].to_numpy(dtype=object),
181
+ )
182
+ value = post.value
183
+ policy_clipped_point_count = post.policy_clipped_point_count
184
+ instrument_overflow_point_count = post.instrument_overflow_point_count
185
+ bound_kind = post.bound_kind
186
+ pre_observed_point_count = 0
187
+ pre_integration_point_count = 0
188
+ pre_max_interior_gap_h = 0.0
189
+ if signal_kind == "response" and reduction.response_basis == "post_minus_pre":
190
+ if reduction.pre_window_duration_h is None or pre_window_end_h is None:
191
+ raise ValueError(f"{trace_id}: delta response lacks an explicit pre-event window.")
192
+ pre = reduce_temporal_trace(
193
+ trace["time"].to_numpy(dtype=float),
194
+ trace["value"].to_numpy(dtype=float),
195
+ spec=four_state_event_window_temporal_spec(
196
+ reduction,
197
+ request.quality,
198
+ absolute_window_h=(
199
+ pre_window_end_h - reduction.pre_window_duration_h,
200
+ pre_window_end_h,
201
+ ),
202
+ ),
203
+ trace_id=f"{trace_id}:pre",
204
+ policy_clipped=trace["value_policy_clipped"].to_numpy(dtype=bool),
205
+ instrument_overflow=trace["value_instrument_overflow"].to_numpy(dtype=bool),
206
+ bound_kinds=trace["value_bound_kind"].to_numpy(dtype=object),
207
+ )
208
+ value -= pre.value
209
+ policy_clipped_point_count += pre.policy_clipped_point_count
210
+ instrument_overflow_point_count += pre.instrument_overflow_point_count
211
+ bound_kind = combine_bound_kinds(bound_kind, invert_bound_kind(pre.bound_kind))
212
+ pre_observed_point_count = pre.observed_point_count
213
+ pre_integration_point_count = pre.evaluation_point_count
214
+ pre_max_interior_gap_h = pre.max_interior_gap_h
215
+ rows.append(
216
+ {
217
+ "design_id": str(design_id),
218
+ "state": str(state),
219
+ "position": str(position),
220
+ f"{signal_kind}_well": float(value),
221
+ f"{signal_kind}_observed_point_count": post.observed_point_count,
222
+ f"{signal_kind}_integration_point_count": post.evaluation_point_count,
223
+ f"{signal_kind}_max_interior_gap_h": post.max_interior_gap_h,
224
+ f"{signal_kind}_pre_observed_point_count": pre_observed_point_count,
225
+ f"{signal_kind}_pre_integration_point_count": pre_integration_point_count,
226
+ f"{signal_kind}_pre_max_interior_gap_h": pre_max_interior_gap_h,
227
+ f"{signal_kind}_policy_clipped_point_count": policy_clipped_point_count,
228
+ f"{signal_kind}_instrument_overflow_point_count": instrument_overflow_point_count,
229
+ f"{signal_kind}_bound_kind": bound_kind,
230
+ }
231
+ )
232
+ result = pd.DataFrame.from_records(rows)
233
+ if result.empty:
234
+ raise ValueError(f"{signal_kind} reduction produced no well records.")
235
+ return result
236
+
237
+
238
+ def source_experiment_id(frame: pd.DataFrame) -> str:
239
+ return str(frame["experiment_id"].iloc[0])
240
+
241
+
242
+ def _trace_record(source: ExperimentSource, *, request: FourStateEventWindowAnalysisSpec) -> pd.DataFrame:
243
+ frames: list[pd.DataFrame] = []
244
+ for signal_kind, frame in (
245
+ ("response", source.response),
246
+ ("magnitude", source.magnitude),
247
+ ("growth", source.trajectory),
248
+ ):
249
+ selected = frame.loc[
250
+ :,
251
+ [
252
+ "experiment_id",
253
+ "design_id",
254
+ "position",
255
+ "state",
256
+ "time",
257
+ "time_from_event_h",
258
+ "value",
259
+ "value_policy_clipped",
260
+ "value_instrument_overflow",
261
+ "value_bound_kind",
262
+ ],
263
+ ].copy()
264
+ selected["signal_kind"] = signal_kind
265
+ selected["is_reference"] = selected["design_id"].astype(str).eq(request.source.reference_design_id)
266
+ frames.append(selected)
267
+ result = pd.concat(frames, ignore_index=True)
268
+ key = ["experiment_id", "design_id", "position", "state", "signal_kind", "time"]
269
+ if result.duplicated(subset=key).any():
270
+ raise ValueError(f"{source.experiment_id}: trace record identity is not unique.")
271
+ return result.sort_values(key, kind="mergesort").reset_index(drop=True)
272
+
273
+
274
+ __all__ = ["materialize_experiment"]
@@ -0,0 +1,97 @@
1
+ """Descriptive resampling primitives for within-experiment observations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from .contracts import FourStateEventWindowAnalysisSpec, ObservationStat
9
+ from .seeds import stable_seed
10
+ from .sources import STATE_ORDER
11
+
12
+
13
+ def descriptive_resampling_records(wells: pd.DataFrame, *, request: FourStateEventWindowAnalysisSpec) -> pd.DataFrame:
14
+ """Resample observed wells jointly without implying population inference."""
15
+
16
+ anchor_id = request.source.reference_design_id
17
+ anchor = wells.loc[wells["design_id"].astype(str).eq(anchor_id)]
18
+ records: list[pd.DataFrame] = []
19
+ for design_id, design_wells in wells.groupby("design_id", sort=True):
20
+ state_draws: dict[str, np.ndarray] = {}
21
+ for state in STATE_ORDER:
22
+ state_wells = design_wells.loc[design_wells["state"].astype(str).eq(state)]
23
+ anchor_wells = anchor.loc[anchor["state"].astype(str).eq(state)]
24
+ response_values = state_wells["response_well"].to_numpy(dtype=float)
25
+ magnitude_values = state_wells["magnitude_well"].to_numpy(dtype=float)
26
+ anchor_values = anchor_wells["magnitude_well"].to_numpy(dtype=float)
27
+ minimum = request.quality.min_observations_per_state
28
+ if min(len(response_values), len(magnitude_values), len(anchor_values)) < minimum:
29
+ raise ValueError(f"{design_id}:{state} lacks support for within-experiment descriptive resampling.")
30
+ reduction_id = str(state_wells["reduction_id"].iloc[0])
31
+ experiment_id = str(state_wells["experiment_id"].iloc[0])
32
+ rng = np.random.default_rng(
33
+ stable_seed(request.aggregation.random_seed, experiment_id, str(design_id), state, reduction_id)
34
+ )
35
+ response_draws, anchored_magnitude_draws = joint_state_descriptive_resampling_draws(
36
+ response_values,
37
+ magnitude_values,
38
+ anchor_values,
39
+ samples=request.aggregation.descriptive_resampling_draws,
40
+ stat=request.aggregation.observation_stat,
41
+ rng=rng,
42
+ paired_anchor=str(design_id) == anchor_id,
43
+ )
44
+ state_draws[f"r{state}"] = response_draws
45
+ state_draws[f"b{state}"] = anchored_magnitude_draws
46
+ frame = pd.DataFrame(state_draws)
47
+ frame.insert(0, "draw_index", np.arange(len(frame), dtype=int))
48
+ frame.insert(0, "reduction_id", str(design_wells["reduction_id"].iloc[0]))
49
+ frame.insert(0, "design_id", str(design_id))
50
+ frame.insert(0, "experiment_id", str(design_wells["experiment_id"].iloc[0]))
51
+ frame["is_reference"] = str(design_id) == anchor_id
52
+ records.append(frame)
53
+ return pd.concat(records, ignore_index=True)
54
+
55
+
56
+ def joint_state_descriptive_resampling_draws(
57
+ response_values: np.ndarray,
58
+ magnitude_values: np.ndarray,
59
+ anchor_values: np.ndarray,
60
+ *,
61
+ samples: int,
62
+ stat: ObservationStat,
63
+ rng: np.random.Generator,
64
+ paired_anchor: bool = False,
65
+ ) -> tuple[np.ndarray, np.ndarray]:
66
+ """Resample paired observed wells and either paired or independent reference wells."""
67
+
68
+ response = np.asarray(response_values, dtype=float)
69
+ magnitude = np.asarray(magnitude_values, dtype=float)
70
+ anchor = np.asarray(anchor_values, dtype=float)
71
+ if stat not in {"mean", "median"}:
72
+ raise ValueError("joint descriptive resampling requires stat to be 'mean' or 'median'.")
73
+ if response.ndim != 1 or magnitude.ndim != 1 or anchor.ndim != 1:
74
+ raise ValueError("joint descriptive-resampling inputs must be one-dimensional arrays.")
75
+ if len(response) != len(magnitude) or len(response) == 0 or len(anchor) == 0:
76
+ raise ValueError(
77
+ "joint descriptive resampling requires aligned design observations and non-empty reference observations."
78
+ )
79
+ if samples < 2:
80
+ raise ValueError("joint descriptive resampling requires at least two draws.")
81
+ if paired_anchor and not np.array_equal(magnitude, anchor):
82
+ raise ValueError(
83
+ "paired reference descriptive resampling requires identical ordered magnitude and anchor observations."
84
+ )
85
+ design_indexes = rng.integers(0, len(response), size=(samples, len(response)))
86
+ anchor_indexes = design_indexes if paired_anchor else rng.integers(0, len(anchor), size=(samples, len(anchor)))
87
+ aggregate = np.median if stat == "median" else np.mean
88
+ response_draws = aggregate(response[design_indexes], axis=1)
89
+ magnitude_draws = aggregate(magnitude[design_indexes], axis=1)
90
+ anchor_draws = aggregate(anchor[anchor_indexes], axis=1)
91
+ return response_draws, magnitude_draws - anchor_draws
92
+
93
+
94
+ __all__ = [
95
+ "descriptive_resampling_records",
96
+ "joint_state_descriptive_resampling_draws",
97
+ ]
@@ -0,0 +1,62 @@
1
+ """Four-state event-window adapter to the assay-neutral temporal reducer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from reader_workbench.domains.time_series import (
6
+ IntervalSelection,
7
+ TemporalReductionSpec,
8
+ TemporalSupportPolicy,
9
+ combine_bound_kinds,
10
+ invert_bound_kind,
11
+ reduce_temporal_trace,
12
+ )
13
+
14
+ from .contracts import QualitySpec, ReductionSpec
15
+
16
+
17
+ def four_state_event_window_temporal_spec(
18
+ reduction: ReductionSpec,
19
+ quality: QualitySpec,
20
+ *,
21
+ absolute_window_h: tuple[float, float] | None = None,
22
+ ) -> TemporalReductionSpec:
23
+ """Project four-state event-window policy into the neutral temporal contract.
24
+
25
+ Four-state event-window reductions remain event-relative unless an explicit
26
+ absolute pre-event interval is supplied. The projection preserves the
27
+ established interpolation, log2 output, support-gap, and censor behavior.
28
+ """
29
+
30
+ if absolute_window_h is None:
31
+ selection = IntervalSelection(
32
+ time_basis="event_relative",
33
+ start_h=reduction.window_start_event_h,
34
+ end_h=reduction.window_end_event_h,
35
+ )
36
+ else:
37
+ selection = IntervalSelection(
38
+ time_basis="absolute",
39
+ start_h=float(absolute_window_h[0]),
40
+ end_h=float(absolute_window_h[1]),
41
+ )
42
+ return TemporalReductionSpec(
43
+ selection=selection,
44
+ method=reduction.method,
45
+ output_space="log2",
46
+ support=TemporalSupportPolicy(
47
+ boundary_support="covered",
48
+ minimum_observations=0,
49
+ maximum_interior_gap_h=quality.max_interior_gap_h,
50
+ positive_floor=quality.positive_floor,
51
+ positive_value_scope="entire_trace",
52
+ censored_values="allow",
53
+ ),
54
+ )
55
+
56
+
57
+ __all__ = [
58
+ "combine_bound_kinds",
59
+ "invert_bound_kind",
60
+ "reduce_temporal_trace",
61
+ "four_state_event_window_temporal_spec",
62
+ ]
@@ -0,0 +1,15 @@
1
+ """Deterministic seed derivation for four-state event-window resampling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+
8
+ def stable_seed(base: int, *parts: str) -> int:
9
+ """Derive a reproducible unsigned seed from an explicit identity tuple."""
10
+
11
+ digest = hashlib.sha256((str(base) + ":" + ":".join(parts)).encode()).digest()
12
+ return int.from_bytes(digest[:8], "little")
13
+
14
+
15
+ __all__ = ["stable_seed"]