fabricpc 0.4.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 (62) hide show
  1. fabricpc/__init__.py +84 -0
  2. fabricpc/core/__init__.py +111 -0
  3. fabricpc/core/_frozen.py +59 -0
  4. fabricpc/core/activations.py +390 -0
  5. fabricpc/core/energy.py +520 -0
  6. fabricpc/core/inference.py +389 -0
  7. fabricpc/core/initializers.py +482 -0
  8. fabricpc/core/learning.py +60 -0
  9. fabricpc/core/mupc.py +421 -0
  10. fabricpc/core/positional.py +49 -0
  11. fabricpc/core/scaling.py +111 -0
  12. fabricpc/core/state_ops.py +54 -0
  13. fabricpc/core/topology.py +84 -0
  14. fabricpc/core/types.py +217 -0
  15. fabricpc/experiments/__init__.py +33 -0
  16. fabricpc/experiments/ab_experiment.py +648 -0
  17. fabricpc/experiments/statistics.py +135 -0
  18. fabricpc/graph_assembly/__init__.py +5 -0
  19. fabricpc/graph_assembly/graph_construction.py +260 -0
  20. fabricpc/graph_initialization/__init__.py +24 -0
  21. fabricpc/graph_initialization/params_initializer.py +63 -0
  22. fabricpc/graph_initialization/state_initializer.py +386 -0
  23. fabricpc/jax_config.py +122 -0
  24. fabricpc/models/__init__.py +5 -0
  25. fabricpc/models/transformer.py +160 -0
  26. fabricpc/nodes/__init__.py +58 -0
  27. fabricpc/nodes/base.py +746 -0
  28. fabricpc/nodes/convolutional.py +241 -0
  29. fabricpc/nodes/identity.py +163 -0
  30. fabricpc/nodes/linear.py +228 -0
  31. fabricpc/nodes/linear_explicit_grad.py +174 -0
  32. fabricpc/nodes/linear_residual.py +202 -0
  33. fabricpc/nodes/pooling.py +428 -0
  34. fabricpc/nodes/skip_connection.py +144 -0
  35. fabricpc/nodes/storkey_hopfield.py +421 -0
  36. fabricpc/nodes/transformer.py +512 -0
  37. fabricpc/nodes/transformer_v2.py +422 -0
  38. fabricpc/training/__init__.py +66 -0
  39. fabricpc/training/multi_gpu.py +43 -0
  40. fabricpc/training/natural_gradients.py +117 -0
  41. fabricpc/training/optimizers.py +13 -0
  42. fabricpc/training/train.py +845 -0
  43. fabricpc/training/train_autoregressive.py +758 -0
  44. fabricpc/training/train_backprop.py +714 -0
  45. fabricpc/tuning/__init__.py +3 -0
  46. fabricpc/tuning/bayesian_tuner.py +418 -0
  47. fabricpc/utils/__init__.py +24 -0
  48. fabricpc/utils/dashboarding/__init__.py +80 -0
  49. fabricpc/utils/dashboarding/_aim_available.py +68 -0
  50. fabricpc/utils/dashboarding/callbacks.py +171 -0
  51. fabricpc/utils/dashboarding/extractors.py +287 -0
  52. fabricpc/utils/dashboarding/inference_tracking.py +312 -0
  53. fabricpc/utils/dashboarding/trackers.py +544 -0
  54. fabricpc/utils/data/__init__.py +25 -0
  55. fabricpc/utils/data/data_utils.py +45 -0
  56. fabricpc/utils/data/dataloader.py +657 -0
  57. fabricpc/utils/helpers.py +19 -0
  58. fabricpc-0.4.0.dist-info/METADATA +184 -0
  59. fabricpc-0.4.0.dist-info/RECORD +62 -0
  60. fabricpc-0.4.0.dist-info/WHEEL +5 -0
  61. fabricpc-0.4.0.dist-info/licenses/LICENSE +21 -0
  62. fabricpc-0.4.0.dist-info/top_level.txt +1 -0
