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)
[0.5        0.4        0.40000004]

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)
[[0.5        0.4        0.40000004]
 [0.3        0.5        0.6       ]]

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.