Design issues around tasks and projects

Projects and Task Configuration

This page is to discuss some upcoming design decisions we have to
make about tasks, projects, their user interfaces, and workflows in general.
There are several issues that constrain how development proceeds.

Task and Project Overview

Before we describe issues and propose solutions, let’s settle on some terminology

  • A project is a resource that collects other resources and assigns them roles.
  • A project’s resources are indexed by their roles.
  • A project owns a directory on the filesystem but is encoded as an index file inside that directory. Resources which belong to the project are, by default, kept in the project’s directory.
  • A task is some work that must be performed by a user, but whose completability can be measured programmatically.
  • Only one task can be active at a time. Changes to which task is active can be observed by UI elements.
  • Each task can be in one of several states:
    State Description
    Irrelevant The user’s work in prior tasks mean this task needs no user input.
    Unavailable The task’s dependencies are unmet.
    Incomplete The task is available but its objective is not accomplished.
    Completable The task is available and accomplished but has not been marked complete.
    Completed The task has been marked completed by the user.
  • Task state transitions may be observed.
  • A task may have a dependency on another task. If task B depends on task A, then B will be Unavailable until A is marked Completed.
  • Tasks may store configuration information in JSON, including whether a user has marked them complete or not.
  • When a task changes state, any number of task adaptors observing it may run. These adaptors modify the configuration of other, related tasks.
  • Thus, there are two types of barriers that prevent tasks from reaching completion:
    • Administrative barriers, in the form of a designer-mandated dependency on the completion of some prior task.
    • Technical barriers, in the form of incomplete configuration because a prior or co-occurring task’s adaptor has not run yet.
  • A task may have any number of style identifiers, which can be used to configure user interface elements when it becomes active. For example, an application plugin may provide a behavior which switches what SMTK components are visible whenever the active task changes. Rather than assume the C++ type of the task determines which components should be visible, the behavior asks the task for its style identifiers and searches its configuration to decide which components to hide/show.

Example configuration

To make tasks more clear, let’s consider a pretend example workflow with 3 tasks:

  1. Gather geometric model resources for a combined structural and electromagnetic (EM) simulation.
  2. Assign boundary conditions to model components.
  3. Assign materials to model components.

