Understanding Commands
Overview
Commands are one of the central pillars of
ngcsimlib, the dependency
library that drives ngc-learn’s simulation backend.
In general, commands provide the instructions and logic for what each component
should be doing at any given time. In addition, they are the normal way that an
outside user would interact with ngc-learn models. Commands live inside a model’s
controller and are generally made with the add_command
method.
Abstract Command
Contained within ngcsimlib is an abstract class for every command included in ngcsimlib. It is strongly recommended that custom commands are built using this base class (but there is nothing enforcing this inside of ngcsimlib).
At its base the abstract command forces two things: firstly, the constructor
for the base class requires a list of components, and a list of attributes that
each component should have. Secondly, all commands must implement their
__call__
command, taking in only *args
and **kwargs
.
Constructing Commands
It is common that commands will need to have values passed into them to control their internal behavior, such as a value to clamp, or a flag for freezing synaptic weight values. To do this, we introduce the notion of binding keywords to commands. Specifically, commands will take strings in during their construction and then look for those strings when called inside the list of keyword arguments in order to get their arguments.
Calling Commands
When commands are called, they will take in only *args
and **kwargs
.
While custom commands can break this by adding in additional arguments
without any problem, it is not recommended to do this as multiple instances
of a command with different parameters will then use the same keyword for their
call.
Creating Custom Commands
It is recommended that all custom commands inherit from the base class
provided within ngcsimlib. This provides a good starting point for designing a
component that will seamlessly interact with ngcsimlib’s internal simulation mechanics.
These mechanics, which characterize the core operation of a simulation controller,
entail that, for each command supplied to a controller, a command will call the
same function with the same parameters on each component provided
to that very command. It is also expected that there is error handling within the
constructor to catch as many runtime errors as possible. Note that base
command class provides a list to check required calls such as reset
or evolve
.
It is important to note that, if commands are going to be constructed via a controller, they should have keyword arguments with default values that error out on bad input instead of positional arguments.
Example Command (reset)
Below, we present the key bits of source code that characterize a reset command – a very commonly used, built-in command for models designed in ngc-learn – and its internal operation:
from ngcsimlib.commands import Command
from ngcsimlib.utils import extract_args
from ngcsimlib.logger import warn, error
class Reset(Command):
def __init__(self, components=None, reset_name=None, command_name=None,
**kwargs):
super().__init__(components=components, command_name=command_name,
required_calls=['reset'])
if reset_name is None:
error(self.name, "requires a \'reset_name\' to bind to for construction")
self.reset_name = reset_name
def __call__(self, *args, **kwargs):
try:
vals = extract_args([self.reset_name], *args, **kwargs)
except RuntimeError:
warn(self.name, ",", self.reset_name,
"is missing from keyword arguments and no positional arguments were provided")
return
if vals[self.reset_name]:
for component in self.components:
self.components[component].reset()
Custom Command Template
Here, we show the generic command template which shows how one would go about designing the key operational bits that make up a useful command.
from ngcsimlib.commands.command import Command
from ngcsimlib.utils import extract_args
from ngcsimlib.logger import error
class CustomCommand(Command):
def __init__(self, components=None, BINDING_VALUE=None, ADDITIONAL_INPUT=None, command_name=None,
**kwargs):
super().__init__(components=components, command_name=None, required_calls=['CUSTOM_CALL'])
# Make sure additional input is passed in
if ADDITIONAL_INPUT is None:
error(self.name, "requires a \'ADDITIONAL_INPUT\' for construction")
# Make sure command is bound to a value
if BINDING_VALUE is None:
error(self.name, "requires a \'BINDING_VALUE\' to bind to for construction")
self.BOUND_VALUE = BINDING_VALUE
self.ADDITION_VALUE = ADDITIONAL_INPUT
def __call__(self, *args, **kwargs):
# Extract the bound value from the arguments
try:
vals = extract_args([self.BOUND_VALUE], *args, **kwargs)
except RuntimeError:
error(self.name, ",", str(self.BOUND_VALUE), "is missing from keyword arguments or a positional "
"arguments can be provided")
#Use extracted value to call a method on each component
for component in self.components:
self.components[component].CUSTOM_CALL(self.ADDITION_VALUE, vals[self.BOUND_VALUE])
Notes
All components added to commands must have a name
attribute and the word
name
is automatically appended to any provided list of required attributes
to the base class constructor.
As all built-in commands use extract_args
when called with a controller via
myController.COMMAND(ARGUMENT)
, there is no need to use keywords as it will
use args
if there are no keyword arguments. (Keywords will still work, however.)
When commands are constructed via a controller, they are also provided with the
keyword arguments controller
and command_name
. It is not recommended to
use these for any core logic (just use them for error messages), unless
it using them is absolutely essential in achieving the desired functionality.