1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Unified, variance-stabilized, batch-adjusted, outlier-robust value
//! transform shared by all encoders.
//!
//! # Model
//!
//! The upstream PB adjustment (`data_beans::alg::collapse_data`) fits a
//! **multiplicative** model (matching the generative comment in
//! `optimize`: `E[y] = E[μ_resid] · E[μ_adj]`):
//!
//! ```text
//! μ_mixed ≈ μ_adjusted · μ_residual
//! ```
//!
//! `μ_residual` is the per-gene/per-PB fold-factor (~1 for batch-clean
//! cells, >1 for batch-inflated genes, <1 for batch-depleted genes). The
//! encoder-side batch correction is therefore **division** — `y / μ_resid`
//! recovers the batch-clean count. Subtraction would be nearly a no-op
//! because `μ_resid` sits on a ratio scale (~1) while `y` sits on a count
//! scale (10s–1000s).
//!
//! After division we Anscombe-transform (`2√(x + 3/8)`) so the downstream
//! Gaussian embedding sees ~Poisson-standardized input with unit variance.
//!
//! # Formula
//!
//! ```text
//! clean_ig = y_ig / max(x0_ig, ε_div) # multiplicative batch correction
//! a_ig = 2√(clean_ig + 3/8) # Anscombe variance stabilize
//! r_ig = a_ig − mean_g(a_i,·) # per-cell library-size center
//! s_g = k · std_n(r_·,g) + ε # per-gene clip scale
//! r̃_ig = s_g · tanh(r_ig / s_g) # soft winsorize
//! ```
//!
//! When `x0` is `None`: skip the division, everything else the same.
//!
//! # Why this handles spikes
//!
//! A raw 10000-count spike becomes `2√10000 ≈ 200` after Anscombe — already
//! tamed. The per-gene tanh then bounds it to roughly `k·std_g`, where
//! `std_g ≈ 1` under Poisson (Anscombe's whole point is that
//! Anscombe(Poisson) has unit variance). Spikes end up contributing ~k ≈ 4
//! to the encoder input, not 10000.
//!
//! # Why the `ε_div` floor
//!
//! `μ_resid` can get small for rare genes with sparse counterfactual
//! matches. Without a floor, `y / tiny` blows up. `ε_div = 0.1` caps the
//! amplification at ~10×, which is far beyond any plausible biological
//! batch effect and easily handled by the downstream tanh clip.
use ;
const TANH_K: f64 = 4.0;
const EPS: f64 = 1e-6;
const EPS_DIV: f64 = 0.1;
/// Apply the unified value transform. Output shape matches `y_nf`.
///
/// `y_nf` is non-negative count-space (raw PB counts or indexed subset).
/// `x0_nf`, when `Some`, is the per-cell multiplicative batch residual
/// `μ_residual` (ratio-space, centered at ~1) with the same shape as
/// `y_nf`. `mu_f`, when `Some`, is the per-feature mean expression rate
/// `μ_d`, broadcast across rows (shape `[1, D]` or `[D]`). When both
/// are supplied they compose multiplicatively in the same divisive
/// step under `E[y] = batch_effect · gene_mean · biological_deviation`,
/// so the transform sees the cell's biological deviation. Division is
/// floored at `EPS_DIV` to prevent blowup on rare genes.
/// Anscombe variance-stabilizing transform: `2√(t + 3/8)`. Standardizes
/// Poisson-like counts to ~unit variance; works elementwise.
/// Batch- and gene-mean-corrected, Anscombe-stabilized values for the
/// packed indexed encoder.
///
/// Both `values_null` (per-cell μ_residual = batch effect) and
/// `values_mean` (per-gene μ_d = typical expression rate) are
/// **multiplicative** count-rate corrections under the generative model
/// `E[y] = batch_effect · gene_mean · biological_deviation`. We compose
/// them in the same divisive step so the value transform recovers the
/// pure biological-deviation rate before Anscombe stabilization. With
/// only batch correction this reduces to the original behaviour;
/// adding the per-gene mean places housekeeping observations near `1.0`
/// (so cells expressing housekeeping at typical levels contribute a
/// constant Anscombe(1) ≈ 2.35 to the encoder pool, which `bn_z`
/// absorbs as a constant offset).
///
/// Formula:
/// ```text
/// clean_ik = values_ik / max(values_null_ik, ε)
/// / max(values_mean_ik, ε) (each null floored)
/// out_ik = 2√(clean_ik + 3/8) Anscombe
/// ```
/// The count-rate **"clean"** value: `values` divided by the composed
/// multiplicative null (`values_null · values_mean`), floored at `EPS_DIV`.
///
/// This is the divisive correction at the heart of [`anscombe_lite`].
/// Both nulls are fused into one divisor, so the value tensor goes
/// through a single `broadcast_div` regardless of how many corrections
/// are active; the clamp lives on the joint divisor so its floor
/// (`EPS_DIV ≈ 0.1`) caps amplification at `1/EPS_DIV ≈ 10×` no matter
/// which factor is small.