sofa-framework / sofa

Real-time multi-physics simulation with an emphasis on medical simulation.
https://www.sofa-framework.org
GNU Lesser General Public License v2.1
906 stars 309 forks source link

Updating the dependency graph of component & data/ #597

Open damienmarchal opened 6 years ago

damienmarchal commented 6 years ago

Hi all,

There has been quite a lot of discussion recently about how the to implement update mecanisms. There is of course the DataTracker, but we are quite a lot to implement complementary mecanism in our respectives plugins so it may be a good idea to see what can be factorized into SofaCore. To do that it would be nice to have examples / snippets / draft of the different approaches we have tested or in mind so that we can evaluate the code impact and interoperability.

@bruno-marques, @hugtalbot, @epernod, @jnbrunet feel free to add your ideas


Dedicated Gitter room : https://gitter.im/sofa-framework/data-update?utm_source=share-link&utm_medium=link&utm_campaign=share-link

damienmarchal commented 6 years ago

From gitter:

initData(data1, callback=myUnion), 
initData(data2, callback=myUnion), 
initData(data3, callback=myUnion), 

void myUnion(BaseData* d){ changedData.push_back(d) ; }
bool needUpdate() { return changedData.size() != 0;  }

The idea here was to allow users to add different, per data callback function still allowing a way to implement a "centralized" update. The idea was also to specify in the initData which data are "cached" and thus should trigger update/reinit on change.

damienmarchal commented 6 years ago

From discussion @bruno-marques A kind of DataEngine that detect changes and propagates idleevent on child.

I let bruno explain ;)

Question: what is the difference between DataEngine/ImplicitDataEngine ?

damienmarchal commented 6 years ago

Plugin SofaCoreAsync https://github.com/SofaDefrost/sofa/blob/pluginSofaCoreAsync/applications/plugins/SofaCoreAsync/Sofa/Core/Async/tests/AsyncComponentTracker_test.cpp

Associate to the component a Data<'state'>. This data state can be used to keep track of component state change and propagate lazy update so that the component that depend on other can be updated appropriately. The general design was attempting to combine both synchonous and asynchronous components in the same scene so that "normal" Sofa object can still interact with the one using asynchronous updates.

In the following example a change in python1,2,3 file is reloaded and the ImplicitFeldRenderer & MeshGeneration are updated, each in an asynchronous way.

  Node : {
       ImplicitField : { name : "python1",  src : "python1.py" }
       ImplicitField : { name : "python2", src : "python2.py" }
       ImplicitField : { name : "python3", src : "python3.py" }

       ImplicitFieldRenderer : { src : "@python1" }
       ImplicitFieldRenderer : { src : "@python2" }
       ImplicitFieldRenderer : { src : "@python2" }

       TetrahedralMeshGeneration : { src : "@python1", name = "mesh1"  }
       SurfaceMeshGeneration : { src : "@python2", name = "mesh2"  }
       TetrahedralMeshGeneration : { src : "@python1", name = "mesh3"  }

       MechanicalFEM3D : { src : "@mesh1" }
       MechanicalFEM2D : { src : "@mesh2" }
       MechanicalFEM3D : { src : "@mesh3" }
  }

Note 1: When used in asyncrhonous mode, the current approach does not guarante that a change is propagated immediately. So one change in a component at a given IDLEEvent may be updated in this or an other IDLEEvent. It depend on "when" the dependencies checks the validity of their input.

Node 2: To fix that a queue may be used to keep track of what still needs to be done. Looks good on paper but this kind of implementation are often much more complex than their initial drafted idea.

damienmarchal commented 6 years ago

Hand made tracking

By adding a DataTracker to each component and either overload the HandleEvent function or specific overloads from BaseObject(draw/drawVisual/etc...) to check if the tracked data have changed and trigger an update function if this is the case.

damienmarchal commented 6 years ago

Some references:

damienmarchal commented 6 years ago

