In the bidirected triangle network as shown below, 4 agents $\{s1,s2,s3,s4\}$ have their own destination $\{t1,t2,t3,t4\}$. I am trying to model this problem with a python script without any game theory library as a challenge.
Since the cost of most of the edges (except edge $vu$ and $wu$) depend on the number of agents using it, I need to know all the strategy for all agents before getting the final cost of each edge.
So my algorithm outlines as:
- Loop the strategy of agent 1, add cost to the edges he used
- Inside the previous loop, loop the strategy of agent 2, add cost to the edges he used
- Inside the previous loop, loop the strategy of agent 3, add cost to the edges he used
- Inside the previous loop, loop the strategy of agent 4, add cost to the edges he used
- After knowing all their strategy, add calculate the cost for each agent
- Before exploring alternative strategy, remove 1 cost on the edge that has given up by the agent who's changing his strategy.
- Repeat step 1-6
Shown below is my python script, but you can see I have 1 for loop for 1 agent. It would be difficult for me to try 10 agents in the future. Anyway to improve the algorithm?
# Initializing Paraemeters
agent_dict = {
'agent1': [['uv'], ['uw','wv']],
'agent2': [['uw'], ['uv','vw']],
'agent3': [['vw'], ['vu','uw']],
'agent4': [['wv'], ['wu', 'uv']],
}
# Variables
uv = 0
uw = 0
wv = 0
vw = 0
path_to_cost = {
'uv': uv,
'uw': uw,
'wv': wv,
'vw': vw,
'vu': 0,
'wu': 0
}
cost_list = []
strategy_set = []
for path1 in agent_dict['agent1']:
for edge1 in path1: # Step 1
if edge1 == 'vu' or edge1 == 'wu':
pass
else:
path_to_cost[edge1]+=1
for path2 in agent_dict['agent2']: # Step 2
for edge2 in path2:
if edge2 == 'vu' or edge2 == 'wu':
pass
else:
path_to_cost[edge2]+=1
for path3 in agent_dict['agent3']: # Step 3
for edge3 in path3:
if edge3 == 'vu' or edge3 == 'wu':
pass
else:
path_to_cost[edge3]+=1
for path4 in agent_dict['agent4']: # Step 4
for edge4 in path4:
if edge4 == 'vu' or edge4 =='wu':
pass
else:
path_to_cost[edge4]+=1
# Step 5
cost1 = [path_to_cost[x] for x in path1]
cost2 = [path_to_cost[x] for x in path2]
cost3 = [path_to_cost[x] for x in path3]
cost4 = [path_to_cost[x] for x in path4]
social_cost = sum(cost1) + sum(cost2) + sum(cost3) + sum(cost4)
cost_list.append(social_cost)
strategy_set.append([path1,path2,path3,path4])
print('agent1 is using {}, agent2 is using {}, agent3 is using {}, and agent4 is using {}'.format(path1,path2,path3,path4))
print('agent1 costs {}, agent2 costs {}, agent3 costs {}, agent4 is using {}'.format(cost1,cost2,cost3,cost4))
print('Social cost = {}\n'.format(social_cost))
# Step 6: Resetting cost for each agent for next trial
for edge4 in path4: # resetting agent 4 strategy
if edge4 == 'vu' or edge4 =='wu':
pass
else:
path_to_cost[edge4]-=1
for edge3 in path3:
if edge3 == 'vu' or edge3 == 'wu':
pass
else:
path_to_cost[edge3]-=1
for edge2 in path2:
if edge2 == 'vu' or edge2 == 'wu':
pass
else:
path_to_cost[edge2]-=1
for edge1 in path1:
if edge1 == 'vu' or edge1 == 'wu':
pass
else:
path_to_cost[edge1]-=1
My 2nd attempt to the problem using S0AndS0's suggestions
from points import Point
points = {
'u': Point(address = 'u', neighbors = {'v': 0, 'w': 0}),
'v': Point(address = 'v', neighbors = {'u': 0, 'w': 0}),
'w': Point(address = 'w', neighbors = {'u': 0, 'v': 0}),
}
# Strategy 1 u-v (if choice == 'v')
agent1_pos = 'u'
agent1_target = 'v'
agent2_pos = 'u'
agent2_target = 'w'
agent3_pos = 'v'
agent3_target = 'w'
agent4_pos = 'w'
agent4_target = 'v'
choice_a1 = agent1_target
points[agent1_pos]['neighbors'][choice_a1]+=1
choice_a2 = agent2_target
points[agent2_pos]['neighbors'][choice_a2]+=1
choice_a3 = agent3_target
points[agent3_pos]['neighbors'][choice_a3]+=1
choice_a4 = agent4_target
points[agent4_pos]['neighbors'][choice_a4]+=1
edge_cost = lambda base_cost, drivers: base_cost * drivers # no information about the driver along the edges

