.. _mechanism_abi: Mechanism ABI ============= Code describing membrane mechanisms can be implemented in 3 ways: * custom C++/CUDA compiled into your Arbor program; * NMODL, compiled through Arbor's ``modcc`` compiler; * custom code exposed through the Arbor Mechanism Application Binary Interface (ABI). Here you will find the information needed to connect Arbor to mechanism implementations through the mechanism ABI. This ABI makes use of the C type calling convection, so any language that can expose itself through a `C FFI `_ can be used to implement mechanisms. The advantage of this method is that you can write and compile mechanisms in a multitude of languages, and store and distribute those mechanism libraries in compiled and optimized form. Any Arbor application can use them without compilation, of course assuming that both the mechanism and Arbor are compiled for the same target platform. All functionality is offered via a single C header file in the Arbor include directory, ``mechanism_abi.h``. The central datatypes here are :c:struct:`arb_mechanism_type` and :c:struct:`arb_mechanism_interface`, laying out the metadata and backend implementations respectively. A single :c:struct:`arb_mechanism_type` instance may be used by multiple :c:struct:`arb_mechanism_interface` instances. .. Note:: Note that ``mechanism_abi.h`` is heavily commented and is useful as documentation in its own right, and also when writing mechanisms in languages other than C(++). Metadata: ``arb_mechanism_type`` -------------------------------- This type collects all information independent of the backend. .. c:struct:: arb_mechanism_type This type collects all information independent of the backend. Metadata: .. c:member:: unsigned long abi_version mechanism was built using this ABI, should be ARB_MECH_ABI_VERSION .. c:member:: arb_mechanism_fingerprint fingerprint unique ID, currently ignored .. c:member:: const char* name (catalogue-level) unique name .. c:member:: arb_mechanism_kind kind one of: * point * density * reversal_potential * junction .. c:member:: bool is_linear synapses only: if the state G is governed by dG/dt = f(v, G, M(t)), where: M(t) =Σ wᵢδᵢ(t) weighted incoming events, then f is linear in G and M. If true, mechanisms must adhere to this contract. Ignored for everything else. .. c:member:: bool has_post_events implements post_event hook Tables: .. c:member:: arb_field_info* globals .. c:member:: arb_size_type n_globals .. c:member:: arb_field_info* state_vars .. c:member:: arb_size_type n_state_vars .. c:member:: arb_field_info* parameters .. c:member:: arb_size_type n_parameters .. c:member:: arb_ion_info* ions .. c:member:: arb_size_type n_ions Tables '''''' All tables are given as an integer size and an array. There are two kinds of tables: :c:struct:`arb_field_info` and :c:struct:`arb_ion_info`. :c:struct:`arb_field_info` holds parameters, state variables, and global constants; :c:struct:`arb_ion_info` holds ion dependencies. .. c:struct:: arb_field_info .. c:member:: const char* name Field name, can be used from Arbor to query/set field values. .. c:member:: const char* unit Physical units, just for introspection, not checked .. c:member:: arb_value_type default_value values will be initialised to this value .. c:member:: arb_value_type range_low valid range, lower bound, will be enforced .. c:member:: arb_value_type range_high valid range, upper bound, will be enforced .. c:struct:: arb_ion_info .. c:member:: const char* name Ion name, eg Ca, K, ... .. c:member:: bool write_int_concentration writes Xi? .. c:member:: bool write_ext_concentration writes Xo? .. c:member:: bool write_rev_potential writes Er? .. c:member:: bool read_rev_potential uses Er? .. c:member:: bool read_valence Uses valence? .. c:member:: bool verify_valence Checks valence? .. c:member:: int expected_valence Expected value Interlude: Parameter packs -------------------------- In order to explain the interface type, we have to introduce the type :c:struct:`arb_mechanism_ppack`. This record is used to pass all information to and from the interface methods. Objects of this type are always created and allocated by Arbor and passed fully formed to the interface. At this point: - Global data values are initialised - pointers in ``ion_state_view`` are set to their associated values in shared state on the Arbor side - pointers to state, parameters, globals, and constants are allocated and initialised to the given defaults. - SIMD only: :c:member:`arb_mechanism_ppack.index_constraints` is set up. .. c:struct:: arb_mechanism_ppack Global data: .. c:member:: arb_index_type width Number of CVs of this mechanism, size of arrays .. c:member:: arb_index_type n_detectors Number of threshold detectors .. c:member:: arb_index_type* vec_ci [Array] Map CV to cell .. c:member:: arb_index_type* vec_di [Array] Map .. c:member:: arb_value_type* vec_dt [Array] time step .. c:member:: arb_value_type* vec_v [Array] potential .. c:member:: arb_value_type* vec_i [Array] current .. c:member:: arb_value_type* vec_g [Array] conductance .. c:member:: arb_value_type* temperature_degC [Array] Temperature in celsius .. c:member:: arb_value_type* diam_um [Array] CV diameter .. c:member:: arb_value_type* area_um2 [Array] CV lateral surface area in :math:`\mu m^2` .. c:member:: arb_value_type* time_since_spike Times since last spike; one entry per cell and detector. .. c:member:: arb_index_type* node_index Indices of CVs covered by this mechanism, size is width .. c:member:: arb_index_type* peer_index Indices of peer CV of each CV in ``node_index``, needed for gap-junction connections, size is width. .. c:member:: arb_index_type* multiplicity [Unused] .. c:member:: arb_value_type* weight [Array] Weight .. c:member:: arb_size_type mechanism_id Unique ID for this mechanism on this cell group .. c:member:: arb_deliverable_event_stream events Events during the last period .. c:member:: arb_constraint_partition index_constraints Index restrictions, not initialised for all backends. User data: .. c:member:: arb_value_type** parameters [Array] setable parameters .. c:member:: arb_value_type** state_vars [Array] integrable state .. c:member:: arb_value_type* globals global constant state .. c:member:: arb_ion_state* ion_states [Array] views into shared state Members tagged as ``[Array]`` represent one value per CV. To access the values belonging to your mechanism, a level of indirection via :c:member:`arb_mechanism_ppack.node_index` is needed. .. admonition:: Example Let's assume mechanism ``hh`` is defined on two regions: ``R`` comprising CVs ``0`` and ``1``, ``R'`` with a single CV ``9``. Then ``node_index = [0, 1, 9]`` and ``width = 3``. Arrays like ``vec_v`` will be of size ``3`` as well. To access the CVs' diameters, one would write: .. code:: c++ for (auto cv = 0; cv < ppack.width; ++cv) { auto idx = node_index[cv]; auto d = ppack_um[idx]; } .. warning:: Note that values in :c:member:`arb_mechanism_ppack.diam_um` cover _all_ CV's regardless of whether they are covered by the current mechanisms. Reading or writing to those values is considered undefined behaviour. The same holds for all other fields of :c:struct:`arb_mechanism_ppack`. User Data ''''''''' This section is derived from the tables passed in via the metadata struct, see above. One entry per relevant table entry is provided in the same order. So, if .. code:: c arb_field_info globals[] = { arb_field_info { .name="A", .unit="lb ft / s", .default_value=42.0, .range_low=0, .range_high=123 }, arb_field_info { .name="B", .unit="kg m / s", .default_value=42.0, .range_low=0, .range_high=123 }}; arb_mechanism_type m = { .n_globals=2, .globals=globals }; the ``globals`` field of the corresponding parameter pack would have two entries, the first corresponding to ``A`` and initialised to 42.0 and the second for ``B`` set to 42.0. The evolution of the state variables is left to the implementation via ``integrate_state``, while ``globals`` and ``parameters`` are considered read-only. The ion states internal concentration ``Xi``, external concentration ``Xo``, trans-membrane current ``iX`` may also be read and written. Note that concurrent updates by multiple mechanisms might occur in any order, and each mechanism will only observe the initial values at the time step boundary. All contributions by mechanisms are summed up into a final value. Further note that accessing these values without declaring this via a relevant ``arb_ion_info`` in the ``arb_mechanism_type`` is undefined behaviour. Parameter packs are specific to a backend. Implementation: ``arb_mechanism_interface`` ------------------------------------------- The interface methods will be called with allocated and initialised :c:struct:`arb_mechanism_ppack` data. The actual layout is unspecified, but all pointers are allocated and set by Arbor. This means that your code must read/write to these locations in memory, and that you cannot change the pointer to point to another slice of memory allocated by your code. .. c:type:: void (*arb_mechanism_method)(arb_mechanism_ppack*); This is the type of all interface methods. These are collected in the record below with some metadata about the backend. .. c:struct:: arb_mechanism_interface .. c:member:: arb_backend_kind backend one of - cpu - gpu .. c:member:: arb_size_type partition_width granularity for this backed, e.g., SIMD lanes Interface methods: .. c:member:: arb_mechanism_method init_mechanism - called once during instantiation, - setup initial state, corresponds to NMODL's INITIAL block, - will receive an allocated and initialised pack object .. c:member:: arb_mechanism_method compute_currents - compute ionic currents and set them through pointers in `ion_state`, currents live in `current_density` - called during each integration time step, and - reversal potential mechanisms: at the beginning of the time step, *before* current reset - after event delivery, if enabled, e.g. by defining ``NET_RECEIVE`` in NMODL .. c:member:: arb_mechanism_method apply_events This method is expected to consume a set of :c:struct:`arb_deliverable_event` and apply effects to internal state, found in :c:member:`arb_mechanism_ppack.events` which is of type :c:struct:`arb_deliverable_event_stream`. These structures are set up correctly externally, but are only valid during this call. The data is read-only for :c:member:`arb_mechanism_interface.apply_events`. - called during each integration time step, right after resetting currents - corresponding to ``NET_RECEIVE`` .. c:member:: arb_mechanism_method advance_state - called during each integration time step, after solving Hines matrices - perform integration on state variables - state variables live in `state_vars`, with a layout described above .. c:member:: arb_mechanism_method write_ions - update ionic concentrations via the pointers in `ion_state` - called during each integration time step, after state integration .. c:member:: arb_mechanism_method post_event - used to implement spike-timing-dependent plasticity - consumes :c:member:`arb_mechanism_ppack.time_since_spike` - called during each integration time step, after checking for spikes - if implementing this, also set :c:member:`arb_mechanism_type.has_post_events` to ``true`` in the metadata .. c:struct:: arb_deliverable_event .. c:member:: arb_size_type mech_id mechanism type identifier (per cell group). .. c:member:: arb_size_type mech_index instance of the mechanism .. c:member:: arb_float_type weight connection weight .. c:struct:: arb_deliverable_event_stream .. c:member:: arb_size_type n_streams number of streams .. c:member:: const arb_deliverable_event* events array of event data items .. c:member:: const arb_index_type* begin array of offsets to the beginning of marked events .. c:member:: const arb_index_type* end array of offsets to the end of marked events SIMDization ----------- If a mechanism interface processes arrays in SIMD bundles, it needs to set :c:member:`arb_mechanism_interface.partition_width` to that bundle's width in units of ``arb_value_type``. The library will set up :c:member:`arb_mechanism_ppack.index_constraints` in the parameter pack. This structure describes which bundles can be loaded/stored as a contiguous block, which ones must be gathered/scattered, which are to be broadcast from a constant, and so on. The reason for this is the indirection via :c:member:`arb_mechanism_ppack.node_index` mentioned before. Please refer to the documentation of our :ref:`SIMD interface layer ` for more information. Making A Loadable Mechanism --------------------------- Mechanisms interface with Arbor by providing a single function, returning a structure .. c:struct:: arb_mechanism .. c:member:: arb_get_mechanism_type type Pointer to a function ``arb_mechanism_type get_type()`` .. c:member:: arb_get_mechanism_interface i_cpu Pointer to a function ``arb_mechanism_interface get_interface()`` that returns a pointer to the CPU interface which may be ``null``. .. c:member:: arb_get_mechanism_interface i_gpu Pointer to a function ``arb_mechanism_interace get_interface()`` that returns a pointer to the GPU interface which may be ``null``. You can create mechanisms with both ``i_gpu`` and ``i_cpu`` returning ``null``, but at least one of the interfaces must be provided or Arbor will refuse to load the catalogue containing this mechanism. The naming scheme is shown in the example below .. code:: C arb_mechanism make_arb_default_catalogue_pas(); Writing Mechanisms Directly Against the ABI ------------------------------------------- .. _abi_raw: .. warning:: This is a measure of last resort. Usage is not recommended unless you have a dire need and a solid understanding of Arbor, its internals, and the ABI. If your use case requires features not supported in NMODL, you can write mechanisms in C++ directly. See the ABI documentation for what callbacks need to be filled in. These mechanisms can be compiled with ``arbor-build-catalogue`` as well and must be present in the same folder as the NMODL files. Example .. code-block:: $> ls mod A.mod B.hpp B_cpu.cpp B_gpu.cpp B_gpu.cu C.hpp C_cpu.cpp C_gpu.cpp C_gpu.cu $> arbor-build-catalogue my mod --raw B C Building catalogue 'my' from mechanisms in 'mod' * NMODL * A * Raw * B * C The ``--raw`` flag must go last due to the argument parsing. For this to compile, the following must be upheld: - For each mechanism ``M`` these files must be present - ``M.hpp`` must define the mechanism metadata and declare the used interfaces. - ``M_cpu.cpp`` must define the CPU interface. (You can disable this for raw mechanisms by passing ``-C``.) - If GPU support is used - ``M_gpu.cpp`` must define the GPU interface. - ``M_gpu.cu`` must define CUDA kernels. - The interface names must adhere to the chosen catalogue name, eg here ``make_arb_my_catalogue_B_interface_multicore();`` - names may only contain alphanumeric characters and underscores. - names must not contain multiple successive underscores. - in general, a valid ``C++`` variable name should be used.