Skip to content

Homeostasis

Homeostatic regulation: self-stabilising SNN without manual tuning.

Adjusts population firing thresholds and learning-rate scaling to maintain target firing rates. The public regulator rejects malformed scalar and array inputs before changing thresholds or learning rates, including bool aliases, empty rate vectors, non-finite values, non-numeric arrays, and invalid sleep-consolidation seeds.

  • Threshold adaptation: overactive populations raise thresholds; quiet populations lower thresholds.
  • Learning-rate scaling: high firing-rate variance reduces the caller-provided learning rate.
  • Sleep consolidation: finite non-empty weight arrays receive deterministic power-law decay plus optional replay noise.
Python
from sc_neurocore.homeostasis import NetworkRegulator, SleepConsolidation

See Tutorial 68: Homeostasis.

sc_neurocore.homeostasis

Homeostatic regulation: self-stabilizing SNN without manual tuning.

NetworkRegulator

Network-wide homeostatic regulator.

Monitors population firing rates and adjusts thresholds, learning rates, and weights to maintain target activity levels.

Parameters

target_rate : float Target mean firing rate (spikes per step). rate_tolerance : float Acceptable deviation from target (fraction). threshold_step : float Per-step threshold adjustment magnitude. lr_scale_factor : float Multiplicative LR adjustment factor.

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
class NetworkRegulator:
    """Network-wide homeostatic regulator.

    Monitors population firing rates and adjusts thresholds, learning rates,
    and weights to maintain target activity levels.

    Parameters
    ----------
    target_rate : float
        Target mean firing rate (spikes per step).
    rate_tolerance : float
        Acceptable deviation from target (fraction).
    threshold_step : float
        Per-step threshold adjustment magnitude.
    lr_scale_factor : float
        Multiplicative LR adjustment factor.
    """

    def __init__(
        self,
        target_rate: float = 0.1,
        rate_tolerance: float = 0.5,
        threshold_step: float = 0.01,
        lr_scale_factor: float = 0.95,
    ) -> None:
        self.target_rate = _validate_real_scalar("target_rate", target_rate, minimum=0.0)
        self.rate_tolerance = _validate_real_scalar(
            "rate_tolerance",
            rate_tolerance,
            minimum=0.0,
            maximum=1.0,
        )
        self.threshold_step = _validate_real_scalar(
            "threshold_step",
            threshold_step,
            minimum=0.0,
        )
        self.lr_scale_factor = _validate_real_scalar(
            "lr_scale_factor",
            lr_scale_factor,
            minimum=0.0,
            maximum=1.0,
            lower_open=True,
        )

    def regulate(
        self,
        firing_rates: np.ndarray[Any, Any],
        thresholds: np.ndarray[Any, Any],
        learning_rate: float,
        weights: list[np.ndarray[Any, Any]] | None = None,
    ) -> tuple[np.ndarray[Any, Any], float, StabilityMetrics]:
        """Apply homeostatic regulation.

        Parameters
        ----------
        firing_rates : ndarray of shape (N,)
            Current per-neuron firing rates.
        thresholds : ndarray of shape (N,)
            Current per-neuron thresholds.
        learning_rate : float
            Current learning rate.
        weights : list of ndarray, optional
            Weight matrices for norm monitoring.

        Returns
        -------
        (new_thresholds, new_lr, StabilityMetrics)
        """
        self._validate_regulate_inputs(firing_rates, thresholds, learning_rate, weights)

        mean_rate = float(firing_rates.mean())
        rate_var = float(firing_rates.var())
        metrics = StabilityMetrics(
            mean_firing_rate=mean_rate,
            rate_variance=rate_var,
        )

        if weights:
            norms = [float(np.linalg.norm(w)) for w in weights]
            metrics.weight_norm = float(np.mean(norms))

        new_thresholds = thresholds.copy()
        new_lr = learning_rate

        lo = self.target_rate * (1 - self.rate_tolerance)
        hi = self.target_rate * (1 + self.rate_tolerance)

        # Too active → raise thresholds
        if mean_rate > hi:
            new_thresholds += self.threshold_step
            metrics.adjustments_made.append(f"thresholds +{self.threshold_step:.3f}")
            metrics.is_stable = False

        # Too quiet → lower thresholds
        elif mean_rate < lo:
            new_thresholds -= self.threshold_step
            metrics.adjustments_made.append(f"thresholds -{self.threshold_step:.3f}")
            metrics.is_stable = False

        # High variance → reduce LR
        if rate_var > self.target_rate * 2:
            new_lr *= self.lr_scale_factor
            metrics.adjustments_made.append(f"lr *{self.lr_scale_factor}")

        return new_thresholds, new_lr, metrics

    @staticmethod
    def _validate_regulate_inputs(
        firing_rates: np.ndarray[Any, Any],
        thresholds: np.ndarray[Any, Any],
        learning_rate: float,
        weights: list[np.ndarray[Any, Any]] | None,
    ) -> None:
        _validate_finite_numeric_array(
            "regulate firing_rates",
            firing_rates,
            ndim=1,
            non_empty=True,
            non_negative=True,
        )
        _validate_finite_numeric_array(
            "regulate thresholds",
            thresholds,
            ndim=1,
            non_empty=True,
        )
        if thresholds.shape != firing_rates.shape:
            raise ValueError("regulate thresholds must match firing_rates shape")
        _validate_real_scalar("regulate learning_rate", learning_rate, minimum=0.0)
        if weights is not None:
            for weight in weights:
                _validate_finite_numeric_array(
                    "weights",
                    weight,
                    non_empty=True,
                )