A centralized appraoch in its own branch (with a dedicated visitor instead o "hijacking animationstep & idle)). https://github.com/SofaDefrost/sofa/tree/addUpdateTriggerByData

hugtalbot commented 6 years ago

A bit more indirectly, this relates to #265 & #235

marques-bruno commented 6 years ago

Here's a small test class that displays the different features I implemented in that ImplicitDataEngine mother class of mine:

struct TestEngine : public ImplicitDataEngine
{
  SOFAOR_CALLBACK_SYSTEM(TestEngine); // Required to setup the callback mechanism

 public:
  sofa::Data<int> d_a;
  sofa::Data<int> d_b;
  sofa::Data<int> d_c;
  sofa::Data<int> d_a_out;
  sofa::Data<int> d_b_out;

  SOFA_CLASS(TestEngine, ImplicitDataEngine);

  TestEngine()
      : d_a(initData(&d_a, 0, "a", "An input with a callback method")),
        d_b(initData(&d_b, 0, "b", "An input without callback methods")),
        d_c(initData(&d_c, 0, "c", "A simple data field with a callback"))
        d_d(initData(&d_d, 0, "d", "A simple data field without callback"))
  {
    d_a_out.setName("a_out"); // the processed output of a
    d_b_out.setName("a_out"); // the processed output of b
  }

  void init()
  {
    SOFAOR_ADD_INPUT_CALLBACK(&d_a, &TestEngine::increment, false);
    addInput(&d_a);
    addInput(&d_b);
    SOFAOR_ADD_CALLBACK(&d_c);

    addOutput(&d_a_out);
    addOutput(&d_b_out);
    addOutput(&d_c_out);
  }

  void update()
  {
    // do something that's generic for any of the data fields. Called AFTER the data callbacks
  }

 private:
  void increment(sofa::core::objectmodel::BaseData* data)
  {
    d_a.setValue(d_a.getValue() + 1);
  }
  void decrement(sofa::core::objectmodel::BaseData* data)
  {
    d_b.setValue(d_b.getValue() - 1);
  }
};

And here's what happens in the base class:

define SOFAOR_CALLBACK_SYSTEM(T) \

typedef T SOFAOR_CLASS; \ class Callback : public sofaor::common::CallbackFunctor \ { \ typedef void (SOFAOR_CLASS::Func)(sofa::core::objectmodel::BaseData o); \ \ SOFAOR_CLASS m_obj; \ Func m_func; \ \ public: \ Callback(SOFAOR_CLASS _this, Func f) : m_obj(_this), m_func(f) {} \ void call(sofa::core::objectmodel::BaseData data = 0) \ { \ (m_obj->m_func)(data); \ } \ }

define SOFAOR_ADD_CALLBACK(data, callback) \

addDataCallback(data, new Callback(this, callback))

define SOFAOR_ADD_INPUT_CALLBACK(data, callback, trackOnly) \

addInput(data, trackOnly, new Callback(this, callback))


When reinit is called on the component, all callbacks are called and the update class is called (maybe it's a mistake, maybe only the callbacks should be called..). Then outputs are set to dirty, and an IdleEvent visitor is propagated:

void ImplicitDataEngine::reinit() { cleanTrackers(); update(); setDirtyOutputs(); sofa::core::objectmodel::IdleEvent ie; sofa::simulation::PropagateEventVisitor v( sofa::core::ExecParams::defaultInstance(), &ie); this->getContext()->getRootContext()->executeVisitor(&v); }



This allows for a complete and instant refresh of all components taking as an input the dirty outputs without calling the previous components in the pipeline again.
Limitations are that those following components have to be initialized AFTER (in terms of scene graph, so either in subnodes, or after the current engine, in the same node).

Another feature that I don't like much but that was requested by someone who was supposed to use and contribute to my plugin but never did, is the "autolink" feature:

<MySofaORComponent name="mycomp" autolink="true" />

This field allows you to *implicitely* link your data, so that you could avoid setting dozens of fields in your scene description file. The way if works is quite basic:
If autolink is true, then when calling addInput, the internal code checks if the variable has been set. If it has been it doesn't do anything, but if it has:
a previous ImplicitDataEngine in the graph is searched for and if it contains a matching variable with a similar name, it binds them together by calling setParent() on the data. If not, the next engine is searched backwards and the same operation is done, recursively until reaching the first engine in the current node.

If AUTOLINK is set to true, then a big fat msg_advice() is printed in the console, warning the user that this implicit binding can potentially do things they do not expect...
Limitations are, again, that:
- It is not possible to bind implicitely datas that aren't in the same node, or datas that are declared AFTER the current engine (regarding the scene graph)
- The data field name has to be EXACTLY as expected (a input named "points" will only be bound to an output named EXACTLY "points_out"... 

So that's it, It's not perfect but does the job with the way datas are handled in SOFA :)
damienmarchal commented 6 years ago

thanks @bruno-marques for sharing and all the details.