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.