If this is your first time here it might be a good idea to start at the Points series, and it also might be helpful (though not required) to review the how the Hybrid_Iterator class was used within the priority buffer series, because the following Agent class will also be inheriting from Hybrid_Iterator


What I’ll be asking of an Agent any time next() or __next__() is called, is to look at it’s current Point’s neighbors, consider those that the Agent has not visited and set the one of those to self['heading'], then return itself for further consideration. When the Agent no longer has anywhere left to travel (without backtracking), they’ll throw/raise the specified error object to signal to the calling process that the they’re finished.

As a refresher here’s a sketch of Hybrid_Iterator’s super relationships with dict and Iterator classes, and a link to hybrid_iterator/__init__.py

And a sketch of what the following code hopes to build

#!/usr/bin/env python
from __future__ import absolute_import

import sys
sys.dont_write_bytecode = True

from random import randint

from hybrid_iterator import Hybrid_Iterator


class Agent(Hybrid_Iterator):
    """
    Let `point` be a `Point` instance of where this `Agent` is currently
    """

    def __init__(self, name, point, **kwargs):
        super(Agent, self).__init__(**kwargs)
        self.update(
            name = name,
            point = point,
            visited = [],
            heading = {})

    def set_heading(self, routes = None):
        """
        Sets `self['heading']` from unvisited `routes`
        If multiple choices are available a random one is picked.
        """
        if not routes:
            routs = self['point']['neighbors']

        self['heading'] = {}
        courses = {}
        visited_addresses = [x.keys()[0] for x in self['visited']]
        for address, cost in routes.items():
            if address not in visited_addresses:
                courses.update({address: cost})

        target_key = 0
        if len(courses.keys()) > 1:
            # ... choose a random heading if
            #     multiple courses are available
            target_key = randint(0, int(len(courses.keys()) - 1))

        if courses:
            address = courses.keys()[target_key]
            self['heading'] = {address: courses.pop(address)}

        return self['heading']

    def next(self):
        """
        Calls `self.set_heading()`, `raises` a `GeneratorExit`
        when there is no where left to go.
        """
        self.set_heading(routes = self['point']['neighbors'])

        if not self['heading']:
            self.throw(GeneratorExit)

        return self


if __name__ == '__main__':
    raise Exception("This file must be used as a module!")

By separating out the behavior of an Agent from the next calls, sub-classing custom a new agent with a different set_heading method should be relatively easy, and not require too much editing of code further up the stack.

Hint, making a Frugal_Agent is even easier, here’s a really quick example of what that could look like…

from graph.agents import Agent


class Frugal_Agent(Agent):
    def next(self):
        neighbors = self['point']['neighbors']
        cheapest = self['point'].cheapest(routs = neighbors)
        self.set_heading(routes = cheapest)

        if not self['heading']:
            self.throw(GeneratorExit)

        return self

In order to test more easily this new class that makes use of Hybrid_Iterator from a parent directory, a temporary script file should be written under the root directory for the Python project, eg. python_examples/bills_adventure.py. This test file could begin a bit like…

#!/usr/bin/env python

import sys
sys.dont_write_bytecode = True


from graph.points import Point
from graph.agents import Agent


X = 0.2
O = 0.7

points = {
    'u': Point(address = 'u', neighbors = {'v': X, 'w': X}),
    'v': Point(address = 'v', neighbors = {'u': O, 'w': X}),
    'w': Point(address = 'w', neighbors = {'u': O, 'v': X}),
}

Initializing a set of four Agents could then look like…

agents = {
    'Bill': Agent(name = 'Bill', point = points['u']),
    'Alice': Agent(name = 'Alice', point = points['u']),
    'Ted': Agent(name = 'Ted', point = points['v']),
    'Jain': Agent(name = 'Jain', point = points['w']),
}

Note, if ya wanted to update the points population with names of Agents that could look like…

for agent in agents.values():
    agent['point']['population'].append(agent['name'])

… though that does then require updating a Point’s population every-time an Agent wants to move; that would be on-top of updating an Agent’s reference to a Point too. And definitely read the Warning later on in this post about recursion limits.

… and letting Bill wander about, safely, could look like…

agent_key = 'Bill'
name = agents[agent_key]['name']
for i, _ in enumerate(agents[agent_key]):
    here = agents[agent_key]['point']['address']
    there = agents[agent_key]['heading'].keys()[0]
    cost = agents[agent_key]['heading'][there]

    print("{name} paid {cost} to get from {here} to {there}".format(
        name = name, cost = cost,
        here = here, there = there))

    # ... update various states to simulate an
    #     Agent moving from `Point`-to-`Point`
    points[here]['population'].remove(name)
    points[there]['population'].append(name)
    agents[agent_key]['visited'].append({there: cost})
    agents[agent_key]['point'] = points[there]

    if i > 4:
        raise Exception("Hunt for bugs!")


# ... `print` about the adventure Bill had
print("Places that {name} has visited -> {visited}".format(
    name = agents[agent_key]['name'],
    visited = agents[agent_key]['visited']
))

print("\tCurrently at -> {there}".format(
    there = there))

print("\tPaid in {total} for travel costs".format(
    total = sum([sum(x.values()) for x in agents[agent_key]['visited']])
))

That safety looping with i and enumerate is a good habit to get into when testing a classs’ iterative behavior; without it one of the many loops could run indefinitely from some bug or uncaught edge case. Not completely impossible to get stuck, just more difficult to mess-up.

If/when that happens, a loop that never ends, try mashing Ctrl^c for a bit, or searching for the Process ID so that it can be terminated with kill -9 <PID>.

… output may look something like…

Bill paid 0.2 to get from u to w
Bill paid 0.2 to get from w to v
Bill paid 0.7 to get from v to u
Places that Bill has visited -> [{'w': 0.2}, {'v': 0.2}, {'u': 0.7}]
        Currently at -> u
        Paid in 1.1 for travel costs

… and not hitting the safety Exception means that things are working as designed!

This is probably a good place to pause and experiment with the classes so far introduced. Perhaps consider making a slightly absent minded agent, one that randomly forgets where they’ve been but not the costs, eg. agents['Ted']['visited'] $\implies$ list({'somewhere': 1.8}, {'u': 0.2}, ...), and see what kinds of excellent adventures your agents can have.

When you’re ready I’ve begun covering one way of constructing a Graph class that enables moving more than one Agent at a time.

Warnings

What I did there by assigning a full reference to a point within an Agent, eg. agents['Bill']['point'] $\implies$ points['u'], is dangerous!

Not used on it’s own, but let’s suppose at some point ya thought it would be a good idea to do the same with a point’s population listing, eg. points['u']['population'] $\implies$ list([agents['Bill'], agents['Alice']])… and let’s also suppose that the other bits of code to make that happen where patched without needing to debug… at the time everything might work.

But at some point later in the future someone will (maybe you maybe not), dump an instance of agent or point, and get less than helpful results. Here’s a quick example of how messy things can get when references self refer…

d = {'d': {}}
print(d)
# -> {'d': {}}

d.update({'d': d})
# ... uhhoh...
print(d)
# -> {'d': {...}}
print(d['d'])
# -> {'d': {...}}
print(d['d']['d'])
# -> {'d': {...}}

… thankfully Python’s kind enough to not try and print the inner values when things like this happen; but don’t count on it!… especially with other languages.

Some might wonder then why I’d even assign references of points within an Agent… It’s a matter of future convenience, I think an Agent should be able to gather information about their surroundings, eg. population, and allowing’em access to that info makes modeling behaviors of agents a bit easier.