Adding a control mechanism to a scenario¶
Now that we integrated our first simulator into mosaik and tested it in a simple scenario, we should implement a control mechanism and mess around with our example simulator a little bit.
As you remember, our example models had a value to which they added something in each step. Eventually, their value will end up beeing very high. We’ll use a multi-agent system to keep the values of our models in [-3, 3]. The agents will monitor the current value of their respective models and when it reaches -3/3, they will set delta to 1/-1 for their model.
Implementing the Sim API for control strategies is very similar to implementing
it for normal simulators. We start again by importing the mosaik_api
package and defining the simulator meta data:
# controller.py
"""
A simple demo controller.
"""
import mosaik_api
META = {
'models': {
'Agent': {
'public': True,
'params': [],
'attrs': ['val_in'],
},
},
}
Our control mechanism will use agents to control other entities. The agent has no parameters and only one attribute for incoming values.
Lets continue and implement mosaik_api.Simulator
:
class Controller(mosaik_api.Simulator):
def __init__(self):
super().__init__(META)
self.agents = []
Again, nothing special is going on here. We pass our meta data dictionary to our super class and set an empty list for our agents.
Since our agent doesn’t have any parameters, we don’t need to implement
init()
. The default implementation will return
the meta data, so there’s nothing we need to do in this case.
Implementing create()
is also straight forward:
def create(self, num, model):
n_agents = len(self.agents)
entities = []
for i in range(n_agents, n_agents + num):
eid = 'Agent_%d' % i
self.agents.append(eid)
entities.append({'eid': eid, 'type': model})
return entities
Every agent gets an ID like “Agent_*<num>*”. Because there might be multiple
create()
calls, we need to keep track of how many
agents we already created in order to generate correct entity IDs. We also
create a list of {‘eid’: ‘Agent_<num>’, ‘type’: ‘Agent’} dictionaries for
mosaik.
You may have noticed that we, in contrast to our example simulator, did not
actually instantiate any real simulation models this time. We just pretend to
do it. This okay, since we’ll implement the agent’s “intelligence” directly in
step()
:
def step(self, time, inputs):
commands = {}
for agent_eid, attrs in inputs.items():
values = attrs.get('val_in', {})
The commands
dict will contain the commands that our control mechanism is
going to send to the example simulator. We’ll fill this dict in the following
loop. In that loop, we iterate over all inputs and extract the input values for each
agent; so values
is a dict containing the current values of all models
connected to that agent, e.g., values == {‘Model_0’: 1}
We now check every input value:
for model_eid, value in values.items():
if value >= 3:
delta = -1
elif value <= -3:
delta = 1
else:
continue
If the value is ≤ -3 or ≥ 3, we have to set a new delta value. Else, we don’t need to do anything and can continue with a new iteration of the loop.
If we have a new delta, we add it to the commands
dict:
if agent_eid not in commands:
commands[agent_eid] = {}
if model_eid not in commands[agent_eid]:
commands[agent_eid][model_eid] = {}
commands[agent_eid][model_eid]['delta'] = delta
After finishing the loop, the commands
dict may look like this:
{
'Agent_0': {'Model_0': {'delta': 1}},
'Agent_2': {'Model_2': {'delta': -1}},
}
Agent_0 sets for Model_0 the new delta = 1. Agent_2 sets for Model_2 the new delta = -1. Agent_1 did not set a new delta.
So now that we created all commands – how do they get to the example simulator?
The way via get_data()
is not possible since it
would require a circular data-flow like connect(model, agent); connect(agent,
model)
which mosaik cannot resolve (You can read the details if you are curious why).
Instead, we use one of the asynchronous requests that you can perform within
step()
, namely
set_data()
. The
Simulator
makes these requests available via the
mosaik
attribute:
yield self.mosaik.set_data(commands)
# this works only for Python versions >=3.3.
The set_data()
actively sends the
commands to mosaik which will pass them to the example simulator in its next
step.
Note
yield??? If you are new to Python and don’t know what the yield
keyword does: Don’t worry! In this case, it will just block the execution of
step()
until all commands are sent to mosaik. After that, the method
will normally continue its execution.
When all commands were sent to mosaik, we are done with our step and return the time for our next one (which should be in one minute).
That’s it. Since there’s no data to be retrieved, we don’t need to implement
get_data()
. The default implementation will raise
an error for us if it should be called accidentally.
Here is the complete code for our (very simple) controller / mutli-agent system:
# controller.py
"""
A simple demo controller.
"""
import mosaik_api
META = {
'models': {
'Agent': {
'public': True,
'params': [],
'attrs': ['val_in'],
},
},
}
class Controller(mosaik_api.Simulator):
def __init__(self):
super().__init__(META)
self.agents = []
def create(self, num, model):
n_agents = len(self.agents)
entities = []
for i in range(n_agents, n_agents + num):
eid = 'Agent_%d' % i
self.agents.append(eid)
entities.append({'eid': eid, 'type': model})
return entities
def step(self, time, inputs):
commands = {}
for agent_eid, attrs in inputs.items():
values = attrs.get('val_in', {})
for model_eid, value in values.items():
if value >= 3:
delta = -1
elif value <= -3:
delta = 1
else:
continue
if agent_eid not in commands:
commands[agent_eid] = {}
if model_eid not in commands[agent_eid]:
commands[agent_eid][model_eid] = {}
commands[agent_eid][model_eid]['delta'] = delta
yield self.mosaik.set_data(commands)
# this works only for Python versions >=3.3.
# For older versions use: raise StopIteration(time + 60)
return time + 60
def main():
return mosaik_api.start_simulation(Controller())
if __name__ == '__main__':
main()
Next, we’ll create a new scenario to test our controller.