Example 1: NetLogo interaction through the pyNetLogo connector

This notebook provides a simple example of interaction between a NetLogo model and the Python environment, using the Wolf Sheep Predation model included in the NetLogo example library (Wilensky, 1999). This model is slightly modified to add additional agent properties and illustrate the exchange of different data types. All files used in the example are available from the pyNetLogo repository at https://github.com/quaquel/pyNetLogo.

We start by instantiating a link to NetLogo, loading the model, and executing the setup command in NetLogo.

[1]:
%matplotlib inline

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('white')
sns.set_context('talk')

import pyNetLogo

netlogo = pyNetLogo.NetLogoLink(gui=True)

netlogo.load_model('./models/Wolf Sheep Predation_v6.nlogo')
netlogo.command('setup')

We can use the write_NetLogo_attriblist method to pass properties to agents from a Pandas dataframe – for instance, initial values for given attributes. This improves performance by simultaneously setting multiple properties for multiple agents in a single function call.

As an example, we first load data from an Excel file into a dataframe. Each row corresponds to an agent, with columns for each attribute (including the who NetLogo identifier, which is required). In this case, we set coordinates for the agents using the xcor and ycor attributes.

[2]:
agent_xy = pd.read_excel('./data/xy_DataFrame.xlsx')
agent_xy[['who','xcor','ycor']].head(5)
[2]:
who xcor ycor
0 0 -24.000000 -24.000000
1 1 -23.666667 -23.666667
2 2 -23.333333 -23.333333
3 3 -23.000000 -23.000000
4 4 -22.666667 -22.666667

We can then pass the dataframe to NetLogo, specifying which attributes and which agent type we want to update:

[3]:
netlogo.write_NetLogo_attriblist(agent_xy[['who','xcor','ycor']], 'a-sheep')

We can check the data exchange by returning data from NetLogo to the Python workspace, using the report method. In the example below, this returns arrays for the xcor and ycor coordinates of the sheep agents, sorted by their who number. These are then plotted on a conventional scatter plot.

The report method directly passes a string to the NetLogo instance, so that the command syntax may need to be adjusted depending on the NetLogo version. The netlogo_version property of the link object can be used to check the current version. By default, the link object will use the most recent NetLogo version which was found.

[4]:
if netlogo.netlogo_version == '6':
    x = netlogo.report('map [s -> [xcor] of s] sort sheep')
    y = netlogo.report('map [s -> [ycor] of s] sort sheep')
elif netlogo.netlogo_version == '5':
    x = netlogo.report('map [[xcor] of ?1] sort sheep')
    y = netlogo.report('map [[ycor] of ?1] sort sheep')
[5]:
fig, ax = plt.subplots(1)

ax.scatter(x, y, s=4)
ax.set_xlabel('xcor')
ax.set_ylabel('ycor')
ax.set_aspect('equal')
fig.set_size_inches(5,5)

plt.show()
../_images/_docs_introduction_8_0.png

We can then run the model for 100 ticks and update the Python coordinate arrays for the sheep agents, and return an additional array for each agent’s energy value. The latter is plotted on a histogram for each agent type.

[6]:
#We can use either of the following commands to run for 100 ticks:

netlogo.command('repeat 100 [go]')
#netlogo.repeat_command('go', 100)

if netlogo.netlogo_version == '6':
    #Return sorted arrays so that the x, y and energy properties of each agent are in the same order
    x = netlogo.report('map [s -> [xcor] of s] sort sheep')
    y = netlogo.report('map [s -> [ycor] of s] sort sheep')
    energy_sheep = netlogo.report('map [s -> [energy] of s] sort sheep')
elif netlogo.netlogo_version == '5':
    x = netlogo.report('map [[xcor] of ?1] sort sheep')
    y = netlogo.report('map [[ycor] of ?1] sort sheep')
    energy_sheep = netlogo.report('map [[energy] of ?1] sort sheep')

energy_wolves = netlogo.report('[energy] of wolves') #NetLogo returns these in random order
[7]:
from mpl_toolkits.axes_grid1 import make_axes_locatable

fig, ax = plt.subplots(1, 2)

sc = ax[0].scatter(x, y, s=50, c=energy_sheep,
                   cmap=plt.cm.coolwarm)
ax[0].set_xlabel('xcor')
ax[0].set_ylabel('ycor')
ax[0].set_aspect('equal')
divider = make_axes_locatable(ax[0])
cax = divider.append_axes('right', size='5%', pad=0.1)
cbar = plt.colorbar(sc, cax=cax, orientation='vertical')
cbar.set_label('Energy of sheep')

sns.distplot(energy_sheep, kde=False, bins=10,
             ax=ax[1], label='Sheep')
sns.distplot(energy_wolves, kde=False, bins=10,
             ax=ax[1], label='Wolves')
ax[1].set_xlabel('Energy')
ax[1].set_ylabel('Counts')
ax[1].legend()
fig.set_size_inches(14,5)

plt.show()
../_images/_docs_introduction_11_0.png

The repeat_report method returns a Pandas dataframe containing reported values over a given number of ticks, for one or multiple reporters. By default, this assumes the model is run with the “go” NetLogo command; this can be set by passing an optional go argument.

The dataframe is indexed by ticks, with labeled columns for each reporter. In this case, we track the number of wolf and sheep agents over 200 ticks; the outcomes are first plotted as a function of time. The number of wolf agents is then plotted as a function of the number of sheep agents, to approximate a phase-space plot.

