-
Notifications
You must be signed in to change notification settings - Fork 36
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.