From 5420501d365adf8f1b31e563f7b430c7df592acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fernando=20S=C3=A1nchez?= Date: Mon, 7 May 2018 18:57:41 +0200 Subject: [PATCH] Fix state and networkx dynamic attributes --- soil/VERSION | 2 +- soil/agents/__init__.py | 22 ++++++++++++++------ soil/environment.py | 45 ++++++++++++++++++++--------------------- soil/history.py | 15 +++++++++++--- tests/test_main.py | 18 ++++++++++++++++- 5 files changed, 68 insertions(+), 34 deletions(-) diff --git a/soil/VERSION b/soil/VERSION index 027934e..2bb6a82 100644 --- a/soil/VERSION +++ b/soil/VERSION @@ -1 +1 @@ -0.11.1 \ No newline at end of file +0.11.3 \ No newline at end of file diff --git a/soil/agents/__init__.py b/soil/agents/__init__.py index 6399e23..29562e3 100644 --- a/soil/agents/__init__.py +++ b/soil/agents/__init__.py @@ -53,7 +53,7 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent): self.alive = True real_state = deepcopy(self.defaults) real_state.update(state or {}) - self._state = real_state + self.state = real_state self.interval = interval if not hasattr(self, 'level'): @@ -67,10 +67,17 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent): @property def state(self): - return self._state + ''' + Return the agent itself, which behaves as a dictionary. + Changes made to `agent.state` will be reflected in the history. + + This method shouldn't be used, but is kept here for backwards compatibility. + ''' + return self @state.setter def state(self, value): + self._state = {} for k, v in value.items(): self[k] = v @@ -79,21 +86,24 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent): key, t_step = key k = history.Key(key=key, t_step=t_step, agent_id=self.id) return self.env[k] - return self.state.get(key, None) + return self._state.get(key, None) def __delitem__(self, key): - self.state[key] = None + self._state[key] = None def __contains__(self, key): - return key in self.state + return key in self._state def __setitem__(self, key, value): - self.state[key] = value + self._state[key] = value k = history.Key(t_step=self.now, agent_id=self.id, key=key) self.env[k] = value + def items(self): + return self._state.items() + def get(self, key, default=None): return self[key] if key in self else default diff --git a/soil/environment.py b/soil/environment.py index 552bf36..0790d9d 100644 --- a/soil/environment.py +++ b/soil/environment.py @@ -263,31 +263,30 @@ class SoilEnvironment(nxsim.NetworkEnvironment): history = self[agent.id, None, None] if not history: continue - for t_step, state in reversed(sorted(list(history.items()))): - for attribute, value in state.items(): - if attribute == 'visible': - nowvisible = state[attribute] - if nowvisible and not lastvisible: - laststep = t_step - if not nowvisible and lastvisible: - spells.append((laststep, t_step)) + for t_step, attribute, value in sorted(list(history)): + if attribute == 'visible': + nowvisible = value + if nowvisible and not lastvisible: + laststep = t_step + if not nowvisible and lastvisible: + spells.append((laststep, t_step)) - lastvisible = nowvisible - else: - key = 'attr_' + attribute - if key not in attributes: - attributes[key] = list() - if key not in lastattributes: - lastattributes[key] = (state[attribute], t_step) - elif lastattributes[key][0] != value: - last_value, laststep = lastattributes[key] - value = (last_value, t_step, laststep) - if key not in attributes: - attributes[key] = list() - attributes[key].append(value) - lastattributes[key] = (state[attribute], t_step) + lastvisible = nowvisible + continue + key = 'attr_' + attribute + if key not in attributes: + attributes[key] = list() + if key not in lastattributes: + lastattributes[key] = (value, t_step) + elif lastattributes[key][0] != value: + last_value, laststep = lastattributes[key] + commit_value = (last_value, laststep, t_step) + if key not in attributes: + attributes[key] = list() + attributes[key].append(commit_value) + lastattributes[key] = (value, t_step) for k, v in lastattributes.items(): - attributes[k].append((v[0], 0, v[1])) + attributes[k].append((v[0], v[1], None)) if lastvisible: spells.append((laststep, None)) if spells: diff --git a/soil/history.py b/soil/history.py index cf8b725..72ced57 100644 --- a/soil/history.py +++ b/soil/history.py @@ -102,7 +102,9 @@ class History: t_steps=t_steps, keys=keys) r = Records(df, filter=key, dtypes=self._dtypes) - return r.value() + if r.resolved: + return r.value() + return r @@ -216,16 +218,23 @@ class Records(): return i.iloc[ix] except KeyError: return self.dtypes[f.key][2]() - return self + return list(self) def __getitem__(self, k): n = copy.copy(self) n.filter(k) - return n.value() + if n.resolved: + return n.value() + return n def __len__(self): return len(self._df) + def __str__(self): + if self.resolved: + return str(self.value()) + return ''.format(self._filter) + Key = namedtuple('Key', ['agent_id', 't_step', 'key']) Record = namedtuple('Record', 'agent_id t_step key value') diff --git a/tests/test_main.py b/tests/test_main.py index a5d39c2..2b9063f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -233,10 +233,26 @@ class TestMain(TestCase): There is a bug in networkx that prevents it from creating a GEXF file from geometric models. We should work around it. """ - G = nx.random_geometric_graph(20,0.1) + G = nx.random_geometric_graph(20, 0.1) env = environment.SoilEnvironment(topology=G, dry_run=True) env.dump_gexf('/tmp/dump-gexf') + def test_save_graph(self): + ''' + The history_to_graph method should return a valid networkx graph. + + The state of the agent should be encoded as intervals in the nx graph. + ''' + G = nx.cycle_graph(5) + distribution = agents.calculate_distribution(None, agents.BaseAgent) + env = environment.SoilEnvironment(topology=G, network_agents=distribution, dry_run=True) + env[0, 0, 'testvalue'] = 'start' + env[0, 10, 'testvalue'] = 'finish' + nG = env.history_to_graph() + values = nG.node[0]['attr_testvalue'] + assert ('start', 0, 10) in values + assert ('finish', 10, None) in values + def make_example_test(path, config): def wrapped(self):