fabricpc/__init__.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ FabricPC-JAX: Predictive Coding Networks in JAX
3
+ ================================================
4
+
5
+ A functional, high-performance implementation of predictive coding networks
6
+ using JAX for automatic differentiation, JIT compilation, and multi-device parallelism.
7
+
8
+ Key Features:
9
+ - Functional programming paradigm (immutable data structures)
10
+ - JIT-compiled inference and training loops
11
+ - Multi-GPU/TPU support with pmap
12
+ - XLA optimization for maximum performance
13
+
14
+ Example:
15
+ >>> from fabricpc.nodes import Linear
16
+ >>> from fabricpc.core.topology import Edge
17
+ >>> from fabricpc.graph_assembly import TaskMap, graph
18
+ >>> from fabricpc.graph_initialization import initialize_params
19
+ >>> from fabricpc.training import train_pcn, evaluate_pcn
20
+ >>>
21
+ >>> # Define nodes
22
+ >>> input_node = Linear(shape=(784,), name="input")
23
+ >>> hidden = Linear(shape=(128,), name="hidden")
24
+ >>> output = Linear(shape=(10,), name="output")
25
+ >>>
26
+ >>> # Build graph
27
+ >>> structure = graph(
28
+ ... nodes=[input_node, hidden, output],
29
+ ... edges=[
30
+ ... Edge(source=input_node, target=hidden.slot("in")),
31
+ ... Edge(source=hidden, target=output.slot("in")),
32
+ ... ],
33
+ ... task_map=TaskMap(x=input_node, y=output),
34
+ ... inference=InferenceSGD(eta_infer=0.05, infer_steps=10),
35
+ ... )
36
+ >>> params = initialize_params(structure, rng_key)
37
+ >>> trained_params, history, _ = train_pcn(params, structure, train_loader, config)
38
+ >>> metrics = evaluate_pcn(trained_params, structure, test_loader, config)
39
+ """
40
+
41
+ from importlib.metadata import version
42
+
43
+ __version__ = version("fabricpc")
44
+
45
+ # Submodules (for advanced use)
46
+ from fabricpc import (
47
+ core,
48
+ graph_initialization,
49
+ nodes,
50
+ training,
51
+ utils,
52
+ graph_assembly,
53
+ models,
54
+ experiments,
55
+ )
56
+
57
+ # Core API - what most users need
58
+ from fabricpc.graph_initialization import initialize_params
59
+ from fabricpc.training import train_pcn, evaluate_pcn
60
+ from fabricpc.jax_config import setup_jax
61
+
62
+ # Types - for type hints
63
+ from fabricpc.core.types import GraphParams, GraphState, GraphStructure
64
+
65
+ __all__ = [
66
+ # Core API (common use)
67
+ "initialize_params",
68
+ "train_pcn",
69
+ "evaluate_pcn",
70
+ "setup_jax",
71
+ # Types (for type hints)
72
+ "GraphParams",
73
+ "GraphState",
74
+ "GraphStructure",
75
+ # Submodules (advanced use)
76
+ "core",
77
+ "graph_assembly",
78
+ "graph_initialization",
79
+ "models",
80
+ "nodes",
81
+ "training",
82
+ "utils",
83
+ "experiments",
84
+ ]
@@ -0,0 +1,111 @@
1
+ """Core JAX predictive coding components."""
2
+
3
+ # Type definitions
4
+ from fabricpc.core.types import (
5
+ GraphParams,
6
+ GraphState,
7
+ GraphStructure,
8
+ NodeInfo,
9
+ EdgeInfo,
10
+ SlotInfo,
11
+ NodeParams,
12
+ NodeState,
13
+ )
14
+
15
+ # Activation functions
16
+ from fabricpc.core.activations import (
17
+ ActivationBase,
18
+ IdentityActivation,
19
+ SigmoidActivation,
20
+ TanhActivation,
21
+ ReLUActivation,
22
+ LeakyReLUActivation,
23
+ GeluActivation,
24
+ SoftmaxActivation,
25
+ HardTanhActivation,
26
+ )
27
+
28
+ # Energy functions
29
+ from fabricpc.core.energy import (
30
+ EnergyFunctional,
31
+ GaussianEnergy,
32
+ BernoulliEnergy,
33
+ CrossEntropyEnergy,
34
+ LaplacianEnergy,
35
+ HuberEnergy,
36
+ KLDivergenceEnergy,
37
+ compute_energy,
38
+ compute_energy_gradient,
39
+ get_energy_and_gradient,
40
+ )
41
+
42
+ # Inference functions and classes
43
+ from fabricpc.core.inference import (
44
+ InferenceBase,
45
+ InferenceSGD,
46
+ InferenceSGDNormClip,
47
+ gather_inputs,
48
+ run_inference,
49
+ )
50
+
51
+ # Initializers
52
+ from fabricpc.core.initializers import (
53
+ InitializerBase,
54
+ ZerosInitializer,
55
+ OnesInitializer,
56
+ NormalInitializer,
57
+ UniformInitializer,
58
+ XavierInitializer,
59
+ KaimingInitializer,
60
+ MuPCInitializer,
61
+ initialize,
62
+ )
63
+
64
+ __all__ = [
65
+ # Types
66
+ "GraphParams",
67
+ "GraphState",
68
+ "GraphStructure",
69
+ "NodeInfo",
70
+ "EdgeInfo",
71
+ "SlotInfo",
72
+ "NodeParams",
73
+ "NodeState",
74
+ # Activation functions
75
+ "ActivationBase",
76
+ "IdentityActivation",
77
+ "SigmoidActivation",
78
+ "TanhActivation",
79
+ "ReLUActivation",
80
+ "LeakyReLUActivation",
81
+ "GeluActivation",
82
+ "SoftmaxActivation",
83
+ "HardTanhActivation",
84
+ # Energy functions
85
+ "EnergyFunctional",
86
+ "GaussianEnergy",
87
+ "BernoulliEnergy",
88
+ "CrossEntropyEnergy",
89
+ "LaplacianEnergy",
90
+ "HuberEnergy",
91
+ "KLDivergenceEnergy",
92
+ "compute_energy",
93
+ "compute_energy_gradient",
94
+ "get_energy_and_gradient",
95
+ # Inference
96
+ "InferenceBase",
97
+ "InferenceSGD",
98
+ "InferenceSGDNormClip",
99
+ "gather_inputs",
100
+ "run_inference",
101
+ # Initializers
102
+ "InitializerBase",
103
+ "ZerosInitializer",
104
+ "OnesInitializer",
105
+ "NormalInitializer",
106
+ "UniformInitializer",
107
+ "XavierInitializer",
108
+ "KaimingInitializer",
109
+ "MuPCInitializer",
110
+ "initialize",
111
+ ]
@@ -0,0 +1,59 @@
1
+ """Shared immutability mixin for stateless config value objects.
2
+
3
+ ``ActivationBase``, ``EnergyFunctional``, and ``InitializerBase`` place a single
4
+ default instance directly in node ``__init__`` signatures. A signature default
5
+ is evaluated once at import and shared by every defaulted call, so that instance
6
+ is only safe if it cannot be mutated. ``FrozenConfig`` is the single source of
7
+ that freeze for all three families.
8
+ """
9
+
10
+ import types
11
+
12
+ # Immutable scalar types accepted as config values. Tuples are accepted too,
13
+ # recursively. A future structured value (e.g. a per-channel alpha) is a tuple,
14
+ # not a list or an array.
15
+ _IMMUTABLE_SCALARS = (bool, int, float, str, bytes, type(None))
16
+
17
+
18
+ def _validate_immutable(value, key, owner):
19
+ """Reject any config value that is not an immutable scalar or a tuple of them."""
20
+ if isinstance(value, _IMMUTABLE_SCALARS):
21
+ return
22
+ if isinstance(value, tuple):
23
+ for item in value:
24
+ _validate_immutable(item, key, owner)
25
+ return
26
+ raise TypeError(
27
+ f"{owner} config value {key!r} must be an immutable scalar "
28
+ f"(int, float, str, bool, bytes, None) or a tuple of those; "
29
+ f"got {type(value).__name__}"
30
+ )
31
+
32
+
33
+ class FrozenConfig:
34
+ """Freezes an instance after construction.
35
+
36
+ Once ``__init__`` has run, attributes cannot be set or deleted, and
37
+ ``config`` is a read-only mapping whose keys cannot be added, removed, or
38
+ reassigned. Config values are validated at construction: only immutable
39
+ scalars (int, float, str, bool, bytes, None) and tuples of those are
40
+ accepted, so the whole object is immutable, not just its top level.
41
+ """
42
+
43
+ def __init__(self, **config):
44
+ for key, value in config.items():
45
+ _validate_immutable(value, key, type(self).__name__)
46
+ # object.__setattr__ bypasses the freeze below to set these two fields
47
+ # once; every later assignment goes through __setattr__ and is rejected.
48
+ object.__setattr__(self, "config", types.MappingProxyType(config))
49
+ object.__setattr__(self, "_frozen", True)
50
+
51
+ def __setattr__(self, name, value):
52
+ if getattr(self, "_frozen", False):
53
+ raise AttributeError(
54
+ f"{type(self).__name__} is immutable; cannot set {name!r}"
55
+ )
56
+ object.__setattr__(self, name, value)
57
+
58
+ def __delattr__(self, name):
59
+ raise AttributeError(f"{type(self).__name__} is immutable")
@@ -0,0 +1,390 @@
1
+ """
2
+ Activation functions for predictive coding networks in JAX.
3
+
4
+ This module provides:
5
+ - ActivationBase abstract class with constructor-based configuration
6
+ - Built-in activations (Identity, Sigmoid, Tanh, ReLU, LeakyReLU, GELU, Softmax, HardTanh)
7
+
8
+ All functions are pure and compatible with JAX transformations (jit, vmap, grad).
9
+
10
+ User Extensibility
11
+ ------------------
12
+ Users can create custom activations by extending ActivationBase:
13
+
14
+ class MyActivation(ActivationBase):
15
+ def __init__(self, temperature=1.0):
16
+ super().__init__(temperature=temperature)
17
+
18
+ @staticmethod
19
+ def forward(x, config=None):
20
+ temp = config.get("temperature", 1.0) if config else 1.0
21
+ return jnp.tanh(x / temp)
22
+
23
+ @staticmethod
24
+ def derivative(x, config=None):
25
+ temp = config.get("temperature", 1.0) if config else 1.0
26
+ t = jnp.tanh(x / temp)
27
+ return (1 - t**2) / temp
28
+
29
+ Usage
30
+ -----
31
+ Activations are instantiated with their parameters:
32
+
33
+ act = SigmoidActivation()
34
+ act = LeakyReLUActivation(alpha=0.02)
35
+ z_mu = type(act).forward(x, act.config)
36
+ """
37
+
38
+ import math
39
+ from abc import ABC, abstractmethod
40
+ from typing import Dict, Any
41
+ import jax.numpy as jnp
42
+ from jax import nn
43
+
44
+ from fabricpc.core._frozen import FrozenConfig
45
+
46
+ # =============================================================================
47
+ # Activation Base Class
48
+ # =============================================================================
49
+
50
+
51
+ class ActivationBase(FrozenConfig, ABC):
52
+ """
53
+ Abstract base class for activation functions.
54
+
55
+ Activation functions define how pre-activation values are transformed.
56
+ Each activation provides both the forward function and its derivative.
57
+
58
+ All methods are static for JAX compatibility (pure functions, no state).
59
+
60
+ Instances are frozen after construction (see ``FrozenConfig``): attributes
61
+ cannot be set or deleted and ``config`` keys cannot be added, removed, or
62
+ reassigned, so one default instance is safe to share as a signature default.
63
+ The freeze is shallow: construct only with immutable scalar config values.
64
+
65
+ Required methods:
66
+ - forward(): Apply activation function
67
+ - derivative(): Compute derivative w.r.t. pre-activation
68
+
69
+ Example implementation:
70
+ class MyActivation(ActivationBase):
71
+ def __init__(self, temperature=1.0):
72
+ super().__init__(temperature=temperature)
73
+
74
+ @staticmethod
75
+ def forward(x, config=None):
76
+ temp = config.get("temperature", 1.0) if config else 1.0
77
+ return jnp.tanh(x / temp)
78
+
79
+ @staticmethod
80
+ def derivative(x, config=None):
81
+ temp = config.get("temperature", 1.0) if config else 1.0
82
+ t = jnp.tanh(x / temp)
83
+ return (1 - t**2) / temp
84
+ """
85
+
86
+ @staticmethod
87
+ @abstractmethod
88
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
89
+ """
90
+ Apply activation function.
91
+
92
+ Args:
93
+ x: Pre-activation values, any shape
94
+ config: Optional configuration dict for activation parameters
95
+
96
+ Returns:
97
+ Activated values, same shape as x
98
+ """
99
+ pass
100
+
101
+ @staticmethod
102
+ @abstractmethod
103
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
104
+ """
105
+ Compute derivative w.r.t. pre-activation.
106
+
107
+ Args:
108
+ x: Pre-activation values, any shape
109
+ config: Optional configuration dict for activation parameters
110
+
111
+ Returns:
112
+ Derivative values, same shape as x
113
+
114
+ Note:
115
+ This method is NOT called by the default framework path — autodiff
116
+ computes gradient automatically via
117
+ ``jax.value_and_grad`` in ``forward_and_latent_grads()``.
118
+
119
+ It is provided as a convenience for node subclasses that override
120
+ ``forward_and_latent_grads()`` with explicit (non-autodiff) gradient
121
+ computation (see ``LinearExplicitGrad`` for the pattern).
122
+ """
123
+ pass
124
+
125
+ @staticmethod
126
+ def variance_gain(config: Dict[str, Any] = None) -> float:
127
+ """
128
+ Return the Kaiming-style gain for variance preservation.
129
+
130
+ This is the factor g such that when pre-activations have
131
+ Var(z) = g^2, the post-activation output has Var(f(z)) ≈ 1.
132
+ Used by muPC to compensate for activation-induced variance
133
+ contraction in the forward scaling formula:
134
+
135
+ a = gain / sqrt(fan_in * K)
136
+
137
+ Subclasses should override with activation-specific values.
138
+ Default returns 1.0 (no correction, appropriate for identity).
139
+ """
140
+ return 1.0
141
+
142
+ @staticmethod
143
+ def jacobian(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
144
+ """
145
+ Full Jacobian matrix J_{ij} = df_i/dx_j, shape (..., D, D).
146
+
147
+ For element-wise activations, the Jacobian is diag(derivative(x)) —
148
+ use derivative() directly as the diagonal. Override this method for
149
+ non-element-wise activations (e.g., softmax) where off-diagonal
150
+ terms are needed.
151
+
152
+ This is a convenience for explicit (non-autodiff) gradient
153
+ implementations that need the full Jacobian.
154
+
155
+ Args:
156
+ x: Pre-activation values, shape (..., D)
157
+ config: Optional configuration dict
158
+
159
+ Returns:
160
+ Jacobian matrix, shape (..., D, D)
161
+ """
162
+ raise NotImplementedError(
163
+ "jacobian() not implemented for this activation. "
164
+ "For element-wise activations, use derivative() as the diagonal."
165
+ )
166
+
167
+ @staticmethod
168
+ def jacobian_gain(config: Dict[str, Any] = None) -> float:
169
+ """
170
+ Return the Jacobian compensation factor for muPC topdown gradient scaling.
171
+
172
+ In local PC inference, topdown gradients propagate one hop per step
173
+ through the Jacobian diag(act'(z)) @ (a*W). The per-hop gradient
174
+ attenuation is approximately variance_gain * rms(act'(z)), where
175
+ z ~ N(0, variance_gain^2). This factor compensates:
176
+
177
+ jacobian_gain = 1 / (variance_gain * rms(act'(z)))
178
+
179
+ so that topdown_grad_scale = a * jacobian_gain yields ~1.0 per-hop
180
+ gradient propagation factor.
181
+
182
+ Default returns 1.0 (exact for identity, ReLU, LeakyReLU where
183
+ variance_gain * rms(act') = 1.0 by construction).
184
+ """
185
+ return 1.0
186
+
187
+
188
+ # =============================================================================
189
+ # Built-in Activations
190
+ # =============================================================================
191
+
192
+
193
+ class IdentityActivation(ActivationBase):
194
+ """Identity activation: f(x) = x"""
195
+
196
+ def __init__(self):
197
+ super().__init__()
198
+
199
+ @staticmethod
200
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
201
+ return x
202
+
203
+ @staticmethod
204
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
205
+ return jnp.ones_like(x)
206
+
207
+
208
+ class SigmoidActivation(ActivationBase):
209
+ """Sigmoid activation: σ(x) = 1 / (1 + exp(-x))"""
210
+
211
+ def __init__(self):
212
+ super().__init__()
213
+
214
+ @staticmethod
215
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
216
+ return nn.sigmoid(x)
217
+
218
+ @staticmethod
219
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
220
+ s = nn.sigmoid(x)
221
+ return s * (1 - s)
222
+
223
+
224
+ class TanhActivation(ActivationBase):
225
+ """Tanh activation: (exp(x) - exp(-x)) / (exp(x) + exp(-x))"""
226
+
227
+ def __init__(self):
228
+ super().__init__()
229
+
230
+ @staticmethod
231
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
232
+ return jnp.tanh(x)
233
+
234
+ @staticmethod
235
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
236
+ t = jnp.tanh(x)
237
+ return 1 - t**2
238
+
239
+ @staticmethod
240
+ def variance_gain(config: Dict[str, Any] = None) -> float:
241
+ return math.sqrt(5.0 / 3.0)
242
+
243
+ @staticmethod
244
+ def jacobian_gain(config: Dict[str, Any] = None) -> float:
245
+ # rms(tanh'(z)) ≈ 0.6144 for z ~ N(0, 5/3)
246
+ # jacobian_gain = 1 / (sqrt(5/3) * 0.6144) ≈ 1.261
247
+ return 1.261
248
+
249
+
250
+ class ReLUActivation(ActivationBase):
251
+ """ReLU activation: max(0, x)"""
252
+
253
+ def __init__(self):
254
+ super().__init__()
255
+
256
+ @staticmethod
257
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
258
+ return nn.relu(x)
259
+
260
+ @staticmethod
261
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
262
+ return (x > 0).astype(jnp.float32)
263
+
264
+ @staticmethod
265
+ def variance_gain(config: Dict[str, Any] = None) -> float:
266
+ return math.sqrt(2.0)
267
+
268
+
269
+ class LeakyReLUActivation(ActivationBase):
270
+ """
271
+ Leaky ReLU activation: max(alpha * x, x)
272
+
273
+ Args:
274
+ alpha: Negative slope (default: 0.01)
275
+ """
276
+
277
+ def __init__(self, alpha=0.01):
278
+ super().__init__(alpha=alpha)
279
+
280
+ @staticmethod
281
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
282
+ alpha = config.get("alpha", 0.01) if config else 0.01
283
+ return jnp.where(x > 0, x, alpha * x)
284
+
285
+ @staticmethod
286
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
287
+ alpha = config.get("alpha", 0.01) if config else 0.01
288
+ return jnp.where(x > 0, 1.0, alpha)
289
+
290
+ @staticmethod
291
+ def variance_gain(config: Dict[str, Any] = None) -> float:
292
+ alpha = config.get("alpha", 0.01) if config else 0.01
293
+ return math.sqrt(2.0 / (1.0 + alpha**2))
294
+
295
+
296
+ class GeluActivation(ActivationBase):
297
+ """GELU activation: x * 0.5 * (1 + erf(x / sqrt(2)))"""
298
+
299
+ def __init__(self):
300
+ super().__init__()
301
+
302
+ @staticmethod
303
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
304
+ return nn.gelu(x)
305
+
306
+ @staticmethod
307
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
308
+ # Gelu(x): = x * normal_CDF
309
+ sqrt_2_over_pi = jnp.sqrt(2 / jnp.pi)
310
+ x_cubed = x**3
311
+ apx_norm_cdf = 0.5 * (
312
+ 1 + jnp.tanh(sqrt_2_over_pi * (x + 0.044715 * x_cubed))
313
+ ) # Approximation of normal CDF via tanh
314
+ norm_cdf_prime = (0.5 * sqrt_2_over_pi * (1 + 3 * 0.044715 * x**2)) * (
315
+ 1 - jnp.tanh(sqrt_2_over_pi * (x + 0.044715 * x_cubed)) ** 2
316
+ )
317
+ return apx_norm_cdf + x * norm_cdf_prime
318
+
319
+ @staticmethod
320
+ def variance_gain(config: Dict[str, Any] = None) -> float:
321
+ return math.sqrt(2.0)
322
+
323
+ @staticmethod
324
+ def jacobian_gain(config: Dict[str, Any] = None) -> float:
325
+ # rms(gelu'(z)) ≈ 0.605 for z ~ N(0, 2)
326
+ # jacobian_gain = 1 / (sqrt(2) * 0.605) ≈ 1.168
327
+ return 1.168
328
+
329
+
330
+ class SoftmaxActivation(ActivationBase):
331
+ """Softmax activation: exp(x) / sum(exp(x)) along the last axis"""
332
+
333
+ def __init__(self):
334
+ super().__init__()
335
+
336
+ @staticmethod
337
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
338
+ exp_x = jnp.exp(
339
+ x - jnp.max(x, axis=-1, keepdims=True)
340
+ ) # relative to max value for numerical stability
341
+ return exp_x / jnp.sum(exp_x, axis=-1, keepdims=True)
342
+
343
+ @staticmethod
344
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
345
+ s = SoftmaxActivation.forward(x)
346
+ # Diagonal of the Jacobian diag(s) - s @ s.T.
347
+ # The off-diagonal terms are omitted; valid for element-wise PC gradients.
348
+ return s * (1 - s)
349
+
350
+ @staticmethod
351
+ def jacobian(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
352
+ """Full softmax Jacobian: J_{ij} = s_i * (delta_{ij} - s_j)."""
353
+ s = SoftmaxActivation.forward(x, config)
354
+ eye = jnp.eye(s.shape[-1])
355
+ return jnp.expand_dims(s, -1) * (eye - jnp.expand_dims(s, -2))
356
+
357
+
358
+ class HardTanhActivation(ActivationBase):
359
+ """
360
+ Hard tanh activation: clip(x, min_val, max_val)
361
+
362
+ Args:
363
+ min_val: Minimum output value (default: -1.0)
364
+ max_val: Maximum output value (default: 1.0)
365
+ """
366
+
367
+ def __init__(self, min_val=-1.0, max_val=1.0):
368
+ super().__init__(min_val=min_val, max_val=max_val)
369
+
370
+ @staticmethod
371
+ def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
372
+ min_val = config.get("min_val", -1.0) if config else -1.0
373
+ max_val = config.get("max_val", 1.0) if config else 1.0
374
+ return jnp.clip(x, min_val, max_val)
375
+
376
+ @staticmethod
377
+ def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
378
+ min_val = config.get("min_val", -1.0) if config else -1.0
379
+ max_val = config.get("max_val", 1.0) if config else 1.0
380
+ return ((x > min_val) & (x < max_val)).astype(jnp.float32)
381
+
382
+ @staticmethod
383
+ def variance_gain(config: Dict[str, Any] = None) -> float:
384
+ return math.sqrt(5.0 / 3.0)
385
+
386
+ @staticmethod
387
+ def jacobian_gain(config: Dict[str, Any] = None) -> float:
388
+ # rms(hardtanh'(z)) ≈ 0.749 for z ~ N(0, 5/3)
389
+ # jacobian_gain = 1 / (sqrt(5/3) * 0.749) ≈ 1.035
390
+ return 1.035