somatize_health/thresholds.rs
1//! Where every arguable number lives, together and in one place.
2
3/// The bounds a [`verdict`](crate::verdict) is taken at.
4///
5/// **The whole of the opinion, and it is data.** Nothing else in this crate
6/// holds a number, which is what lets a record be judged again tomorrow with
7/// other bounds and lets two people disagree about a network by comparing two
8/// of these rather than two codebases.
9///
10/// The defaults come from the original soma, which tuned them for
11/// LayerNorm-ish activations and Adam-sized steps, plus the literature for the
12/// three it did not have. They are a starting point and they are meant to be
13/// argued with.
14#[derive(Debug, Clone, PartialEq)]
15pub struct Thresholds {
16 /// Below this parameter-gradient norm, nothing is being learnt here.
17 pub grad_low: f64,
18 /// Above it, the next step will not be a step.
19 pub grad_high: f64,
20 /// A value this close to zero counts as off.
21 pub dead_eps: f64,
22 /// More of the output than this being off, on any one step, is dead.
23 pub dead_frac: f64,
24 /// A value this large counts as pinned.
25 pub saturated_at: f64,
26 /// More of the output than this being pinned, on any one step, is saturated.
27 pub saturated_frac: f64,
28 /// Below this update-to-weight ratio a node is not going to arrive.
29 ///
30 /// Practice puts a healthy one near `1e-3`; a decade either side of that is
31 /// where the two bounds sit, because the useful signal is an order of
32 /// magnitude and not a percentage.
33 pub update_low: f64,
34 /// And above it, each step throws away where it was.
35 pub update_high: f64,
36 /// A channel whose mean normalised activation is under this is dormant
37 /// (Sokar et al., ICML 2023).
38 pub dormant_tau: f64,
39 /// This much of a layer dormant is part of what says it is losing
40 /// plasticity.
41 pub dormant_frac: f64,
42 /// Linear CKA above this, between two groups meant to stay apart, is
43 /// leakage (Kornblith et al., 2019).
44 pub leakage_cka: f64,
45 /// The update's stable rank falling below this fraction of its own recent
46 /// median is narrowing. **`0.0` by default, which never fires.**
47 ///
48 /// It is off because it was measured and the measurement did not support
49 /// it. Huang et al. (2026) monitor the spectrum of `W_t - W_{t-d}` and find
50 /// it collapses thousands of steps before the loss — but their certificate
51 /// is the **deviation from a healthy baseline run**, and a single training
52 /// run has no baseline. Against its own recent median, on a 4-layer GELU
53 /// net learning a fixed teacher, three healthy runs dipped to 0.69-0.71 of
54 /// their own median and six destabilised ones ranged 0.43-0.86: the two
55 /// overlap in both directions, so no bound separates them. The numbers are
56 /// in `docs/use-cases.md`.
57 ///
58 /// What is kept is the **metric**, which is recorded and drawn: the
59 /// collapse is visible to a person looking at the curve, and that is a
60 /// weaker and honest claim. Set this yourself if you have a baseline to
61 /// compare against, which is what the paper actually asks for.
62 pub narrowing_of_usual: f64,
63 /// Below this share of what all the inputs are worth, the model is not
64 /// using that input.
65 ///
66 /// A twentieth: with four inputs an even split is a quarter each, so this
67 /// is five times below its fair share before anything is said. Generous,
68 /// because the finding is loud and being wrong about it sends somebody
69 /// looking at their data for a week.
70 pub ignored_input: f64,
71 /// And above this share, one input is carrying everything.
72 pub sole_reliance: f64,
73 /// How fast a thing has to be moving, per step and relative to itself, to
74 /// count as growing or shrinking rather than wobbling.
75 pub plasticity_growth: f64,
76 /// How far the signal may grow over a stretch nobody normalises before
77 /// that is worth saying. **One side only, and it is the upper one.**
78 ///
79 /// A decade, because the drift is geometric and the useful signal is an
80 /// order of magnitude rather than a percentage. Measured: everything that
81 /// trained sat at 2.8x or below and everything that did not was at 100x or
82 /// above, so the bound has 3.6x of margin below it and 10x above.
83 ///
84 /// There is **no lower bound and that is a finding, not an omission**. A
85 /// plain stack whose signal arrives five ten-thousandths of the size it
86 /// went in trained as well as the healthy one — Adam is scale-invariant per
87 /// parameter, so a signal that shrank does not stop a step being taken. See
88 /// `health/tests/normalisation.py`.
89 pub gain_drift: f64,
90}
91
92impl Default for Thresholds {
93 fn default() -> Self {
94 Self {
95 grad_low: 1e-7,
96 grad_high: 1e3,
97 dead_eps: 1e-7,
98 dead_frac: 0.95,
99 saturated_at: 50.0,
100 saturated_frac: 0.5,
101 update_low: 1e-4,
102 update_high: 1e-2,
103 dormant_tau: 0.1,
104 dormant_frac: 0.5,
105 leakage_cka: 0.95,
106 narrowing_of_usual: 0.0,
107 ignored_input: 0.05,
108 sole_reliance: 0.9,
109 plasticity_growth: 1e-3,
110 gain_drift: 10.0,
111 }
112 }
113}