Expanding Support for Expressions

Currently in SMTK’s attribute resource, a ValueItem can either be a constant or can reference an attribute that represents an expression. Expressions are then used in the exporting process and are transcribed into a format needed by the simulation. However they are not evaluated by the attribute resources and are treated as “black boxed” entities.

We are now seeing the need to begin evaluating these expressions with SMTK both in terms of the GUI as well as in scripts. In some cases the expression would not be transcribed in the export process but, in the case of an expression that represents a “constant”, instead be evaluated and the result would be used instead.

Expressions as Variables

Consider the case where the designer provides a set of variables that the engineer can use or combine into more complex expressions:

a = 15
b = 30
c = a * cos(b)

The value item could be set to a, b, or c. In the GUI panel the current evaluation of the expression would be displayed. Note that expressions can be associated with items that are non-scalar so the same would need to be true concerning these variable expressions. Also care must be taken to avoid recursion. In the case of the above example, a could not be set to b * c.

Expressions as functions of time and/or space (or other “free” variables)

This is the case where the expression truly is a function that needs to be evaluated with additional parameters and is the more common case of expressions.

Possible Approaches

Custom Items

We now have the ability to create new Item Types and Definitions. In theory we could create a new type of item that supports the above functionality. There are a couple of issues this brings up:

  1. Expressions are currently referencing attributes not items. In addition to creating a custom item, there would need to be policy concerning the attributes definition as well. For example if an item’s expression requires attributes of type DoubleVariable, then there would need to be an “understanding” that DoubleVariable contains a single item that provided the functionality of double variable.
  2. Evaluation requires going “into” the attribute and examine its internal structure.

Custom Attributes

Currently there is only one class for attributes and one class for attribute definitions. In theory we could extend these classes and have new attribute classes that provide the required API to support variables and expression evaluation. This does mean that, as in the case of new item types, these new classes would need to be registrable. This also does blur the line between information representation and functional evaluation.

Evaluators

We also could add a new class of objects, Evaluators, that take in the attribute and provide an evaluation API. These could be registered to a manager and when an attribute is to be evaluated, the system would check to see if an evaluator had been registered for that type of attribute.

Update

After discussing this with fellow developers we are going to be doing the Evaluator/Query approach meaning that though the attribute will store the content needed to represent an expression, the capabilities of evaluating the expression will be the responsibility of the Evaluator. This should provide a fair amount of flexibility. (Note that the reason I’m using the term evaluator is that the current query mechanism returns a standard function - based on the requirements I’m thinking the evaluator may be more of an object but I could be wrong :slight_smile: ).

Theory of Operation

Requirements

  1. Evaluators should know when they need to update themselves when the attribute(s) they depend on are modified.
  2. There should be a way to determine the attributes that an Evaluator depends on.
  3. An Evaluator should be able to describe why it can not evaluate.
  4. It should be possible to determine if an attribute is being used for evaluation for the following GUI use cases:
    • The user wants to delete an attribute that is used as a “variable”. The GUI should be able to put up a warning saying its in use (and if so who is using it)
    • The user is about to modify an attribute “variable”. The GUI should be able to put up a warning saying what will be effected
    • An item is using an attribute as an expression and needs to update when the “expression” has changed.

Evaluator Concept

An Evaluator contains an attribute (probably a weak pointer to one). As its name implies an Evaluator provides the means of evaluating an attribute. It provides the following methods:

  1. bool canEvaluate(smtk::io::Logger&) - returns true if the evaluator can do an evaluation. Since the attribute (and other dependences - more about that later) can change during the lifetime, we will need the ability to ask if the evaluator is still evaluatable during its lifetime.

  2. bool evaluate(…) - perform an evaluation. The reason for the ellipsis is that the user may need to provide arguments.

    • Alternatively we could pass in an object that provides access to the arguments. For example it could provide something like
      • setArgDouble(int i, double val), bool argDouble(int i, double &val)
      • setArgString(int i, const string&val), bool argInt(int i, int &val)
      • setArgInt(int i, int val), bool argString(int i, string &val)
  3. unsign int numberOfArgs() - returns how many independent arguments are needed. For the use cases we have these could be all of the same type int/double - not sure if we need strings lol.

  4. result() - this could also be part of the evaluate method. The one think I would like to avoid is a heavy weight mechanism like using an attribute as the result. Unlike operations, evaluations should be relatively lite weight. For our current use cases we could once again assume that the result type is the same as the argument type or adopt the same mechanism for arguments (returning an object that contains the results).

  5. attribute() - returns the attribute of the Evaluator.

