Peer configuration¶
Peer configuration is stored in one or more files, in INI-like format. The
most important (and mandatory) config file is the one stored in the same
directory as the peer source code file. Its name is the same as the peer it
configures: for a peer implemented in file peer_a.py
you define
peer_a.ini
. This file is called basic config. Treat it as a part of
peer implementation. Basic config is always parsed and is the reference for
validation of any configuration overrides.
Configuration file contains four sections:
[config_sources]
[local_params]
[external_params]
[launch_dependencies]
Parameters defined in these sections can be overwritten in scenario file and/or in additional configuration file loaded at the scenario start.
[local_params]
is the most straightforward section. Here we define config
parameters which are owned by the peer – its properties. For an amplifier
such properties would be sampling rate or number of channels.:
[local_params]
sampling_rate = 128
number_of_channels = 8
super_important_param = 0
A peer may be bound to parameters from other peers. For example a
signal filter would need sampling rate of an amplifier from which it takes the
signal. Each peer is identified by peer_id
, which must be unique in the
scope of a running experiment. Configuration file though has to be usable in
many experiments, so we cannot hardcode exact peer ID’s there. Instead we
define symbolic names for peers in [config_sources]
section, from which
configured peer should take parameters.:
[config_sources]
signal_properties_source=
; or maybe just
amplifier=
Here we just define internal (for this peer) names of configuration sources, hence empty spaces after equals sign. In run-time real ``peer_id``s are assigned to these config sources using information stored in scenario file.
In a similar fashion we define [launch_dependencies]
. Launch dependencies
are peers we need to synchronize with: a peer will not start its actual work
until all the peers it depends on report that they are ready.:
[launch_dependencies]
amplifier=
Both config and launch dependencies’ names are visible in the scope of the
configuration file. So ‘amplifier’ from the example above is the same peer as
the one in [config_sources]
.
Now the [external_params]
section. Let’s say a peer needs a parameter
‘sampling_rate’ from some other peer which we symbolically named ‘amplifier’.
It will store the parameter as ‘amp_sampling_rate’. We can represent this as
follows:
[external_params]
amp_sampling_rate = amplifier.sampling_rate
All parameter names we define in a config file should be unique. You can move
parameters between [external_params]
and [local_params]
in config files
loaded after the basic config. This may be useful during module development
when we want to quickly test how the module works without loading other peers.
Also the other way may be useful if in some experiment we want to change the
way parameters are passed.
Config overriding¶
As mentioned above, you can pass custom configuration files to the peer. The
one restriction is that you cannot define any new parameter names that are not defined in
basic config. You can move a parameter form local to external section, add a
launch or config dependency. If a config dependency is not referenced in
[external_params]
configuration, peer will not require passing dependency’s
real peer_id
on launch.
Command line - peer invocation¶
Below is the somewhat messy usage help generated by the config processing module using argparse:
usage: some_peer.py peer_id [options]
positional arguments:
peer_id Unique name for this instance of this peer
optional arguments:
-h, --help show this help message and exit
-p LOCAL_PARAMS LOCAL_PARAMS, --local_params LOCAL_PARAMS LOCAL_PARAMS
Local parameter override value: param_name, value.
-e EXTERNAL_PARAMS EXTERNAL_PARAMS, --external_params EXTERNAL_PARAMS EXTERNAL_PARAMS
External parameter override value: param_name value_def .
-c CONFIG_SOURCES CONFIG_SOURCES, --config_sources CONFIG_SOURCES CONFIG_SOURCES
Config source ID assignment: src_name peer_id
-d LAUNCH_DEPENDENCIES LAUNCH_DEPENDENCIES, --launch_dependencies LAUNCH_DEPENDENCIES LAUNCH_DEPENDENCIES
Launch dependency ID assignment: dep_name peer_id
-f CONFIG_FILE, --config_file CONFIG_FILE
Additional configuration file (overrides):
path_to_file.
When starting a peer with config support you need to provide a peer_id for it and peer_ids of its launch/config dependecies. Custom config files – option -f. You can also override some parameters: using option -p / –local-params param_name value or -e / –external-params param_name value_definitions.
Assume we want to run two peers: peer_a.py
with default configuration
(defined in peer_a.ini in source directory)
[config_sources]
amp1_signal=
peerb=
[launch_dependencies]
peerb=
[external_params]
ext_txt = peerb.text
[local_params]
my_param = 1234
p = some text here
and peer_b.py
with configuration peer_b.ini
[config_sources]
some_peer=
[external_params]
ext_p = some_peer.p
[launch_dependencies]
[local_params]
text = text text tralala
peer_a
takes parameter ‘text’ from a peer ‘peerb’, and peer_b
takes
parameter ‘p’ from a peer ‘some_peer’. Peer_a also waits for ‘peerb’ readiness.
We want them to take those parameters from each other.
Invocation of those peers with just assigning them peer_id’s would look like this:
python peer_a.py i_am_roger -c peerb sue
python peer_b.py sue -c some_peer i_am_roger
We do not need to provide launch dependency ID for peer_a because this time it’s the same peer as in config dependencies - assignment will be automatic.
Programming BCI Framework peers with configuration support¶
To enable configuration processing in a peer, your peer must inherit
ConfiguredPeer
class:
from obci.core.configured_peer import ConfiguredPeer
class MyPeer(ConfiguredPeer)
Then you can use variables defined in config to initialize your peer in
Peer._connections_established
coroutine:
class MyPeer(ConfiguredPeer):
async def _connections_established(self):
await super()._connections_established()
# use variables from config: ::
self.needed_to_work = self.config.get_param('important_setting') # here we read parameter from configuration
# Letting everyone know that MyPeer is configured and ready to work.
# Other peers which have MyPeer in launch_dependencies will wait on their :meth:`await self.ready()`
# until MyPeer invokes next line:
await self.ready()
alternatively you can use param_property as class variable:
class MyPeer(ConfiguredPeer):
wait_time = param_property("wait_time", float) # declaration that we want to read config parameter "wait_time" as float
async def _connections_established(self):
await super()._connections_established()
# use variables from config: ::
if self.wait_time is not None: # if the wait_time parameter was left empty in the config it will change into None
time.sleep(self.wait_time)
# Letting everyone know that MyPeer is configured and ready to work.
# Other peers which have MyPeer in launch_dependencies will wait on their :meth:`await self.ready()`
# until MyPeer invokes next line:
await self.ready()