The high-level task configuration looks like this:

  /* Task configuration: dependencies, style identifiers, & state. */
  "tasks": [ … ],
  /* Task-adaptor configuration: tasks to adapt and how to do so. */
  "adaptors": [ … ],
  /* Map from style identifiers to user interface configuration. */
  "styles": { … },
   /* Dictionary of smtk::view::Configuration objects.
    * These currently reside in attribute resources, but placing
    * them here is an alternative.
  "views": { … }

The tasks section might look like this after a user has completed the first task:

[ { "id": 1, "type": "smtk::task::GatherResources",
    "title": "Load geometry and spec", "state": "completed",
    "resources-by-role": [
      { "role": "structural simulation",
        "type": "smtk::markup::Resource",
        "resources": [ "deadbeef-…" ]},
      { "role": "cavity geometry", "max": 2,
        "type": "smtk::model::Resource",
        "resources": [ "feedface-…" ]},
      { "role": "solver config",
        "type": "smtk::attribute::Resource",
        "resources": [ "b44dc0de-…" ]}
  { "id": 2, "type": "smtk::task::FillOutAttributes",
    "title": "Mark up materials", "state": "incomplete",
    "dependencies": [ 1 ],
    "style": ["material-editing", "show-volumes"],
    "components-by-role": [
      { "role": "material",
        "attributes": [
          { "resource-role": "simulation attribute",
            "resource-type": "smtk::attribute::Resource",
            "component": "*[type=Material]",
            "matching-components": [ "deadc0de-…" ]
  { "id": 3, "type": "smtk::task::FillOutAttributes",
    "title": "Mark up BCs", "state": "incomplete",
    "dependencies": [ 1 ],
    "style": ["bc-editing", "show-surfaces"],
    "components-by-role": [
      { "role": "boundary condition",
        "attributes": [
          { "resource-role": "simulation attribute",
            "resource-type": "smtk::attribute::Resource",
            "component": "*[type=BoundaryCondition]",
            "matching-components": [ "badc0de5-…" ]
    ]} ]

Note that because the first task has been completed, task adaptors for task 1 – which update the configuration of tasks 2 & 3 – have run; these adaptors might have set up resource UUIDs (based on user-assigned resource roles) on tasks 2 & 3.

Also note that tasks 2 & 3 have style identifiers listed. Let’s consider what the style section of the workflow configuration might look like based on a couple of the styles mentioned above:

  /* User interface configurations when assigning materials: */
  "material-editing": {
    /* How to configure the attribute editor */
    "attribute-editor": {
      /* Name a configuration in the 'views' section below */
      "view-configuration": "MaterialView"
    /* We don't have a context menu behavior yet,
       but you can imagine wanting to add material
       assignment options to it. */
    "context-menu": { /* … */ } },
  /* User interface configs for viewing volume meshes. */
  "show-volumes": {
    /* We don't have a 3-D view behavior yet,
       but you can imagine wanting to specify
       the color-by mode and component visibilities. */
    "representation": {
      "color-by": { /* … */ },
      "visiblity": {
        "on": { [ "smtk::model::resource", "volume" ] } 
        "off": { [ "smtk::model::resource", "face" ] } 
      } }
  /* User interface configurations when assigning
     boundary conditions: */
  "bc-editing": {
    /* How to configure the attribute editor */
    "attribute-editor": {
      "view-configuration": {
        /* You could also in-line view configuration here.
         * instead of refering to an entry in the next section.*/
    /* The color-by mode and component visibilities
     * are different for boundary conditions. */
    "representation": {
      "color-by": { /* … */ },
      "visiblity": {
        "on": { [ "smtk::model::resource", "volume" ] } 
        "off": { [ "smtk::model::resource", "face" ] } 
      } }
    /* How to configure the resource browser */
    "resource-panel": {
        "phrase-model" : {
          "type": "smtk::view::ComponentPhraseModel",
          "subphrase-generator": "smtk::view::SubphraseGenerator",
          "badges": [ /* … */ ]
        } }
    } }

Note that in the example above, the 3-D representation configuration is under show-volumes. Because task 2 has two style identifiers listed (material-editing and show-volumes), both will be applied when task 2 becomes active. Being able to assign multiple style identifiers to a single task makes it easy to re-use UI element configurations in several different tasks. For example, we might have another task for mesh inspection where we would also want to show volume components and hide face components. That task might be marked with show-volumes but not material-editing.


Now let’s examine the issues our design has uncovered.

Preserving state vs preserving provenance

Because tasks serialize themselves and their configuration to JSON, it is easy to preserve state. For example, task 2’s state above had specific resources called out by UUID once task 1’s adaptor ran. Portions of this state may change over time as users make decisions. We want to serialize/deserialize the current values in order to get users back to their save point. However, from a provenance viewpoint, it is important to save the time-history of the state in order to be reproducible.

  1. Should we allow both? If so, how?
  2. If we save both, how can we record and display history without it cluttering the current set of tasks a user is focused on?

Designer-specified versus user-specified workflows

In a related fashion, some applications or users want the ability to edit the set of tasks in a workflow to allow iterative exploration/design while other applications or users need to keep the set of tasks fixed to ensure infrequent or novice users are guided through with minimal flexibility (to avoid complexity and clutter).

  1. What is an appropriate interface for accepting task edits by advanced users? How can we know what portions of a task configuration should be copied or not when a user wants to redo a task?
  2. How do we make it clear what types of data are passed between tasks so users can connect and configure them properly?
  3. Also, how would migration to new versions of a project schema work?

Style and preferences

Style identifiers provide a flexible way to tailor user interface elements to each task. However, sometimes users have preferences that may conflict with the styles specified by the workflow designer.

  1. How can we resolve these conflicts?

Storing configuration

We have discussed storing the JSON task configuration in the example above inside SMTK projects.

  1. Is that the best location for it? It presumes that in order to use workflows you must use projects.
  2. Similarly, should we store “current state” with the project but store the “template” for tasks as provided by the workflow designer somewhere else? Storing it somewhere else means common configuration is not part of every user document and might simplify upgrading documents as the workflow changes. However, it complicates things by forcing modelbuilder to look in multiple places each time a project is opened.

@Bob_Obara @johnt @aron.helser @rohith FYI. The prototype style connecting tasks to user-interface configuration is not very fleshed out; some discussion of how we want to configure user interface elements during different tasks would be really helpful.

A few related issues, mostly redux:

Computational jobs: In the past, we have discussed whether computational workloads that take more than a trivial amount time should be represented by (i) a separate design concept from tasks or (ii) a straightforward specialization of tasks. I remain firmly in the “separate design concept” camp and recommend we add “job” or some other suitable name to our workflow system design. Among my reasons are that the most important states to users of computational jobs (queued, provisioning, running, timed out, error, etc.) don’t apply to tasks and aren’t included in the task concept design.

User Interface: There are multiple good ideas for presenting workflow information to users. From a development standpoint I suggest we support two UI designs and implement them in two steps.

  • The first step is to develop a relatively simple UI so that we can get working software in the hands of users as soon as possible. The main idea is to finally get some actionable feedback on what feature set works well, but I also want to use this implementation for relatively simple workflows such as ACE3P projects. In terms of look and feel, I prefer something using a list-style UI with expandable elements, kind of like a cross between the Qt Designer’s property editor and the Vuetify expansion panel.

  • Once we get some traction and get past a few design iterations, I would then undertake a more general-purpose and flexible design. It feels like there is consensus for a node editor approach though alot of details remain to be worked out, of course.

Workflow specification: Might be premature to bring this up, but I hope we can come up with an easier way to author workflow templates than the “big json file” that Bob wrote for ACE3P. Fwiw, with all of the internal cross references, my first thought would be to describe a workflow template with something like an Airtable database and then write a Python script to serialize it out in smtk’s format.

Task instancing: This might be trivial, but just in case… The workflow use cases we discuss typically reuse the same task definition in multiple places. For example, one workflow might include multiple “import model”, “generate mesh”, and/or “submit job” tasks. It might be desirable for each copy to be the same except for id and dependency ids. If that is typical, then our task representation should probably keep the “definition” part of a task segregated from its “placement” in the workflow.

I’m not sure I recall the discussion the same way, but I think we agree they should be separate.

My issue is that tasks reflect user’s work/progress (not computational work), so as far compute jobs are concerned, preparing a job, monitoring a job, or reviewing a job’s output are all fine tasks, but queuing and running the actual job itself are not.

I agree that we don’t want to manually author many of these, but want to hold off on a tool until we have got through user interface prototyping. Since the plan is for at least some workflows to allow users to roll their own workflow, it’s good to start collecting ideas. There are a few different parts to authoring a workflow and we might address them separately:

  1. Instancing tasks (i.e., choosing how to decompose the workflow).
  2. Configuring tasks and adding dependencies/adaptors connecting them.
  3. Adding styles to tasks to control UI components.

This discussion is a precursor to the task-system implementation; see that thread for more information.