Skip to content
Snippets Groups Projects
  • Brent Huisman's avatar
    Port of Brunel example (#1404) · c326ab3b
    Brent Huisman authored
    * Add Python version of brunel.cpp example.
    * Enforce consistency in units: both C++ and Python APIs use unit `kHz` for all frequencies.
    * Align Python version `poisson_schedule` signature with C++ equivalent.
    * Update unit tests to suit API changes.
    * Update documentation to suit, along with minor additions related to networks and interconnectivity.
    * Add `brunel.py` to `basic.yml` tests
    Unverified
    c326ab3b
brunel.py 6.82 KiB
#!/usr/bin/env python3

import arbor
import numpy,argparse

'''
A Brunel network consists of nexc excitatory LIF neurons and ninh inhibitory LIF neurons.
Each neuron in the network receives in_degree_prop * nexc excitatory connections
chosen randomly, in_degree_prop * ninh inhibitory connections and next (external) Poisson connections.
All the connections have the same delay. The strenght of excitatory and Poisson connections is given by
parameter weight, whereas the strength of inhibitory connections is rel_inh_strength * weight.
Poisson neurons all spike independently with expected number of spikes given by parameter poiss_lambda.
Because of the refractory period, the activity is mostly driven by Poisson neurons and
recurrent connections have a small effect.

Call with parameters, for example:
./brunel.py -n 400 -m 100 -e 20 -p 0.1 -w 1.2 -d 1 -g 0.5 -l 5 -t 100 -s 1 -G 50 -S 123 -f spikes.txt

'''

# Samples m unique values in interval [start, end) - gid.
# We exclude gid because we don't want self-loops.
def sample_subset(gid, start, end, m):
    gen = numpy.random.RandomState(gid+42)
    s = set()
    while len(s) < m:
        val = gen.randint(low=start,high=end)
        if val != gid:
            s.add(val)
    return s

class brunel_recipe (arbor.recipe):
    def __init__(self, nexc, ninh, next, in_degree_prop, weight, delay, rel_inh_strength, poiss_lambda, seed = 42):

        arbor.recipe.__init__(self)

        # Make sure that in_degree_prop in the interval (0, 1]
        if not 0.0<in_degree_prop<=1.0:
            print("The proportion of incoming connections should be in the interval (0, 1].")
            quit()

        self.ncells_exc_ = nexc
        self.ncells_inh_ = ninh
        self.delay_ = delay
        self.seed_ = seed

        # Set up the parameters.
        self.weight_exc_ = weight
        self.weight_inh_ = -rel_inh_strength * weight
        self.weight_ext_ =  weight
        self.in_degree_exc_ = round(in_degree_prop * nexc)
        self.in_degree_inh_ = round(in_degree_prop * ninh)
        # each cell receives next incoming Poisson sources with mean rate poiss_lambda, which is equivalent
        # to a single Poisson source with mean rate next*poiss_lambda
        self.lambda_ = next * poiss_lambda

    def num_cells(self):
        return self.ncells_exc_ + self.ncells_inh_

    def cell_kind(self, gid):
        return arbor.cell_kind.lif

    def connections_on(self, gid):
        connections=[]
        # Add incoming excitatory connections.
        for i in sample_subset(gid, 0, self.ncells_exc_, self.in_degree_exc_):
            connections.append(arbor.connection((i,0), (gid,0), self.weight_exc_, self.delay_))
        # Add incoming inhibitory connections.
        for i in sample_subset(gid, self.ncells_exc_, self.ncells_exc_ + self.ncells_inh_, self.in_degree_inh_):
            connections.append(arbor.connection((i,0), (gid,0), self.weight_inh_, self.delay_))
        return connections

    def cell_description(self, gid):
        cell = arbor.lif_cell()
        cell.tau_m = 10
        cell.V_th = 10
        cell.C_m = 20
        cell.E_L = 0
        cell.V_m = 0
        cell.V_reset = 0
        cell.t_ref = 2
        return cell

    def event_generators(self, gid):
        t0 = 0
        sched = arbor.poisson_schedule(t0, self.lambda_, gid + self.seed_)
        return [arbor.event_generator(arbor.cell_member(gid,0), self.weight_ext_, sched)]

    def num_targets(self, gid):
        return 1

    def num_sources(self, gid):
        return 1

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description='Brunel model miniapp.')
    parser.add_argument('-n', '--n-excitatory', dest='nexc', type=int, default=400, help='Number of cells in the excitatory population')
    parser.add_argument('-m', '--n-inhibitory', dest='ninh', type=int, default=100, help='Number of cells in the inhibitory population')
    parser.add_argument('-e', '--n-external', dest='next', type=int, default=40, help='Number of incoming Poisson (external) connections per cell')
    parser.add_argument('-p', '--in-degree-prop', dest='syn_per_cell_prop', type=float, default=0.05, help='Proportion of the connections received per cell')
    parser.add_argument('-w', '--weight', dest='weight', type=float, default=1.2, help='Weight of excitatory connections')
    parser.add_argument('-d', '--delay', dest='delay', type=float, default=0.1, help='Delay of all connections')
    parser.add_argument('-g', '--rel-inh-w', dest='rel_inh_strength', type=float, default=1, help='Relative strength of inhibitory synapses with respect to the excitatory ones')
    parser.add_argument('-l', '--lambda', dest='poiss_lambda', type=float, default=1, help='Mean firing rate from a single poisson cell (kHz)')
    parser.add_argument('-t', '--tfinal', dest='tfinal', type=float, default=100, help='Length of the simulation period (ms)')
    parser.add_argument('-s', '--dt', dest='dt', type=float, default=1, help='Simulation time step (ms)')
    parser.add_argument('-G', '--group-size', dest='group_size', type=int, default=10, help='Number of cells per cell group')
    parser.add_argument('-S', '--seed', dest='seed', type=int, default=42, help='Seed for poisson spike generators')
    parser.add_argument('-f', '--write-spikes', dest='spike_file_output', type=str, help='Save spikes to file')
    # parser.add_argument('-z', '--profile-rank-zero', dest='profile_only_zero', action='store_true', help='Only output profile information for rank 0')
    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Print more verbose information to stdout')

    opt = parser.parse_args()
    if opt.verbose:
        print("Running brunel.py with the following settings:")
        for k,v in vars(opt).items():
            print(f"{k} = {v}")

    context = arbor.context()
    print(context)

    meters = arbor.meter_manager()
    meters.start(context)

    recipe = brunel_recipe(opt.nexc, opt.ninh, opt.next, opt.syn_per_cell_prop, opt.weight, opt.delay, opt.rel_inh_strength, opt.poiss_lambda, opt.seed)

    meters.checkpoint('recipe-create', context)

    hint = arbor.partition_hint()
    hint.cpu_group_size = opt.group_size
    hints = {arbor.cell_kind.lif: hint}
    decomp = arbor.partition_load_balance(recipe, context, hints)
    print(decomp)

    meters.checkpoint('load-balance', context)

    sim = arbor.simulation(recipe, decomp, context)
    sim.record(arbor.spike_recording.all)

    meters.checkpoint('simulation-init', context)

    sim.run(opt.tfinal,opt.dt)

    meters.checkpoint('simulation-run', context)

    # Print profiling information
    print(f'{arbor.meter_report(meters, context)}')

    # Print spike times
    print(f"{len(sim.spikes())} spikes generated.")

    if opt.spike_file_output:
        with open(opt.spike_file_output, 'w') as the_file:
            for meta, data in sim.spikes():
                the_file.write(f"{meta[0]} {data:3.3f}\n")