Generalization of model resources

Proposal: Generalize SMTK’s model resource

Many of our projects have models that do not need a full boundary representation (B-Rep) data structure like the half-edge or winged-edge models. Rather than abuse the B-Rep model by accepting incomplete or invalid models, we should have a more strict B-Rep model but provide some alternative that exposes concepts like vertices, edges, and faces without orientation or sense. Also, we would like to have conceptual models that can embody higher-level entities. For example, the Reactor Geometry Generator (RGG) session models nuclear reactor cores as a series of assemblies of rods, pins, and ducts. Anatomical models might model tissues, organs, and the (mechanical, chemical, functional) relationships between them.

These use cases generalize into the need for an abstract, multipartite graph-based model resource class and potentially a domain-specific language to specify use cases in terms of this graph-based model:

  • components of the resource are graph nodes
  • relationships between components are graph edges.

By defining the problem in terms of a graph, we can concisely describe many different models. Components are characterized by their storage and relationships are characterized by the types of components they connect and constraints on those connections (e.g., limits on the number of edges of a given type connecting components).

Desirable features

  • A resource type should be allowed to restrict node and edge types to a fixed set or allow extensions at run time, but as much type-safety as possible should be guaranteed at compile time.
  • Nodes are blobs of model storage owned by a resource and rather uninteresting (to SMTK) except that they are referenced weakly by edges.
  • Edges are owned by the resource just like nodes. They are directed (unidirectional), meaning that they link an originating node to a target node. They may programmatically or at run-time constrain the type of nodes at each endpoint.
  • Introspection of the allowable node and edge types allows user interfaces to present intuitive displays and controls of the model.

Prototype implementation

A prototype of what this resource might look like is here:

If you build this branch, you can run a simple test like so:

./bin/UnitTests_smtk_graph_testing_cxx TestGraphResource

and you should see this output (with different hash codes and roles, but otherwise identical):

Created smtk::brep::Resource
Node types
  smtk::brep::Vertex 4495210551
  smtk::brep::VertexUse 4495210658
Edge types
  smtk::brep::VertexUsedBy 4495210707 role 200243411
  smtk::brep::VertexSense 4495210792 role 200243496


Some open questions:

  • Ideally, while the prototype provides compile-time type safety, we will want to allow run-time, user-defined models. For example, see this extended version of the B-Rep model from the simple test above, encoded as JSON which we might like to consume at run-time. How would programmable types provide unique roles for edges and type information for nodes and edges? Perhaps via the property system? Special subclasses that provide their own index() and typeName() methods?
  • What is needed for our Python interface to allow custom resource, component (node), and relationship (edge) types defined in Python to interoperate with this class.
  • There are situations where we also want to store data on edges of the graph. Links do not provide a way to do this. An example of this need would be sense/orientation information on edges connecting cells to their use-records in a B-Rep model based on the graph-model resource.
  • How should editing of the graph be accomplished?
    • Is there a good way to enforce editing of the model to occur only inside operations? (For example, we might make objects that edit the graph construct-able only with an Operation::Key instance.)
    • Is there a way to introduce checkpointing and make undo-redo automatic?
  • The prototype does not address serialization/deserialization. The hash codes based on type index vary from run to run, so string names must be used but the data structure does not index by name.

@Bob_Obara @tj.corona @aron.helser This is my take on what we have discussed in recent CMB meetings.

@Andinet_Enquobahrie I plan to use this for the OpenCascade integration Ahmet has requested.

I see where you are going - and I could have sworn that I had a discourse page on this very topic but can’t find it :slight_smile:

I completely agree that not all geometric domains should fit into a full BREP model. Its a lot of overkill for simulation workflows that don’t need it (Current RGG, TRUCHAS, ACE3P, etc…) . Before we go a generalize lets take a step back a look at what are current needs are:

  • The ability to describe a geometric domain as a collection of “Bags of Stuff”. I think this is a good description of Exodus types of geometry.
    • There are a predefined set of “Bag Types” - in the case of Exodus this would be node sets, side sets, element sets.
    • There are no formalisms of the relationship between the different bags. For example the element sets don’t know what side sets form their boundaries.

If there are conceptual linkages between the “bags” couldn’t we just have that model resource contain an internal attribute resource to store these associations?

In terms of using this new model representation for OpenCASCADE, why?? OpenCASCADE’s data representation is a formal BREP, why would you want to use something that doesn’t capture the native model’s structure?

I did look before I posted.

The aeva suite needs relationships between bags of stuff in a few different ways

  • side and node sets
  • annotations that are geometric in nature (areas of interest, measurements, feature points/curves/surfaces)
  • transformations of annotations from one model form to another (patient scan to extracted surface/mesh and vice-versa).
    Using an internal attribute is a possibility, but if things get complicated enough to require that, perhaps an external, explicit attribute system is needed.

I believe the graph-based model resource can capture BReps. And for aeva, I think it would be useful as the CAD model will be developed starting with image or surface data. Plus, it will allow a higher-level functional model to live in the same resource as the geometric model.

