Problem Statement
More often than not, when a node M in a graph resource is connected to another node N with an arc of some type T, there is also a reverse connection from N to M with an arc of type U. Thus, arc types T and U are inverses of one another. There is not currently a way to enforce the consistency of this bidirectional relationship (i.e., it is possible to remove the (M, N) arc of type T without removing the (N, M) arc of type U).
There are 2 broad classes of solution to this problem.
- Make a single arc class that handles relationships in both directions.
- Provide the existing unidirectional arc classes with information about their inverses.
Both have some common problems:
- Arcs of any type may have inverse arcs of any type (the “n-squared” problem), so there is significant combinatorial complexity when adding a new arc type – it must know how to accommodate inverses of all other arc types in the system. This also makes extensible systems difficult. Just considering the base arc types SMTK provides, we have:
Arc Type Inverse Arc Type Note Arc Arc One-to-one in both directions Arc Arcs One-to-many + many-to-one Arcs Arcs One-to-many in both directions OrderedArcs Arc One-to-ordered + one-to-one OrderedArcs Arcs One-to-ordered + one-to-many OrderedArcs OrderedArcs One-to-ordered in both directions - Updating inverse relationships may require application-specific knowledge. This is especially true for ordered arcs where there are additional constraints on how arcs are ordered relative to one another.
The question in both cases is how to provide developers with a way to
- specify how to update relationships in a consistent manner that is specific to the particular pair of arc types.
- edit arcs conveniently and consistently.
Use Cases
Groups
Group membership is often handled by a pair of arc types. The simplest case would be:
class Members : public smtk::graph::arcs::Arcs<Group, Node> { };
class ParentGroup : public smtk::graph::arcs::Arc<Node, Group> { };
However, frequently, a component may be allowed to have membership in multiple groups, so the ParentGroup
arc may also subclass Arcs
rather than Arc
. This complicates removing a member from a group:
- In the single-group-membership case removing node N from group G must assign
nullptr
to that node N’sParentGroup
node (in addition to removing N from G’s membership). - In the multiple-group-membership case, removing node N from group G must remove group G from node N’s set of parent groups.
If we instead had a single container for both arc directions we might express it like so:
class Membership :
public smtk::graph::arcs::BidirectionalArc<
One<Group>,
Many<Node>
>
{ };
The One<>
and Many<>
templates specialize storage to the particular type of relationships required, but now BidirectionalArc<>
has 3 or 4 cases to handle (one-to-one, many-to-one, many-to-many, and possibly one-to-many depending on whether you consider it distinct).
Alternately, we could provide an inverse arc-type parameter to each of our initial unidirectional arcs:
class Members
: public smtk::graph::arcs::Arcs<Group, Node, Inverse=ParentGroup>
{ };
class ParentGroup
: public smtk::graph::arcs::Arc<Node, Group, Inverse=Members>
{ };
but now each arc type must either
- provide some uniform API so that inverse relationships can be updated, or
- handle each possible inverse arc type specially (and thus prevent extensibility to new inverse arc types).
Boundary Representation (BRep) Face Loops
A face loop is an ordered set of oriented edges that bound a face. Given a face and an integer index into the loop, we can get an edge (plus its orientation relative to the loop, achieved by examining which vertices are shared with neighboring arcs).
class Loop : public smtk::graph::arcs::OrderedArcs { };
Its inverse is each edge’s list of faces. Given an edge, we can obtain a list of faces that it participates in bounding:
class Faces : public smtk::graph::arcs::Arcs { };
If we remove a face, in the process of removing its arcs we should also remove references to the face in each edge participating in its boundary. Likewise, removing an edge should “break” the face loops of all attached faces.
However, in the latter case, there may be additional processing required to mark the loops as “open” rather than “closed” (say, by inserting a reference to a special edge with vertices “at infinity”). How can we provide this special processing to the arc classes?
One solution is to provide methods on the subclasses
class Loop : public smtk::graph::arcs::OrderedArcs
{
void removeEdge(std::size_t indexOfEdge)
{
// Remove the edge from self, then
// remove the face from the edge's Faces,
// and finally insert an "edge at infinity."
}
};
class Faces : public smtk::graph::arcs::Arcs
{
void removeFace(const std::shared_ptr<Face>&)
{
// Remove the face from self and then
// remove *all* edges from the face's Loop.
}
};
However, now programmers must know that these specific methods must be called and know not to call the methods on the base arc classes.
Open Questions
- Is a single, bidirectional arc class preferable to two complementary unidirectional arcs classes?
- Is there a simple mechanism for application-specific graph resources to provide methods for editing bidirectional relationships consistently?