Registering Evaluator factory functions

  1. Evaluator Generator functions take in an attribute of a specified definition and returns an evaluator. This could be a smart pointer to an evaluator or a reference to one .
  2. Attribute::Resource::addEvaluatorGenerator(EvaluatorGeneratorMethod, Attribute::Definition/Type) - This allows new generators to be added to a resource. Note that only one Generator can be registered to a Definition. If a second Generator is registered it will override the first.
  3. Attribute::Resource::createEvaluator(Attribute::AttributePtr&) - returns a new evaluator for an attribute.
    • If there is no evaluator assigned to the attribute’s Definition, then it’s base Definition is used if it has one. This continues until either an evaluator is found or there are no more Definitions to look up.

Use Cases

Simple string parse expression

Let’s assume we have a Definition called SimpleExpression with a string item called expString. The aim to to allow the user to create an infix expression string consisting of *,+,-,/ and numerical constants. There is also an Evaluator class called mySimpleExpressionEval that takes in no arguments but returns a double that corresponds to the expression string. A generator method (could simply be a static method of the class) is registered to the attribute::Resource’s evaluator generator functions.

  • canEvaluate() will return false under the following conditions:
    • the attribute’s expString is not set
    • the string is not parsable
  • In the act of determining if we can evaluate, we probably been force to parse
  • evaluation of “(1.5 + 2.5) * 2” will return 8
  • evaluation of (3*b) would fail since b would not be allowed based on our simple rules.

Should evaluate call canEvaluate?

The above example brings up an interesting question. Should evaluate be force to call canEvaluate? Well if the string was never parsed and the parsing occurred in the canEvaluate method then evaluate would have to call canEvaluate at least once. But what about subsequent calls? In reality, the only time it needs to be called is when something is modified - in this case the attribute’s string. In this simple example it would be trivial for the evaluator to store the string it parsed and then do a string comparison on evaluate() to see if it was changed. More complex use case will find this approach difficult as in the case of the next example.

Poly-linear Function

In this simple use case let’s assume the attribute consists of an extensible set of 2D points that represents a poly-linear y=f(x). In this case the table should assume to be monotonically increase in the first column. This time the Evaluator will require 1 argument (x) in the evaluate method.

  • canEvaluate will return false if any of the table’s values are unset or if the first components are not monodically increasing

Should evaluate call canEvaluate?

Note that in this case, it is more difficult to determine if canEvaluate must be called during evaluate(). Simply copying the groups values and then comparing them will be somewhat costly. It does seem that the evaluator will need someway of being notified if the attribute is modified.

Observers or a direct connection to the attribute itself?

In theory we could have the evaluator observe the attribute. The main issue here is that any changes to the attribute that should trigger the observation need to go through an operation and not through the attribute’s API. Alternatively the attribute could be “connected” to the evaluator and thereby notifying it when it is changed.

Modeling Variables

This is one of the most complex use cases. Let’s extend the first use case of Simple Expression to now support variables. In this instance a variable is another evaluatable attribute. So Lets assume we have the following:

  • attribute a’s expString = “5”
  • attribute b’s expString = “a*3”
  • attribute c’s expString = “b*a”

And we want to evaluate c. What do we do? Lets take our simple string parsing evaluator and make a slight change:

  • addInvalidVariable(attribute::AttributePtr& a) - this method tells the evaluator that if it sees a variable attribute a then there is a problem - stop.

Calling canEvaluate on c will do the following :

  • when it comes across b, it will:
    • create an evaluator for b and call bEvaluator->addInvalidVariable( c)
    • call bEvaluator->canEvaluate() - if false stop!
  • when it comes across a, it will:
    • create an evaluator for a and call aEvaluator->addInvalidVariable( c)
    • call aEvaluator->canEvaluate() - if false stop!
      In the end we would have something that looks like the following:


To see why we needed the addInvalidMethod, lets make the following change:

  • attribute a’s expString = “c-5”

Now we have made a recursive loop - since we told the other evaluators c is off-limits we will get error we expect from canEvaluate.

Mix and Match

We can now combine the use cases into one.

  • a is defined as a poly-linear attribute
  • b is a string expression = “a(4)*10”
  • c is a string expression = “a(3)+b”
    If we create an evaluator for c, it will create the appropriate evaluators for a and b.

Representing arguments explicitly

Suggestion - we could reserve the use of $0, $1…$n as arguments in the parse string that need to be specified during evaluation so if we wanted to make c be a “function” it could look like:

  • a is defined as a poly-linear attribute
  • b is a string expression = “a($0)*10”
  • c is a string expression = “a($0)+b”

Meeting the Above Requirements

Evaluators knowing when canEvaluate needs to be called

