Source code for pynfg.classes.iterseminfg

# -*- coding: utf-8 -*-
"""
Implements the iterSemiNFG class

Part of: PyNFG - a Python package for modeling and solving Network Form Games

Created on Mon Feb 18 10:37:29 2013

Copyright (C) 2013 James Bono (jwbono@gmail.com)

GNU Affero General Public License

"""

from __future__ import division
import numpy as np
import scipy as sp
from seminfg import *

[docs]class iterSemiNFG(SemiNFG): """Implements the iterated semi-NFG formalism created by D. Wolpert :arg nodes: members are :class:`nodes.ChanceNode`, :class:`nodes.DecisionNode`, or :class:`nodes.DeterNode`. The basename and time attributes must be set for all nodes used in an iterSemiNFG. :type nodes: set :arg r_functions: One entry for each player. Keys are player names. Values are keyword functions from the basename variables to real numbers. :type r_functions: dict An iterated semi-NFG is a semi-NFG created from iteratively gluing a kernel to a base. It is a Markov iterated semi-NFG if the t'th copy of the kernel is conditionally independent of all nodes in the t-2'th copy and earlier. Instead of utility functions, iterated semi-NFGs use reward functions. .. note:: This class is a subclass of :py:class:`seminfg.SemiNFG`. It inherits all of the SemiNFG functionality except :py:meth:`seminfg.SemiNFG.utiity()` is replaced by :py:meth:`seminfg.iterSemiNFG.reward()`. .. note:: An object that consists of all of these elements except the reward functions is called a iterated semi-Bayes net. When initialized with `r_functions=None`, the result is an iterated semi-Bayes net. .. note:: For a node in nodes, the parent attribute, e.g. :py:attr:`nodes.ChanceNode.parents`, must not have parents that are not in the set of nodes passed to :class:`seminfg.SemiNFG`. Example:: import scipy.stats.distributions as randvars dist1 = randvars.randint params1 = [0, 4] space1 = range(10) distip1 = (dist1, params1, space1) C1 = ChanceNode('C10', distip=distip1, description='root CN randint from 0 to 3', basename='C1', time=0) D1 = DecisionNode('D10', '1', [0, 1], parents=[C1], description='child node of C1. belongs to p1', basename='D1', time=0) D2 = DecisionNode('D20', '2', [0, 1], parents=[C1], description='child node of C1. belongs to p2', basename='D2', time=0) def funcf(var1, var2, var3): total = var1+var2+var3 if total>10: return total-8 elif total<1: return total+2 else: return total paramsf = {'var1': D1, 'var2': D2, 'var3': C1} continuousf = False spacef = range(11) F1 = DeterNode('F10', funcf, paramsf, continuousf, space=spacef, description='a disc. DeterNode child of D1, D2, C1', basename='F1', time=0) nodeset = set([C1,D1,D2,F1]) for t in range(1,4): params1 = [0, F1] distip1 = (dist1, params1, space1) C1 = ChanceNode('C1%s' %t, distip=distip1, description='CN randint from 0 to 3', basename='C1', time=t) nodeset.add(C1) D1 = DecisionNode('D1%s' %t, '1', [0, 1], parents=[C1], description='child node of C1. belongs to p1', basename='D1', time=t) nodeset.add(D1) D2 = DecisionNode('D2%s' %t, '2', [0, 1], parents=[C1], description='child node of C1. belongs to p2', basename='D2', time=t) nodeset.add(D2) D2.randomCPT(setCPT=True) D2.draw_value() D1.randomCPT(setCPT=True) D1.draw_value() paramsf = {'var1': D1, 'var2': D2, 'var3': C1} F1 = DeterNode('F1%s' %t, funcf, paramsf, continuousf, space=spacef, description='a disc. DeterNode child of D1, D2, C1', basename='F1', time=t) nodeset.add(F1) def reward1(F1=0): if F1<3: if F1%2 == 0: x=1 else: x=-1 elif F1<=7: if F1%2 == 1: x=1 else: x=-1 else: if F1%2 == 0: x=1 else: x=-1 return x def reward2(F1=0): return -1*reward1(F1) rfuncs = {'1': reward1, '2': reward2} G = iterSemiNFG(nodeset, rfuncs) G.reward('1', 2) Some useful methods: * :py:meth:`seminfg.SemiNFG.ancestors()` * :py:meth:`seminfg.SemiNFG.descendants()` * :py:meth:`seminfg.SemiNFG.children()` * :py:meth:`seminfg.SemiNFG.loglike()` * :py:meth:`seminfg.SemiNFG.sample()` * :py:meth:`seminfg.SemiNFG.draw_graph()` * :py:meth:`seminfg.iterSemiNFG.reward()` * :py:meth:`seminfg.iterSemiNFG.sample_timesteps()` Upon initialization, the following private methods are called: * :py:meth:`seminfg.SemiNFG._set_node_dict()` * :py:meth:`seminfg.SemiNFG._set_partition()` * :py:meth:`seminfg.SemiNFG._set_edges()` * :py:meth:`seminfg.SemiNFG._topological_sort()` * :py:meth:`seminfg.iterSemiNFG._set_time_partition()` * :py:meth:`seminfg.iterSemiNFG.self._set_bn_part()` """ def __init__(self, nodes, r_functions=None): self.nodes = nodes self.starttime = min([x.time for x in self.nodes]) self.endtime = max([x.time for x in self.nodes]) self._set_node_dict() self._set_edges() self._topological_sort() self._set_partition() self.players = [p for p in self.partition.keys() if p!='nature'] self._set_time_partition() self._set_bn_part() self.r_functions = r_functions def _set_time_partition(self): """Set the time_partition :py:attr:`seminfg.iterSemiNFG.time_partition` :py:attr:`seminfg.iterSemiNFG.time_partition` is a partition of the nodes into their corresponding timesteps. It is a dictionary, where keys are integers 0 and greater corresponding to timesteps, and values are lists of nodes that belong in that timestep, where the order of the list is given by the topological order in :py:attr:`seminfg.iterSemiNFG.iterator` """ self.time_partition = {} for n in self.nodes: if n.time not in self.time_partition.keys(): self.time_partition[n.time] = [n] else: self.time_partition[n.time].append(n) for t in range(self.endtime): self.time_partition[t] = \ [n for n in self.iterator if n in self.time_partition[t]] def _set_bn_part(self): """Set the bn_part :py:attr:`seminfg.iterSemiNFG.bn_part` :py:attr:`seminfg.iterSemiNFG.bn_part` is a partition of the nodes into groups according to nodes in a theoretical base/kernel. It is a dictionary, where keys are basenames, and values are lists of nodes that correspond to that basename. The order of the list is given by the time attribute. """ self.bn_part = {} for n in self.nodes: if n.basename not in self.bn_part.keys(): self.bn_part[n.basename] = [n] else: self.bn_part[n.basename].append(n) for bn in self.bn_part.keys(): self.bn_part[bn].sort(key=lambda nod: nod.time)
[docs] def reward(self, player, t, nodeinput={}): """Evaluate the reward of the specified player in the specified time. :arg player: The name of a player with a reward function specified. :type player: str. :arg nodeinput: Optional. Keys are node names. Values are node values. The values in nodeinput merely override the current node values, so nodeinput does not need to specify values for every argument to a player's reward function. :type nodeinput: dict """ if not self.r_functions: raise AssertionError('This is a semi-Bayes net, not a semi-NFG') kw = {} nodenames = inspect.getargspec(self.r_functions[player])[0] for nam in nodenames: if nam in nodeinput: kw[nam] = nodeinput[nam] else: kw[nam] = self.bn_part[nam][t].value r = self.r_functions[player](**kw) return r
[docs] def npv_reward(self, player, start, delta, nodeinput={}): """Return the npv of rewards from start using delta discount factor :arg player: the name of the player to evaluate :type player: str :arg start: the starting time step :type start: int :arg delta: the discount factor for the npv calculation :type delta: float :arg nodeinput: Optional dict of node name, node values for use in calculating the rewards """ count = 0 npvreward = 0 for t in range(start, self.endtime+1): count += 1 npvreward += (delta**count)*self.reward(player, t, nodeinput) return npvreward
[docs] def sample_timesteps(self, start, stop=None, basenames=None): """Sample the nodes from a starting time through a stopping time. :arg start: the first timestep to be sampled :type start: integer :arg stop: (Optional) the last timestep to be sampled. If unspecified, the net will be sampled to completion. :type stop: integer :arg basenames: (Optional) a list of strings that give the basenames the user wants to collect as output. If omitted, there is no output. :returns: a list of lists of values drawn from the joint distribution given by the net. The order of the lists in the return list is given by the time step, and the order of the values in the list is given by the order of the nodes in the attribute list :py:attr:`seminfg.iterSemiNFG.iterator`. .. warning:: The decision nodes must have CPTs before using this function. """ values = [] if stop==None or stop>self.endtime: stop = self.endtime if basenames: outdict = dict(zip(basenames, [[] for x in range(len(basenames))])) for t in range(start, stop+1): for n in self.time_partition[t]: if n.basename in basenames: outdict[n.basename].append(n.draw_value()) else: n.draw_value() return outdict else: for t in range(start, stop+1): for n in self.time_partition[t]: n.draw_value()