pynetlogo

Author

Daniel Vartanian

Published

September 27, 2025

Project Status: Inactive – The project has reached a stable, usable state but is no longer being actively developed; support/maintenance will be provided as time allows. License: CC0-1.0

Overview

This document reproduces the examples of the pynetlogo Python package, which provides an interface to control NetLogo from Python. These examples are available in the package documentation.

Installation

Install pynetlogo and other dependencies in a virtual environment:

python -m venv .venv
source .venv/bin/activate
pip install pynetlogo

pip install ipyparallel
pip install multiprocessing
pip install nbclient
pip install nbformat
pip install openpyxl
pip install pyyaml
pip install SALib
pip install session_info
pip install sobol

You will also need to ensure that the path to Java Virtual Machine (JVM) is properly configured in the JAVA_HOME environment variable. You can set this variable in your shell configuration file (e.g., .bashrc):

For Arch Linux, use:

export JAVA_HOME="/usr/lib/jvm/default"

Example 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,
    thd = False,
    netlogo_home = "/opt/netlogo-7-0-0"
)
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jpype.JPypeContext in an unnamed module (file:/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/org.jpype.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$ (file:/opt/netlogo-7-0-0/lib/app/scala3-library_3-3.7.0.jar)
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Sep 27, 2025 12:22:06 AM com.sun.javafx.application.PlatformImpl startup
WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @5d5d9e5'
netlogo.load_model("./nlogox/Wolf Sheep Predation_v6.nlogox")
netlogo.command("setup")
agent_xy = pd.read_excel("./data/xy_DataFrame.xlsx")
agent_xy[["who", "xcor", "ycor"]].head(5)
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
netlogo.write_NetLogo_attriblist(agent_xy[["who", "xcor", "ycor"]], "a-sheep")
x = netlogo.report("map [s -> [xcor] of s] sort sheep")
y = netlogo.report("map [s -> [ycor] of s] sort sheep")
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()

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

counts = netlogo.repeat_report(["count wolves", "count sheep"], 200, go="go")
counts = pd.DataFrame(counts)
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()

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()

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

countdown_df.to_excel("countdown.xlsx")
netlogo.patch_set("countdown", countdown_df.max() - countdown_df)
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()

netlogo.kill_workspace()

Example 2

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style("white")
sns.set_context("talk")

import pynetlogo

# Import the sampling and analysis modules for a Sobol variance-based
# sensitivity analysis
from SALib.sample import sobol as sobolsample
from SALib.analyze import sobol
problem = {
    "num_vars": 6,
    "names": [
        "random-seed",
        "grass-regrowth-time",
        "sheep-gain-from-food",
        "wolf-gain-from-food",
        "sheep-reproduce",
        "wolf-reproduce",
    ],
    "bounds": [
        [1, 100000],
        [20.0, 40.0],
        [2.0, 8.0],
        [16.0, 32.0],
        [2.0, 8.0],
        [2.0, 8.0],
    ],
}
n = 2 ** 5  # Changed
param_values = sobolsample.sample(problem, n, calc_second_order=True)
param_values.shape
(448, 6)

Running the Experiments in Parallel Using ipyparallel

import ipyparallel as ipp

cluster = ipp.Cluster(n=4)
cluster.start_cluster_sync();
Starting 4 engines with <class 'ipyparallel.cluster.launcher.LocalEngineSetLauncher'>
rc = cluster.connect_client_sync()
rc.wait_for_engines(n=4) # Added
rc.ids
  0%|          | 0/4 [00:00<?, ?engine/s] 25%|██▌       | 1/4 [00:05<00:16,  5.40s/engine]100%|██████████| 4/4 [00:05<00:00,  1.35s/engine]
[0, 1, 2, 3]
direct_view = rc[:]
import os

# Push the current working directory of the notebook to a "cwd" variable on the engines that can be accessed later
direct_view.push(dict(cwd=os.getcwd()), block=True)
[None, None, None, None]
# Push the "problem" variable from the notebook to a corresponding variable on the engines
direct_view.push(dict(problem=problem), block=True)
[None, None, None, None]
%%px
import os
os.chdir(cwd)

import pynetlogo
import numpy as np
import pandas as pd

netlogo = pynetlogo.NetLogoLink(
    gui = False,
    thd = False,
    netlogo_home = "/opt/netlogo-7-0-0"
)

netlogo.load_model("./nlogox/Wolf Sheep Predation_v6.nlogox")
[stderr:0] WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jpype.JPypeContext in an unnamed module (file:/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/org.jpype.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$ (file:/opt/netlogo-7-0-0/lib/app/scala3-library_3-3.7.0.jar)
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
[stderr:1] WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jpype.JPypeContext in an unnamed module (file:/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/org.jpype.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$ (file:/opt/netlogo-7-0-0/lib/app/scala3-library_3-3.7.0.jar)
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
[stderr:3] WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jpype.JPypeContext in an unnamed module (file:/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/org.jpype.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$ (file:/opt/netlogo-7-0-0/lib/app/scala3-library_3-3.7.0.jar)
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
[stderr:2] WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jpype.JPypeContext in an unnamed module (file:/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/org.jpype.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$ (file:/opt/netlogo-7-0-0/lib/app/scala3-library_3-3.7.0.jar)
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:00<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:01<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:02<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:   0%|          | 0/4 [00:03<?, ?tasks/s]%px:  50%|█████     | 2/4 [00:03<00:00, 19.53tasks/s]%px:  75%|███████▌  | 3/4 [00:04<00:00,  8.64tasks/s]%px: 100%|██████████| 4/4 [00:04<00:00,  1.04s/tasks]
def simulation(experiment):
    # Set the input parameters
    for i, name in enumerate(problem["names"]):
        if name == "random-seed":
            # The NetLogo random seed requires a different syntax
            netlogo.command("random-seed {}".format(experiment[i]))
        else:
            # Otherwise, assume the input parameters are global variables
            netlogo.command("set {0} {1}".format(name, experiment[i]))

    netlogo.command("setup")
    # Run for 100 ticks and return the number of sheep and wolf agents at each time step
    counts = netlogo.repeat_report(["count sheep", "count wolves"], 100)

    results = pd.Series( # Added
        [np.mean(counts["count sheep"]), np.mean(counts["count wolves"])],
        index=["Avg. sheep", "Avg. wolves"],
    )

    # Original code:
    #
    # results = pd.Series(
    #     [counts["count sheep"].values.mean(), counts["count wolves"].values.mean()],
    #     index=["Avg. sheep", "Avg. wolves"],
    # )

    return results
lview = rc.load_balanced_view()
results = pd.DataFrame(lview.map_sync(simulation, param_values))
results.to_csv("./data/Sobol_parallel.csv")
results.head(5)
Avg. sheep Avg. wolves
0 101.594059 28.465347
1 100.504950 33.247525
2 85.574257 20.376238
3 179.871287 108.752475
4 104.237624 33.504950

Using SALib for Sensitivity Analysis

fig, ax = plt.subplots(1, len(results.columns), sharey=True)

for i, n in enumerate(results.columns):
    ax[i].hist(results[n], 20)
    ax[i].set_xlabel(n)
ax[0].set_ylabel("Counts")

fig.set_size_inches(10, 4)
fig.subplots_adjust(wspace=0.1)

plt.show()

import scipy

nrow = 2
ncol = 3

fig, ax = plt.subplots(nrow, ncol, sharey=True)

y = results["Avg. sheep"]

for i, a in enumerate(ax.flatten()):
    x = param_values[:, i]
    sns.regplot(
        x=x,
        y=y,
        ax=a,
        ci=None,
        color="k",
        scatter_kws={"alpha": 0.2, "s": 4, "color": "gray"},
    )
    pearson = scipy.stats.pearsonr(x, y)
    a.annotate(
        "r: {:6.3f}".format(pearson[0]),
        xy=(0.15, 0.85),
        xycoords="axes fraction",
        fontsize=13,
    )
    if divmod(i, ncol)[1] > 0:
        a.get_yaxis().set_visible(False)
    a.set_xlabel(problem["names"][i])
    a.set_ylim([0, 1.1 * np.max(y)])

fig.set_size_inches(9, 9, forward=True)
fig.subplots_adjust(wspace=0.2, hspace=0.3)

plt.show()

Si = sobol.analyze(
    problem,
    results["Avg. sheep"].values,
    calc_second_order=True,
    print_to_console=False,
)
/home/danielvartan/Git/pynetlogo/.venv/lib/python3.13/site-packages/SALib/util/__init__.py:274: FutureWarning: unique with argument that is not not a Series, Index, ExtensionArray, or np.ndarray is deprecated and will raise in a future version.
  names = list(pd.unique(groups))
Si_filter = {k: Si[k] for k in ["ST", "ST_conf", "S1", "S1_conf"]}
Si_df = pd.DataFrame(Si_filter, index=problem["names"])
Si_df
ST ST_conf S1 S1_conf
random-seed 0.056705 0.039673 0.036567 0.092495
grass-regrowth-time 0.246658 0.215558 0.162614 0.188970
sheep-gain-from-food 0.779248 0.565153 0.252965 0.371308
wolf-gain-from-food 0.539663 0.301630 0.384790 0.279285
sheep-reproduce 0.147828 0.099676 -0.049013 0.253315
wolf-reproduce 0.402822 0.229209 0.266902 0.352499
fig, ax = plt.subplots(1)

indices = Si_df[["S1", "ST"]]
err = Si_df[["S1_conf", "ST_conf"]]

indices.plot.bar(yerr=err.values.T, ax=ax)
fig.set_size_inches(8, 4)

plt.show()

%matplotlib inline
import itertools
from math import pi
from matplotlib.legend_handler import HandlerPatch
def normalize(x, xmin, xmax):
    return (x - xmin) / (xmax - xmin)
def plot_circles(ax, locs, names, max_s, stats, smax, smin, fc, ec, lw, zorder):
    s = np.asarray([stats[name] for name in names])
    s = 0.01 + max_s * np.sqrt(normalize(s, smin, smax))

    fill = True
    for loc, name, si in zip(locs, names, s):
        if fc == "w":
            fill = False
        else:
            ec = "none"

        x = np.cos(loc)
        y = np.sin(loc)

        circle = plt.Circle(
            (x, y),
            radius=si,
            ec=ec,
            fc=fc,
            transform=ax.transData._b,
            zorder=zorder,
            lw=lw,
            fill=True,
        )
        ax.add_artist(circle)
def filter(sobol_indices, names, locs, criterion, threshold):
    if criterion in ["ST", "S1", "S2"]:
        data = sobol_indices[criterion]
        data = np.abs(data)
        data = data.flatten()  # flatten in case of S2
        # TODO:: remove nans

        filtered = [(name, locs[i]) for i, name in enumerate(names) if data[i] > threshold]
        filtered_names, filtered_locs = zip(*filtered)
    elif criterion in ["ST_conf", "S1_conf", "S2_conf"]:
        raise NotImplementedError
    else:
        raise ValueError("unknown value for criterion")

    return filtered_names, filtered_locs
def plot_sobol_indices(sobol_indices, criterion="ST", threshold=0.01):
    """plot sobol indices on a radial plot

    Parameters
    ----------
    sobol_indices : dict
                    the return from SAlib
    criterion : {'ST', 'S1', 'S2', 'ST_conf', 'S1_conf', 'S2_conf'}, optional
    threshold : float
                only visualize variables with criterion larger than cutoff

    """
    max_linewidth_s2 = 15  # 25*1.8
    max_s_radius = 0.3

    # prepare data
    # use the absolute values of all the indices
    # sobol_indices = {key:np.abs(stats) for key, stats in sobol_indices.items()}

    # dataframe with ST and S1
    sobol_stats = {key: sobol_indices[key] for key in ["ST", "S1"]}
    sobol_stats = pd.DataFrame(sobol_stats, index=problem["names"])

    smax = sobol_stats.max().max()
    smin = sobol_stats.min().min()

    # dataframe with s2
    s2 = pd.DataFrame(sobol_indices["S2"], index=problem["names"], columns=problem["names"])
    s2[s2 < 0.0] = 0.0  # Set negative values to 0 (artifact from small sample sizes)
    s2max = s2.max().max()
    s2min = s2.min().min()

    names = problem["names"]
    n = len(names)
    ticklocs = np.linspace(0, 2 * pi, n + 1)
    locs = ticklocs[0:-1]

    filtered_names, filtered_locs = filter(sobol_indices, names, locs, criterion, threshold)

    # setup figure
    fig = plt.figure()
    ax = fig.add_subplot(111, polar=True)
    ax.grid(False)
    ax.spines["polar"].set_visible(False)

    ax.set_xticks(locs)
    ax.set_xticklabels(names)

    ax.set_yticklabels([])
    ax.set_ylim(top=1.4)
    legend(ax)

    # plot ST
    plot_circles(
        ax,
        filtered_locs,
        filtered_names,
        max_s_radius,
        sobol_stats["ST"],
        smax,
        smin,
        "w",
        "k",
        1,
        9,
    )

    # plot S1
    plot_circles(
        ax,
        filtered_locs,
        filtered_names,
        max_s_radius,
        sobol_stats["S1"],
        smax,
        smin,
        "k",
        "k",
        1,
        10,
    )

    # plot S2
    for name1, name2 in itertools.combinations(zip(filtered_names, filtered_locs), 2):
        name1, loc1 = name1
        name2, loc2 = name2

        weight = s2.loc[name1, name2]
        lw = 0.5 + max_linewidth_s2 * normalize(weight, s2min, s2max)
        ax.plot([loc1, loc2], [1, 1], c="darkgray", lw=lw, zorder=1)

    return fig
class HandlerCircle(HandlerPatch):
    def create_artists(
        self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans
    ):
        center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
        p = plt.Circle(xy=center, radius=orig_handle.radius)
        self.update_prop(p, orig_handle, legend)
        p.set_transform(trans)
        return [p]
def legend(ax):
    some_identifiers = [
        plt.Circle((0, 0), radius=5, color="k", fill=False, lw=1),
        plt.Circle((0, 0), radius=5, color="k", fill=True),
        plt.Line2D([0, 0.5], [0, 0.5], lw=8, color="darkgray"),
    ]
    ax.legend(
        some_identifiers,
        ["ST", "S1", "S2"],
        loc=(1, 0.75),
        borderaxespad=0.1,
        mode="expand",
        handler_map={plt.Circle: HandlerCircle()},
    )
sns.set_style("whitegrid")
fig = plot_sobol_indices(Si, criterion="ST", threshold=0.005)
fig.set_size_inches(7, 7)
plt.show()

Example 3

Running the Experiments in Parallel Using a Process Pool

from multiprocessing import Pool
import os
import pandas as pd
import numpy as np

import pynetlogo

from SALib.sample import sobol as sobolsample
def initializer(modelfile):
    """initialize a subprocess

    Parameters
    ----------
    modelfile : str

    """

    # we need to set the instantiated netlogo
    # link as a global so run_simulation can
    # use it
    global netlogo

    netlogo = pynetlogo.NetLogoLink(
      gui = False,
      thd = False,
      netlogo_home = "/opt/netlogo-7-0-0"
    )

    netlogo.load_model(modelfile)
def run_simulation(experiment):
    """run a netlogo model

    Parameters
    ----------
    experiments : dict

    """

    # Set the input parameters
    for key, value in experiment.items():
        if key == "random-seed":
            # The NetLogo random seed requires a different syntax
            netlogo.command("random-seed {}".format(value))
        else:
            # Otherwise, assume the input parameters are global variables
            netlogo.command("set {0} {1}".format(key, value))

    netlogo.command("setup")
    # Run for 100 ticks and return the number of sheep and
    # wolf agents at each time step
    counts = netlogo.repeat_report(["count sheep", "count wolves"], 100)

    results = pd.Series( # Added
        [np.mean(counts["count sheep"]), np.mean(counts["count wolves"])],
        index=["Avg. sheep", "Avg. wolves"],
    )

    # Original code:
    #
    # results = pd.Series(
    #     [counts["count sheep"].values.mean(), counts["count wolves"].values.mean()],
    #     index=["Avg. sheep", "Avg. wolves"],
    # )

    return results
if __name__ == "__main__":
    modelfile = os.path.abspath("./nlogox/Wolf Sheep Predation_v6.nlogox")

    problem = {
        "num_vars": 6,
        "names": [
            "random-seed",
            "grass-regrowth-time",
            "sheep-gain-from-food",
            "wolf-gain-from-food",
            "sheep-reproduce",
            "wolf-reproduce",
        ],
        "bounds": [[1, 100000], [20.0, 40.0], [2.0, 8.0], [16.0, 32.0], [2.0, 8.0], [2.0, 8.0]],
    }

    n = 2 ** 1 # Changed
    param_values = sobolsample.sample(problem, n, calc_second_order=True)

    # cast the param_values to a dataframe to
    # include the column labels
    experiments = pd.DataFrame(param_values, columns=problem["names"])

    with Pool(4, initializer=initializer, initargs=(modelfile,)) as executor:
        results = []
        for entry in executor.map(run_simulation, experiments.to_dict("records")):
            results.append(entry)
        results = pd.DataFrame(results)
results.head(5)
Avg. sheep Avg. wolves
0 180.455446 62.386139
1 208.336634 50.861386
2 208.237624 59.564356
3 167.475248 40.495050
4 132.247525 81.089109

Session Info

import session_info

session_info.show(cpu = True, jupyter = True, dependencies = True)
Click to view session information
-----
SALib               NA
ipyparallel         9.0.1
matplotlib          3.10.6
mpl_toolkits        NA
numpy               2.3.3
pandas              2.3.2
pynetlogo           0.5.2
scipy               1.16.2
seaborn             0.13.2
session_info        v1.0.1
-----
Click to view modules imported as dependencies
PIL                 11.3.0
asttokens           NA
comm                0.2.3
cycler              0.12.1
cython_runtime      NA
dateutil            2.9.0.post0
debugpy             1.8.17
decorator           5.2.1
dill                0.4.0
et_xmlfile          2.0.0
executing           2.2.1
ipykernel           6.30.1
jedi                0.19.2
jpype               1.6.0
kiwisolver          1.4.9
matplotlib_inline   0.1.7
multiprocess        0.70.18
netLogoLink         NA
openpyxl            3.1.5
packaging           25.0
parso               0.8.5
platformdirs        4.4.0
prompt_toolkit      3.0.52
psutil              7.1.0
pure_eval           0.2.3
pydev_ipython       NA
pydevconsole        NA
pydevd              3.2.3
pydevd_file_utils   NA
pydevd_plugins      NA
pydevd_tracing      NA
pygments            2.19.2
pyparsing           3.2.5
pytz                2025.2
six                 1.17.0
stack_data          0.6.3
tornado             6.5.2
tqdm                4.67.1
traitlets           5.14.3
wcwidth             0.2.14
zmq                 27.1.0
-----
IPython             9.5.0
jupyter_client      8.6.3
jupyter_core        5.8.1
-----
Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813]
Linux-6.16.8-arch1-1-x86_64-with-glibc2.42
12 logical CPU cores
-----
Session information updated at 2025-09-27 00:23

License

This content is licensed under CC0 1.0 Universal, placing these materials in the public domain. You may freely copy, modify, distribute, and use this work, even for commercial purposes, without permission or attribution.