As previously mentioned, we have 2 possible options:

  • Using observers so that an evaluator knows its “cache” is invalid and needs to recheck.
  • Having the attribute hold a list of evaluators that are using it (via a weak pointer).

Either method should work even for the complex use cases since if the evaluator contains evaluators for sub-expressions, it can test those when checking to see if it needs to apply its canEvaluate method.

Determining the attribute(s) an Evaluator depends on

Since the dependency is modeled as internal evaluators and they can provide their attribute dependancies this should be easy to satisfy.

Determining issues with evaluation

This is why canEvaluate takes in a Logger so that it can return issues it encounters. Alternatively we could provide a reporting method that could return a const reference to an internal Logger instead.

Determining if an attribute is being used in an expression (or by other "variables)

If we opt for the approach where the attribute knows what evaluators are using it, then this will be a simple thing to support. If instead, we were to use Observers, I’m not sure how this would be done since no one is responsible for keeping track of evaluators.

Note: that this would not tell use which items are using an attribute “variable” but using the links system (and the fact that ValueItem uses a ReferenceItem to maintain its “link” to the expression, we could indicate which attributes would be effected (and if need be do a search to find the item(s) using it).

Determining if a GUI for a ValueItem needs to be updated due to attribute changes

In theory we could solve this using the Observer mechanism. Currently the Signal operation (which is used by the GUI) is ignored by the SMTK’s Qt View classes since this Signal comes from bridging the Qt Signal mechanism to SMTK’s Observer mechanism. If the Item’s qtItem class was to instead observe the Operation Manager for any changes (including the Signal operation) to the attributes its expression depends on we should be to properly update its value.

Other Requirements/Topics to discussion

Predefined Definitions/Evaluators

  • Should SMTK provide some ready to use expression definitions and evaluators?
    • If yes - should they be always added into any attribute resource?
      • if yes then this would be trivial to do. You could even derived from these Definitions and still use the core Evaluators.
      • If no, then how does the designer indicate he/she wants to use them? One possibility is to have a set of utility functions/methods that would insert these Definitions into an Attribute Resource. We could then have a “Builtin” section to the SBT file indicating which definitions should be added.

How to Extend String Parsing?

The current design assumes that the Evaluator has everything it needs to parse an expression. But what if a specific workflow wants to add operators/functions that where not present in the core? I can think of a few possible approaches:

Override the Evaluator Registration Function

In theory a plugin could define a replacement Evaluator that adds the new functionality. Depending on how the original Evaluator is structured will dictate the amount of code that can be reused.

Provide a “Context” object that can be passed into the Evaluator

The Context could provide access to newly added functionality as well as providing access to information not defined within the scope of the attribute that maybe required to perform the evaluation. One of the biggest issue with this approach is the GUI side. Without forcing designers to produce custom View and ItemViews I’m not sure how the GUI elements would know which context to create/use.

Provide a Parsing Manager

With T.J.'s modifications to creating managers, we could define one that a plugin could register new operators/functions when it is loaded.

1 Like

As resources that inherit from smtk::geometry::Resource, attribute resources and their components already have a mechanism that can perform the task of your Evaluator: Query objects. There are several examples of their use, including computing a component’s bounding box and the distance between a point and a component.

Actually Query is defined for all resources, but most of the existing ones are defined in geometry::Resource.

To make sure I am calibrated,

Does that statement imply that the value item stores the name of the variable (“a”, “b”, or “c”) and that there is some TBD mechanism to represent the variable and its evaluation method(s)?

If so, my first inclination is that this feature should be a new/separate set of classes that, ideally, can be used across all resources not just the attribute system. A component bounding box evaluator, for example, might be useful in any application using model resources whether or not attribute resources are involved.

Do you mean something like this?

Yeah I sure think so. I would also want it to derive from a common base class with all of the potential evaluation methods/signatures and some way to query which ones are applicable to each subclass.

I think the smtk::resource::Query solution may have the functionality you are describing. Though it is light on documentation at the moment, it works like this:

Producing classes (e.g. those that compute the bounding box) register their Query type with the resource upon construction (or later via registration, but this is WIP):

namespace
{
typedef std::tuple<MyBoundingBox> QueryList;
}

Resource::Resource()
{
  queries().registerQueries<QueryList>();
}

Consuming classes (e.g. those that use the bounding box) ask the resource for a smtk::geometry::BoundingBox query object:

  auto& boundingBox = resource->queries().get<smtk::geometry::BoundingBox>();
  std::array<double, 6> extent = boundingBox(resource);

Internally, the Query system walks the inheritance tree of its Query types and picks a suitable class that inherits from the interface class (in this case smtk::geometry::BoundingBox).

Another way of thinking of it is like this: the pattern enforces an API with an abstract base class (e.g. smtk::geometry::BoundingBox) and concrete derived classes (e.g. smtk::mesh::BoundingBox), but also facilitates runtime registration (e.g. for plugins) of Query instances. That way, you can write a better BoundingBox implementation using an optional dependency and not have to change the source code of the resource that uses it.

If you have any questions, I’d be happy to chat with you about how the Query works in more detail.

The Query design also has logic for handling data caching and clearing stale caches when resources and components are manipulated.

My intention was that variables are represented as attributes that can be evaluated. So the current mechanism of using a reference item implementation would still be in effect.

Does that mean expression symbols can be used to represent scalar or vectors of types including

  • double
  • resource
  • component
  • integer (?)
  • string (?)

Right now the plan is that anything that can be assigned an expression today should be all to use the mechanism being discussed here (so all ValueItems for now but not resource or component).

However - that is not to say the Queries/Evaluators could not be more general in scope and could be used for other non-expression activities.

smtk::resource::Query (which resides in smtk/resource/query/Query.h) is a different animal from smtk::resource::queryOperation(). It has the ability to cache information (for example, it could store an instance of smtk::common::TypeMap that would connect string values to int, double, float, …). I think that this pattern would be very useful for implementing what you currently call an Evaluator.

I have recently abstracted SMTK’s managers to the point where a manager can now be registered. I would suggest exploiting this feature to keep Evaluator registration conceptually similar to everything else in SMTK that is registered.

Beyond what @tj.corona has pointed out, what we are discussing seems to involve 3 patterns already present in SMTK. It seems like it might be useful to provide some class declarations to talk about:

  • the Query pattern allows different resources to implement a common API; thus we can generalize the Evaluator into something like
    class Context;
    class Function
    {
    public:
      // Put some bounds on the types of values expressions can return.
      // This list could be longer, but ultimately it should be fixed-length.
      using ValueType = std::variant<
        std::string,
        long long,
        unsigned long long,
        double,
        std::vector<double>>;
      // A function can be evaluated given a context.
      // If it returns true, the result is valid. If it returns false,
      // then at least one error message had been added to the log.
      // The context is the source of available variables as well as
      // the object used to detect cyclic expressions.
      bool operator() (
        Context& context, ValueType& result, smtk::io::Logger& log);
    
      // Functions must also be able to provide dependencies.
      // A context is required because it provides lookups for variable names.
      // The output dependentExpressions is a set of variables referenced by
      // this function which were present in the context. The unmetDependencies
      // are missing ("free") variables without which function evaluation will fail.
      bool dependencies(
        Context& context,
        std::set<std::string>& dependentExpressions,
        std::set<std::string>& unmetDependencies,
        smtk::io::Logger& log);
      };
    
    class Context
    {
    public:
      // Keep track of components we've already evaluated
      std::set<Component*> m_visitedExpressions;
    
      // Return a component to evaluate given a name.
      // Even free variable names (time, space) would be modeled as components;
      // those components would be created by the context subclass.
      virtual Component* expression(const std::string& name) const = 0;
    };
    
    class Evaluator : public Query
    {
    public:
      virtual Function operator() (resource::Component& subject, smtk::io::Logger& log);
    };
    
  • Next, the Generator pattern (smtk::common::Generator) could be used by the attribute system’s specific Evaluator implementation. Specifically, the Context would use the generator pattern to look up how to evaluate an attribute of a given Definition type.
    using DefinitionName = std::string;
    using Generator =
      smtk::common::Generator<DefinitionName, std::unique_ptr<Function>>;
    
  • Finally, plugins allow the registration of custom context types and custom attribute-definition evaluator-generators. This would allow plugins to inject context types that provide application-specific information (e.g., application version, username, current date+time, etc.).
1 Like

I have some minor comments/questions that might be obsolete by now, nonetheless:

1. Might want to consider overloading the second argument to use a query filter string, so that we can extend to other resource type, e.g., a bounding box evaluator for model entities.

2. Would the “a” and “b” in the expression strings be the attribute name? (I presume so)

3. std::variant is c++17, yes? So I presume we’re talking another boost library for now.

4. Side note: There might be a use case on Aashish’s project for evaluators that produce string output. It seems that some ATS features take strings of the form RegionName_Property, e.g. “east-side-mesh_porosity”. Go figure…

Yes. Sigh.

After some discussion, I’ll be adding a section to the top post of this topic to describe in more detail how this sketch might handle all the use cases and requirements.

Also, one thing the sketch doesn’t include is positional parameters which may be useful in some cases.

Privacy Notice