regulate(firing_rates, thresholds, learning_rate, weights=None)

Apply homeostatic regulation.

Parameters

firing_rates : ndarray of shape (N,) Current per-neuron firing rates. thresholds : ndarray of shape (N,) Current per-neuron thresholds. learning_rate : float Current learning rate. weights : list of ndarray, optional Weight matrices for norm monitoring.

Returns

(new_thresholds, new_lr, StabilityMetrics)

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def regulate(
    self,
    firing_rates: np.ndarray[Any, Any],
    thresholds: np.ndarray[Any, Any],
    learning_rate: float,
    weights: list[np.ndarray[Any, Any]] | None = None,
) -> tuple[np.ndarray[Any, Any], float, StabilityMetrics]:
    """Apply homeostatic regulation.

    Parameters
    ----------
    firing_rates : ndarray of shape (N,)
        Current per-neuron firing rates.
    thresholds : ndarray of shape (N,)
        Current per-neuron thresholds.
    learning_rate : float
        Current learning rate.
    weights : list of ndarray, optional
        Weight matrices for norm monitoring.

    Returns
    -------
    (new_thresholds, new_lr, StabilityMetrics)
    """
    self._validate_regulate_inputs(firing_rates, thresholds, learning_rate, weights)

    mean_rate = float(firing_rates.mean())
    rate_var = float(firing_rates.var())
    metrics = StabilityMetrics(
        mean_firing_rate=mean_rate,
        rate_variance=rate_var,
    )

    if weights:
        norms = [float(np.linalg.norm(w)) for w in weights]
        metrics.weight_norm = float(np.mean(norms))

    new_thresholds = thresholds.copy()
    new_lr = learning_rate

    lo = self.target_rate * (1 - self.rate_tolerance)
    hi = self.target_rate * (1 + self.rate_tolerance)

    # Too active → raise thresholds
    if mean_rate > hi:
        new_thresholds += self.threshold_step
        metrics.adjustments_made.append(f"thresholds +{self.threshold_step:.3f}")
        metrics.is_stable = False

    # Too quiet → lower thresholds
    elif mean_rate < lo:
        new_thresholds -= self.threshold_step
        metrics.adjustments_made.append(f"thresholds -{self.threshold_step:.3f}")
        metrics.is_stable = False

    # High variance → reduce LR
    if rate_var > self.target_rate * 2:
        new_lr *= self.lr_scale_factor
        metrics.adjustments_made.append(f"lr *{self.lr_scale_factor}")

    return new_thresholds, new_lr, metrics

SleepConsolidation

Sleep-phase synaptic renormalization for memory consolidation.

During sleep: suppress external input, apply power-law weight decay, allow spontaneous replay through recurrent dynamics.

Reference: Sleep-Based Homeostatic Regularization (arXiv Jan 2026)

Parameters

