Imperative interface#

MiV-Simulator includes an imperative interface built on machinable that can be useful in more custom use cases that are not supported out-of-the-box.

The interface API works different from the declarative mode described in prior sections where the entire simulation was specified using YAML. Instead, the API gives you full control to construct the simulation imperatively using Python.

Generally, the process breaks down into (1) generating the cells.h5/connections.h5 that represent the neural systems, and (2) loading the cells and connection into the execution runtime to simulate the dynamics.

Constructing the neural H5#

To construct the neuro H5 files that describe the specific network instantiation, the miv_simulator.interface module provides configurable machinable components that can be chained as follows:

 1from machinable import get
 2from miv_simulator.utils import from_yaml
 3
 4synapses_config = from_yaml("simulation/config/synapses.yml")
 5
 6h5_types = get(
 7    "miv_simulator.interface.h5_types",
 8    [
 9        {
10            "cell_distributions": {
11                "STIM": {"SO": 0, "SP": 64, "SR": 0, "SLM": 0},
12                "PYR": {"SO": 0, "SP": 223, "SR": 0, "SLM": 0},
13                "PVBC": {"SO": 35, "SP": 50, "SR": 8, "SLM": 0},
14                "OLM": {"SO": 21, "SP": 0, "SR": 0, "SLM": 0},
15            },
16            "projections": {
17                post: list(pre.keys()) for post, pre in synapses_config.items()
18            },
19        },
20    ],
21).launch()
22
23network = get(
24    "miv_simulator.interface.network_architecture",
25    {
26        "filepath": h5_types.output_filepath,
27        "cell_distributions": h5_types.config.cell_distributions,
28        "layer_extents": {
29            "SO": [[0.0, 0.0, 0.0], [200.0, 200.0, 5.0]],
30            "SP": [[0.0, 0.0, 5.0], [200.0, 200.0, 50.0]],
31            "SR": [[0.0, 0.0, 50.0], [200.0, 200.0, 100.0]],
32            "SLM": [[0.0, 0.0, 100.0], [200.0, 200.0, 150.0]],
33        },
34    },
35    uses=h5_types,
36).launch()
37
38measure_distances = network.measure_distances().launch()
39
40synapse_forest = {
41    population: network.generate_synapse_forest(
42        {
43            "population": population,
44            "morphology": f"./simulation/morphology/{population}.swc",
45        },
46        uses=measure_distances,
47    ).launch()
48    for population in ["PYR", "PVBC", "OLM"]
49}
50
51synapses = {
52    population: network.distribute_synapses(
53        {
54            "forest_filepath": synapse_forest[population].output_filepath,
55            "cell_types": "from_file('simulation/config/cell_types.yml')",
56            "population": population,
57            "distribution": "poisson",
58            "mechanisms_path": "./simulation/mechanisms",
59            "template_path": "./simulation/templates",
60            "io_size": 1,
61            "write_size": 0,
62        },
63        uses=list(synapse_forest.values()),
64    ).launch()
65    for population in ["PYR", "PVBC", "OLM"]
66}
67
68connections = {
69    population: network.generate_connections(
70        {
71            "synapses": synapses_config,
72            "forest_filepath": synapses[population].output_filepath,
73            "axon_extents": {
74                "STIM": {"default": {"width": [200, 200], "offset": [0, 0]}},
75                "PYR": {"default": {"width": [200, 200], "offset": [0, 0]}},
76                "PVBC": {"default": {"width": [200, 200], "offset": [0, 0]}},
77                "OLM": {"default": {"width": [200, 200], "offset": [0, 0]}},
78            },
79            "template_path": "./simulation/templates",
80            "io_size": 1,
81            "cache_size": 20,
82            "write_size": 100,
83        },
84        uses=list(synapses.values()),
85    ).launch()
86    for population in ["PYR", "PVBC", "OLM"]
87}
88
89graph = get(
90    "miv_simulator.interface.neuroh5_graph",
91    uses=[
92        network,
93        *synapse_forest.values(),
94        *synapses.values(),
95        *connections.values(),
96    ],
97).launch()
98
99graph.files()

You are free to modify or entirely replace any step in the pipeline. Notably, machinable will automatically keep track of configuration changes to determine if generation steps need to be re-executed. This means that file geneneration is cached and managed behind the scences. You can access the resulting filepaths of the generated H5 files via graph.files().

Launching the simulation#

To simulate the network, the generated H5 files can be loaded into the execution environment, for example:

 1from miv_simulator import simulator
 2from machinable.utils import load_file
 3
 4        h = configure_hoc(mechanisms_directory=self.config.mechanisms)
 5        env = simulator.ExecutionEnvironment(seed=self.seed)
 6
 7        def log(m):
 8            if env.rank == 0:
 9                print(m)
10
11        env.load_cells(
12            filepath=self.config.cells,
13            cell_types=to_dict(self.config.cell_types),
14            templates=self.config.templates,
15        )
16
17        env.load_connections(
18            filepath=self.config.connections,
19            cell_filepath=self.config.cells,
20            synapses=self.config.synapses,

This will load and construct all cells and connections that will be part of the simulation. You are free to add additional elements to the system, such as an LFP to read out signals, for example:

 1from miv_simulator.lfp import LFP
 2from machinable.utils import load_file
 3
 4        lfp = LFP(
 5            "ReadoutElectrode",
 6            env.pc,
 7            pop_gid_dict={
 8                pop_name: set(env.cells[pop_name].keys()).difference(
 9                    set(env.artificial_cells[pop_name].keys())
10                )
11                for pop_name in env.cells.keys()
12            },
13            pos=[500.0, 500.0, 0.0],  # top-center
14            rho=333.0,
15            dt_lfp=0.1,
16            fdst=0.1,
17            maxEDist=100.0,  # radius
18            seed=self.seed + 1,

The execution environment provides access to all NEURON objects, allowing to set up and modify elements as needed. For example, you can use NEURON usual cell.play function to generate stimulation patterns:

1
2            for gid, cell in env.artificial_cells["STIM"].items():
3                if not (env.pc.gid_exists(gid)):
4                    continue
5                spike_train = np.array(stimulus)[int(gid) :: 10]
6                print(f"{env.rank}: Stimulating with spike train {spike_train}")

Finally, you can launch and control the simulation using the standard NEURON API:

 1
 2        h.v_init = -65
 3        h.stdinit()
 4        h.secondorder = 2  # crank-nicholson
 5        h.dt = 0.025
 6        env.pc.timeout(600.0)
 7
 8        h.finitialize(h.v_init)
 9
10        log("Finished initialization, starting the simulation")
11
12        env.pc.psolve(self.config.t_end)
13
14        log("Simulation finished, saving LFP")
15
16        if env.rank == 0:
17            out = np.column_stack([lfp.t, lfp.meanlfp])

The full code example using the above code for reservoir computing can be found here.