A minimal wireless distributed protocol simulator in Python.
The entire simulator is under 50 lines of Python.
No dependency hell. No XML configuration. No 200-page manual. Just subclass Node, override on_receive, and start building distributed protocols over a simulated wireless network.
PyWiSim is a discrete-event wireless network simulator designed for teaching and rapid prototyping. While tools like ns-3 and OMNeT++ are powerful, their complexity can obscure the core ideas behind distributed protocols. PyWiSim strips away everything non-essential so you can focus on protocol logic.
The simulator provides:
- A realistic-enough wireless channel (transmission range, packet loss, carrier-sense collision avoidance)
- Broadcast and unicast primitives
- A discrete-event engine for scheduling
Two optional companion modules extend pywisim to mobile and intermittently connected networks — without touching the core:
mobility.py(~33 lines) — moves nodes over time for MANET simulationsencounter.py(~42 lines) — schedules random pairwise encounters for DTN/ICMN simulations
Protocols use the same Node API (on_receive, broadcast, unicast, schedule) across all three settings. Everything else — routing, consensus, epidemic forwarding — is your code.
PyWiSim has three classes:
| Class | Role |
|---|---|
EventLoop |
Priority-queue discrete-event scheduler. Drives simulation time forward. |
Node |
Base class for your protocol. Override on_receive(msg, sender) to define behavior. Call broadcast(), unicast(), and schedule() to act. |
WirelessNetwork |
Manages node positions, neighbor discovery, transmission simulation, carrier-sense collision avoidance, and distance-dependent packet loss. |
git clone https://github.com/ANRGUSC/PyWiSim.git
cd PyWiSim
python examples/flooding.pyNo dependencies beyond the Python standard library.
- Subclass
Nodeand overrideon_receive(msg, sender) - Place nodes in the network with
add_node(node, x, y) - Kick things off by scheduling an initial action
- Run the event loop
from pywisim import EventLoop, Node, WirelessNetwork
class MyNode(Node):
def on_receive(self, msg, sender):
print(f"{self.nid} got {msg} from {sender}")
# React: broadcast, unicast, schedule timers, etc.
loop = EventLoop()
net = WirelessNetwork(loop, tx_range=1.5)
for nid, x, y in [('A',0,0), ('B',1,0), ('C',2,0)]:
net.add_node(MyNode(nid), x, y)
loop.schedule(1.0, net.nodes['A'].broadcast, ('HELLO',))
loop.run(until=10)All examples use a simple line or star topology and print step-by-step traces.
Reliable broadcast over a multi-hop network. Node A floods a message; each node rebroadcasts it once, ensuring delivery across the entire network even when nodes are out of direct range.
A --flood--> B --rebroadcast--> C --rebroadcast--> D --> E
Key idea: duplicate suppression via a seen set prevents infinite rebroadcast loops.
Builds a spanning tree from an initiator node. The initiator sends EXPLORE messages outward; leaf nodes reply with ECHO. Each node waits for all echoes before forwarding upward, converging to a rooted tree.
Key idea: a node picks the first sender as its parent and counts echoes to know when its subtree is complete.
Reactive (on-demand) route discovery. Node A wants to reach Node E, so it floods a Route Request (RREQ). When E receives the RREQ, it unicasts a Route Reply (RREP) back along the reverse path, populating routing tables at each hop.
Key idea: sequence numbers prevent routing loops; hop counts select shortest paths.
Flood-and-majority consensus on a binary value. Each node proposes 0 or 1, floods its vote, collects all votes, and decides by majority. Demonstrates how even a simple protocol can achieve agreement in a connected network.
Key idea: all-to-all information dissemination via flooding, then local majority decision.
Each node picks a random value and announces it via flooding. Every node tracks the highest value seen. After convergence, the node holding the maximum value is the elected leader.
Key idea: distributed extrema-finding — every node independently converges to the same answer by keeping the max.
Classic distributed transaction protocol adapted for wireless. A coordinator sends PREPARE to all participants, collects YES/NO votes, and broadcasts COMMIT or ABORT. Demonstrates coordinator-participant interaction using unicast.
Key idea: atomic commitment — if any participant votes NO, the entire transaction aborts.
The core simulator assumes a static topology. Two optional companion modules extend it to dynamic networks while preserving the same Node API — protocols are written with on_receive, broadcast, unicast, and schedule regardless of the network model.
![]() |
![]() |
MobilityManager periodically updates node positions via the event loop. Since neighbors(), dist(), and the loss model all read positions dynamically, moving a node automatically changes its connectivity — no topology rebuild needed.
Two standard models are included:
- Random Waypoint — each node picks a random destination within bounds, moves toward it at constant speed, then picks a new destination on arrival.
- Random Walk — each node moves a fixed step in a random direction at each interval.
from mobility import MobilityManager
mob = MobilityManager(net, interval=1.0, speed=0.5, bounds=(10, 10))
mob.start('waypoint') # or 'walk'Demonstrates reactive routing in a network where topology changes. Seven nodes move via random waypoint. The simulation runs in two phases:
- Phase 1 — nodes move for 5 seconds, mobility pauses. AODV discovers a route from A to G (e.g., A → B → F → G).
- Phase 2 — nodes move for another 15 seconds, topology reshuffles. AODV runs again and finds a completely different route (e.g., A → C → G).
The protocol code is identical to the static AODV example — only the network underneath has changed.
EncounterManager models intermittently connected mobile networks where nodes are typically out of range and only communicate during brief pairwise encounters. Behind the scenes, non-encountering nodes are kept far apart (no radio neighbors). When a pair encounters, both are co-located so that standard unicast/broadcast works through pywisim's normal _send/_deliver path. After the encounter window, positions are restored.
Encounters follow a Poisson process with a configurable rate.
from encounter import EncounterManager
enc = EncounterManager(net, rate=1.5, duration=1.0)
enc.start()Demonstrates store-and-forward epidemic dissemination across 8 nodes. Node A originates a file. Whenever two nodes encounter, each sends its buffered data to the other via unicast — pywisim delivers it losslessly. The file propagates hop by hop through chance encounters:
t=0.1 A originates file.pdf
t=0.7 A encounters F → F gets the file
t=2.9 E encounters G → G gets the file (via E, who got it from A earlier)
...
t=7.3 G encounters D → D gets the file (last node)
All 8 nodes eventually receive the file, with the propagation time depending on encounter patterns.
- 49 lines of simulator code — read the whole thing in a few minutes
- Optional mobility and encounter modules — ~33 and ~42 lines respectively, for MANET and DTN simulations
- Zero dependencies — only Python standard library (
heapq,math,random) - Discrete-event simulation — priority queue drives time, no busy loops
- Distance-based topology — neighbors determined by Euclidean distance and transmission range
- Carrier-sense collision avoidance — nodes back off when the channel is busy
- Distance-dependent packet loss — farther nodes have higher loss probability
- Seeded randomness — reproducible results across runs
- Broadcast and unicast — two communication primitives, just like real wireless
- Timer scheduling —
schedule(delay, fn)for timeouts, periodic tasks, etc. - Verbose timestamped logging — trace every send and receive
schedule(delay, fn, *args)— schedulefn(*args)afterdelaytime unitsrun(until=100)— run the simulation until timeuntiltime— current simulation time
__init__(nid)— create a node with identifiernidon_receive(msg, sender)— override this to define your protocolbroadcast(msg)— sendmsgto all neighborsunicast(dest, msg)— sendmsgto a specific neighborschedule(delay, fn, *args)— schedule a future action
__init__(loop, tx_range=1.6, loss=0.02, tx_time=0.5, seed=42, verbose=True)add_node(node, x, y)— place a node at coordinates(x, y)neighbors(nid)— list of nodes within transmission rangelog(msg)— print timestamped message (if verbose)
__init__(net, interval=1.0, speed=0.5, bounds=(10, 10))start(model='waypoint')— begin moving nodes ('waypoint'or'walk')stop()— pause movement
__init__(net, rate=1.0, duration=1.0)— Poisson encounter rate; duration of each encounter windowstart()— begin scheduling encountersstop()— stop scheduling encounters
Bhaskar Krishnamachari, USC — built with Claude Code