I definitely think you are speaking to something that needs to be addressed. A couple of thoughts:

  1. Is this a replacement for smtk::model (or its implementation)?

  2. Does the graph structure need to be defined at runtime? A compile-time configurable graph structure would provide a lot more stability by pushing logic checks onto the compiler and allowing for a more tailored API. A runtime-configurable graph structure could be a specialization of a compile-time configured graph, with additional API to accommodate it. The point is that it is generally easier to maintain [EDIT: maintain -> reuse] code if you push it up, not down, the levels of code resolution [e.g. compile-time, static-time, run-time].

  3. Could we decouple the construction, annotation and serialization of the topology into its own independent library? In practice, we can smoosh it back into core, but it seems like it could have a nicely self-contained set of data structures to enforce its logic.

Why enforce this condition? This would be the first instance of restricting a resource’s API to only work within an operation (i.e. there is no precedent), and I don’t see what it buys us.

It needs to mature quite a bit for that, but I am willing for it to be a replacement.

I am OK with it if you have a suggestion. You are most probably thinking of Resource::addEdge and Resource::removeEdge that currently look like this:

  template<typename EdgeType>
  bool addEdge(
    const std::shared_ptr<smtk::resource::Component>& nodeA,
    const std::shared_ptr<smtk::resource::Component>& nodeB)
    return EdgeType::insert(
      std::dynamic_pointer_cast<typename EdgeType::Outgoing>(nodeA),
      std::dynamic_pointer_cast<typename EdgeType::Incoming>(nodeB),
      dynamic_cast<typename EdgeType::ResourceType>(*this));

and which you want to change to something like

  template<typename EdgeType>
  bool addEdge(
    const std::shared_ptr<typename EdgeType::Outgoing>& nodeA,
    const std::shared_ptr<typename EdgeType::Incoming>& nodeB)
    // some kind of enable_if to ensure *this is of EdgeType::ResourceType
    return EdgeType::insert(nodeA, nodeB, 
      dynamic_cast<typename EdgeType::ResourceType>(*this));

I put it in a branch of smtk to make prototyping easy. I am fine putting it in a separate repo in theory, but that is a whole lot of work (build system, CI, contract testing, …) and I have too much on my plate for it.

It enforces what we have been requiring; resources (other than attributes, which to date live on the client in our mental model) should only be modified inside an operation so that applications can be assured their GUI is in sync with the model. This will need to be enforced more rigorously if we are going to really do the client-server split, since operations will run remotely from the client.

I meant more dependency-wise, not project-wise. If we can come up with (or find) a couple of data structures to describe multipartite graph description, the implementation could support a broad range of uses. It would also help to keep its implementation free of model-specific stuff (that would come with its inclusion into a resource).

I agree that, in ModelBuilder, “resourses should only be modified inside an operation”. I am a big fan of resources being useful on their own (outside the context of management, operation, ModelBuilder, etc). That way, operations can guarantee resource-scoped thread safety, but are not required for simple resource manipulation (e.g. within a script). That way we can maintain better encapsulation.

Following up some more on moving things to compile time:

  • Is there something in smtk::resource::DerivedFrom<Self, Parent> that I can use inside std::enable_if to ensure the resource type is acceptable to the edge?

  • It feels like doing things this way (using EdgeType to specify the resource) will disallow compositions we want to permit (i.e., have multiple model resources use graph node and edge types from other resources. Example: aeva might want to compose a resource that pulls in a B-Rep model from one resource type and image segmentation node types from a different resource type. But that starts looking like an inheritance diamond.)

    Maybe a way around this is to pass a base resource but turn Resource::session() into a templated method (which could be templated on the graph type). That would allow modeling-kernel specific stuff to live somewhere that it could be composed without multiple inheritance?

  • It feels like we are going to need something like Brigand if we want to be able to compose models. But having worked on a few session types now, that seems like a feature we want.

I’m still not sure I understand what you want to pull out. Are you saying smtk/graph should not define a Resource class? I think the json serialization/deserialization stuff we already have will separate model-specific stuff from the base graph implementation. The base graph resource could come with Read and Write operations that won’t compile if there’s no to_json()/from_json() methods to handle the node and edge classes registered with it. Similarly, Read and Write would need Import/Export operators for session data. We could make subclasses inherit that burden or add something to detect session data and have it import/export.

Even if we do not enforce resource modification inside operations, we should make it easy on modelbuilder/aeva/etc. I think one way to make it easy is to only define methods that make modifications to resources/components on objects that one must explicitly construct. I don’t necessarily think we need to give up on “operational security” just because of scripting as a use case. We just need to make the scripting use case easy.

@tj.corona So, I am starting to pick this up again. We could change things to be more compile-time by registering node and bond types via tuples:

Desired API

class ExampleResource
  : public smtk::graph::DerivedResource<
  // Types of nodes we want to allow:
  using NodeTypes = std::tuple<
  // Types of bonds we want to allow:
  // Note: using "Bond" here instead of "Edge" since edge conflicts
  // with geometric edges (which are model components).
  using BondTypes = std::tuple<
    ConnectAToB, ConnectBToA
  ExampleResource() { }
  virtual ~ExampleResource() { }

