- 
                Notifications
    
You must be signed in to change notification settings  - Fork 38
 
Introducing PartialOrder #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 21 commits
0aca75d
              da4fe74
              b7d8835
              1c92c81
              031e3a9
              87c1a67
              6824321
              ed26c84
              b79cb47
              f44387f
              e00f63c
              e5a685a
              164a68d
              a88c502
              ccbca4b
              55ee2e8
              2adbd02
              d89ee63
              93f3952
              9651875
              f05da6f
              8c8d637
              d4941f5
              744ccba
              4fcb952
              829926c
              ab10364
              23ae475
              07268fd
              a5341ad
              d28fb49
              31b963a
              a9e96ab
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """ | ||
| ******************************************************************************** | ||
| compas_fab.datastructures | ||
| ******************************************************************************** | ||
| 
     | 
||
| .. currentmodule:: compas_fab.datastructures | ||
| 
     | 
||
| Plan | ||
| ----- | ||
| 
     | 
||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| :nosignatures: | ||
| 
     | 
||
| Action | ||
| DependencyIdException | ||
| IntegerIdGenerator | ||
| Plan | ||
| 
     | 
||
| """ | ||
| 
     | 
||
| from .plan import ( | ||
| Action, | ||
| DependencyIdException, | ||
| IntegerIdGenerator, | ||
| Plan | ||
| ) | ||
| 
     | 
||
| 
     | 
||
| __all__ = [ | ||
| 'Action', | ||
| 'DependencyIdException', | ||
| 'IntegerIdGenerator', | ||
| 'Plan', | ||
| ] | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,328 @@ | ||||||
| from __future__ import absolute_import | ||||||
| from __future__ import division | ||||||
| from __future__ import print_function | ||||||
| 
     | 
||||||
| import threading | ||||||
| from collections import OrderedDict | ||||||
| from copy import deepcopy | ||||||
| from itertools import count | ||||||
| 
     | 
||||||
| import compas | ||||||
| from compas.base import Base | ||||||
| from compas.datastructures import Datastructure | ||||||
| from compas.datastructures import Graph | ||||||
| 
     | 
||||||
| __all__ = [ | ||||||
| 'Action', | ||||||
| 'DependencyIdException', | ||||||
| 'IntegerIdGenerator', | ||||||
| 'Plan', | ||||||
| ] | ||||||
| 
     | 
||||||
| 
     | 
||||||
| class IntegerIdGenerator(Base): | ||||||
| """Generator object yielding integers sequentially in a thread safe manner. | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| start_value : :obj:`int` | ||||||
| First value to be yielded by the generator. | ||||||
| """ | ||||||
| def __init__(self, start_value=1): | ||||||
| super(IntegerIdGenerator, self).__init__() | ||||||
| self.last_generated = start_value - 1 | ||||||
| self._lock = threading.Lock() | ||||||
| self._generator = count(start_value) | ||||||
| 
     | 
||||||
| def __next__(self): | ||||||
| with self._lock: | ||||||
| self.last_generated = next(self._generator) | ||||||
| return self.last_generated | ||||||
| 
     | 
||||||
| # alias for ironpython | ||||||
| next = __next__ | ||||||
| 
     | 
||||||
| @property | ||||||
| def data(self): | ||||||
| return { | ||||||
| 'start_value': self.last_generated + 1 | ||||||
| } | ||||||
| 
     | 
||||||
| def to_data(self): | ||||||
| return self.data | ||||||
| 
     | 
||||||
| @classmethod | ||||||
| def from_data(cls, data): | ||||||
| return cls(data['start_value']) | ||||||
| 
     | 
||||||
| @classmethod | ||||||
| def from_json(cls, filepath): | ||||||
| data = compas.json_load(filepath) | ||||||
| return cls.from_data(data) | ||||||
| 
     | 
||||||
| def to_json(self, filepath): | ||||||
| compas.json_dump(self.data, filepath) | ||||||
| 
     | 
||||||
| 
     | 
||||||
| class DependencyIdException(Exception): | ||||||
| """Indicates invalid ids given as dependencies.""" | ||||||
| def __init__(self, invalid_ids, pa_id=None): | ||||||
| message = self.compose_message(invalid_ids, pa_id) | ||||||
| super(DependencyIdException, self).__init__(message) | ||||||
| 
     | 