decay_exponent : float Power-law exponent for weight decay (higher = more aggressive). noise_amplitude : float Spontaneous activity noise during sleep. duration_fraction : float Sleep duration as fraction of epoch (0.1 = 10% of time sleeping).

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
class SleepConsolidation:
    """Sleep-phase synaptic renormalization for memory consolidation.

    During sleep: suppress external input, apply power-law weight decay,
    allow spontaneous replay through recurrent dynamics.

    Reference: Sleep-Based Homeostatic Regularization (arXiv Jan 2026)

    Parameters
    ----------
    decay_exponent : float
        Power-law exponent for weight decay (higher = more aggressive).
    noise_amplitude : float
        Spontaneous activity noise during sleep.
    duration_fraction : float
        Sleep duration as fraction of epoch (0.1 = 10% of time sleeping).
    """

    def __init__(
        self,
        decay_exponent: float = 0.5,
        noise_amplitude: float = 0.01,
        duration_fraction: float = 0.1,
    ) -> None:
        self.decay_exponent = _validate_real_scalar(
            "decay_exponent",
            decay_exponent,
            minimum=0.0,
        )
        self.noise_amplitude = _validate_real_scalar(
            "noise_amplitude",
            noise_amplitude,
            minimum=0.0,
        )
        self.duration_fraction = _validate_real_scalar(
            "duration_fraction",
            duration_fraction,
            minimum=0.0,
            maximum=1.0,
            lower_open=True,
        )

    def apply(
        self,
        weights: list[np.ndarray[Any, Any]],
        seed: int = 42,
    ) -> list[np.ndarray[Any, Any]]:
        """Apply sleep consolidation to weights.

        High-activity synapses (large |w|) undergo proportionally more decay.
        Low-activity synapses are relatively preserved.

        Parameters
        ----------
        weights : list of ndarray
            Non-empty finite numeric weight arrays.
        seed : int, default=42
            Deterministic NumPy ``RandomState`` seed in ``[0, 2**32 - 1]``.

        Returns
        -------
        list of ndarray
            Renormalized weights.
        """
        self._validate_weights(weights)
        rng = np.random.RandomState(_validate_seed(seed))

        consolidated = []
        for w in weights:
            abs_w = np.abs(w)
            # Power-law decay: larger weights decay more
            max_w = max(_max_abs_weight(w), 1e-8)
            relative = abs_w / max_w
            decay_factor = 1.0 - self.duration_fraction * (relative**self.decay_exponent)
            decay_factor = np.clip(decay_factor, 0.5, 1.0)

            # Apply decay
            w_new = w * decay_factor

            # Add spontaneous replay noise
            w_new += rng.randn(*w.shape) * self.noise_amplitude

            consolidated.append(w_new)
        return consolidated

    def should_sleep(self, epoch: int, total_epochs: int) -> bool:
        """Determine if this epoch should include a sleep phase.

        Parameters
        ----------
        epoch : int
            Zero-based epoch index.
        total_epochs : int
            Positive total epoch count for caller-side schedule validation.

        Returns
        -------
        bool
            ``True`` when the epoch is a positive multiple of the interval
            implied by ``duration_fraction``.
        """
        if type(epoch) is not int or epoch < 0:
            raise ValueError("epoch must be a non-negative integer")
        if type(total_epochs) is not int or total_epochs <= 0:
            raise ValueError("epoch total_epochs must be a positive integer")

        interval = max(1, int(1.0 / self.duration_fraction))
        return epoch > 0 and epoch % interval == 0

    @staticmethod
    def _validate_weights(weights: list[np.ndarray[Any, Any]]) -> None:
        if not isinstance(weights, list) or len(weights) == 0:
            raise ValueError("weights must be a non-empty list of numpy arrays")
        for weight in weights:
            if isinstance(weight, np.ndarray) and weight.size == 0:
                raise ValueError("weights must contain non-empty arrays")
            _validate_finite_numeric_array("weights", weight, non_empty=True)

apply(weights, seed=42)

Apply sleep consolidation to weights.

High-activity synapses (large |w|) undergo proportionally more decay. Low-activity synapses are relatively preserved.

Parameters

weights : list of ndarray Non-empty finite numeric weight arrays. seed : int, default=42 Deterministic NumPy RandomState seed in [0, 2**32 - 1].

Returns

