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,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+ from pydantic import Field, model_validator
7
+
8
+ from reader_workbench.plotting.sinks import PlotFigure
9
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
10
+ from reader_workbench.workbench.ports import dataframe_input
11
+ from reader_workbench.workbench.registry import PluginConfig
12
+
13
+
14
+ class TimeSeriesFigureCfg(PluginConfig):
15
+ """Presentation options consumed by the time-series renderer."""
16
+
17
+ figsize: tuple[float, float] | None = None
18
+ dpi: int | None = Field(default=None, gt=0)
19
+ ext: str | None = Field(default=None, min_length=1)
20
+ rc: dict[str, Any] | None = None
21
+ rasterize_zorder: float | None = None
22
+ line_alpha: float | None = Field(default=None, ge=0.0, le=1.0)
23
+ mean_marker_alpha: float | None = Field(default=None, ge=0.0, le=1.0)
24
+ observation_alpha: float | None = Field(default=None, ge=0.0, le=1.0)
25
+ axis_label_size: float | None = Field(default=None, gt=0.0)
26
+ title_fontsize: float | None = Field(default=None, gt=0.0)
27
+ tick_label_size: float | None = Field(default=None, gt=0.0)
28
+ legend_fontsize: float | None = Field(default=None, gt=0.0)
29
+ legend_marker_size: float | None = Field(default=None, gt=0.0)
30
+ mean_marker_size: float | None = Field(default=None, gt=0.0)
31
+ observation_marker_size: float | None = Field(default=None, gt=0.0)
32
+ line_width: float | None = Field(default=None, gt=0.0)
33
+ top: float | None = Field(default=None, ge=0.0, le=1.0)
34
+ bottom: float | None = Field(default=None, ge=0.0, le=1.0)
35
+ left: float | None = Field(default=None, ge=0.0, le=1.0)
36
+ right: float | None = Field(default=None, ge=0.0, le=1.0)
37
+ wspace: float | None = Field(default=None, ge=0.0)
38
+ hspace: float | None = Field(default=None, ge=0.0)
39
+
40
+ @model_validator(mode="after")
41
+ def validate_geometry(self) -> TimeSeriesFigureCfg:
42
+ if self.figsize is not None and any(value <= 0.0 for value in self.figsize):
43
+ raise ValueError("time_series.fig.figsize values must be positive")
44
+ if self.left is not None and self.right is not None and self.left >= self.right:
45
+ raise ValueError("time_series.fig.left must be less than fig.right")
46
+ if self.bottom is not None and self.top is not None and self.bottom >= self.top:
47
+ raise ValueError("time_series.fig.bottom must be less than fig.top")
48
+ return self
49
+
50
+
51
+ class TimeSeriesCfg(PluginConfig):
52
+ x: str = "time"
53
+ xlabel: str | None = None
54
+ y: list[str] | None = None
55
+ ylabel_map: dict[str, str] = Field(default_factory=dict)
56
+ hue_label_map: dict[str, str] = Field(default_factory=dict)
57
+ hue: str = "treatment"
58
+ partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
59
+ fig: TimeSeriesFigureCfg = Field(default_factory=TimeSeriesFigureCfg)
60
+ channels: list[str] | None = None
61
+ add_sheet_line: bool = False
62
+ sheet_line_kwargs: dict[str, Any] = Field(default_factory=dict)
63
+ log_transform: bool | list[str] = False
64
+ time_window: list[float] | None = None
65
+ observation_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
66
+ observation_interval_alpha: float = Field(default=0.15, ge=0.0, le=1.0)
67
+ observation_resamples: int = Field(default=100, ge=1)
68
+ observation_seed: int = 0
69
+ legend_loc: str = "upper left"
70
+ show_observations: bool = False
71
+ shared_legend: bool = False
72
+ filename: str | None = None
73
+
74
+
75
+ class TimeSeriesPlot(FigurePlotPlugin):
76
+ ConfigModel = TimeSeriesCfg
77
+
78
+ @classmethod
79
+ def input_ports(cls):
80
+ return {"df": dataframe_input("df", "tidy.v1")}
81
+
82
+ def render(self, ctx, inputs, cfg: TimeSeriesCfg) -> list[PlotFigure]:
83
+ df: pd.DataFrame = inputs["df"]
84
+ from reader_workbench.domains.plate_reader.plots.time_series import plot_time_series # noqa: PLC0415
85
+
86
+ partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
87
+
88
+ return plot_time_series(
89
+ df=df,
90
+ x=cfg.x,
91
+ xlabel=cfg.xlabel,
92
+ y=cfg.y,
93
+ ylabel_map=cfg.ylabel_map,
94
+ hue_label_map=cfg.hue_label_map,
95
+ hue=cfg.hue,
96
+ channels=cfg.channels,
97
+ group_on=partition.group_by,
98
+ pool_sets=partition.collection_items,
99
+ pool_match=partition.match,
100
+ fig_kwargs=cfg.fig.model_dump(exclude_none=True),
101
+ add_sheet_line=cfg.add_sheet_line,
102
+ sheet_line_kwargs=cfg.sheet_line_kwargs,
103
+ log_transform=cfg.log_transform,
104
+ time_window=cfg.time_window,
105
+ palette_book=ctx.palette_book,
106
+ observation_interval_mass=cfg.observation_interval_mass,
107
+ observation_interval_alpha=cfg.observation_interval_alpha,
108
+ observation_resamples=cfg.observation_resamples,
109
+ observation_seed=cfg.observation_seed,
110
+ legend_loc=cfg.legend_loc,
111
+ show_observations=cfg.show_observations,
112
+ shared_legend=cfg.shared_legend,
113
+ filename=cfg.filename,
114
+ )
@@ -0,0 +1,210 @@
1
+ """Two-panel plot (time series + snapshot barplot)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ import pandas as pd
8
+ from pydantic import Field, model_validator
9
+
10
+ from reader_workbench.plotting.sinks import PlotFigure
11
+ from reader_workbench.plugins.plot._shared import FigurePlotPlugin, PlotPartitionCfg, resolve_plot_partition_cfg
12
+ from reader_workbench.workbench.ports import dataframe_input
13
+ from reader_workbench.workbench.registry import PluginConfig
14
+
15
+
16
+ class TSAndSnapFigureCfg(PluginConfig):
17
+ """Figure and presentation options accepted by the composite renderer."""
18
+
19
+ figsize: tuple[float, float] | None = None
20
+ dpi: float | None = Field(default=None, gt=0)
21
+ facecolor: str | None = None
22
+ edgecolor: str | None = None
23
+ frameon: bool | None = None
24
+ clear: bool | None = None
25
+ constrained_layout: bool | None = None
26
+ layout: str | None = None
27
+ rc: dict[str, Any] | None = None
28
+ ext: str | None = None
29
+ axis_label_size: float | None = Field(default=None, gt=0)
30
+ tick_label_size: float | None = Field(default=None, gt=0)
31
+ legend_fontsize: float | None = Field(default=None, gt=0)
32
+ legend_marker_size: float | None = Field(default=None, gt=0)
33
+ line_width: float | None = Field(default=None, gt=0)
34
+ mean_marker_size: float | None = Field(default=None, gt=0)
35
+ mean_marker_every: int | None = Field(default=None, ge=1)
36
+ observation_marker_size: float | None = Field(default=None, gt=0)
37
+ style_legend_loc: str | None = None
38
+ style_legend_title: str | None = None
39
+ snap_tick_rotation: float | None = None
40
+ ts_title: str | None = None
41
+ snap_title: str | None = None
42
+ line_alpha: float | None = Field(default=None, ge=0, le=1)
43
+ mean_marker_alpha: float | None = Field(default=None, ge=0, le=1)
44
+ observation_alpha: float | None = Field(default=None, ge=0, le=1)
45
+ suptitle_y: float | None = None
46
+
47
+
48
+ class TSAndSnapCfg(PluginConfig):
49
+ # grouping
50
+ partition: PlotPartitionCfg = Field(default_factory=PlotPartitionCfg)
51
+ group_layout: Literal["separate", "paired_row"] = "separate"
52
+
53
+ # time series (left)
54
+ ts_x: str = "time"
55
+ ts_channel: str
56
+ ts_hue: str
57
+ ts_style: str | None = None
58
+ order_hue: list[str] | None = None
59
+ order_hue_ref: str | None = None
60
+ order_style: list[str] | None = None
61
+ order_style_ref: str | None = None
62
+ ts_time_window: list[float] | None = None
63
+ ts_add_sheet_line: bool = False
64
+ ts_sheet_line_kwargs: dict[str, Any] = Field(default_factory=dict)
65
+ ts_mark_snap_time: bool = False
66
+ ts_snap_line_kwargs: dict[str, Any] = Field(default_factory=dict)
67
+ ts_log_transform: bool | list[str] = False
68
+ ts_observation_interval_mass: float = Field(default=0.95, gt=0.0, lt=1.0)
69
+ ts_observation_interval_alpha: float = Field(default=0.15, ge=0.0, le=1.0)
70
+ ts_observation_resamples: int = Field(default=100, ge=1)
71
+ ts_observation_seed: int = 0
72
+ ts_show_observations: bool = False
73
+ ts_legend_loc: str = "upper right"
74
+
75
+ # snapshot (right)
76
+ snap_x: str = "treatment"
77
+ snap_channel: str | None = None
78
+ snap_hue: str | None = None
79
+ order_x: list[str] | None = None
80
+ order_x_ref: str | None = None
81
+ order_snap_hue: list[str] | None = None
82
+ order_snap_hue_ref: str | None = None
83
+ snap_time: float = Field(..., description="Snapshot time (hours); required for deterministic plotting.")
84
+ snap_agg: Literal["mean", "median"] = "mean"
85
+ snap_dispersion: Literal["sd", "iqr", "none"] = "sd"
86
+ snap_time_tolerance: float = 0.51
87
+ snap_show_legend: bool = False
88
+ snap_legend_loc: str = "upper right"
89
+ snap_color_by_x: bool = False
90
+ square_panels: bool = False
91
+
92
+ # figure
93
+ fig: TSAndSnapFigureCfg = Field(default_factory=TSAndSnapFigureCfg)
94
+ filename: str | None = None
95
+ title: str | None = None
96
+
97
+ @model_validator(mode="after")
98
+ def validate_order_sources(self) -> TSAndSnapCfg:
99
+ for arg_name in ("order_hue", "order_style", "order_x", "order_snap_hue"):
100
+ if getattr(self, arg_name) is not None and getattr(self, f"{arg_name}_ref") is not None:
101
+ raise ValueError(f"ts_and_snap: {arg_name} and {arg_name}_ref are mutually exclusive")
102
+ if self.order_style is not None and self.ts_style is None:
103
+ raise ValueError("ts_and_snap: order_style requires ts_style")
104
+ if self.order_style_ref is not None and self.ts_style is None:
105
+ raise ValueError("ts_and_snap: order_style_ref requires ts_style")
106
+ if self.order_snap_hue is not None and self.snap_hue is None:
107
+ raise ValueError("ts_and_snap: order_snap_hue requires snap_hue")
108
+ if self.order_snap_hue_ref is not None and self.snap_hue is None:
109
+ raise ValueError("ts_and_snap: order_snap_hue_ref requires snap_hue")
110
+ if self.snap_color_by_x and self.snap_hue is not None:
111
+ raise ValueError("ts_and_snap: snap_color_by_x requires snap_hue to be omitted")
112
+ return self
113
+
114
+
115
+ class TSAndSnapPlot(FigurePlotPlugin):
116
+ ConfigModel = TSAndSnapCfg
117
+
118
+ @classmethod
119
+ def input_ports(cls):
120
+ return {"df": dataframe_input("df", "tidy.v1")}
121
+
122
+ @classmethod
123
+ def _resolve_semantic_orders(cls, *, experiment, cfg: TSAndSnapCfg):
124
+ return (
125
+ experiment.annotations.resolve_order_arg(
126
+ order=cfg.order_hue,
127
+ order_ref=cfg.order_hue_ref,
128
+ column=cfg.ts_hue,
129
+ arg_name="order_hue",
130
+ ),
131
+ experiment.annotations.resolve_order_arg(
132
+ order=cfg.order_style,
133
+ order_ref=cfg.order_style_ref,
134
+ column=cfg.ts_style,
135
+ arg_name="order_style",
136
+ ),
137
+ experiment.annotations.resolve_order_arg(
138
+ order=cfg.order_x,
139
+ order_ref=cfg.order_x_ref,
140
+ column=cfg.snap_x,
141
+ arg_name="order_x",
142
+ ),
143
+ experiment.annotations.resolve_order_arg(
144
+ order=cfg.order_snap_hue,
145
+ order_ref=cfg.order_snap_hue_ref,
146
+ column=cfg.snap_hue,
147
+ arg_name="order_snap_hue",
148
+ ),
149
+ )
150
+
151
+ @classmethod
152
+ def validate_semantic_references(cls, *, experiment, cfg: TSAndSnapCfg) -> None:
153
+ cls._resolve_semantic_orders(experiment=experiment, cfg=cfg)
154
+
155
+ def render(self, ctx, inputs, cfg: TSAndSnapCfg) -> list[PlotFigure]:
156
+ df: pd.DataFrame = inputs["df"]
157
+ from reader_workbench.domains.plate_reader.plots.ts_and_snap import plot_ts_and_snap # noqa: PLC0415
158
+
159
+ partition = resolve_plot_partition_cfg(ctx=ctx, partition=cfg.partition)
160
+ if ctx.experiment is None:
161
+ raise ValueError("ts_and_snap requires experiment semantics in the run context")
162
+ resolved_order_hue, resolved_order_style, resolved_order_x, resolved_order_snap_hue = type(
163
+ self
164
+ )._resolve_semantic_orders(experiment=ctx.experiment, cfg=cfg)
165
+
166
+ return plot_ts_and_snap(
167
+ df=df,
168
+ group_on=partition.group_by,
169
+ pool_sets=partition.collection_items,
170
+ pool_match=partition.match,
171
+ group_layout=cfg.group_layout,
172
+ # ts (left)
173
+ ts_x=cfg.ts_x,
174
+ ts_channel=cfg.ts_channel,
175
+ ts_hue=cfg.ts_hue,
176
+ ts_style=cfg.ts_style,
177
+ order_hue=resolved_order_hue,
178
+ order_style=resolved_order_style,
179
+ ts_time_window=cfg.ts_time_window,
180
+ ts_add_sheet_line=cfg.ts_add_sheet_line,
181
+ ts_sheet_line_kwargs=cfg.ts_sheet_line_kwargs,
182
+ ts_mark_snap_time=cfg.ts_mark_snap_time,
183
+ ts_snap_line_kwargs=cfg.ts_snap_line_kwargs,
184
+ ts_log_transform=cfg.ts_log_transform,
185
+ ts_observation_interval_mass=cfg.ts_observation_interval_mass,
186
+ ts_observation_interval_alpha=cfg.ts_observation_interval_alpha,
187
+ ts_observation_resamples=cfg.ts_observation_resamples,
188
+ ts_observation_seed=cfg.ts_observation_seed,
189
+ ts_show_observations=cfg.ts_show_observations,
190
+ ts_legend_loc=cfg.ts_legend_loc,
191
+ # snap (right)
192
+ snap_x=cfg.snap_x,
193
+ snap_channel=cfg.snap_channel,
194
+ snap_hue=cfg.snap_hue,
195
+ order_x=resolved_order_x,
196
+ order_snap_hue=resolved_order_snap_hue,
197
+ snap_time=cfg.snap_time,
198
+ snap_agg=cfg.snap_agg,
199
+ snap_dispersion=cfg.snap_dispersion,
200
+ snap_time_tolerance=cfg.snap_time_tolerance,
201
+ snap_show_legend=cfg.snap_show_legend,
202
+ snap_legend_loc=cfg.snap_legend_loc,
203
+ snap_color_by_x=cfg.snap_color_by_x,
204
+ square_panels=cfg.square_panels,
205
+ # fig
206
+ fig_kwargs=cfg.fig.model_dump(exclude_none=True),
207
+ filename=cfg.filename,
208
+ title=cfg.title,
209
+ palette_book=ctx.palette_book,
210
+ )
File without changes
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from reader_workbench.domains.logic.four_state_vector.builder import (
7
+ FourStateVectorBuildResult,
8
+ build_four_state_vector_from_tidy,
9
+ )
10
+ from reader_workbench.domains.logic.four_state_vector.treatment_semantics import (
11
+ bind_four_state_vector_treatment_semantics,
12
+ )
13
+
14
+
15
+ def build_four_state_vector_plugin_result(*, ctx, df: pd.DataFrame, cfg) -> FourStateVectorBuildResult:
16
+ if ctx.experiment is None:
17
+ raise ValueError("four_state_vector requires experiment semantics in the run context")
18
+ state_space = ctx.experiment.annotations.resolve_ordered_state_space(ref=cfg.state_map_ref)
19
+ semantics = bind_four_state_vector_treatment_semantics(
20
+ state_ids=state_space.state_ids,
21
+ source_column=state_space.column,
22
+ source_values=state_space.source_values,
23
+ case_sensitive=state_space.case_sensitive,
24
+ treatment_column=cfg.treatment_column,
25
+ )
26
+ run_cfg = semantics.inject(cfg.model_dump(exclude={"state_map_ref"}))
27
+ return build_four_state_vector_from_tidy(df.copy(), run_cfg)
28
+
29
+
30
+ def log_four_state_vector_plugin_result(*, ctx, result: FourStateVectorBuildResult) -> None:
31
+ vector = result.vector
32
+ four_state_vector_cfg = result.cfg
33
+ sel_logic = result.sel_logic
34
+ sel_int = result.sel_int
35
+ label_col = four_state_vector_cfg.design_by[0] if four_state_vector_cfg.design_by else "design_id"
36
+ idx_cols = [col for col in four_state_vector_cfg.design_by if col]
37
+
38
+ if sel_logic.time_warning:
39
+ ctx.logger.warning("four_state_vector: %s", sel_logic.time_warning)
40
+
41
+ try:
42
+ chosen = sel_logic.chosen_time
43
+ flats = int(vector["flat_logic"].sum()) if "flat_logic" in vector.columns else 0
44
+ r_stats = vector["r_logic"].describe() if "r_logic" in vector.columns else None
45
+ ref_info = result.log.get("reference", {})
46
+
47
+ if flats:
48
+ fraction = float(result.log.get("flat_logic_fraction", 0.0))
49
+ samples = [str(value) for value in result.log.get("flat_logic_sample_design_ids", ())]
50
+ sample_note = f" Sample design_ids: {', '.join(samples)}." if samples else ""
51
+ ctx.logger.warning(
52
+ "four_state_vector: flat logic detected for %d/%d designs (%.1f%%).%s",
53
+ flats,
54
+ len(vector),
55
+ fraction * 100.0,
56
+ sample_note,
57
+ )
58
+
59
+ ctx.logger.info(
60
+ "four_state_vector • inputs: [accent]logic[/accent]=%s → v00..v11 | [accent]intensity[/accent]=%s → y*00..y*11",
61
+ four_state_vector_cfg.response.logic_channel,
62
+ four_state_vector_cfg.response.intensity_channel,
63
+ )
64
+ ctx.logger.info(
65
+ "four_state_vector • transform semantics:"
66
+ " v: log2(LOGIC) → per-row min-max → [0,1] (persisted NOT log)."
67
+ " y*: log2( ((INTENSITY + eps_abs)/max(A + alpha, eps_ref)) + delta ) (persisted in log2)."
68
+ )
69
+ ctx.logger.info(
70
+ "four_state_vector • knobs: eps_ratio=%.1e eps_range=%.1e eps_ref=%.1e eps_abs=%.1e | alpha=%.3g delta=%.3g",
71
+ float(four_state_vector_cfg.eps_ratio),
72
+ float(four_state_vector_cfg.eps_range),
73
+ float(four_state_vector_cfg.eps_ref),
74
+ float(four_state_vector_cfg.eps_abs),
75
+ float(four_state_vector_cfg.ref_add_alpha),
76
+ float(four_state_vector_cfg.log2_offset_delta),
77
+ )
78
+ ctx.logger.info(
79
+ "four_state_vector • r_logic definition: per design dynamic range on LOGIC (linear, ε-guarded): max(L_i)/min(L_i)"
80
+ )
81
+ ctx.logger.info(
82
+ "four_state_vector • design_by=%s\n"
83
+ " time: mode=%s target=%.3g tol=%.3g • chosen=%s\n"
84
+ " reference: requested=%r → resolved=%r • sequence=%r • observation_stat=%s\n"
85
+ " rows: per_corner_logic=%d per_corner_intensity=%d vector=%d (flat=%d)\n"
86
+ " r_logic: median=%.3g iqr=[%.3g, %.3g]",
87
+ ", ".join(four_state_vector_cfg.design_by),
88
+ four_state_vector_cfg.time_mode,
89
+ float(four_state_vector_cfg.target_time_h or np.nan),
90
+ float(four_state_vector_cfg.time_tolerance_h),
91
+ (float(chosen) if chosen is not None else None),
92
+ ref_info.get("design_id"),
93
+ ref_info.get("design_id_resolved"),
94
+ ref_info.get("sequence"),
95
+ ref_info.get("observation_stat"),
96
+ int(len(sel_logic.per_corner)),
97
+ int(len(sel_int.per_corner)),
98
+ int(len(vector)),
99
+ int(flats),
100
+ (float(r_stats["50%"]) if r_stats is not None else float("nan")),
101
+ (float(r_stats["25%"]) if r_stats is not None else float("nan")),
102
+ (float(r_stats["75%"]) if r_stats is not None else float("nan")),
103
+ )
104
+
105
+ _log_four_state_vector_observation_summary(
106
+ ctx=ctx,
107
+ design_by=four_state_vector_cfg.design_by,
108
+ idx_cols=idx_cols,
109
+ sel_logic=sel_logic,
110
+ sel_int=sel_int,
111
+ )
112
+ _log_four_state_vector_vector_preview(
113
+ ctx=ctx,
114
+ vector=vector,
115
+ idx_cols=idx_cols,
116
+ label_col=label_col,
117
+ logic_channel=four_state_vector_cfg.response.logic_channel,
118
+ intensity_channel=four_state_vector_cfg.response.intensity_channel,
119
+ sel_logic=sel_logic,
120
+ )
121
+ except Exception:
122
+ pass
123
+
124
+
125
+ def _log_four_state_vector_observation_summary(
126
+ *, ctx, design_by: list[str], idx_cols: list[str], sel_logic, sel_int
127
+ ) -> None:
128
+ try:
129
+ logic_counts = sel_logic.points.set_index(idx_cols)[["n00", "n10", "n01", "n11"]].rename(
130
+ columns={"n00": "n00_L", "n10": "n10_L", "n01": "n01_L", "n11": "n11_L"}
131
+ )
132
+ intensity_counts = sel_int.points.set_index(idx_cols)[["n00", "n10", "n01", "n11"]].rename(
133
+ columns={"n00": "n00_I", "n10": "n10_I", "n01": "n01_I", "n11": "n11_I"}
134
+ )
135
+ joined = logic_counts.join(intensity_counts, how="outer").reset_index().sort_values(idx_cols)
136
+ for _, row in joined.iterrows():
137
+ key = " | ".join(f"{col}={row[col]}" for col in design_by if col in row.index)
138
+ logic_observations = [
139
+ int(row.get("n00_L", 0) or 0),
140
+ int(row.get("n10_L", 0) or 0),
141
+ int(row.get("n01_L", 0) or 0),
142
+ int(row.get("n11_L", 0) or 0),
143
+ ]
144
+ intensity_observations = [
145
+ int(row.get("n00_I", 0) or 0),
146
+ int(row.get("n10_I", 0) or 0),
147
+ int(row.get("n01_I", 0) or 0),
148
+ int(row.get("n11_I", 0) or 0),
149
+ ]
150
+ ctx.logger.info(
151
+ "four_state_vector • %s: observations (logic)=%s (intensity)=%s",
152
+ key,
153
+ logic_observations,
154
+ intensity_observations,
155
+ )
156
+ except Exception:
157
+ pass
158
+
159
+
160
+ def _log_four_state_vector_vector_preview(
161
+ *,
162
+ ctx,
163
+ vector: pd.DataFrame,
164
+ idx_cols: list[str],
165
+ label_col: str,
166
+ logic_channel: str,
167
+ intensity_channel: str,
168
+ sel_logic,
169
+ ) -> None:
170
+ try:
171
+ observation_count_map: dict[tuple, tuple[int, ...]] = {}
172
+ if not sel_logic.points.empty:
173
+ counts = sel_logic.points.set_index(idx_cols)[["n00", "n10", "n01", "n11"]]
174
+ for key, values in counts.iterrows():
175
+ normalized_key = key if isinstance(key, tuple) else (key,)
176
+ observation_count_map[normalized_key] = tuple(int(item) for item in values.to_list())
177
+
178
+ sort_cols = [col for col in [label_col] if col in vector.columns]
179
+ lines: list[str] = []
180
+ for _, row in vector.sort_values(sort_cols).iterrows():
181
+ key = " | ".join(f"{col}={row[col]}" for col in sort_cols)
182
+ v_txt = [f"{float(row[col]):.3f}" for col in ("v00", "v10", "v01", "v11")]
183
+ y_txt = [f"{float(row[col]):.3f}" for col in ("y00_star", "y10_star", "y01_star", "y11_star")]
184
+ observation_key = tuple(row[col] for col in idx_cols if col in vector.columns)
185
+ count_text = ""
186
+ if observation_key in observation_count_map:
187
+ count_text = f" observation_n={list(observation_count_map[observation_key])}"
188
+ lines.append(
189
+ f" • {key}: v={v_txt} | y*={y_txt} | r_logic={float(row.get('r_logic', np.nan)):.3g} "
190
+ f"(max/min={float(row.get('r_logic_max', np.nan)):.3g}/{float(row.get('r_logic_min', np.nan)):.3g}; "
191
+ f"corners {row.get('r_logic_corner_max', '?')}/{row.get('r_logic_corner_min', '?')}; "
192
+ f"span_log2={float(row.get('logic_span_log2', np.nan)):.3g}){count_text}"
193
+ )
194
+ if lines:
195
+ more = "" if len(lines) <= 12 else f"\n … (+{len(lines) - 12} more)"
196
+ ctx.logger.info(
197
+ "four_state_vector • vector per design [muted](v from log2(%s) min-max; y* is log2 of anchor-normalized %s)[/muted]\n%s%s",
198
+ logic_channel,
199
+ intensity_channel,
200
+ "\n".join(lines[:12]),
201
+ more,
202
+ )
203
+ except Exception:
204
+ pass
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ import pandas as pd
6
+
7
+
8
+ def build_norm_map(mapping: Mapping[str, str], *, case_insensitive: bool) -> Mapping[str, str]:
9
+ if case_insensitive:
10
+ return {str(key).casefold(): str(value) for key, value in mapping.items()}
11
+ return {str(key): str(value) for key, value in mapping.items()}
12
+
13
+
14
+ def log_label_summary(
15
+ ctx,
16
+ *,
17
+ column: str,
18
+ output_column: str,
19
+ rules_total: int,
20
+ used_rules: int,
21
+ changed_rows: int,
22
+ unique_values: int,
23
+ examples: list[str],
24
+ unused_rules_preview: list[str],
25
+ label: str,
26
+ ) -> None:
27
+ unused_count = len(unused_rules_preview)
28
+ ctx.logger.info(
29
+ "%s • [accent]%s[/accent] → %s • rules=%d used=%d changed=%d uniques=%d%s",
30
+ label,
31
+ column,
32
+ output_column,
33
+ rules_total,
34
+ used_rules,
35
+ changed_rows,
36
+ unique_values,
37
+ f" • unused={unused_count}" if unused_count else "",
38
+ )
39
+ if examples or unused_rules_preview:
40
+ examples_block = "\n".join(f" • {entry}" for entry in examples) if examples else " —"
41
+ unused_block = (
42
+ "\n".join(f" • {entry}" for entry in unused_rules_preview) if unused_rules_preview else " —"
43
+ )
44
+ ctx.logger.debug(
45
+ ("%s details • %s → %s\n examples:\n%s\n unused_rule_keys:\n%s"),
46
+ label,
47
+ column,
48
+ output_column,
49
+ examples_block,
50
+ unused_block,
51
+ )
52
+
53
+
54
+ def apply_label_mappings(
55
+ *,
56
+ ctx,
57
+ df: pd.DataFrame,
58
+ mappings: Mapping[str, Mapping[str, str]],
59
+ output_names: Mapping[str, str],
60
+ in_place: bool,
61
+ case_insensitive: bool,
62
+ label: str,
63
+ ) -> pd.DataFrame:
64
+ out = df.copy()
65
+ for column, mapping in mappings.items():
66
+ if column not in out.columns:
67
+ raise ValueError(f"{label}: column '{column}' not found in dataframe")
68
+
69
+ before = out[column].astype(str)
70
+ unique_values = int(before.nunique(dropna=False))
71
+ normalized_before = before.str.casefold() if case_insensitive else before
72
+ normalized_map = build_norm_map(mapping, case_insensitive=case_insensitive)
73
+ rules_total = len(normalized_map)
74
+
75
+ mapped = normalized_before.map(normalized_map)
76
+ after_series = mapped.fillna(before)
77
+
78
+ output_column = column if in_place else output_names[column]
79
+ if in_place:
80
+ out[column] = after_series
81
+ else:
82
+ out[output_column] = after_series
83
+
84
+ changed_mask = mapped.notna() & (mapped.astype(str) != before)
85
+ changed_rows = int(changed_mask.sum())
86
+ used_rule_keys = set(normalized_before[normalized_before.isin(normalized_map.keys())].unique())
87
+ used_rules = len(used_rule_keys)
88
+
89
+ if changed_rows:
90
+ sample = pd.DataFrame({"raw": before, "alias": after_series})[changed_mask].drop_duplicates("raw")
91
+ examples = [f"{raw!r} → {alias!r}" for raw, alias in sample.head(6).itertuples(index=False)]
92
+ else:
93
+ examples = []
94
+ unused_rules = sorted(set(normalized_map.keys()) - used_rule_keys)
95
+ unused_rules_preview = [repr(key) for key in unused_rules[:6]]
96
+
97
+ log_label_summary(
98
+ ctx,
99
+ column=column,
100
+ output_column=output_column,
101
+ rules_total=rules_total,
102
+ used_rules=used_rules,
103
+ changed_rows=changed_rows,
104
+ unique_values=unique_values,
105
+ examples=examples,
106
+ unused_rules_preview=unused_rules_preview,
107
+ label=label,
108
+ )
109
+ return out