// ...
auto exResource = resourceManger->create<ExampleResource>();
auto compA = exResource->addNode<ExampleComponentTypeA>();
auto compB = exResource->addNode<ExampleComponentTypeB>("fancyConstructor");
auto linkAtoB = exResource->addBond<ConnectAToB>(compA, compB);

No further work would be required to create a new graph-based resource, since it would inherit component insertion/removal and bond insertion/removal from its parent class (more below). It would require work to define subclasses of Component and especially some structs or classes that describe what types of bonds are allowed between nodes and how to manage them.

Bonds between components

Specifically, bonds like ConnectAToB and ConnectBToA in the example above would need to provide:

  • a tuple of acceptable (source node type, destination node type) pairs;
  • a unique Role to use for links representing this type of bond;
  • an alias (using/typedef) to another bond type if inverse links should be added;
  • a functor to validate that a given source and destination node are allowed to be linked with the bond’s role. Some utilities might provide common validators (1-to-1, 1-to-many, many-to-1).

Base resource class

Given the constraints above on nodes and bonds, the graph resource class would need to look like:

namespace smtk::graph {

template<typename ChildClass>
class Resource
  : public smtk::resource::DerivedFrom<
  template<typename NodeType, typename ...Params>
  NodeType* addNode(Params ...params)
    // Verify that NodeType is an element of the
    // ChildClass::NodeTypes tuple.
    static_assert(tuple_has(ChildClass::NodeTypes, NodeType,
      "Nodes of this type are not allowed in the resource.");

    auto node = std::make_shared(NodeType(params...));
    return node.get();

    typename BondType,
    typename SourceNodeType,
    typename DestinationNodeType>
  Links::Key addBond(
    SourceNodeType* src, DestinationNodeType* dst
    // Verify that (1) BondType is an element of the
    // ChildClass::EdgeTypes tuple, that (2)
    // SourceNodeType is an element of the
    // BondType::SourceNodeTypes tuple and
    // that (3) DestinationNodeType is an element of
    // the BondType::DestinationNodeTypes tuple.
      "Resource does not allow bonds of this type.");
      "Bond cannot link these node types.");

    // Run-time checks also allowed (for example, to force
    // partitions to have exclusive membership):
    if (!BondType::allowBond(src, dst))
      return Links::Key{};

    // Making up some new ResourceLinks API:
    Key link = this->links().addComponentLink(
      src->id(), dst->id(), BondType::Role);

    // Use something to add the inverse link (from
    // dst->id() to src->id() if BondType::Inverse exists)

    return link;
  1. Does this make sense? Is it OK for smtk::graph::resource to be templated on its child class like this since it must be an abstract class? I seem to recall you saying that Resource classes should not have template parameters (hence smtk::resource::DerivedFrom<>) but can’t recall why.
  2. I have some more questions about Links and how to handle the special case where I want the resource to store an ordering of the links. I had mentioned earlier that perhaps components could store an ordering of links, but upon reflection that would make removing components from the resource difficult; the nice thing about links is that their data is owned by the resource.

I’ve pushed a semi-functional prototype of this new design to the same branch as above:

and will continue to work on it.

Graph-based (Node and Arc) Modeling Resource

After much back and forth with @tj.corona (who ended up writing the final version), the multipartite graph resource is now in master (SMTK MR 2102). For some introductory documentation of the final design, see our user’s guide as well as a simple-to-read unit test.

CAD Model Session

Also, we have started work on an OpenCASCADE (OCC) session that uses the graph-based modeling resource and can import STEP, IGES, and OCC files for viewing. There are no other operations yet, but we plan to expand on it to support our aeva (annotation and exchange of virtual anatomy) application. Here’s one of the sample files we test with:

The number of model vertices, edges, and faces of CAD models can get quite large and we are working on scaling up SMTK and ParaView to deal with them.

Other Uses

While our prototypes are focused on traditional boundary-representation models (because they are among the most complex models we deal with), the idea behind the graph-based model resource is to express any kind of model. High-level models might:

  • live “on top” of geometric models
    • models that group parts into functional areas – like parts of engineered products related to power generation, distribution, control, human interaction, etc.;
    • models that classify geometric models for further analysis – like satellite imagery that marks geometric regions with land-use categories for environmental simulation, or
  • have no geometric interpretation at all
    • because they are abstract in nature – such as social network models that work with nodes that represent people, conversation topics, or trends; and relationships between them such as co-occurrence frequencies; or
    • because they do not have a physical embodiment design yet —such as thermodynamic process models like Otto, Brayton, Stirling or other cycles that are being studied before selection for a detailed design.

The fact that these models may not have geometry associated with some or all of the components is irrelevant and orthogonal to the modeling process. You can choose to provide renderable geometry for components by implementing a geometry backend as part of your resource. Or you can choose not to.