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,
    jvm_path="/Users/jhkwakkel/Downloads/jdk-19.0.2.jdk/Contents/MacOS/libjli.dylib",
)

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.

[4]:
x = netlogo.report("map [s -> [xcor] of s] sort sheep")
y = netlogo.report("map [s -> [ycor] of s] 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)


# 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")

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.histplot(energy_sheep, kde=False, bins=10, ax=ax[1], label="Sheep")
sns.histplot(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 dictionary with the reporter as key. The value is a list order by ticks. By default, this assumes the model is run with the “go” NetLogo command; this can be set by passing an optional go argument.

Often, the dictionary can easily be converted into a dataframe, for easy further analysis.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")
[12]:
counts = pd.DataFrame(counts)
[13]:
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")


for ax in [ax1, ax2]:
    ax.set_aspect(1 / ax.get_data_ratio())


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. Note that the number of sheep and wolves vary over time. This means that for each tick, the size of the array will be different. So, we cannot straightforwardly convert these results into a 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).

[16]:
results = netlogo.repeat_report(
    [
        "[energy] of wolves",
        "[energy] of sheep",
        "[sheep_str] of sheep",
        "count sheep",
        "glob_str",
    ],
    5,
)

fig, ax = plt.subplots(1)

sns.histplot(results["[energy] of wolves"][-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
[18]:
list(results.keys())
[18]:
['[energy] of wolves',
 '[energy] of sheep',
 '[sheep_str] of sheep',
 'count sheep',
 'glob_str']

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()
[ ]: