From ba8ae28655fa0084e22fb1191aa04fd68f11289d Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Wed, 15 Oct 2014 08:48:51 +0200 Subject: [PATCH 1/9] Simple population dynamics system based on the ideas described in #3. - Add the `ipdt popdyn --generations 10` command - Add the popdyn.py module that run a small population dynamics model, where the geometric growth rate of the proportion of a strategy in the population is computed from its payoff weighted by its abundance. - (DRAFT) HTML visualization of popdyn experiments with a D3.js script (use --html) --- bin/ipdt | 49 +++++++++++++--------- ipdt/export.py | 108 ++++++++++++++++++++++++++++++++++++++++++++----- ipdt/popdyn.py | 64 +++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 ipdt/popdyn.py diff --git a/bin/ipdt b/bin/ipdt index ccd2478..d00b84a 100644 --- a/bin/ipdt +++ b/bin/ipdt @@ -19,7 +19,7 @@ parser = argparse.ArgumentParser(description=__doc__,formatter_class=argparse.Ra parser.add_argument('command', help='Command', type=str, - choices=['tournament', 'list', 'match']) + choices=['tournament', 'list', 'match', 'popdyn']) parser.add_argument('-p','--players', nargs="*", default=[], @@ -28,7 +28,6 @@ parser.add_argument('-e','--exclude', nargs="*", default=[], help="Strategies to exclude codenames") - parser.add_argument('-v','--verbose', help='Verbosity level : -v warning, -vv info, -vvv debug', action="count", @@ -37,6 +36,10 @@ parser.add_argument('-T','--turns', type=int, help='Number of turns', default=100) +parser.add_argument('-g','--generations', + type=int, + help='Number of generations (used only if command is popdyn)', + default=100) parser.add_argument('--replicas', type=int, help='Number of repetition of the tournament', @@ -76,11 +79,13 @@ logger.addHandler(ch) import ipdt.tournament import ipdt.players import ipdt.export +import ipdt.popdyn # === SET THE PARAMETERS === param = {} param["T"] = args.turns param["replicas"] = args.replicas +param["generations"] = args.generations # Payoff matrix order = ["cc","cd","dc","dd"] @@ -94,37 +99,34 @@ if len(args.players) == 0: if len(args.exclude) != 0: args.players = list(set(args.players) - set(args.exclude)) +players = [ getattr(ipdt.players, name).Player for name in args.players] +if args.html: + info = {} + for code,P in zip(args.players,players): + info[P.name] = {"author":P.author, + "name":P.name, + "code":code, + "description":P.__doc__} + if args.command == "match": - if hasattr(ipdt.players, args.players[0]): - P1= getattr(ipdt.players, args.players[0]).Player - if hasattr(ipdt.players, args.players[1]): - P2= getattr(ipdt.players, args.players[1]).Player - - payoff = ipdt.tournament.match(P1,P2,param) + payoff = ipdt.tournament.match(players[0],players[1],param) if payoff[0]>payoff[1]: - winner = "P1" + winner = players[0].name elif payoff[0]==payoff[1]: winner = "NOBODY" else: - winner = "P2" - print("Match endend: {} WINS !".format(winner)) + winner = players[1].name + print("{} vs {}: {} WINS !".format(players[0].name,players[1].name,winner)) logger.info("Payoffs: {}".format(payoff)) if args.command == "tournament": - players = [ getattr(ipdt.players, name).Player for name in args.players] ranking,details = ipdt.tournament.tournament(players,param) print("Tournament ended ! Ranking:") for n,(score,name) in enumerate(ranking): print("{}: {} ({} points)".format(n+1,name,score)) if args.html: - info = {} - for code,P in zip(args.players,players): - info[P.name] = {"author":P.author, - "name":P.name, - "code":code, - "description":P.__doc__} exporter = ipdt.export.HTMLexporter(args.html+".html", ranking, details, @@ -132,7 +134,16 @@ if args.command == "tournament": info) exporter.save() - +if args.command == "popdyn": + time_series, details = ipdt.popdyn.popdyn(players,param) + + if args.html: + exporter = ipdt.export.HTMLexporterTS(args.html+".html", + time_series, + details, + param, + info) + exporter.save() if args.command == "list": print("Available strategies are:") diff --git a/ipdt/export.py b/ipdt/export.py index 22e23f5..b3460a7 100644 --- a/ipdt/export.py +++ b/ipdt/export.py @@ -125,6 +125,20 @@ font-family:monospace; text-align:left; } + +#chart svg { + height: 400px; + color: white; +} + +#ts th, #ts td { + color: black; + text-align:center; + background-color: white; +} + + + """ TEMPLATE = """ @@ -137,7 +151,7 @@ {title} - + @@ -156,6 +170,42 @@ """ +SCRIPT =""" + + + + + +""" + FOOTER = 'GT-MathsBio -- Page generated on the {date} by ipdt.' class HTMLexporter(object): @@ -173,7 +223,7 @@ def __init__(self,path,ranking,payoff_matrix,param,info_strategies): self.sections = [ ("info","Informations",self.general_info(param)), ("ranking","Ranking",self.ranking(ranking,info_strategies)), - ("details","Detailed Results",self.details(ranking, payoff_matrix, + ("details","Detailed Results",self.details(zip(*ranking)[1], payoff_matrix, info_strategies, param)), ] @@ -208,14 +258,13 @@ def general_info(self,param): def get_color(self,score,param): - diff = sorted([(abs(score-param[m]*param["T"]),m) + diff = sorted([(abs(score-param[m]*param["T"]*param["replicas"]),m) for m in ["cc","dc","cd","dd"]], key=lambda x:x[0]) return(self.move_color[diff[0][1]]) - def details(self,ranking,po,info,param): + def details(self,order,po,info,param): s = '\n\n' - order = zip(*ranking)[1] max_po = max([max(po[k].values()) for k in order]) min_po = min([min(po[k].values()) for k in order]) @@ -259,9 +308,9 @@ def ranking(self,ranking,info): for score,code in ranking: s+= ("
  • {1} " "({2}) - {0} pts
  • \n").format(score, - info[code]["name"], - info[code]["author"], - info[code]["description"]) + info[code]["name"], + info[code]["author"], + info[code]["description"]) s += "\n" s += "(Do a mouseover on the name of each strategy to get a short description.)" return s @@ -269,7 +318,7 @@ def ranking(self,ranking,info): def output(self): body = "" for code,title,text in self.sections: - body += "\n\n

    {}

    \n{}".format(code,title,text) + body += "\n\n

    {}

    \n{}\n
    ".format(code,title,text) footer = FOOTER.format(date=time.asctime()) out = TEMPLATE.format(body=body, css=CSS, title="ipdt report",footer=footer) return out @@ -278,6 +327,45 @@ def save(self): with open(self.path,'w') as f: f.write(self.output()) + + +class HTMLexporterTS(HTMLexporter): + def __init__(self,path,time_series,payoff_matrix,param,info_strategies): + self.path = path + + moves = sorted([(move, param[move]) for move in ["cc","dc","cd","dd"]], + key=lambda x:-x[1]) + colors = ['class="green"','class="blue"','class="yellow"','class="red"'] + self.move_color = {} + for n,move in enumerate(moves): + self.move_color[move[0]] = colors[n] + + + self.sections = [ + ("info","Informations",self.general_info(param)), + ("ts","Time series",self.time_series_d3(time_series,info_strategies)), + ("details","Detailed Results",self.details(time_series.keys(), payoff_matrix, + info_strategies, param)), + ] + + def time_series_d3(self,time_series,info): + data = "[\n" + offset = None + for k,v in time_series.items(): + values = [] + if offset is None: + offset = [0]*len(v) + for j,p in enumerate(v): + values.append([j,p]) + offset[j] += p + data += '{{ "key": "{}", "values":{}}},'.format(k,values) + + data += "]\n" + s = SCRIPT.format(data=data) + s += '
    ' + return s + + if __name__ == "__main__": from ipdt.tournament import DEFAULT_PARAM as param a = HTMLexporter("test.html", @@ -292,3 +380,5 @@ def save(self): "naivecoop":{"name":"Naive cooperator","author":"Axelrod"} }) a.save() + + diff --git a/ipdt/popdyn.py b/ipdt/popdyn.py new file mode 100644 index 0000000..1ed8db5 --- /dev/null +++ b/ipdt/popdyn.py @@ -0,0 +1,64 @@ +"""Popdyn.py: population dynamics facilities. """ + +from __future__ import division +import logging +from ipdt.tournament import tournament +logger = logging.getLogger("ipdt") + + +def new_proportions(proportions,payoffs): + """Compute the new proportion of each strategy given the old + proportion and the payoff matrix + + Args: + proportions (list): proportion of each strategy. + payoff (dict of dict): payoff of strategy i against j in the tournament. + Returns: + (list): new proportion of each strategy. + + """ + payoff_w = {} # Payoff weighted by proportions. + out = {} # New proportions + + for i,ai in proportions.items(): + payoff_w[i] = sum([ai*aj*payoffs[i][j] + for j,aj in proportions.items()]) + + total = sum([aj*payoff_w[j] for j,aj in proportions.items()]) + + for i,ai in proportions.items(): + out[i] = (ai * payoff_w[i]/total) + return out + + +def normalize_po(payoffs): + out = {} + order = payoffs.keys() + max_po = max([max(payoffs[k].values()) for k in order]) + min_po = min([min(payoffs[k].values()) for k in order]) + + norm = lambda x: int(100*(x - min_po) / (max_po-min_po)) + + for k in order: + out[k] = {} + for j in order: + out[k][j] = norm(payoffs[k][j]) + return out + +def popdyn(players,param): + + # Strategies start in equiproportion. + proportions = dict([(P.name, 1/len(players)) for P in players]) + # General payoff as given by the tournament. + _,payoffs = tournament(players,param) + + payoffs = normalize_po(payoffs) + + time_series = dict([(k,[v]) for k,v in proportions.items()]) + for g in range(param["generations"]): + proportions = new_proportions(proportions,payoffs) + for k,v in proportions.items(): + time_series[k].append(v) + + + return time_series,payoffs From 4129e6860544af25d57f586c8acb26d1095e51e2 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Wed, 15 Oct 2014 11:33:20 +0200 Subject: [PATCH 2/9] No Normalization if max==min. Fixes #8. --- ipdt/export.py | 2 +- ipdt/popdyn.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ipdt/export.py b/ipdt/export.py index b3460a7..2bb6ed4 100644 --- a/ipdt/export.py +++ b/ipdt/export.py @@ -269,7 +269,7 @@ def details(self,order,po,info,param): max_po = max([max(po[k].values()) for k in order]) min_po = min([min(po[k].values()) for k in order]) - norm = lambda x: int(100*(x - min_po) / (max_po-min_po)) + norm = lambda x: int(100*(x - min_po) / (max_po-min_po)) if (max_po-min_po) else x if len(order)>5: for n,k in enumerate(order) : diff --git a/ipdt/popdyn.py b/ipdt/popdyn.py index 1ed8db5..d6e8578 100644 --- a/ipdt/popdyn.py +++ b/ipdt/popdyn.py @@ -37,7 +37,7 @@ def normalize_po(payoffs): max_po = max([max(payoffs[k].values()) for k in order]) min_po = min([min(payoffs[k].values()) for k in order]) - norm = lambda x: int(100*(x - min_po) / (max_po-min_po)) + norm = lambda x: int(100*(x - min_po) / (max_po-min_po)) if (max_po-min_po) else x for k in order: out[k] = {} From afe6ebeea1808a247d39db49efaa9e91ce845c8b Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Wed, 15 Oct 2014 18:31:24 +0200 Subject: [PATCH 3/9] added a small mutation process --- ipdt/popdyn.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ipdt/popdyn.py b/ipdt/popdyn.py index d6e8578..efbedbb 100644 --- a/ipdt/popdyn.py +++ b/ipdt/popdyn.py @@ -3,6 +3,7 @@ from __future__ import division import logging from ipdt.tournament import tournament +import random logger = logging.getLogger("ipdt") @@ -31,6 +32,27 @@ def new_proportions(proportions,payoffs): return out +def mutation(proportions,mu=1e-2): + N = len(proportions) + + # Get a random partition of the unit segment: + mt = sorted([random.random() for x in range(N-1)]) + [1] + mt = [mt[0]*mu] + [(x-y)*mu for x,y in zip(mt[1:],mt[:-1])] + + # Take the opposite + mmt = [-x for x in mt] + + # Suffle everything + random.shuffle(mt) + random.shuffle(mmt) + + # Add to each proportion a positive and a negative number + # so the sum of proportion does not change. + for k,p,m in zip(proportions.keys(),mt,mmt): + proportions[k] += p - m + + return proportions + def normalize_po(payoffs): out = {} order = payoffs.keys() @@ -56,7 +78,7 @@ def popdyn(players,param): time_series = dict([(k,[v]) for k,v in proportions.items()]) for g in range(param["generations"]): - proportions = new_proportions(proportions,payoffs) + proportions = mutation(new_proportions(proportions,payoffs)) for k,v in proportions.items(): time_series[k].append(v) From 1810ca9a70a02b5df3d6aa31c8de9da66cb0ae48 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 09:46:21 +0200 Subject: [PATCH 4/9] Added a command to control mutation level. --- bin/ipdt | 8 +++++++- ipdt/popdyn.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/ipdt b/bin/ipdt index d00b84a..fcd14b4 100644 --- a/bin/ipdt +++ b/bin/ipdt @@ -36,9 +36,14 @@ parser.add_argument('-T','--turns', type=int, help='Number of turns', default=100) +parser.add_argument('--mu', + type=float, + help='Mutation level (used only if command is popdyn).', + default=0) + parser.add_argument('-g','--generations', type=int, - help='Number of generations (used only if command is popdyn)', + help='Number of generations (used only if command is popdyn).', default=100) parser.add_argument('--replicas', type=int, @@ -86,6 +91,7 @@ param = {} param["T"] = args.turns param["replicas"] = args.replicas param["generations"] = args.generations +param["mu"] = args.mu # Payoff matrix order = ["cc","cd","dc","dd"] diff --git a/ipdt/popdyn.py b/ipdt/popdyn.py index efbedbb..2390471 100644 --- a/ipdt/popdyn.py +++ b/ipdt/popdyn.py @@ -78,7 +78,7 @@ def popdyn(players,param): time_series = dict([(k,[v]) for k,v in proportions.items()]) for g in range(param["generations"]): - proportions = mutation(new_proportions(proportions,payoffs)) + proportions = mutation(new_proportions(proportions,payoffs),param["mu"]) for k,v in proportions.items(): time_series[k].append(v) From b10170841f55a1c59717987f0d26b821219c1448 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 16:11:41 +0200 Subject: [PATCH 5/9] Style adjustment for popdyn export --- ipdt/export.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/ipdt/export.py b/ipdt/export.py index 2bb6ed4..bf1aaa7 100644 --- a/ipdt/export.py +++ b/ipdt/export.py @@ -127,8 +127,11 @@ } #chart svg { - height: 400px; - color: white; + height: 600px; +} + +table{ +margin: auto; } #ts th, #ts td { @@ -137,7 +140,11 @@ background-color: white; } - +.'twhite'{ + fill: rgb(255,255,255); + stroke: rgb(255,255,255); + color: rgb(255,255,255); +} """ @@ -196,6 +203,14 @@ .datum(data) .transition().duration(500).call(chart); + d3.selectAll('#chart svg text') + .style('fill','#839496'); + + d3.selectAll('#nv-controlsWrap') + .style('display','None'); + + + nv.utils.windowResize(chart.update); return chart; From 37be58b39d5f1d9feb236e98c9e959e438867fe2 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 16:25:23 +0200 Subject: [PATCH 6/9] Export for popdyn --- ipdt/popdyn.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ipdt/popdyn.py b/ipdt/popdyn.py index 2390471..993bdea 100644 --- a/ipdt/popdyn.py +++ b/ipdt/popdyn.py @@ -81,6 +81,11 @@ def popdyn(players,param): proportions = mutation(new_proportions(proportions,payoffs),param["mu"]) for k,v in proportions.items(): time_series[k].append(v) - + logger.debug("{}\n{}\n\n".format(g,",\n".join(["{1:<6.0%} {0}".format(n,x) + for n,x in proportions.items()]))) + print "Population dynamics ended: {} generations.".format(g+1) + print "\n".join(["{1:<6.1%} {0}".format(n,x) + for n,x in proportions.items()]) + return time_series,payoffs From 07583d1c54cf36dbd0e500758d4f304a89a3cace Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 16:33:11 +0200 Subject: [PATCH 7/9] Documented popdyn --- README | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README b/README index c00b3c5..d02c500 100644 --- a/README +++ b/README @@ -8,7 +8,7 @@ It has been written for the "[Mathematics and Biology Workgroup](http://www.gt-m École normale supérieure (Paris, France) and it gives [this kind of output](http://www.eleves.ens.fr/home/doulcier/projects/math/ipdt.html). ## How to -### Just test it +### Play around To test it, if you have python2.7 and pip available the installation is quite simple: ```bash @@ -36,7 +36,20 @@ $ ipdt tournament --exclude naivecoop defector ``` -You can also run a single match between two strategies: +We have implemented a small model of population dynamics: + +- The payoffs are computed by a simple tournament. +- Each strategies start with an equal proportion in the population. +- Each generation, the geometric growth of their relative abundance is given + by their payoffs weighted by the encounter probability (product of abundances). + +```bash +# Run a population dynamics model for 10 generations with a null mutation level. +$ ipdt +popdyn --generations 10 --mu 0 ``` + + +Finally, you can also run a single match between two strategies: ```bash $ ipdt match -p naivecoop randomplayer ``` @@ -46,7 +59,7 @@ If you want more detailed info on the output, you can use the options `-vv` info or `-vvv` debug. You can have a [nice HTML5 -export](http://www.eleves.ens.fr/home/doulcier/projects/math/ipdt.html) by +export](http://www.eleves.ens.fr/home/doulcier/projects/math/ipdt.html) (and [for population dynamics](http://www.eleves.ens.fr/home/doulcier/projects/math/popdyn.html)) by using the `--html filename` option: it will create a `filename.html` file in your current folder. From aa59ba414d97e707026426be4ecde0a2f3b5cf38 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 16:34:59 +0200 Subject: [PATCH 8/9] typo fixing in readme --- README | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README b/README index d02c500..38f9c47 100644 --- a/README +++ b/README @@ -45,11 +45,12 @@ We have implemented a small model of population dynamics: ```bash # Run a population dynamics model for 10 generations with a null mutation level. -$ ipdt -popdyn --generations 10 --mu 0 ``` +$ ipdt popdyn --generations 10 --mu 0 +``` Finally, you can also run a single match between two strategies: + ```bash $ ipdt match -p naivecoop randomplayer ``` From eaaec2c8d46cf9d4a38b875461736bf444f6fc66 Mon Sep 17 00:00:00 2001 From: Guilhem Doulcier Date: Thu, 16 Oct 2014 16:36:29 +0200 Subject: [PATCH 9/9] added popdyn in example output --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 38f9c47..0af788f 100644 --- a/README +++ b/README @@ -5,7 +5,7 @@ to organize iterated prisoner's dilemma competition between different strategies. It has been written for the "[Mathematics and Biology Workgroup](http://www.gt-mathsbio.biologie.ens.fr/)" in the -École normale supérieure (Paris, France) and it gives [this kind of output](http://www.eleves.ens.fr/home/doulcier/projects/math/ipdt.html). +École normale supérieure (Paris, France) and it gives [this kind](http://www.eleves.ens.fr/home/doulcier/projects/math/ipdt.html) [of output](http://www.eleves.ens.fr/home/doulcier/projects/math/popdyn.html ). ## How to ### Play around