Markdown exporter

This is the developer documentation for mdexport package, that provides information about understanding baseexport’s standard dictionary, standard markdown expander MdExpander and writing custom expand functions.

Standard dictionary

The package baseexport collects all the required Brian model information and arranges them in an intuitive way, that can potentially be used for various exporters and use cases. Therefore, understanding the representation will be helpful for further manipulation.

The dictionary contains a list of run dictionaries with each containing information about the particular run simulation.

# list of run dictionaries
[
    . . . .
    {   # run dictionary
        duration: <Quantity>,
        components: {
                        . . .
                    },
        initializers_connectors: [. . .],
        inactive: [ . . .]
    },
    . . . .
]

Typically, a run dictionary has four fields,

  • duration: simulation length
  • components: dictionary of network components like NeuronGroup, Synapses, etc.
  • initializers_connectors: list of initializers and synaptic connections
  • inactive: list of components that were inactive for the particular run

All the Brian Network components that are under components field, have components like, NeuronGroup, Synapses, etc and would look like,

{
    'neurongroup': [. . . .],
    'poissongroup': [. . . .],
    'spikegeneratorgroup': [. . . .],
    'statemonitor': [. . . .],
    'synapses': [. . . .],
    . . . .
}

Each component field has a list of objects of that component defined in the run time. The dictionary representation of NeuronGroup and its similar types would look like,

neurongroup: [
    {
        'name': <name of the group>,
        'N': <population size>,
        'user_method': <integration method>,
        'equations': <model equations> {
            '<variable name>':{ 'unit': <unit>,
                                'type': <equation type>
                                'var_type': <variable dtype>
                                'expr': <expression>,
                                'flags': <list of flags>
            }
            . . .
        }
        'events': <events> {
            '<event_name>':{'threshold':{'code': <threshold code>,
                                         'when': <when slot>,
                                         'order': <order slot>,
                                         'dt': <clock dt>
                            },
                            'reset':{'code': <reset code>,
                                     'when': <when slot>,
                                     'order': <order slot>,
                                     'dt': <clock dt>
                            },
                            'refractory': <refractory period>
            }
            . . .
        }
        'run_regularly': <run_regularly statements>
        [
            {
                'name': <name of run_regularly>
                'code': <statement>
                'dt': <run_regularly clock dt>
                'when': <when slot of run_regularly>
                'order': <order slot of run_regularly>
            }
            . . .
        ]
        'when': <when slot of group>,
        'order': <order slot of group>,
        'identifiers': {'<name>': <value>,
        . . .
        }
    }
]

Similarly, StateMonitor and its similar types are represented like,

statemonitor: [
    {
        'name': <name of the group>,
        'source': <name of source>,
        'variables': <list of monitored variables>,
        'record': <list of monitored members>,
        'dt': <time step>
        'when': <when slot of group>,
        'order': <order slot of group>,
    }
. . .
]

As Synapses has many similarity with NeuronGroup, the dictionary of the same also looks similar to it, however some of the Synapses specific fields are,

synapses: [
    {
        'name': <name of the synapses object>,
        'equations': <model equations> {
            '<variable name>':{ 'unit': <unit>,
                                'type': <equation type>
                                'var_type': <variable dtype>
                                'expr': <expression>,
                                'flags': <list of flags>
            }
            . . .
        }

        'summed_variables': <summed variables>
        [
            {
                'target': <name of target group>,
                'code': <variable name>,
                'name': <name of the summed variable>,
                'dt': <time step>,
                'when': <when slot of run_regularly>,
                'order': <order slot of run_regularly>
            }
            . . .
        ]

        'pathways': <synaptic pathways>
        [
            {
                'prepost': <pre or post event>,
                'event': <event name>,
                'code': <variable name>,
                'source': <source group name>,
                'name': <name of the summed variable>,
                'clock': <time step>,
                'when': <when slot of run_regularly>,
                'order': <order slot of run_regularly>,
            }
            . . .
        ]
    }
]

Also, the identifiers takes into account of TimedArray and custom user functions. The initializers_connectors field contains a list of initializers and synaptic connections, and their structure would look like,

