From 3712655b84ae8ce7110214febf85c9ef481660ae Mon Sep 17 00:00:00 2001 From: "Olivier J.N. Bertrand" <bolirev@hotmail.com> Date: Sat, 9 Dec 2017 18:22:33 +0100 Subject: [PATCH] Add moving function and their test --- navipy/database/__init__.py | 18 ++++ navipy/moving/agent.py | 187 +++++++++++++++++++++++++++++------- navipy/moving/maths.py | 35 ++++--- 3 files changed, 189 insertions(+), 51 deletions(-) diff --git a/navipy/database/__init__.py b/navipy/database/__init__.py index ddd6ae2..7db665c 100644 --- a/navipy/database/__init__.py +++ b/navipy/database/__init__.py @@ -306,6 +306,24 @@ database posorient.set_index('id', inplace=True) return posorient + def read_posorient(self, posorient=None, rowid=None): + assert (posorient is None) or (rowid is None),\ + 'posorient and rowid can not be both None' + if posorient is not None: + rowid = self.get_posid(posorient) + # Read images + tablename = 'position_orientation' + toreturn = pd.read_sql_query( + """ + SELECT * + FROM {} + WHERE (rowid={}) + """.format(tablename, rowid), self.db) + toreturn = toreturn.loc[0, :] + toreturn.name = toreturn.id + toreturn.drop('id') + return toreturn + def read_image(self, posorient=None, rowid=None): """Read an image at a given position-orientation or given id of row in the \ database. diff --git a/navipy/moving/agent.py b/navipy/moving/agent.py index ced17a7..5561fde 100644 --- a/navipy/moving/agent.py +++ b/navipy/moving/agent.py @@ -3,32 +3,38 @@ """ import numpy as np import pandas as pd +import networkx as nx from pandas.api.types import is_numeric_dtype from navipy.database import DataBaseLoad -from .maths import next_pos, \ - closest_pos, \ - closest_pos_memory_friendly, \ - __mode_moves_supported +import navipy.moving.maths as navimomath class AbstractAgent(): - # Define a default mode_of_motion - __mode_move = {'mode': 'on_cubic_grid', - 'param': {'grid_spacing': 1}} def __init__(self, database, memory_friendly=False): if isinstance(database, DataBaseLoad): - self.__db = database + self.db = database else: raise TypeError('database should be of type DataBaseLoad') if memory_friendly: self.__posorients = None else: - self.__posorients = self.__db.get_posorients() + self.__posorients = self.db.get_posorients() + # set mode of motion + mode_move = {'mode': 'on_cubic_grid', + 'param': {'grid_spacing': 1}} + self.mode_of_motion = mode_move + + @property + def posorients(self): + toreturn = self.__posorients + if toreturn is not None: + toreturn = toreturn.copy() + return toreturn @property def mode_of_motion(self): @@ -36,7 +42,8 @@ class AbstractAgent(): """ toreturn = self.__mode_move toreturn['describe'] = \ - __mode_moves_supported[self.__mode_move['mode']]['describe'] + navimomath.mode_moves_supported()[ + self.__mode_move['mode']]['describe'] return toreturn @mode_of_motion.setter @@ -50,8 +57,9 @@ class AbstractAgent(): raise KeyError("'mode' is not a key of mode") if 'param' not in mode: raise KeyError("'param' is not a key of mode") - if mode['mode'] in __mode_moves_supported.keys: - for param in __mode_moves_supported[mode['mode']]['param']: + if mode['mode'] in navimomath.mode_moves_supported().keys(): + for param in navimomath.mode_moves_supported()[ + mode['mode']]['param']: if param not in mode['param']: raise KeyError( "'{}' is not in mode['param']".format(param)) @@ -61,40 +69,50 @@ class AbstractAgent(): def abstractmove(self, posorients_vel): - # NEED TO CHECK posorients_vel - + if isinstance(posorients_vel, pd.Series) is False: + raise TypeError('posorients_vel should be a pandas Series') + for col in ['x', 'y', 'z', 'yaw', 'pitch', 'roll', + 'dx', 'dy', 'dz', 'dyaw', 'dpitch', 'droll']: + if col not in posorients_vel.index: + raise KeyError( + 'posorients_vel should have {} as index'.format(col)) # Compute the next position - posorients_vel = next_pos(posorients_vel, - move_mode=self.__mode_move['mode'], - move_param=self.__mode_move['param']) + posorients_vel = navimomath.next_pos( + posorients_vel, + move_mode=self.__mode_move['mode'], + move_param=self.__mode_move['param']) # Compute the closest possible position if posorients_vel is None: posorients_vel[['x', 'y', 'z', 'yaw', 'pitch', 'roll']] = \ - closest_pos_memory_friendly(posorients_vel, self.__db) + navimomath.closest_pos_memory_friendly( + posorients_vel, + self.db) else: posorients_vel[['x', 'y', 'z', 'yaw', 'pitch', 'roll']] = \ - closest_pos(posorients_vel, self.__posorients) + navimomath.closest_pos( + posorients_vel, + self.__posorients) return posorients_vel class Single(AbstractAgent): - __posorientvel = pd.Series(['x', 'y', 'z', - 'yaw', 'pitch', 'roll', - 'dx', 'dy', 'dy', - 'dyaw', 'dpitch', 'droll'], - dtype=np.float, - data=np.nan) - # Define a list of supported mode in a dictionary - # key mode, val is list of required parameters def __init__(self, database, initial_condition, memory_friendly=False): super().__init__(database, memory_friendly) + + self.__posorientvel = pd.Series( + index=['x', 'y', 'z', + 'yaw', 'pitch', 'roll', + 'dx', 'dy', 'dz', + 'dyaw', 'dpitch', 'droll'], + dtype=np.float) + if isinstance(initial_condition, pd.Series): if is_numeric_dtype(initial_condition): for key in self.__posorientvel.index: @@ -154,13 +172,16 @@ class Single(AbstractAgent): raise TypeError('vel should be a pandas Series') -def Multi(AbstractAgent): +class Multi(AbstractAgent): - def __init__(self, database, - memory_friendly=False): - super().__init__(database, memory_friendly) + def __init__(self, database): + super().__init__(database, False) # Init the graph - self.__graph = None + self.__graph = nx.DiGraph() + for row_id, posor in self.db.get_posorients().iterrows(): + posor.name = row_id + self.__graph.add_node(row_id, + posorient=posor) @property def graph(self): @@ -168,12 +189,104 @@ def Multi(AbstractAgent): @graph.setter def graph(self, graph): - # Check that graph is properly formatted - pass + if isinstance(graph, nx.DiGraph) is False: + raise TypeError('graph is not a nx.DiGraph') + self.__graph = graph.copy() + self.check_graph() def build_graph(self, callback_function): # Build a graph with luises code - pass + for node in self.__graph.nodes: + posorient = self.__graph[node]['posorient'] + next_node = callback_function(posorient) + if next_node not in self.__graph.nodes(): + raise ValueError('Node does not exist') + else: + self.__graph.add_edge(node, next_node) + self.check_graph() + + def check_graph(self): + self.check_single_target() + + def check_single_target(self): + for node in self.__graph.nodes: + # not connected -> loop not ran + for count, _ in enumerate(self.__graph.neighbors(node)): + # count == 0 -> connected to one node + # count == 1 -> connected to two nodes + if count > 0: + raise ValueError( + 'Node {} leads to several locations'.format(node)) + + def find_attractors(self): + """Return a list of node going to each attractor in a graph + """ + attractors = list() + for attractor in nx.attracting_components(self.__graph): + att = dict() + att['attractor'] = attractor + attractors.append(att) + return attractors + + def find_attractors_sources(self, attractors=None): + """Find all sources going to each attractors + """ + if attractors is None: + attractors = self.find_attractors() + + if isinstance(attractors, list) is False: + raise TypeError('Attractors should be a list of dict') + elif len(attractors) == 0: + raise ValueError('No attractors found') + + # Check attractor + for att in attractors: + keyatt = att.keys() + if 'attractor' not in keyatt: + raise ValueError( + 'Each attractors should contain the key attractor') + + # Calculate connection + for att_i, att in enumerate(attractors): + + # [0] because one node of the attractor is enough + # all other node of the attractor are connected to this one + target = list(att['attractor'])[0] + attractors[att_i]['paths'] = \ + nx.shortest_path(self.graph, target=target) + attractors[att_i]['sources'] = \ + list(attractors[att_i]['paths'].keys()) + return attractors + + def catchment_area(self, attractors=None): + """Return the catchment area for attractors + """ + if attractors is None: + attractors = self.find_attractors_sources() + + if isinstance(attractors, list) is False: + raise TypeError('Attractors should be a list of dict') + elif len(attractors) == 0: + raise ValueError('No attractors found') + + # Check attractor + for att in attractors: + keyatt = att.keys() + if 'sources' not in keyatt: + raise ValueError( + 'Each attractors should contains a list of sources') + + return [len(att['sources']) for att in attractors] + + def reach_goals(self, goals): + """ Return all paths to the goals """ + return nx.shortest_path(self.__graph, target=goals) - def reach_goals(): - pass + def neighboring_nodes(self, target): + """ Return the nodes going to the target """ + # Reverse graph because nx.neighbors give the end node + # and we want to find the start node going to target + # not where target goes. + tmpgraph = self.__graph.reverse(copy=True) + neighbors = tmpgraph.neighbors(target) + return neighbors diff --git a/navipy/moving/maths.py b/navipy/moving/maths.py index 4487a37..49b3471 100644 --- a/navipy/moving/maths.py +++ b/navipy/moving/maths.py @@ -5,16 +5,18 @@ geometry, and predefined grids shapes import numpy as np import pandas as pd -__mode_moves_supported = { - 'on_cubic_grid': { - 'param': + +def mode_moves_supported(): + return { + 'on_cubic_grid': { + 'param': ['grid_spacing'], 'describe': "Agent restricted to move on a grid"}, - 'free_run': { - 'param': [], - 'describe': - "Freely moving agent, pos(t+dt)=pos+speed (dt=1)"}} + 'free_run': { + 'param': [], + 'describe': + "Freely moving agent, pos(t+dt)=pos+speed (dt=1)"}} def next_pos(motion_vec, move_mode, move_param=None): @@ -28,23 +30,28 @@ def next_pos(motion_vec, move_mode, move_param=None): ..todo: add literal include for supported_grid_mode """ - assert isinstance(motion_vec, pd.Series),\ - 'motion vector must be a pandas Series' - assert move_mode in __mode_moves_supported,\ - 'move mode must is not supported {}'.format(move_mode) + if isinstance(motion_vec, pd.Series) is False: + raise TypeError('motion vector must be a pandas Series') + if move_mode not in mode_moves_supported().keys(): + raise KeyError( + 'move mode must is not supported {}'.format(move_mode)) speed = motion_vec.loc[['dx', 'dy', 'dz']] - position = motion_vec.loc[['x', 'y', 'z']] if move_mode == 'on_cubic_grid': grid_spacing = move_param['grid_spacing'] - speed /= np.linalg.norm(speed) + if np.linalg.norm(speed) > 0: + speed /= np.linalg.norm(speed) scaling = grid_spacing / (2 * np.sin(np.pi / 8)) elif move_mode is 'free_run': scaling = 1 # <=> dt = 1, user need to scale speed in dt units else: raise ValueError('grid_mode is not supported') - return position + speed.rename({'dx': 'x', 'dy': 'y', 'dz': 'z'}) * scaling + toreturn = motion_vec + toreturn.loc[['x', 'y', 'z']] += speed.rename({'dx': 'x', + 'dy': 'y', + 'dz': 'z'}) * scaling + return toreturn def closest_pos(pos, positions): -- GitLab