Tutorial 4: Advanced Patterns - E-I Networks and Kernel-Based Connectivity#

This tutorial explores biologically-inspired connectivity patterns including excitatory-inhibitory networks and kernel-based receptive fields.

Overview#

We’ll cover:

  • Excitatory-Inhibitory Networks: Implementing Dale’s principle with separate E and I populations

  • Kernel-Based Connectivity: Creating receptive field patterns using spatial kernels

  • Combining Patterns: Building complex networks from multiple connectivity patterns

import brainunit as u
import matplotlib.pyplot as plt
import numpy as np

import braintools.conn as conn
import braintools.visualize as vis

1. Excitatory-Inhibitory Networks#

1.1 Dale’s Principle#

Dale’s principle states that a neuron releases the same neurotransmitter(s) at all of its synapses. This means neurons are either excitatory or inhibitory, never both.

In cortical circuits:

  • ~80% of neurons are excitatory (glutamatergic)

  • ~20% of neurons are inhibitory (GABAergic)

  • Excitatory connections have positive weights

  • Inhibitory connections have negative weights

  • The E-I balance is critical for stable network dynamics

1.2 Creating a Basic E-I Network#

# Create E-I network with 80% excitatory neurons
n_neurons = 500

ei_net = conn.ExcitatoryInhibitory(
    exc_ratio=0.8,  # 80% excitatory
    exc_prob=0.1,  # Excitatory connection probability
    inh_prob=0.2,  # Inhibitory connection probability (higher)
    exc_weight=1.0 * u.nS,  # Positive excitatory weights
    inh_weight=-0.8 * u.nS,  # Negative inhibitory weights
    seed=42
)

result = ei_net(pre_size=n_neurons, post_size=n_neurons)

print(f"Total connections: {len(result.pre_indices)}")
print(f"Excitatory neurons: {result.metadata['n_excitatory']}")
print(f"Inhibitory neurons: {result.metadata['n_inhibitory']}")
Total connections: 30028
Excitatory neurons: 400
Inhibitory neurons: 100

1.3 Visualizing E-I Connectivity Structure#

Let’s visualize the connectivity matrix showing the four connection types:

  • E→E: Excitatory to Excitatory

  • E→I: Excitatory to Inhibitory

  • I→E: Inhibitory to Excitatory

  • I→I: Inhibitory to Inhibitory

# Create connectivity matrix
conn_mat = np.zeros((n_neurons, n_neurons))
weights_vals = u.Quantity(result.weights).mantissa
conn_mat[result.pre_indices, result.post_indices] = weights_vals