[8]:
counts = netlogo.repeat_report(['count wolves','count sheep'], 200, go='go')
[9]:
counts
[9]:
count wolves count sheep
100.0 45 284
101.0 48 274
102.0 52 263
103.0 55 257
104.0 56 254
105.0 60 249
106.0 64 248
107.0 71 242
108.0 74 232
109.0 79 231
110.0 82 230
111.0 87 226
112.0 89 213
113.0 92 203
114.0 94 199
115.0 101 195
116.0 105 193
117.0 109 194
118.0 116 192
119.0 112 187
120.0 114 177
121.0 114 175
122.0 125 168
123.0 130 167
124.0 130 162
125.0 123 162
126.0 126 161
127.0 127 159
128.0 127 154
129.0 130 142
... ... ...
270.0 110 227
271.0 116 225
272.0 123 220
273.0 126 214
274.0 131 206
275.0 134 201
276.0 131 192
277.0 129 183
278.0 128 173
279.0 132 163
280.0 131 162
281.0 130 155
282.0 129 150
283.0 130 146
284.0 129 143
285.0 128 139
286.0 132 133
287.0 134 129
288.0 133 130
289.0 139 123
290.0 142 122
291.0 139 116
292.0 139 115
293.0 136 116
294.0 143 114
295.0 150 108
296.0 157 104
297.0 159 104
298.0 159 108
299.0 149 104

200 rows × 2 columns

[10]:
fig, (ax1, ax2) = plt.subplots(1, 2)

counts.plot(ax=ax1, use_index=True, legend=True)
ax1.set_xlabel('Ticks')
ax1.set_ylabel('Counts')

ax2.plot(counts['count wolves'], counts['count sheep'])
ax2.set_xlabel('Wolves')
ax2.set_ylabel('Sheep')
fig.set_size_inches(12,5)
plt.tight_layout()
plt.show()
../_images/_docs_introduction_15_0.png

The repeat_report method can also be used with reporters that return a NetLogo list. In this case, the list is converted to a numpy array. As an example, we track the energy of the wolf and sheep agents over 5 ticks, and plot the distribution of the wolves’ energy at the final tick recorded in the dataframe.

To illustrate different data types, we also track the [sheep_str] of sheep reporter (which returns a string property across the sheep agents, converted to a numpy object array), count sheep (returning a single numerical variable), and glob_str (returning a single string variable).

[11]:
energy_df = netlogo.repeat_report(['[energy] of wolves',
                                   '[energy] of sheep',
                                   '[sheep_str] of sheep',
                                   'count sheep',
                                   'glob_str'], 5)

fig, ax = plt.subplots(1)

sns.distplot(energy_df['[energy] of wolves'].iloc[-1], kde=False, bins=20, ax=ax)
ax.set_xlabel('Energy')
ax.set_ylabel('Counts')
fig.set_size_inches(4,4)

plt.show()
../_images/_docs_introduction_17_0.png
[12]:
energy_df.head()
[12]:
[energy] of wolves [energy] of sheep [sheep_str] of sheep count sheep glob_str
300.0 [8.294958114624023, 23.694859385490417, 22.863... [5.172172546386719, 3.6378173828125, 25.859207... [sheep, sheep, sheep, sheep, sheep, sheep, she... 104 global
301.0 [40.23937809467316, 8.457512378692627, 3.58932... [24.6978759765625, 42.978271484375, 20.4865303... [sheep, sheep, sheep, sheep, sheep, sheep, she... 102 global
302.0 [13.11083984375, 9.551701173186302, 61.8630952... [12.33160400390625, 11.172172546386719, 13.954... [sheep, sheep, sheep, sheep, sheep, sheep, she... 106 global
303.0 [8.551701173186302, 17.88397216796875, 4.79515... [5.399667739868164, 8.6378173828125, 33.122662... [sheep, sheep, sheep, sheep, sheep, sheep, she... 105 global
304.0 [4.9644129276275635, 21.67865353822708, 36.266... [10.68287181854248, 10.33160400390625, 14.1128... [sheep, sheep, sheep, sheep, sheep, sheep, she... 106 global

The patch_report method can be used to return a dataframe which (for this example) contains the countdown attribute of each NetLogo patch. This dataframe essentially replicates the NetLogo environment, with column labels corresponding to the xcor patch coordinates, and indices following the pycor coordinates.

[13]:
countdown_df = netlogo.patch_report('countdown')

fig, ax = plt.subplots(1)

patches = sns.heatmap(countdown_df, xticklabels=5, yticklabels=5,
                      cbar_kws={'label':'countdown'}, ax=ax)
ax.set_xlabel('pxcor')
ax.set_ylabel('pycor')
ax.set_aspect('equal')
fig.set_size_inches(8,4)

plt.show()
../_images/_docs_introduction_21_0.png

The dataframes can be manipulated with any of the existing Pandas functions, for instance by exporting to an Excel file. The patch_set method provides the inverse functionality to patch_report, and updates the NetLogo environment from a dataframe.

[14]:
countdown_df.to_excel('countdown.xlsx')
netlogo.patch_set('countdown', countdown_df.max()-countdown_df)
[15]:
countdown_update_df = netlogo.patch_report('countdown')

fig, ax = plt.subplots(1)

patches = sns.heatmap(countdown_update_df, xticklabels=5, yticklabels=5, cbar_kws={'label':'countdown'}, ax=ax)
ax.set_xlabel('pxcor')
ax.set_ylabel('pycor')
ax.set_aspect('equal')
fig.set_size_inches(8,4)

plt.show()
../_images/_docs_introduction_24_0.png

Finally, the kill_workspace() method shuts down the NetLogo instance.

[16]:
netlogo.kill_workspace()