||||||
| @staticmethod | ||||||
| def compose_message(invalid_ids, pa_id): | ||||||
| if pa_id: | ||||||
| return 'Planned action {} has invalid dependency ids {}'.format(pa_id, invalid_ids) | ||||||
| return 'Found invalid dependency ids {}'.format(invalid_ids) | ||||||
| 
     | 
||||||
| 
     | 
||||||
| class Plan(Datastructure): | ||||||
                
       | 
||||||
| """Data structure extending :class:`compas.datastructures.Graph` for creating | ||||||
| and maintaining a partially ordered plan (directed acyclic graph). | ||||||
| The content of any event of the plan is contained in an | ||||||
| :class:`compas_fab.datastructures.Action`. The dependency ids of a planned | ||||||
| action can be thought of as pointers to the parents of that planned action. | ||||||
| While actions can be added and removed using the methods of | ||||||
| :attr:`compas_fab.datastructures.Plan.graph`, it is strongly recommended | ||||||
| that the methods ``plan_action``, ``append_action`` and ``remove_action`` | ||||||
| are used instead. | ||||||
| 
     | 
||||||
| Attributes | ||||||
| ---------- | ||||||
| graph : :class:`compas.datastructures.Graph | ||||||
| id_generator : Generator[Hashable, None, None] | ||||||
                
       | 
||||||
| Object which generates keys (via ``next()``) for | ||||||
| :class:`compas_fab.datastructures.Action`s added using this object's | ||||||
| methods. Defaults to :class:`compas_fab.datastructures.IntegerIdGenerator`. | ||||||
| """ | ||||||
| def __init__(self, id_generator=None): | ||||||
| super(Plan, self).__init__() | ||||||
| self.graph = Graph() | ||||||
| self.graph.node = OrderedDict() | ||||||
| self._id_generator = id_generator or IntegerIdGenerator() | ||||||
| 
     | 
||||||
| @property | ||||||
| def networkx(self): | ||||||
| """A new NetworkX DiGraph instance from ``graph``.""" | ||||||
| return self.graph.to_networkx() | ||||||
| 
     | 
||||||
| @property | ||||||
| def actions(self): | ||||||
| """A dictionary of id-:class:`compas_fab.datastructures.Action` pairs.""" | ||||||
| return {action_id: self.get_action(action_id) for action_id in self.graph.nodes()} | ||||||
| 
     | 
||||||
| def get_action(self, action_id): | ||||||
| """Gets the action for the associated ``action_id`` | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| action_id : hashable | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| :class:`compas_fab.datastructures.Action` | ||||||
| """ | ||||||
| action = self.graph.node_attribute(action_id, 'action') | ||||||
| if action is None: | ||||||
| raise Exception("Action with id {} not found".format(action_id)) | ||||||
| return action | ||||||
| 
     | 
||||||
| def remove_action(self, action_id): | ||||||
| action = self.get_action(action_id) | ||||||
| self.graph.delete_node(action_id) | ||||||
| return action | ||||||
| 
     | 
||||||
| def plan_action(self, action, dependency_ids): | ||||||
| """Adds the action to the plan with the given dependencies, | ||||||
| and generates an id for the newly planned action. | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| action : :class:`comaps_fab.datastructures.Action` | ||||||
| The action to be added to the plan. | ||||||
| dependency_ids : set or list | ||||||
| The keys of the already planned actions that the new action | ||||||
| is dependent on. | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| The id of the newly planned action. | ||||||
| """ | ||||||
| self.check_dependency_ids(dependency_ids) | ||||||
| action_id = self._get_next_action_id() | ||||||
| self.graph.add_node(action_id, action=action) | ||||||
| for dependency_id in dependency_ids: | ||||||
| self.graph.add_edge(dependency_id, action_id) | ||||||
| return action_id | ||||||
| 
     | 
||||||
| def append_action(self, action): | ||||||
                
       | 
