Skip to main content

A package for simulating population protocols.

Project description

ppsim Python package

The ppsim package is used for simulating population protcols. The core of the simulator uses a batching algorithm which gives significant asymptotic gains for protocols with relatively small reachable state sets. The package is designed to be run in a Python notebook, to concisely describe complex protocols, efficiently simulate their dynamics, and provide helpful visualization of the simulation.

Installation

The package can be installed with pip via

pip install ppsim

The most important part of the package is the Simulation class, which is responsible for parsing a protocol, performing the simulation, and giving data about the simulation.

from ppsim import Simulation

Example protcol

A state can be any hashable Python object. The simplest way to describe a protocol is a dictionary mapping pairs of input states to pairs of output states. For example, here is a description of the classic 3-state approximate majority protocol. There are two initial states A and B, and the protocol converges with high probability to the majority state with the help of a third "undecided" state U.

a, b, u = 'A', 'B', 'U'
approximate_majority = {
    (a,b): (u,u),
    (a,u): (a,a),
    (b,u): (b,b)
}

Example Simulation

To instantiate a Simulation, we must specify a protocol along with an initial condition, which is a dictionary mapping states to counts. Let's simulate approximate majority with in a population of one billion agents with a slight majority of A agents.

n = 10 ** 9
init_config = {a: 0.501 * n, b: 0.499 * n}
sim = Simulation(init_config, approximate_majority)

Now let's run this simulation for 10 units of parallel time (10 * n interactions). We will record the configuration every 0.1 units of time.

sim.run(10, 0.1)

The Simulation class can display all these configurations in a pandas dataframe in the attribute history.

sim.history
A B U
time
0.000000 501000000 499000000 0
0.100010 478285585 476280966 45433449
0.200039 459449200 457428961 83121839
0.300056 443650105 441607805 114742090
0.400098 430264769 428196457 141538774
... ... ... ...
9.601324 352755117 314377928 332866955
9.701327 353418258 313744457 332837285
9.801359 354106871 313093847 332799282
9.901364 354824761 312423976 332751263
10.001375 355556526 311728627 332714847

101 rows × 3 columns

sim.history.plot()

png

Without specifying an end time, run will run the simulation until the configuration is silent (all interactions are null). In this case, that will be when the protcol reaches a silent majority consensus configuration.

sim.run()
sim.history.plot()

png

As currently described, this protocol is one-way, where these interactions only take place if the two states meet in the specified order. We can see this by checking print_reactions.

sim.print_reactions()
A, B  -->  U, U      with probability 0.5
A, U  -->  A, A      with probability 0.5
B, U  -->  B, B      with probability 0.5

Here we have unorder pairs of reactants, and the probability 0.5 is because these interactions as written depended on the order of the agents. If we wanted to consider the more sensible symmetric variant of the protocol, one approach would explicitly give all non-null interactions:

approximate_majority_symmetric = {
    (a,b): (u,u), (b,a): (u,u),
    (a,u): (a,a), (u,a): (a,a),
    (b,u): (b,b), (u,b): (b,b)
}
sim = Simulation(init_config, approximate_majority_symmetric)

But a quicker equivalent approach is to tell Simulation that all interactions should be interpreted as symmetric, so if we specify interaction (a,b) but leave (b,a) as null, then (b,a) will be interpreted as having the same output pair.

sim = Simulation(init_config, approximate_majority, transition_order='symmetric')
sim.print_reactions()
sim.run()
sim.history.plot()
A, B  -->  U, U
A, U  -->  A, A
B, U  -->  B, B

png

A key result about this protocol is it converges in expected O(log n) time, which surprisingly is very nontrivial to prove. We can use this package to very quickly gather some convincing data that the convergence really is O(log n) time, with the function time_trials.

from ppsim import time_trials
import numpy as np

ns = [int(n) for n in np.geomspace(10, 10 ** 8, 20)]
def initial_condition(n):
    return {'A': n // 2, 'B': n // 2}
df = time_trials(approximate_majority, ns, initial_condition, num_trials=100, max_wallclock_time = 30, transition_order='symmetric')
df
n time
0 10 14.100000
1 10 9.000000
2 10 7.700000
3 10 14.900000
4 10 7.200000
... ... ...
1359 42813323 48.937840
1360 42813323 50.391446
1361 42813323 56.390054
1362 100000000 50.265050
1363 100000000 53.851442

1364 rows x 2 columns

This dataframe collected time from up to 100 trials for each population size n across a many orders of magnitude, limited by the budget of 30 seconds of wallclock time that we gave it. We can now use the seaborn library to get a convincing plot of the data.

import seaborn as sns
lp = sns.lineplot(x='n', y='time', data=df)
lp.set_xscale('log')

png

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ppsim-0.0.4.tar.gz (218.2 kB view hashes)

Uploaded Source

Built Distribution

ppsim-0.0.4-cp37-cp37m-win_amd64.whl (277.1 kB view hashes)

Uploaded CPython 3.7m Windows x86-64

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page