# Visualize with center_zero to highlight positive/negative weights
fig, ax = plt.subplots(figsize=(10, 8))
vis.connectivity_matrix(
    conn_mat,
    cmap='RdBu_r',
    center_zero=True,
    show_colorbar=True,
    ax=ax,
    title='E-I Network Connectivity (Red=Inhibitory, Blue=Excitatory)',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

# Add lines to separate E and I populations
n_exc = result.metadata['n_excitatory']
ax.axhline(y=n_exc - 0.5, color='black', linewidth=2, linestyle='--', alpha=0.5)
ax.axvline(x=n_exc - 0.5, color='black', linewidth=2, linestyle='--', alpha=0.5)
ax.text(n_exc / 2, -20, 'E', ha='center', fontsize=14, fontweight='bold')
ax.text(n_exc + (n_neurons - n_exc) / 2, -20, 'I', ha='center', fontsize=14, fontweight='bold')
ax.text(-20, n_exc / 2, 'E', va='center', fontsize=14, fontweight='bold')
ax.text(-20, n_exc + (n_neurons - n_exc) / 2, 'I', va='center', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.show()
../_images/7a86b70d28c7d113b69622a289626186858751284b5c5950ae40e629d98ad710.png

1.4 Analyzing E-I Connection Statistics#

Let’s analyze the degree distributions separately for excitatory and inhibitory neurons.

# Separate excitatory and inhibitory connections
n_exc = result.metadata['n_excitatory']
exc_mask = result.pre_indices < n_exc
inh_mask = result.pre_indices >= n_exc

# Calculate in-degrees from E and I separately
in_degree_from_E = np.bincount(result.post_indices[exc_mask], minlength=n_neurons)
in_degree_from_I = np.bincount(result.post_indices[inh_mask], minlength=n_neurons)
in_degree_total = np.bincount(result.post_indices, minlength=n_neurons)

# Visualize degree distributions
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

vis.distribution_plot(
    in_degree_from_E,
    bins=20,
    alpha=0.7,
    colors=['steelblue'],
    edgecolor='black',
    ax=axes[0],
    xlabel='In-Degree from E',
    ylabel='Count',
    title='Excitatory Input Distribution'
)

vis.distribution_plot(
    in_degree_from_I,
    bins=20,
    alpha=0.7,
    colors=['coral'],
    edgecolor='black',
    ax=axes[1],
    xlabel='In-Degree from I',
    ylabel='Count',
    title='Inhibitory Input Distribution'
)

vis.distribution_plot(
    in_degree_total,
    bins=25,
    alpha=0.7,
    colors=['purple'],
    edgecolor='black',
    ax=axes[2],
    xlabel='Total In-Degree',
    ylabel='Count',
    title='Total Input Distribution'
)

plt.tight_layout()
plt.show()
../_images/f87a69d9546bd441db4bce679ee842bc5104b83cbde3328061bcea8864908c52.png

1.5 Weight Distribution Analysis#

# Separate excitatory and inhibitory weights
exc_weights = weights_vals[exc_mask]
inh_weights = weights_vals[inh_mask]

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

vis.distribution_plot(
    exc_weights,
    bins=30,
    alpha=0.7,
    colors=['steelblue'],
    edgecolor='black',
    ax=axes[0],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Excitatory Weight Distribution'
)

vis.distribution_plot(
    inh_weights,
    bins=30,
    alpha=0.7,
    colors=['coral'],
    edgecolor='black',
    ax=axes[1],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Inhibitory Weight Distribution'
)

plt.tight_layout()
plt.show()

print(f"E/I weight ratio (magnitude): {np.abs(np.mean(exc_weights) / np.mean(inh_weights)):.2f}")
../_images/e465302e15fe5fdebd2ad00d51df4e65bca758328d9fada22a1e68e98ff9ed8a.png
E/I weight ratio (magnitude): 1.25

2. Kernel-Based Connectivity#

Kernel-based connectivity creates connections weighted by spatial functions, mimicking receptive field structures found in sensory systems.

2.1 Gaussian Kernel#

Gaussian kernels create smooth, radially symmetric receptive fields. Connections closer to the center have stronger weights.

# Create 2D grid of neurons
grid_size = 20
n_neurons_2d = grid_size * grid_size

# Create grid positions
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
xx, yy = np.meshgrid(x, y)
positions = np.stack([xx.flatten(), yy.flatten()], axis=1) * u.um

# Create Gaussian kernel connectivity
gaussian = conn.GaussianKernel(
    sigma=100 * u.um,
    max_distance=300 * u.um,
    normalize=True,
    weight=5.0 * u.nS,
    seed=42
)

result_gaussian = gaussian(
    pre_size=n_neurons_2d,
    post_size=n_neurons_2d,
    pre_positions=positions,
    post_positions=positions
)

print(f"Gaussian kernel connections: {len(result_gaussian.pre_indices)}")
print(f"Average connections per neuron: {len(result_gaussian.pre_indices) / n_neurons_2d:.1f}")
Gaussian kernel connections: 20096
Average connections per neuron: 50.2

2.2 Visualizing Gaussian Connectivity#

# Create connectivity matrix
conn_mat_gaussian = np.zeros((n_neurons_2d, n_neurons_2d))
weights_gaussian = u.Quantity(result_gaussian.weights).mantissa
conn_mat_gaussian[result_gaussian.pre_indices, result_gaussian.post_indices] = weights_gaussian

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Full connectivity matrix
vis.connectivity_matrix(
    conn_mat_gaussian,
    cmap='viridis',
    center_zero=False,
    show_colorbar=True,
    ax=axes[0],
    title='Gaussian Kernel Connectivity Matrix',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

# Receptive field of a single neuron (center neuron)
center_neuron = n_neurons_2d // 2 + grid_size // 2
receptive_field = conn_mat_gaussian[:, center_neuron].reshape(grid_size, grid_size)

im = axes[1].imshow(receptive_field, cmap='viridis', origin='lower')
axes[1].set_title(f'Receptive Field of Center Neuron ({center_neuron})')
axes[1].set_xlabel('X Position')
axes[1].set_ylabel('Y Position')
plt.colorbar(im, ax=axes[1], label='Weight (nS)')

plt.tight_layout()
plt.show()
../_images/03d57d9021596667e304748299c706f5f09a6605334fad3cccab05f63860c632.png

2.3 Weight Distribution for Gaussian Kernel#

# Analyze Gaussian weight distribution
fig, ax = plt.subplots(figsize=(8, 5))

vis.distribution_plot(
    weights_gaussian,
    bins=40,
    alpha=0.7,
    colors=['teal'],
    edgecolor='black',
    ax=ax,
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Gaussian Kernel Weight Distribution'
)

plt.tight_layout()
plt.show()
../_images/85394a12fb0297b542495225a53cc2bd4142c89e40b3a796cca73cd8cadcc962.png

3. Gabor Kernel for Orientation Selectivity#

Gabor kernels combine Gaussian envelopes with sinusoidal gratings, creating orientation-selective receptive fields similar to V1 simple cells.

# Create Gabor kernel with 45-degree orientation
gabor = conn.GaborKernel(
    sigma=80 * u.um,
    frequency=0.015,  # Cycles per micrometer
    theta=np.pi / 4,  # 45 degrees
    phase=0.0,
    max_distance=240 * u.um,
    weight=2.0 * u.nS,
    seed=42
)

result_gabor = gabor(
    pre_size=n_neurons_2d,
    post_size=n_neurons_2d,
    pre_positions=positions,
    post_positions=positions
)

print(f"Gabor kernel connections: {len(result_gabor.pre_indices)}")
print(f"Average connections per neuron: {len(result_gabor.pre_indices) / n_neurons_2d:.1f}")
Gabor kernel connections: 22400
Average connections per neuron: 56.0

3.1 Visualizing Gabor Receptive Fields#

# Create connectivity matrix
conn_mat_gabor = np.zeros((n_neurons_2d, n_neurons_2d))
weights_gabor = u.Quantity(result_gabor.weights).mantissa
conn_mat_gabor[result_gabor.pre_indices, result_gabor.post_indices] = weights_gabor

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Receptive field of center neuron
center_neuron = n_neurons_2d // 2 + grid_size // 2
receptive_field_gabor = conn_mat_gabor[:, center_neuron].reshape(grid_size, grid_size)

im0 = axes[0].imshow(receptive_field_gabor, cmap='RdBu_r', origin='lower')
axes[0].set_title(f'Gabor Receptive Field (θ=45°)')
axes[0].set_xlabel('X Position')
axes[0].set_ylabel('Y Position')
plt.colorbar(im0, ax=axes[0], label='Weight (nS)')

# Show a portion of the connectivity matrix
vis.connectivity_matrix(
    conn_mat_gabor[:100, :100],
    cmap='RdBu_r',
    center_zero=True,
    show_colorbar=True,
    ax=axes[1],
    title='Gabor Connectivity Matrix (subset)',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

plt.tight_layout()
plt.show()
../_images/cdbde629e2e7ddcab8ee2397ac08d1d38184551531a657ff9365d1f2fd707212.png

3.2 Comparing Multiple Orientations#

# Create Gabor kernels at different orientations
orientations = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4]
orientation_names = ['0°', '45°', '90°', '135°']

fig, axes = plt.subplots(2, 4, figsize=(16, 8))

for idx, (theta, name) in enumerate(zip(orientations, orientation_names)):
    gabor_oriented = conn.GaborKernel(
        sigma=80 * u.um,
        frequency=0.015,
        theta=theta,
        phase=0.0,
        max_distance=240 * u.um,
        weight=2.0 * u.nS,
        seed=42
    )

    result = gabor_oriented(
        pre_size=n_neurons_2d,
        post_size=n_neurons_2d,
        pre_positions=positions,
        post_positions=positions
    )

    conn_mat = np.zeros((n_neurons_2d, n_neurons_2d))
    weights = u.Quantity(result.weights).mantissa
    conn_mat[result.pre_indices, result.post_indices] = weights

    # Show receptive field
    rf = conn_mat[:, center_neuron].reshape(grid_size, grid_size)
    im = axes[0, idx].imshow(rf, cmap='RdBu_r', origin='lower')
    axes[0, idx].set_title(f'Receptive Field ({name})')
    axes[0, idx].set_xlabel('X')
    axes[0, idx].set_ylabel('Y')
    plt.colorbar(im, ax=axes[0, idx])

    # Show weight distribution
    vis.distribution_plot(
        weights,
        bins=30,
        alpha=0.7,
        colors=['steelblue'],
        edgecolor='black',
        ax=axes[1, idx],
        xlabel='Weight (nS)',
        ylabel='Count',
        title=f'Weights ({name})'
    )

plt.tight_layout()
plt.show()
../_images/a2c7390eb403616056ec62857f8b964aa54e1a25c0dde544a1992ef15ee04d88.png

4. Difference of Gaussians (DoG) Kernel#

DoG kernels create center-surround receptive fields found in retinal ganglion cells and LGN neurons. The center and surround have opposite signs, creating edge detection properties.

# Create DoG kernel with center-surround structure
dog = conn.DoGKernel(
    sigma_center=50 * u.um,
    sigma_surround=100 * u.um,
    amplitude_center=1.0,
    amplitude_surround=0.8,
    max_distance=300 * u.um,
    weight=3.0 * u.nS,
    seed=42
)

result_dog = dog(
    pre_size=n_neurons_2d,
    post_size=n_neurons_2d,
    pre_positions=positions,
    post_positions=positions
)

print(f"DoG kernel connections: {len(result_dog.pre_indices)}")
print(f"Average connections per neuron: {len(result_dog.pre_indices) / n_neurons_2d:.1f}")
DoG kernel connections: 31240
Average connections per neuron: 78.1

4.1 Visualizing Center-Surround Structure#

# Create connectivity matrix
conn_mat_dog = np.zeros((n_neurons_2d, n_neurons_2d))
weights_dog = u.Quantity(result_dog.weights).mantissa
conn_mat_dog[result_dog.pre_indices, result_dog.post_indices] = weights_dog

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Receptive field showing center-surround
center_neuron = n_neurons_2d // 2 + grid_size // 2
receptive_field_dog = conn_mat_dog[:, center_neuron].reshape(grid_size, grid_size)

im0 = axes[0].imshow(receptive_field_dog, cmap='RdBu_r', origin='lower')
axes[0].set_title('DoG Receptive Field (Center-Surround)')
axes[0].set_xlabel('X Position')
axes[0].set_ylabel('Y Position')
plt.colorbar(im0, ax=axes[0], label='Weight (nS)')

# Connectivity matrix
vis.connectivity_matrix(
    conn_mat_dog[:100, :100],
    cmap='RdBu_r',
    center_zero=True,
    show_colorbar=True,
    ax=axes[1],
    title='DoG Connectivity Matrix (subset)',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

# Weight distribution
vis.distribution_plot(
    weights_dog,
    bins=40,
    alpha=0.7,
    colors=['purple'],
    edgecolor='black',
    ax=axes[2],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='DoG Weight Distribution'
)

plt.tight_layout()
plt.show()
../_images/d2328e7c1d2e2162acf411f503ef68043d4ceb0f3b3d17c1a1850b4e796d425d.png

4.2 Mexican Hat (Laplacian of Gaussian)#

The Mexican Hat kernel is a special case of DoG that approximates the Laplacian of Gaussian, creating strong lateral inhibition.

# Create Mexican Hat kernel
mexican = conn.MexicanHat(
    sigma=60 * u.um,
    max_distance=240 * u.um,
    weight=3.0 * u.nS,
    seed=42
)

result_mexican = mexican(
    pre_size=n_neurons_2d,
    post_size=n_neurons_2d,
    pre_positions=positions,
    post_positions=positions
)

# Visualize
conn_mat_mexican = np.zeros((n_neurons_2d, n_neurons_2d))
weights_mexican = u.Quantity(result_mexican.weights).mantissa
conn_mat_mexican[result_mexican.pre_indices, result_mexican.post_indices] = weights_mexican

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Receptive field
rf_mexican = conn_mat_mexican[:, center_neuron].reshape(grid_size, grid_size)
im0 = axes[0].imshow(rf_mexican, cmap='RdBu_r', origin='lower')
axes[0].set_title('Mexican Hat Receptive Field')
axes[0].set_xlabel('X Position')
axes[0].set_ylabel('Y Position')
plt.colorbar(im0, ax=axes[0], label='Weight (nS)')

# Weight distribution
vis.distribution_plot(
    weights_mexican,
    bins=40,
    alpha=0.7,
    colors=['darkgreen'],
    edgecolor='black',
    ax=axes[1],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Mexican Hat Weight Distribution'
)

plt.tight_layout()
plt.show()
../_images/80e28a4198fac9bbd52d8632d9ed94249f740a9f3253af37ad64d462f5c48f41.png

5. Custom Kernels#

You can create arbitrary kernel functions for specialized connectivity patterns.

# Define a custom spiral kernel
def spiral_kernel(x, y):
    """Custom spiral kernel function."""
    r = np.sqrt(x ** 2 + y ** 2)
    theta = np.arctan2(y, x)
    # Spiral pattern: decays with distance, modulated by angle
    return np.exp(-r / 100) * np.cos(r / 30 + 3 * theta)


# Create custom kernel connectivity
custom = conn.CustomKernel(
    kernel_func=spiral_kernel,
    kernel_size=400 * u.um,
    threshold=0.1,
    weight=2.0 * u.nS,
    seed=42
)

result_custom = custom(
    pre_size=n_neurons_2d,
    post_size=n_neurons_2d,
    pre_positions=positions,
    post_positions=positions
)

print(f"Custom kernel connections: {len(result_custom.pre_indices)}")
Custom kernel connections: 11990

5.1 Visualizing Custom Kernel#

# Create connectivity matrix
conn_mat_custom = np.zeros((n_neurons_2d, n_neurons_2d))
weights_custom = u.Quantity(result_custom.weights).mantissa
conn_mat_custom[result_custom.pre_indices, result_custom.post_indices] = weights_custom

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Receptive field
rf_custom = conn_mat_custom[:, center_neuron].reshape(grid_size, grid_size)
im0 = axes[0].imshow(rf_custom, cmap='RdBu_r', origin='lower')
axes[0].set_title('Custom Spiral Receptive Field')
axes[0].set_xlabel('X Position')
axes[0].set_ylabel('Y Position')
plt.colorbar(im0, ax=axes[0], label='Weight (nS)')

# Connectivity matrix
vis.connectivity_matrix(
    conn_mat_custom[:100, :100],
    cmap='RdBu_r',
    center_zero=True,
    show_colorbar=True,
    ax=axes[1],
    title='Custom Kernel Connectivity (subset)',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

# Weight distribution
vis.distribution_plot(
    weights_custom,
    bins=40,
    alpha=0.7,
    colors=['darkorange'],
    edgecolor='black',
    ax=axes[2],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Custom Kernel Weight Distribution'
)

plt.tight_layout()
plt.show()
../_images/32855f5412262d17f02e998aedefa7feeb5bcde37f3c3f667d96bc6887942505.png

6. Comparing All Kernel Types#

Let’s compare the receptive fields of all kernel types side by side.

# Compare receptive fields
kernels = [
    (receptive_field_gabor, 'Gabor (45°)'),
    (receptive_field_dog, 'DoG'),
    (rf_mexican, 'Mexican Hat'),
    (rf_custom, 'Custom Spiral')
]

fig, axes = plt.subplots(1, 4, figsize=(16, 4))

for idx, (rf, name) in enumerate(kernels):
    im = axes[idx].imshow(rf, cmap='RdBu_r', origin='lower')
    axes[idx].set_title(name)
    axes[idx].set_xlabel('X')
    axes[idx].set_ylabel('Y')
    plt.colorbar(im, ax=axes[idx])

plt.tight_layout()
plt.show()
../_images/d2f65e87564291a3c0be34af975ddfa85753aedee8111ac982b43171569cd96f.png

7. Combining E-I Networks with Kernel Connectivity#

Real neural circuits often combine excitatory-inhibitory structure with spatial organization. Let’s create a network that has both properties.

# Create positions for E-I network
n_combined = 400
positions_combined = np.random.uniform(0, 1000, (n_combined, 2)) * u.um

# First, establish E-I structure
ei_spatial = conn.ExcitatoryInhibitory(
    exc_ratio=0.8,
    exc_prob=0.0,  # Will use kernel instead
    inh_prob=0.0,  # Will use kernel instead
    seed=42
)

# Create separate Gaussian kernels for E and I
# E neurons have broader connectivity
gauss_e = conn.GaussianKernel(
    sigma=120 * u.um,
    max_distance=360 * u.um,
    weight=1.0 * u.nS,
    seed=42
)

# I neurons have narrower, stronger connectivity
gauss_i = conn.GaussianKernel(
    sigma=60 * u.um,
    max_distance=180 * u.um,
    weight=-1.2 * u.nS,
    seed=43
)

# Generate E connections
n_exc_combined = int(n_combined * 0.8)
result_e = gauss_e(
    pre_size=n_exc_combined,
    post_size=n_combined,
    pre_positions=positions_combined[:n_exc_combined],
    post_positions=positions_combined
)

# Generate I connections
result_i = gauss_i(
    pre_size=n_combined - n_exc_combined,
    post_size=n_combined,
    pre_positions=positions_combined[n_exc_combined:],
    post_positions=positions_combined
)

# Adjust I indices to account for E neurons
result_i_pre = result_i.pre_indices + n_exc_combined

# Combine both
combined_pre = np.concatenate([result_e.pre_indices, result_i_pre])
combined_post = np.concatenate([result_e.post_indices, result_i.post_indices])
combined_weights = np.concatenate([
    u.Quantity(result_e.weights).mantissa,
    u.Quantity(result_i.weights).mantissa
])

print(f"Combined E-I spatial network:")
print(f"  E connections: {len(result_e.pre_indices)}")
print(f"  I connections: {len(result_i.pre_indices)}")
print(f"  Total connections: {len(combined_pre)}")
Combined E-I spatial network:
  E connections: 23207
  I connections: 2593
  Total connections: 25800

7.1 Visualizing Combined E-I Spatial Network#

# Create connectivity matrix
conn_mat_combined = np.zeros((n_combined, n_combined))
conn_mat_combined[combined_pre, combined_post] = combined_weights

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Full connectivity matrix
vis.connectivity_matrix(
    conn_mat_combined,
    cmap='RdBu_r',
    center_zero=True,
    show_colorbar=True,
    ax=axes[0],
    title='Combined E-I Spatial Network',
    xlabel='Postsynaptic Neuron',
    ylabel='Presynaptic Neuron'
)

# Add separation line
axes[0].axhline(y=n_exc_combined - 0.5, color='black', linewidth=2, linestyle='--', alpha=0.5)
axes[0].axvline(x=n_exc_combined - 0.5, color='black', linewidth=2, linestyle='--', alpha=0.5)

# Weight distribution
vis.distribution_plot(
    combined_weights,
    bins=50,
    alpha=0.7,
    colors=['steelblue'],
    edgecolor='black',
    ax=axes[1],
    xlabel='Weight (nS)',
    ylabel='Count',
    title='Combined Network Weight Distribution'
)

plt.tight_layout()
plt.show()
../_images/920a44b04606f1a8c350ceaf6bf32b67d803e3e433f3fafeada802280c88d3a4.png

7.2 Spatial Analysis of Combined Network#

# Analyze spatial properties
# Calculate connection distances
pos_vals = u.Quantity(positions_combined).mantissa
distances = np.sqrt(np.sum((pos_vals[combined_pre] - pos_vals[combined_post]) ** 2, axis=1))

# Separate E and I distances
e_mask = combined_pre < n_exc_combined
i_mask = combined_pre >= n_exc_combined
distances_e = distances[e_mask]
distances_i = distances[i_mask]

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Distance distributions
vis.distribution_plot(
    distances_e,
    bins=30,
    alpha=0.7,
    colors=['steelblue'],
    edgecolor='black',
    ax=axes[0],
    xlabel='Distance (μm)',
    ylabel='Count',
    title='E Connection Distance Distribution'
)

vis.distribution_plot(
    distances_i,
    bins=30,
    alpha=0.7,
    colors=['coral'],
    edgecolor='black',
    ax=axes[1],
    xlabel='Distance (μm)',
    ylabel='Count',
    title='I Connection Distance Distribution'
)

plt.tight_layout()
plt.show()

print(f"Mean E connection distance: {np.mean(distances_e):.1f} μm")
print(f"Mean I connection distance: {np.mean(distances_i):.1f} μm")
../_images/2668e548a6347014577e552f8a62e7a5fd9bc5006944ac01fd5f9f9b10878325.png
Mean E connection distance: 168.6 μm
Mean I connection distance: 104.1 μm