The following portions are what I'll attempt to aid with as I'm on a similar path...
... however, unfortunately helping with the
tons of for loops I cannot, but I can help in spreading'em out such thatcyclomatic complexitychecker tools don't flag down what you're trying to accomplish, and codeprofilerscan then give meaningful output for determining where libraries such asnumpyare a good fit.Looking through your code, I think you're on the right track with using dictionaries for some of this, well that is if you're dedicated to minimal library use; which I think is totally a legit way of learning.
The next step seems to be to use Python's OOP (Object Oriented Programing) tendencies, and
__doc__strings to your advantage; first step you've already completed by getting some code that produces your desired output. Because you've stated this is a challenge I'll not be posting a complete answer, but I'll provide some code to illustrate what I mean.#Nodes/PointsThe current graph looks sorta like the following when unwrapped, note I'm just drawing a sketch to better frame what the code'll be covering.
$$ \color{#000}{\fbox{ w }}{ \color{#2E8B57}{ \xleftarrow[]{\color{#000}{X}} }} \color{#00A}{ \fbox{ u } }{ \color{#2E8B57}{ \xrightarrow[]{\color{#000}{X}} }} \color{#000}{\fbox{ v }} \\ \color{#000}{\fbox{ u }}{ \color{#FF0000}{ \xleftarrow[]{\color{#000}{O}} }} \color{#00A}{ \fbox{ v } }{ \color{#2E8B57}{ \xrightarrow[]{\color{#000}{X}} }} \color{#000}{\fbox{ w }} \\ \color{#000}{\fbox{ v }}{ \color{#2E8B57}{ \xleftarrow[]{\color{#000}{X}} }} \color{#00A}{ \fbox{ w } }{ \color{#FF0000}{ \xrightarrow[]{\color{#000}{O}} }} \color{#000}{\fbox{ u }} $$
Points (the items from the middle column above and defined in the following linked toclass) are only concerned with where it is (address), and thecosts to each of it'sneighbors'saddresses. Thecheapest(ofroutes) method (defined within thePointclass) calculates the returned value at call time so thatneighborscan be added, removed, or modified at any other time.Link to
points/__init__.pyIf ya saved the above to a file path such as
points/__init__.py, importing could look like...Initializing a similarly shaped three pointed graph with bi-directional traveling costs between each point's neighbors could look like...
Getting the information back out would then look like...
The
cheapestofroutesdiffer between pointsuandv, and so far the information of all possible routes are preserved, because this class inherits from thedictclass, that's what theclass Point(dict)did, it's possible to dump it's saved/current state via...$$ \color{#CD8C00}{\fbox{ dict }{ \color{#00A}{\xleftarrow[]{ \color{#000}{\text{super}_{\left(key\_word\_args\right)}} }} \over{ \xrightarrow[\color{#000}{\text{returned value}}]{} } }} \color{#00A}{\fbox{ Point }} $$
Essentially with one class we can model the graph portion of the graph (minus the agents and something to coordinate iterating states through time), plus it abstracted three
forloops and other logic to a something that can be questioned reliably. Utilizing this methodology of splitting things into manageable chunks on the rest of the challenges will make adding more agents, changing algorithms, and even mutating the graph much simpler.#CustomizingPoints (updates)I believe that the total cost is a result of a function that takes
points['w']['population']s travel plans for instance (a property that'll be updated iteratively) andcost(X, orYdepending uponPoint). And not to get too lost in the details but bothXandOcould be functions.Here's two quick examples of how that could look in Python using
lambdas...coststatesXandO, or in this casefirstandbusinessclass tickets.These states a
Pointfor the most part totally doesn't care about from it's frame of reference as a destination that agents leave. At most in the second of the last to examples it would only care about getting it's output by feeding afirst class functioncall. One way to look at is maybe aPointcould be like a dispatcher (a stationary agent) who keeps a roaster of other agents in town and maybe picks up calls (_if they must, and it's not a Monday_), from agents about recent traveling conditions after arriving at a neighbor. Defined this way aPointcould intentionally give bad information to another agent.If you wish to question an edge I think it maybe helpful to re-frame an edge (define a
class) as the points that are populated on an edge, so that things can be asked about those on an edge. This isn't to say that an edge must contain every point between, computers and $\infty$ usually don't mix, but instead an edge could have a way of calculating things like distances between agents or their destination.Sorta; it depends upon how you use those values or choose to update them. I took some care trying to model only part of the problem because covering everything in one post can cause readers to feel a bit like Joel Miller; which is not my intent, I'd rather hope code be seen as another way to break a problem down to the atomic level if need be.
Think of
Pointas a starting point for organizing questions about it's state, the using of those states and final calculations are still a bit further up the stack as far as code execution depth if you want fine-grain control. Suppose in the future you want to model the rising and falling travel of costs based on hour of day or number of iterations of a parent's loop. These and other things can be done within another class that either imports or inherits thePointclass.Inheriting a
Pointto model something based on construction hours for example could look something like...$$ \color{#CD8C00}{\fbox{ dict }{ \color{#00A}{\xleftarrow[]{ \color{#000}{\text{super}_{\left(key\_word\_args\right)}} }} \over{ \xrightarrow[\color{#000}{\text{returned value}}]{} } }} \color{#00A}{\fbox{ Point }{ \color{#2E8B57}{\xleftarrow[]{ \color{#000}{\text{super}_{\left(key\_word\_args\right)}} }} \over{\color{#00A}{ \xrightarrow[\color{#000}{\text{returned value}}]{} }} }} \color{#2E8B57}{\fbox{ Construction }} $$
Link to
points/construction.pyBy having the
Constructionclassinherit fromPointwe gain thesuperpowers of pre-processingcheapestwithin the scope ofConstruction.cheapestmethod. Yes that also means there's now someforloop stacking, but the this is just an example of using the scope stacking that Python almost expects of authors, while also addressing one way to have changingcostcalculations done by a point.Importing the
Constructionpoint if saved underpoints/construction.pycould then look like...Adding a fourth point
cto thepointsdictionary defined previously might then look like...... and updating
points['c']'s neighbors could look like...Which then would require some adjustments to the
forloop that was being used to iterate over points such as...Indeed I've just nested even more loops, so when you're ready, I've addressed one way of mitigating this overall problem on a somewhat related question using
Iterators that allow for further unwrapping of loops into their ownclasses; instances of which are iterable withnext()method calls. Right now what is important to grasp from the adjusted model is that we now have a way to consistently mask the initializedcostvalues within a point based off some other state that is updated. And that regardless of if aPointis a normal point or special point likeConstruction, thecheapestofroutsmethods are what is called by what ever process asking the questions ofpoints.Okay I think it's time for another pause for digestion, hopefully these pointers have helped ya plan out a course.
AgenthintsYou're really close! There are many ways to handle abstracting this part of the problem, I've left some hints within the comments of the question and the answer, but here's just a bit more...
... then having
Bobgo on a random adventure could look a bit like...Output may look similar to...
Those last few lines with
heres populationremovesome name,theres populatedappendsome name, andagents reassignmentaddressare what updates the object states. Figuring out how ya want to store states in a more efficient manor can be the challenge after getting something that works for the problem you want modeled.Now with these tips it's getting really close to a solution which is something that I've been trying to dance about so that there's still a challenge left. So I'm going to call this answer's version complete so there's at least some space left for answers on other aspects.
Hope this all helps in making the task of modeling even more complexity something that is within reach.