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.