list of ndarray Renormalized weights.

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def apply(
    self,
    weights: list[np.ndarray[Any, Any]],
    seed: int = 42,
) -> list[np.ndarray[Any, Any]]:
    """Apply sleep consolidation to weights.

    High-activity synapses (large |w|) undergo proportionally more decay.
    Low-activity synapses are relatively preserved.

    Parameters
    ----------
    weights : list of ndarray
        Non-empty finite numeric weight arrays.
    seed : int, default=42
        Deterministic NumPy ``RandomState`` seed in ``[0, 2**32 - 1]``.

    Returns
    -------
    list of ndarray
        Renormalized weights.
    """
    self._validate_weights(weights)
    rng = np.random.RandomState(_validate_seed(seed))

    consolidated = []
    for w in weights:
        abs_w = np.abs(w)
        # Power-law decay: larger weights decay more
        max_w = max(_max_abs_weight(w), 1e-8)
        relative = abs_w / max_w
        decay_factor = 1.0 - self.duration_fraction * (relative**self.decay_exponent)
        decay_factor = np.clip(decay_factor, 0.5, 1.0)

        # Apply decay
        w_new = w * decay_factor

        # Add spontaneous replay noise
        w_new += rng.randn(*w.shape) * self.noise_amplitude

        consolidated.append(w_new)
    return consolidated

should_sleep(epoch, total_epochs)

Determine if this epoch should include a sleep phase.

Parameters

epoch : int Zero-based epoch index. total_epochs : int Positive total epoch count for caller-side schedule validation.

Returns

bool True when the epoch is a positive multiple of the interval implied by duration_fraction.

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def should_sleep(self, epoch: int, total_epochs: int) -> bool:
    """Determine if this epoch should include a sleep phase.

    Parameters
    ----------
    epoch : int
        Zero-based epoch index.
    total_epochs : int
        Positive total epoch count for caller-side schedule validation.

    Returns
    -------
    bool
        ``True`` when the epoch is a positive multiple of the interval
        implied by ``duration_fraction``.
    """
    if type(epoch) is not int or epoch < 0:
        raise ValueError("epoch must be a non-negative integer")
    if type(total_epochs) is not int or total_epochs <= 0:
        raise ValueError("epoch total_epochs must be a positive integer")

    interval = max(1, int(1.0 / self.duration_fraction))
    return epoch > 0 and epoch % interval == 0

StabilityMetrics dataclass

Network stability measurements.

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
 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
@dataclass
class StabilityMetrics:
    """Network stability measurements."""

    mean_firing_rate: float = 0.0
    rate_variance: float = 0.0
    ei_ratio: float = 1.0
    weight_norm: float = 0.0
    is_stable: bool = True
    adjustments_made: list[str] = field(default_factory=list)

    def summary(self) -> str:
        """Render a multi-line human-readable network-stability report.

        Returns
        -------
        str
            Text report containing stability status, firing-rate statistics,
            E/I ratio, weight norm, and any applied regulation actions.
        """
        status = "STABLE" if self.is_stable else "UNSTABLE"
        lines = [
            f"Network Stability: {status}",
            f"  Mean firing rate: {self.mean_firing_rate:.4f}",
            f"  Rate variance: {self.rate_variance:.4f}",
            f"  E/I ratio: {self.ei_ratio:.2f}",
            f"  Weight norm: {self.weight_norm:.4f}",
        ]
        if self.adjustments_made:  # pragma: no cover
            lines.append(f"  Adjustments: {', '.join(self.adjustments_made)}")
        return "\n".join(lines)

summary()

Render a multi-line human-readable network-stability report.

Returns

str Text report containing stability status, firing-rate statistics, E/I ratio, weight norm, and any applied regulation actions.

Source code in src/sc_neurocore/homeostasis/regulator.py
Python
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def summary(self) -> str:
    """Render a multi-line human-readable network-stability report.

    Returns
    -------
    str
        Text report containing stability status, firing-rate statistics,
        E/I ratio, weight norm, and any applied regulation actions.
    """
    status = "STABLE" if self.is_stable else "UNSTABLE"
    lines = [
        f"Network Stability: {status}",
        f"  Mean firing rate: {self.mean_firing_rate:.4f}",
        f"  Rate variance: {self.rate_variance:.4f}",
        f"  E/I ratio: {self.ei_ratio:.2f}",
        f"  Weight norm: {self.weight_norm:.4f}",
    ]
    if self.adjustments_made:  # pragma: no cover
        lines.append(f"  Adjustments: {', '.join(self.adjustments_made)}")
    return "\n".join(lines)