||||||
| """Adds the action to the plan dependent on the last action added | ||||||
| to the plan, and generates an id for this newly planned action. | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| action : :class:`comaps_fab.datastructures.Action` | ||||||
| The action to be added to the plan. | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| The id of the newly planned action. | ||||||
| """ | ||||||
| dependency_ids = set() | ||||||
| if self.graph.node: | ||||||
| last_action_id = self._get_last_action_id() | ||||||
| dependency_ids = {last_action_id} | ||||||
| return self.plan_action(action, dependency_ids) | ||||||
| 
     | 
||||||
| def _get_last_action_id(self): | ||||||
| last_action_id, last_action_attrs = self.graph.node.popitem() | ||||||
| self.graph.node[last_action_id] = last_action_attrs | ||||||
| return last_action_id | ||||||
| 
     | 
||||||
| def _get_next_action_id(self): | ||||||
| return next(self._id_generator) | ||||||
| 
     | 
||||||
| def get_dependency_ids(self, action_id): | ||||||
| """Return the identifiers of actions upon which the action with id ``action_id`` is dependent. | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| action_id : hashable | ||||||
| The identifier of the action. | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| :obj:`list` | ||||||
| A list of action identifiers. | ||||||
| 
     | 
||||||
| """ | ||||||
| return self.graph.neighbors_in(action_id) | ||||||
| 
     | 
||||||
| def check_dependency_ids(self, dependency_ids, action_id=None): | ||||||
| """Checks whether the given dependency ids exist in the plan. | ||||||
| 
     | 
||||||
| Parameters | ||||||
| ---------- | ||||||
| dependency_ids : set or list | ||||||
| The dependency ids to be validated. | ||||||
| action_id : hashable | ||||||
| The id of the associated action. Used only in | ||||||
| the error message. Defaults to ``None``. | ||||||
| 
     | 
||||||
| Raises | ||||||
| ------ | ||||||
| :class:`compas_fab.datastructures.DependencyIdException` | ||||||
| """ | ||||||
| dependency_ids = set(dependency_ids) | ||||||
| if not dependency_ids.issubset(self.graph.node): | ||||||
| invalid_ids = dependency_ids.difference(self.graph.node) | ||||||
| raise DependencyIdException(invalid_ids, action_id) | ||||||
| 
     | 
||||||
| def check_all_dependency_ids(self): | ||||||
| """Checks whether the dependency ids of all the planned actions | ||||||
| are ids of planned actions in the plan. | ||||||
| 
     | 
||||||
| Raises | ||||||
| ------ | ||||||
| :class:`compas_fab.datastructures.DependencyIdException` | ||||||
| """ | ||||||
| for action_id in self.actions: | ||||||
| self.check_dependency_ids(self.get_dependency_ids(action_id), action_id) | ||||||
| 
     | 
||||||
| def check_for_cycles(self): | ||||||
| """"Checks whether cycles exist in the dependency graph.""" | ||||||
| from networkx import find_cycle | ||||||
| from networkx import NetworkXNoCycle | ||||||
| try: | ||||||
| cycle = find_cycle(self.networkx) | ||||||
| except NetworkXNoCycle: | ||||||
| return | ||||||
| raise Exception("Cycle found with edges {}".format(cycle)) | ||||||
| 
     | 
||||||
| def get_linear_sort(self): | ||||||
| """Sorts the planned actions linearly respecting the dependency ids. | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| :obj:`list` of :class:`compas_fab.datastructure.Action` | ||||||
| """ | ||||||
| from networkx import lexicographical_topological_sort | ||||||
| self.check_for_cycles() | ||||||
| return [self.get_action(action_id) for action_id in lexicographical_topological_sort(self.networkx)] | ||||||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want these to work in Ironpython?  | 
||||||
| 
     | 
||||||
| def get_all_linear_sorts(self): | ||||||
| """Gets all possible linear sorts respecting the dependency ids. | ||||||
| 
     | 
||||||
| Returns | ||||||
| ------- | ||||||
| :obj:`list` of :obj:`list of :class:`compas_fab.datastructure.Action` | ||||||
                
       | 
||||||
| :obj:`list` of :obj:`list of :class:`compas_fab.datastructure.Action` | |
| :obj:`list` of :obj:`list` of :class:`compas_fab.datastructure.Action` | 
Uh oh!
There was an error while loading. Please reload this page.