Fixed Connection Count Structures#
FixedNumPerPre and FixedNumPerPost represent connectivity with an exact degree constraint. That constraint can be scientifically meaningful, but it does not by itself make a network biologically realistic.
import brainevent
import jax
import jax.numpy as jnp
Why Fix the Number of Connections?#
A fixed fan-out controls the number of targets selected by each presynaptic unit. A fixed fan-in controls the number of sources received by each postsynaptic unit. These are topology constraints; anatomical realism also depends on cell types, spatial organization, weight distributions, delays, and other assumptions.
Fixed Fan-Out with FixedNumPerPre#
Each row stores the same number of postsynaptic indices. Here each of three presynaptic units has exactly two outgoing connections.
fan_out_data = jnp.array([[0.5, -0.2], [0.3, 0.1], [0.4, 0.6]])
post_indices = jnp.array([[0, 2], [0, 1], [1, 2]])
per_pre = brainevent.FixedNumPerPre(fan_out_data, post_indices, shape=(3, 3))
print(per_pre.todense())
[[ 0.5 0. -0.2]
[ 0.3 0.1 0. ]
[ 0. 0.4 0.6]]
Fixed Fan-In with FixedNumPerPost#
Each stored row now corresponds to a postsynaptic unit and lists its presynaptic sources. Here every postsynaptic unit receives exactly two connections.
fan_in_data = jnp.array([[0.5, 0.3], [0.1, 0.4], [-0.2, 0.6]])
pre_indices = jnp.array([[0, 1], [1, 2], [0, 2]])
per_post = brainevent.FixedNumPerPost(fan_in_data, pre_indices, shape=(3, 3))
print(per_post.todense())
[[ 0.5 0. -0.2]
[ 0.3 0.1 0. ]
[ 0. 0.4 0.6]]
Combining Fixed Connectivity with Binary Events#
Binary events select active presynaptic rows. The two structures below encode the same dense matrix, so they should produce the same forward result.
events = brainevent.BinaryArray(jnp.array([True, False, True]))
out_per_pre = events @ per_pre
out_per_post = events @ per_post
assert jnp.allclose(out_per_pre, out_per_post)
print(out_per_pre)
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:569, in XLACustomKernel._register_fallback_lowering.<locals>.fallback_kernel_fn(*args, **kwargs)
568 try:
--> 569 kernel = entry.kernel_generator(**kwargs)
570 except Exception as exc:
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/binary.py:162, in _ell_binary_matvec_numba_kernel(weight_info, spike_info, transpose, **kwargs)
156 def _ell_binary_matvec_numba_kernel(
157 weight_info: jax.ShapeDtypeStruct,
158 spike_info: jax.ShapeDtypeStruct,
159 transpose: bool,
160 **kwargs
161 ):
--> 162 import numba
164 if transpose:
ModuleNotFoundError: No module named 'numba'
The above exception was the direct cause of the following exception:
JaxStackTraceBeforeTransformation Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/binary.py:135, in binary_fcnmv()
134 assert jnp.issubdtype(weights.dtype, jnp.floating), 'Weights must be a floating-point type.'
--> 135 r = binary_fcnmv_p_call(
136 weights,
137 indices,
138 spikes,
139 shape=shape,
140 transpose=transpose,
141 backend=backend,
142 )[0]
143 return u.maybe_decimal(r * v_unit * w_unit)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/binary.py:498, in binary_fcnmv_p_call()
497 assert jnp.issubdtype(weights.dtype, jnp.floating), 'Weights must be a floating-point type.'
--> 498 return binary_fcnmv_p(
499 weights,
500 indices,
501 spikes,
502 outs=[out],
503 shape=shape,
504 transpose=transpose,
505 weight_info=jax.ShapeDtypeStruct(weights.shape, weights.dtype),
506 indices_info=jax.ShapeDtypeStruct(indices.shape, indices.dtype),
507 spike_info=jax.ShapeDtypeStruct(spikes.shape, spikes.dtype),
508 backend=backend,
509 )
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:338, in __call__()
337 flat_outs, tree_def = abstract_arguments(outs)
--> 338 r = self.primitive.bind(*ins, **kwargs, outs=tuple(flat_outs))
339 if len(r) != len(flat_outs):
JaxStackTraceBeforeTransformation: brainevent.KernelCompilationError: Backend 'numba' failed to construct a kernel for primitive 'ell_binary_matvec' on platform 'cpu': ModuleNotFoundError: No module named 'numba'. Available backend(s) for platform 'cpu': ['numba', 'jax_raw']. Switch with backend='<name>' on this call, or ell_binary_matvec.set_default('cpu', '<name>') / brainevent.set_backend('cpu', '<name>') to change the default.
The preceding stack trace is the source of the JAX operation that, once transformed by JAX, triggered the following exception.
--------------------
The above exception was the direct cause of the following exception:
KernelCompilationError Traceback (most recent call last)
Cell In[4], line 2
1 events = brainevent.BinaryArray(jnp.array([True, False, True]))
----> 2 out_per_pre = events @ per_pre
3 out_per_post = events @ per_post
4 assert jnp.allclose(out_per_pre, out_per_post)
5 print(out_per_pre)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_event/binary.py:214, in BinaryArray.__matmul__(self, oc)
212 return binary_densemm(oc, self.value.T, transpose=True).T
213 else:
--> 214 return oc.__rmatmul__(self)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/main.py:460, in FixedNumConn.__rmatmul__(self, other)
458 def __rmatmul__(self, other):
459 """Reflected matrix multiplication ``other @ self`` (logical ``W^T @ other``)."""
--> 460 return self._dispatch(other, transpose_W=True)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/main.py:427, in FixedNumConn._dispatch(self, other, transpose_W)
425 value = other.value
426 if value.ndim == 1:
--> 427 return self._binary_matvec(value, transpose_W)
428 elif value.ndim == 2:
429 if transpose_W:
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_fcn/main.py:313, in FixedNumConn._binary_matvec(self, s, transpose_W)
309 a_shape, ell_transpose = self._ell_plan(transpose_W)
310 if ell_transpose:
311 # Favorable: the event vector indexes the ELL stored axis -> direct
312 # column-scatter over active events.
--> 313 return binary_fcnmv(
314 self.data, self.indices, s,
315 shape=a_shape, transpose=True, backend=self.backend,
316 )
317 # Unfavorable: traverse the cached CSC mirror with the reused, perm-fused
318 # CSR kernel -- it reads ``data[perm[j]]`` so only active columns are
319 # touched (no full-size weight gather). Same shape/transpose as CSR/CSC.
320 csc_indptr, csc_indices, perm = self._weight_indices()
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_misc.py:1770, in NameScope.__call__(self, *args, **kwargs)
1768 backend = kwargs.pop('backend', None)
1769 jit_fn = self._get_jit_fn(backend)
-> 1770 return jit_fn(*args, **kwargs)
[... skipping hidden 18 frame]
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:571, in XLACustomKernel._register_fallback_lowering.<locals>.fallback_kernel_fn(*args, **kwargs)
569 kernel = entry.kernel_generator(**kwargs)
570 except Exception as exc:
--> 571 raise KernelCompilationError(
572 f"Backend '{backend_to_use}' failed to construct a kernel "
573 f"for primitive '{self.name}' on platform '{platform}': "
574 f"{type(exc).__name__}: {exc}." + _construction_error_suffix()
575 ) from exc
577 try:
578 return kernel(*args)
KernelCompilationError: Backend 'numba' failed to construct a kernel for primitive 'ell_binary_matvec' on platform 'cpu': ModuleNotFoundError: No module named 'numba'. Available backend(s) for platform 'cpu': ['numba', 'jax_raw']. Switch with backend='<name>' on this call, or ell_binary_matvec.set_default('cpu', '<name>') / brainevent.set_backend('cpu', '<name>') to change the default.
Build a Fixed-Degree Network#
A batch can be processed as one array operation. This example demonstrates a fixed-degree layer; it makes no claim that the resulting network captures a complete biological circuit.
event_batch = brainevent.BinaryArray(jnp.array([[1, 0, 1], [0, 1, 1]], dtype=bool))
fixed_degree_step = jax.jit(lambda x: x @ per_pre)
batch_output = fixed_degree_step(event_batch)
batch_output.block_until_ready()
print(batch_output)
Memory and Performance Characteristics#
For n_pre sources, n_post targets, and degree k, fixed-count storage scales with n_pre * k for FixedNumPerPre or n_post * k for FixedNumPerPost, rather than with every possible edge. Runtime still depends on orientation, event density, shape, dtype, backend, and compilation. Benchmark the operation that matches the application after warm-up and synchronization.
Choosing Fan-In or Fan-Out Constraints#
Use FixedNumPerPre when exact outgoing degree is the modeled invariant and row-driven propagation is central. Use FixedNumPerPost when exact incoming degree is the invariant or postsynaptic access dominates. If degree varies substantially, CSR/CSC is generally a clearer representation.
Summary and Next Steps#
Fixed-count structures encode an exact degree constraint and make it explicit in the data model. Continue to Just-in-Time Connection Matrices for generated random connectivity, or revisit CSR and CSC for irregular explicit sparsity.