Skip to main content

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}