[
    {   <initializer>
        'source': <source group name>,
        'variable': <variable that is initialized>,
        'index': <indices that are affected>,
        'value': <value>, 'type': 'initializer'
    },
    . . .
    {   <connection>
        {'i': <i>, 'j': <j>,
        'probability': <probability of connection>,
        'n_connections': <number of connections>,
        'synapses': <name of the synapse>,
        'source': <source group name>,
        'target': <target group name>, 'type': 'connect'
    }
    . . .
]

As a working example, to get the standard dictionary of model description when using STDP example,

[{'components':
{'neurongroup': [{'N': 1,
                'equations': {'ge': {'expr': '-ge / taue',
                                    'type': 'differential equation',
                                    'unit': radian,
                                    'var_type': 'float'},
                                'v': {'expr': '(ge * (Ee-v) + El - v) / taum',
                                    'type': 'differential equation',
                                    'unit': volt,
                                    'var_type': 'float'}},
                'events': {'spike': {'reset': {'code': 'v = vr',
                                                'dt': 100. * usecond,
                                                'order': 0,
                                                'when': 'resets'},
                                    'threshold': {'code': 'v>vt',
                                                    'dt': 100. * usecond,
                                                    'order': 0,
                                                    'when': 'thresholds'}}},
                'identifiers': {'Ee': 0. * volt,
                                'El': -74. * mvolt,
                                'taue': 5. * msecond,
                                'taum': 10. * msecond,
                                'vr': -60. * mvolt,
                                'vt': -54. * mvolt},
                'name': 'neurongroup',
                'order': 0,
                'user_method': 'euler',
                'when': 'groups'}],
'poissongroup': [{'N': 1000,
                'name': 'poissongroup',
                'rates': 15. * hertz}],
'spikemonitor': [{'dt': 100. * usecond,
                'event': 'spike',
                'name': 'spikemonitor',
                'order': 1,
                'record': True,
                'source': 'poissongroup',
                'variables': ['i', 't'],
                'when': 'thresholds'}],
'statemonitor': [{'dt': 100. * usecond,
                'n_indices': 2,
                'name': 'statemonitor',
                'order': 0,
                'record': array([0, 1], dtype=int32),
                'source': 'synapses',
                'variables': ['w'],
                'when': 'start'}],
'synapses': [{'equations': {'Apost': {'expr': '-Apost / taupost',
                                    'flags': ['event-driven'],
                                    'type': 'differential equation',
                                    'unit': radian,
                                    'var_type': 'float'},
                            'Apre': {'expr': '-Apre / taupre',
                                    'flags': ['event-driven'],
                                    'type': 'differential equation',
                                    'unit': radian,
                                    'var_type': 'float'},
                            'w': {'type': 'parameter',
                                'unit': radian,
                                'var_type': 'float'}},
            'identifiers': {'dApost': -0.000105,
                            'dApre': 0.0001,
                            'gmax': 0.01,
                            'taupost': 20. * msecond,
                            'taupre': 20. * msecond},
            'name': 'synapses',
            'pathways': [{'clock': 100. * usecond,
                            'code': 'ge += w\n'
                                    'Apre += dApre\n'
                                    'w = clip(w + Apost, 0, gmax)',
                            'event': 'spike',
                            'name': 'synapses_pre',
                            'order': -1,
                            'prepost': 'pre',
                            'source': 'poissongroup',
                            'target': 'neurongroup',
                            'when': 'synapses'},
                            {'clock': 100. * usecond,
                            'code': 'Apost += dApost\n'
                                    'w = clip(w + Apre, 0, gmax)',
                            'event': 'spike',
                            'name': 'synapses_post',
                            'order': 1,
                            'prepost': 'post',
                            'source': 'neurongroup',
                            'target': 'poissongroup',
                            'when': 'synapses'}],
            'source': 'poissongroup',
            'target': 'neurongroup'}]},
'duration': 100. * second,
'initializers_connectors': [{'index': True,
                            'source': 'poissongroup',
                            'type': 'initializer',
                            'value': 15. * hertz,
                            'variable': 'rates'},
                            {'n_connections': 1,
                            'probability': 1,
                            'source': 'poissongroup',
                            'synapses': 'synapses',
                            'target': 'neurongroup',
                            'type': 'connect'},
                            {'identifiers': {'gmax': 0.01},
                            'index': 'True',
                            'source': 'synapses',
                            'type': 'initializer',
                            'value': 'rand() * gmax',
                            'variable': 'w'}]}]

MdExpander

To use the dictionary representation for creating markdown strings, by default MdExpander class is used. The class contains expand functions for different Brian components, such that the user can easily override the particular function without affecting others. Also, different options can be given during the instantiation of the object and pass to the set_device or device.build().

As a simple example, to use GitHub based markdown rendering for mathematical statements, and use Brian specific jargons,

from brian2tools import MdExpander  # import the standard expander
# custom expander
custom = MdExpander(github_md=True, brian_verbose=True)
set_device('markdown', expander=custom)  # pass the custom expander

Details about the monitors are not included by default in the output markdown and to include them,

# custom expander to include monitors
custom_with_monitors = MdExpander(include_monitors=True)
set_device('markdown', expander=custom_with_monitors)

Also, the order of variable initializations and connect statements are not shown in the markdown output by default, this may likely result to inaccurate results, when the values of variables during synaptic connections are contingent upon their order. In that case, the order shall be included to markdown output as,

# custom expander to include monitors
custom = MdExpander(keep_initializer_order=True)
set_device('markdown', expander=custom)

The modified output with details about the order of initialization and Synaptic connection, when running on the Working example would look like,

Network details

Neuron population :

  • Group neurongroup, consisting of 1 neurons.

    Model dynamics:

    =

    =

    The equations are integrated with the 'euler' method.

    Events:

    If , a spike event is triggered and .

    Constants: = , = , = , = , = , and =

Poisson spike source :

  • Name poissongroup, with population size 1000 and rate as .

Synapse :

  • Connections synapses, connecting poissongroup to neurongroup.

    Model dynamics:

    Parameter (dimensionless)

    =

    =

    For each pre-synaptic spike: Increase by , Increase by ,

    For each post-synaptic spike: Increase by ,

    Constants: = , = , = , = , and =

Initializing at start and Synaptic connection :

  • Variable of poissongroup initialized with

  • Connection from poissongroup to neurongroup. Pairwise connections.

  • Variable of synapses initialized with , where = .

The simulation was run for 100. s

Similarly, author and add_meta options can also be customized during object instantiation, to add author name and meta data respectively in the header of the markdown output.

Typically, expand function of the component would follow the structure similar to,

def expand_object(self, object_dict):
    # use object_dict information to write md_string
    md_string = . . . object_dict['field_A']
    return md_string

However, enumerating components like identifiers, pathways have two functions in which the first one simply loops the list and the second one expands the member. For example, with identifiers,

def expand_identifiers(self, identifiers_list):
    # calls `expand_identifier` iteratively
    markdown_str = ''
    for identifier in identifiers_list:
        . . .
        markdown_str += self.expand_identifier(identifier)
    return markdown_str

def expand_identifier(self, identifier):
    # individual identifier expander
    markdown_str = ''
    . . . # use identifier dict to write markdown strings
    return markdown_str

All the individual expand functions are tied to create_md_string function that calls and collects all the returned markdown strings to pass it to device.md_text

Writing custom expand class

With the understanding of standard dictionary representation and default markdown expand class (MdExpander), writing custom expand class becomes very straightforward. As a working example, the custom expander class to write equations in a table like format,

from brian2tools import MdExpander
from markdown_strings import table  # import table from markdown_strings

# custom expander class to do custom modifications for model equations
class Dynamics_table(MdExpander):

    def expand_equation(self, var, equation):
        # if differential equation pass `differential` flag as `True` to
        # render_expression()
        if equation['type'] == 'differential equation':
            return (self.render_expression(var, differential=True) +
                        '=' + self.render_expression(equation['expr']))
        else:
            return (self.render_expression(var) +
                        '=' + self.render_expression(equation['expr']))

    def expand_equations(self, equations):
        diff_rend_eqn = ['Differential equations']
        sub_rend_eqn = ['Sub-Expressions']
        # loop over
        for (var, eqn) in equations.items():
            if eqn['type'] == 'differential equation':
                diff_rend_eqn.append(self.expand_equation(var, eqn))
            if eqn['type'] == 'subexpression':
                sub_rend_eqn.append(self.expand_equation(var, eqn))

        # now pad space for shorter one
        if len(diff_rend_eqn) > len(sub_rend_eqn):
            shorter = diff_rend_eqn
            longer = sub_rend_eqn
        else:
            shorter = sub_rend_eqn
            longer = diff_rend_eqn
        for _ in range(len(longer) - len(shorter)):
            shorter.append('')

        # return table of rendered equations
        return table([shorter, longer])

custom = Dynamics_table()
set_device('markdown', expander=custom)  # pass the custom expander object

when using the above custom class with COBAHH example, the equation part would look like,

Dynamics:

Sub-Expressions Differential equations
= =
= =
= =
= =
= =
= =