32. Reducing model complexity

To improve performance, AGX Dynamics offers a numbers of approaches to reduce the number of bodies active in a simulation but a the same time retain as much dynamic behaviour as possible. Either the merging of bodies can be done manually using MergedBody or via the Adaptive Model Order Reduction system (AMOR).

32.1. MergedBody – many bodies simulated as one

A merged body is can consist of several agx::RigidBody objects merged together, handled as one single agx::RigidBody in the solver. This functionality can be used to dramatically reduce the system size, as seen from the solver, but preserving the mass properties/inertia of a merged system. It can be seen as an extension of AutoSleep functionality, but with the huge benefit, that it support merging/sleeping on moving/dynamics bodies. Whereas AutoSleep has the big limitation that it only support sleeping on non moving bodies (zero velocity in world frame).

The functionality of agx::MergedBody can be used in two ways:

  • Explicitly/manual: By explicitly creating EdgeInteractions (Section 32.1.3) between bodies which are added to an agx::MergedBody the user has full control over how bodies are merged and split.

  • Automatic: Section 32.2 describes functionality for automatic merge/split based on relative velocities, contacts and force measurements.

// Create a new merged body.
agx::MergedBodyRef mergedBody = new agx::MergedBody();
// Active the merged body by adding it to the simulation.
simulation->add( mergedBody );
// Deactivating the merged body by removing it from the simulation.
simulation->remove( mergedBody );

32.1.1. Applications

Let’s have a look at same sample cases. A good start is to have a distinct parent body. This parent body can for example be a ship, the ground, a harbor, a flatbed, etc.

32.1.1.1. Hydraulic cranes on an offshore vessel

A set of hydraulic cranes on an offshore vessel. Each crane may consist of around 15 bodies and 15-20 constraints. The weight of the crane is significant, affecting the motion of the ship, so when the crane isn’t in use, it cannot just be removed (e.g., only rendered instead).

15 bodies and 15-20 constraints are in general fast to solve but imagine three cranes per vessel and 10 vessels. It’s now 450 extra rigid bodies and about 500 constraints that aren’t explicitly in use.

Now, merging the bodies in the cranes to the ship, the constraints will become inactive (since no relative motion), and the solver will only have to solve for ONE rigid body instead of 45.

32.1.1.2. Logs or rocks on a flatbed

A few hundred logs or rocks on a moving flatbed can in general take a significant amount of time in the solver. For this to be a reasonable scenario, the vehicle pulling the flatbed has to feel the weight and inertia of the load, so the load cannot simply be removed to reduce the time in the solver.

If the stacked logs or rocks can be considered steady, i.e., not moving relative to each other, it’s possible to merge the objects to the flatbed. Instead of a few hundred objects + thousands of contacts to solve, the solver only has one single body, attached to the vehicle to solve for.

32.1.2. The dynamics of merged bodies

The motion control of the final merged body is determined by the motion controls of the set of rigid bodies merged, with the following priority: 1. (if one is) KINEMATICS results in KINEMATICS 2. (if at least one is) STATIC and none KINEMATICS, the result is STATIC 3. (if all are) DYNAMICS and none STATIC or KINEMATICS, the result is DYNAMICS

Note

Only one rigid body with KINEMATICS motion control may occur in a merged body at the time! Having more than one is considered undefined since it’s not possible to determine the final velocity of the bodies involved.

If the final motion control is DYNAMICS, the total mass becomes

\[m^{tot} = \sum_{i} m_i\]

the center of mass

\[p^{cm} = \frac{1}{m^{tot}} \sum_{i} m_i p_i^{cm}\]

and finally the inertia

\[I^{tot} = \sum_{i} I_i - m_i \left[p^{cm} - p_i^{cm}\right]^2\]

where

\[\begin{split}[r] = \begin{bmatrix} 0 & -r_z & r_y \\ r_z & 0 & -r_x \\ -r_y & r_x & 0 \end{bmatrix}\end{split}\]

If the motion control is different from DYNAMICS, the calculations of center of mass, mass and inertia are ignored.

In the case of KINEMATICS, the linear- and angular velocity will be the one of the kinematic body. In the DYNAMICS case, the final linear- and angular velocity becomes:

\[v^{tot} = \frac{1}{m^{tot}} \sum_{i} m_iv_i\]
\[\omega^{tot} = \frac{1}{m^{tot}} \sum_{i} m_i \omega_i\]

32.1.2.1. Advanced mass properties

agx::MergedBody supports rigid bodies with advanced mass properties features. Examples of advanced features are damping (Section 11.2), added mass (Section 22.2.2).

32.1.3. Edge interactions – merging objects together

The internal structure of an agx::MergedBody is a graph where the nodes in the graph is an agx::RigidBody and the edges, connecting the nodes, are so called edge interactions.

An edge interaction defines how the merged rigid bodies interacts and what happens when an edge is removed.

32.1.3.1. Empty edge interaction

An empty edge interaction is a pure logical connection between two rigid bodies.

// Merge rb1 <-> rb2 given an empty edge interaction.
mergedBody->add( new agx::MergedBody::EmptyEdgeInteraction( rb1, rb2 ) );

32.1.3.2. Contact generator edge interaction

A contact generator edge interaction is an edge that, depending on when the edge is removed, can perform collision detection between the geometries in the first and the second rigid body.

If the contact generator edge is removed before the collision detection is executed, the edge does nothing. I.e., it acts as an empty edge interaction, assuming the objects will participate in the collision detection given the usual update.

If the contact generator edge is removed after the collision detection pass has been run and before the solver, the edge will check for overlaps between the two bodies and add agxCollide::GeometryContact’s to the system. This prevents the two objects to “fall into” each other when the solver solves the system.

// Merge rb1 <-> rb2 and generate geometry contacts
// between them if we split in a PRE_STEP callback.
mergedBody->add( new agx::MergedBody::ContactGeneratorEdgeInteraction( rb1, rb2 ) );

32.1.3.3. Geometry contact edge interaction

An edge interaction, constructed given an agxCollide::GeometryContact, taking advantage of the contact information, normal and friction forces.

To gain full advantage of this edge, the solver should have seen the geometry contact, i.e., normal and friction forces are available in the contact.

// Post-step: If the objects in the geometry contact
// are at rest, merge them.
for ( auto geometryContact : simulation->getSpace()->getGeometryContacts() ) {
  if ( isRestingContact( geometryContact ) )
    mergedBody->add( new agx::MergedBody::GeometryContactEdgeInteraction( *geometryContact ) );
}

Similar to agx::MergedBody::ContactGeneratorEdgeInteraction, the contact points and normal are added back, as a new agxCollide::GeometryContact, if the edge is removed after collision detection but before the solver executes.

32.1.3.4. Binary constraint edge interaction

A binary constraint edge interaction is an edge constructed given a one or two (hence binary) body constraint.

This edge does nothing when removed from an agx::MergedBody.

// If the hinge motor isn't enabled - merge.
if ( !hinge->getMotor1D()->getEnable() )
  mergedBody->add( new agx::MergedBody::BinaryConstraintEdgeInteraction( hinge ) );

32.1.4. Data and state of the merged rigid bodies

If an agx::RigidBody is merged, it’s possible to access the agx::MergedBody it belongs to by doing:

// Check whether rb belongs to a merged body.
agx::MergedBody* mergedBody = agx::MergedBody::get( rb );
if ( mergedBody != nullptr )
  std::cout << rb->getName() << " is merged." << std::endl;

Having access to the agx::MergedBody object, it’s possible to traverse/inspect/collect the nodes and edges:

// Collect edges connected to 'rb'.
agx::MergedBody::EdgeInteractionRefContainer rbEdges;
agx::MergedBody::EdgeInteractionVisitor visitor = [ &rbEdges ]( agx::MergedBody::EdgeInteraction* edge )
{
  rbEdges.push_back( edge );
};
// 'Visit all edges associated to rb'.
mergedBody->traverse( rb, visitor );

// Print the name of the bodies 'rb' is merged to.
for ( auto edge : rbEdges ) {
  const agx::RigidBody* otherRb = edge->getRigidBody1() == rb ? edge->getRigidBody2() :
                                                                edge->getRigidBody1();
  std::cout << "rb is merged to: " << otherRb->getName() << std::endl;
}

A similar example, visiting all neighboring nodes instead of collecting all edges (there may be an arbitrarily number of edges between two bodies):

agx::RigidBodyPtrVector otherBodies;
agx::MergedBody::RigidBodyVisitor visitor = [ &otherBodies ]( agx::RigidBody* otherRb )
{
  otherBodies.push_back( otherRb );
};
// 'Visit all neigboring nodes to rb'.
mergedBody->traverse( rb, visitor );

// Print the name of the bodies 'rb' is merged to.
for ( auto otherRb : otherBodies )
  std::cout << "rb is merged to: " << otherRb->getName() << std::endl;

Or one can simply do:

const auto* otherBodies = mergedBody->getNeighbors( rb );
if ( otherBodies == nullptr )
  std::cout << "rb is not merged in mergedBody" << std::endl;
else
  for ( auto otherRb : *otherBodies )
    std::cout << "rb is merged to: " << otherRb->getName() << std::endl;

32.1.4.1. Listeners

It’s possible to add listeners to an agx::MergedBody. These listeners are passive, i.e., it’s not valid to change the state and not possible to for example prevent a merge, from within a listener.

Callbacks: - clone() Create a clone of your listener. It’s valid to return the same instance. The clone method is called when the merged body, the listener belongs to, is being split into several agx::MergedBody instances (see: agx::MergedBody::splitIslands). - onAdd( agx::RigidBody* rb, const agx::MergedBody* mb ) Called when a rigid body is added to the merged body. - onRemove( agx::RigidBody* rb, const agx::MergedBody* mb ) Called when a rigid body is being removed from the merged body. - onAdded( EdgeInteraction* edge, const agx::MergedBody* mb ) Called when an edge has been added to the merged body. - onRemoved( const EdgeInteractionRefContainer& edges, const agx::MergedBody* mb ) Called when a set of edges has been removed from the merged body. - onMovedFromTo( const EdgeInteractionRefContainer& edges, const agx::MergedBody* fromMergedBody, const agx::MergedBody* toMergedBody ) Called when a set of edges has been moved from one merged body to another.

32.1.5. Splitting merged rigid bodies

If a rigid body is merged, there are several ways to split it. The most convenient way is to do:

// Split method finds the merged body associated to 'rb'
// and removes 'rb' from it.
const agx::Bool success = agx::MergedBody::split( rb );
if ( success )
  std::cout << rb->getName() << " successfully removed from its merged body." << std::endl;
else
  std::cout << rb->getName() << " failed to split. Was it merged in the first place?" << std::endl;

For more control, use the remove method:

// Find if 'rb' is merged.
agx::MergedBody* mergedBody = agx::MergedBody::get( rb );
// If merged, remove it (split).
if ( mergedBody != nullptr )
  mergedBody->remove( rb );

The remove method guarantees to remove any references to the agx::MergedBody as long as rb is present in mergedBody.

Note

that when you’re managing the agx::MergedBody objects, an instance may become empty when removing a rigid body. E.g., two bodies are merged, remove one of them will result in a single body without any edges being the only one left – so it will be removed as well. Leaving the agx::MergedBody object empty. An update to the above example:

// Find if 'rb' is merged.
agx::MergedBodyRef mergedBody = agx::MergedBody::get( rb );
// If merged, remove it (split).
if ( mergedBody != nullptr ) {
  mergedBody->remove( rb );
  // If the remove of 'rb' results in an empty 'mergedBody',
  // remove 'mergedBody' from the simulation.
  if ( mergedBody->isEmpty() )
    simulation->remove( mergedBody );
  mergedBody = nullptr;
}

It’s also possible to split a rigid body by removing all the edges to its neighboring nodes/bodies:

// Collect edges connected to 'rb'.
agx::MergedBody::EdgeInteractionRefContainer rbEdges;
agx::MergedBody::EdgeInteractionVisitor visitor = [ &rbEdges ]( agx::MergedBody::EdgeInteraction* edge )
{
  rbEdges.push_back( edge );
};
// 'Visit all edges associated to rb'.
mergedBody->traverse( rb, visitor );

// Remove all the edges associated to our 'rb'.
for ( auto edge : rbEdges )
  mergedBody->remove( edge );

// When all edges has been removed, 'rb' hasn't got
// any connections left in 'mergedBody' so it will
// be removed.
agxAssert( agx::MergedBody::get( rb ) == nullptr );

32.1.6. Separate islands within an agx::MergedBody

The agx::MergedBody doesn’t assume that all rigid bodies/nodes are connected. E.g, if you have four bodies rb1, rb2, rb3 and rb4, and add edge interactions rb1 <-> rb2 and rb3 <-> rb4 to the agx::MergedBody – it’s a valid, but maybe not a desired state.

A more natural example. Say we have five rigid bodies, merged like this:

../_images/agx__mergedbody_1.png

Removing rb3 will result in the following merged state:

../_images/agx__mergedbody_2.png

where there aren’t any interactions connecting island 1 to island 2. It’s not possible to query the merged body how many islands it consists of since it needs a full graph traversal to determine this. Still, if the desired behavior is there shouldn’t be separate islands in my merged bodies’ it’s possible to call agx::MergedBody::splitIslands. This method will determine separate islands, create new merged bodies and add them to the simulation.

// Checks for separate, non-interacting, islands in mergedBody.
// NOTE: mergedBody has to be in the simulation and all new
//       agx::MergedBody objects will be added to the same
//       simulation.
agx::MergedBodyRefVector newIslands = mergedBody->splitIslands();
std::cout << "Number of new islands: " << newIslands.size() << std::endl;

Resulting in:

../_images/agx__mergedbody_3.png

32.1.7. Merging two already merged objects

For obvious reasons (it’s rigid, it cannot move in different directions at the same time!), a rigid body may only belong to one agx::MergedBody. As a result of this constraint, an edge will be rejected by a merged body if one (or both) of the bodies included is merged to another merged body. This state has to be explicitly handled by the user.

Given two rigid bodies rb1 and rb2, merged with other objects in mergedBody1 and mergedBody2, it’s possible to merge the two agx::MergedBody objects together given an edge between rb1 and rb2:

agx::MergedBody* mergedBody1 = agx::MergedBody::get( rb1 );
  agx::MergedBody* mergedBody2 = agx::MergedBody::get( rb2 );
  // Copying all data from mergedBody2 into mergedBody1 and
  // adding the empty edge interaction to mergedBody1.
  agx::MergedBody::EmptyEdgeInteractionRef edge = new agx::MergedBody::EmptyEdgeInteraction( rb1, rb2 );
  const agx::Bool success = mergedBody1->merge( mergedBody2, edge );
  // If merge is successful, remove the empty mergedBody2
  // from the simulation.
  if ( success )
    simulation->remove( mergedBody2 );

32.1.8. Active and inactive

Consider the example scenario described in Hydraulic cranes on an offshore vessel. The crane is not in use so we have it merged. The state of the agx::MergedBody containing the objects, is active:

// Adding 'craneMergedBody' will active it and the containing
// rigid bodies will be merged.
simulation->add( craneMergedBody );

When the crane is ready for use, e.g., moving the boom, we want to inactivate the agx::MergedBody. This can of course be done in many different ways, but one convenient thing of the agx::MergedBody objects is that it will preserve its internal structure when removed from the simulation. The objects merged will simply not be merged anymore.

Let a prismatic be the constraint we control the boom hydraulic piston/cylinder with. When the prismatic motor is active we expect the objects to be able to move relative each other, and when the motor is inactive – we may merge the crane objects again. This can be achieved by adding and removing the craneMergedBody:

if ( prismatic->getMotor1D()->getSpeed() != 0.0 ) {
    // Motor is active, split all objects in 'craneMergedBody'
    // by removing it from the simulation.
    if ( craneMergedBody->isInSimulation() )
      simulation->remove( craneMergedBody );
  }
  else {
    // Motor isn't active, merge all objects in 'craneMergedBody'
    // by adding it to the simulation.
    if ( !craneMergedBody->isInSimulation() )
      simulation->add( craneMergedBody );
  }

This means that a rigid body can be associated to an agx::MergedBody but it’s still not actively merged. It’s possible to check whether an objects is actively merged (i.e., not seen by the solver) or passively merged (i.e., part of a merged body, but that merged body is not in a simulation):

// agx::MergedBody::get returns the merged body 'craneBoom' is part of.
const agx::Bool isPartOfAMergedBody = agx::MergedBody::get( craneBoom ) != nullptr;
// agx::MergedBody::getActive returns the merged body 'craneBoom' is part
// of IF that merged body is a part of the simulation.
const agx::Bool isActivelyMerged = agx::MergedBody::getActive( craneBoom ) != nullptr;

During normal usage there’s no need to use the getActive method.

32.2. Adaptive Model Order Reduction AMOR

AGX Adaptive Model Order Reduction (AMOR) are algorithms where a simulated system adapts to a size where the dynamics is presumed to be preserved. It aims to reduce the number of degrees of freedoms given conditions of steady states. I.e., if the relative motion between two objects is zero, it’s possible to consider these two objects as one, as long as the local system isn’t affected by external interactions that may change their relative motion.

The purpose of AMOR is solely to reduce the workload of the dynamics solver and collision detection (broad phase excluded).

In the previous section we covered how to explicitly merge and split rigid bodies to/from each other, using the agx::MergedBody object. This section is about an automatic version of merging and splitting rigid bodies, where the adding, removing and managing of the agx::MergedBody objects is handled by an agxSDK::MergeSplitHandler object.

32.2.1. Enabling and disabling the agxSDK::MergeSplitHandler

Every instance of an agxSDK::Simulation has an instance of agxSDK::MergeSplitHandler. To enable or disable the merge split handler, simply:

simulation->getMergeSplitHandler()->setEnable( true );
simulation->getMergeSplitHandler()->setEnable( false );

Disabling the handler will split all objects merged by it.

32.2.2. agxSDK::MergeSplitProperties

The agxSDK::MergeSplitProperties defines if and how an object may merge and/or split to and from other objects. By default, the objects doesn’t carry an instance of the agxSDK::MergeSplitProperties, and this is interpreted as ‘merge and split of this object is disabled’.

To get an already created or to create a new instance of MergeSplitProperties for an object (RigidBody, Geometry, Constraint or Wire):

agxSDK::MergeSplitProperties* properties = agxSDK::MergeSplitHandler::getOrCreateProperties( obj );
agxAssert( obj == nullptr || properties != nullptr );

Given obj != nullptr then properties != nullptr.

With the merge split properties it’s possible to:
  • Enable and disable merge (disabled by default). When merge is enabled, it means that this object may merge to other objects.

  • Enable and disable split (disabled by default). When split is enabled, it means that this object (if merged), may split from the object it’s merged to.

  • Enable and disable merge and split (disabled by default). Convenience method to enable/disable both merge and split.

  • Manage thresholds used by the AMOR algorithms. Get, create and set thresholds used by contact, constraint and wire AMOR algorithms.

  • Manage merge ignore groups. Merge ignore groups are used to prevent merging between bodies that would otherwise have merged.

Objects that can carry an agxSDK::MergeSplitProperties instance are; agx::RigidBody, agxCollide::Geometry, agx::Constraint and agxWire::Wire:

agxSDK::MergeSplitProperties* rbProperties         = agxSDK::MergeSplitHandler::getOrCreateProperties( rb );
agxSDK::MergeSplitProperties* geometryProperties   = agxSDK::MergeSplitHandler::getOrCreateProperties( geometry );
agxSDK::MergeSplitProperties* constraintProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( constraint );
agxSDK::MergeSplitProperties* wireProperties       = agxSDK::MergeSplitHandler::getOrCreateProperties( wire );

To check if an objects has properties or to inspect the current state:

const agxSDK::MergeSplitProperties* rbProperties         = agxSDK::MergeSplitHandler::getProperties( rb );
const agxSDK::MergeSplitProperties* geometryProperties   = agxSDK::MergeSplitHandler::getProperties( geometry );
const agxSDK::MergeSplitProperties* constraintProperties = agxSDK::MergeSplitHandler::getProperties( constraint );
const agxSDK::MergeSplitProperties* wireProperties       = agxSDK::MergeSplitHandler::getProperties( wire );

32.2.2.1. Properties carried by agxCollide::Geometry or its parent – agx::RigidBody

Since the context of AMOR is ‘bodies’, the properties for agxCollide::Geometry behaves a bit different. It is, as shown above, possible to create and change the merge split properties of a geometry object making it possible to, for example, have merge enabled/disabled in different parts of a rigid body.

A geometry inherits the merge split properties of its parent rigid body. Assume we’ve created merge split properties for the rigid body and enabled merge:

agx::RigidBodyRef rb = new agx::RigidBody();
agxCollide::GeometryRef geometry = new agxCollide::Geometry( new agxCollide::Sphere( 0.5 ) );
rb->add( geometry );
...
// Create merge split properties and enable merge for 'rb'.
agxSDK::MergeSplitProperties* rbProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( rb );
rbProperties->setEnableMerge( true );

Now, for geometries, the semantics of the getProperties method differs from the rigid body, constraint and wire versions in two ways:

  1. There’s a mutable version.

  2. If the geometry hasn’t got any merge split properties (i.e., getOrCreateMergeSplitProperties hasn’t been called), the merge split properties of the parent rigid body will be returned (if created).

// 1.
agxSDK::MergeSplitProperties* geometryProperties = agxSDK::MergeSplitHandler::getProperties( geometry );
// 2.
agxAssert( geometryProperties == agxSDK::MergeSplitHandler::getProperties( rb ) );

Also, in the call to getOrCreateProperties, given a geometry, the merge split properties (if created) of the parent rigid body (if present) will be cloned. Continuing the example from above (remember merge is set to be enabled for rb):

agxSDK::MergeSplitProperties* newProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( geometry );
// We should get a new instance of the merge split properties.
agxAssert( newProperties != geometryProperties );
// The new properties should be a clone of 'rbProperties'.
agxAssert( newProperties->getEnableMerge() == rbProperties->getEnableMerge() );

32.2.3. Merge Split Thresholds

Similar to agxSDK::MergeSplitProperties, each object may carry a set of parameters/thresholds that controls the behavior when merging and splitting the object. The global (simulation specific) merge split thresholds are used by default.

32.2.3.1. Global (simulation specific) merge split thresholds

The agxSDK::MergeSplitHandler has the default thresholds used for objects without explicitly created thresholds:

auto globalContactThresholds = simulation->getMergeSplitHandler()->getGlobalContactThresholds();
// Set global contact thresholds for all objects without explicitly created thresholds.
configureGlobalContactThresholds( globalContactThresholds );

32.2.4. Object specific merge split thresholds

All objects that may have agxSDK::MergeSplitProperties may have a list of thresholds. E.g., agx::RigidBody, agxCollide::Geometry, agxWire::Wire and agx::Constraint. The thresholds may be accessed, created and removed via the merge split properties API.

There are two ways to assign object specific thresholds:

auto rbProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( rb );
// By default the thresholds should be null meaning global thresholds are used.
agxAssert( rbProperties->getContactThresholds() == nullptr );
auto explicitContactThresholds = new agxSDK::GeometryContactMergeSplitThresholds();
rbProperties->setContactThresholds( explicitContactThresholds );
auto rbProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( rb );
// By default the thresholds should be null meaning global thresholds are used.
agxAssert( rbProperties->getContactThresholds() == nullptr );
auto explicitContactThresholds = rbProperties->getOrCreateContactThresholds();

To remove the thresholds and go back to the global default, simply assign null:

rbProperties->setContactThresholds( nullptr );

The explicit thresholds instance may be shared arbitrarily:

auto explicitContactThresholds = new agxSDK::GeometryContactMergeSplitThresholds();
auto rbProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( rb );
auto wireProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( wire );
auto geometryProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( geometry );

rbProperties->setContactThresholds( explicitContactThresholds );
wireProperties->setContactThresholds( explicitContactThresholds );
geometryProperties->setContactThresholds( explicitContactThresholds );

32.2.5. Wire merge split thresholds

The wire merge split properties and thresholds are shared with all objects created/owned by the wire instance. There are currently two occasions with predefined behaviors:

  1. Cutting a wire - When cutting a wire the properties and thresholds for that wire are cloned, i.e., new instances of the properties and thresholds are created. The newly created wire will have identical values as before but when one, e.g., changes thresholds of the original wire it won’t affect the part that has been cut from the original one.

  2. Merging wires - When merging two wires wire1->merge( wire2 ) the properties and thresholds of wire1 will be used for the whole wire.

32.2.6. Merge conditions

Two objects may merge when the objects involved has merge split properties and property merge enabled.

agxSDK::MergeSplitHandler::getOrCreateProperties( rb1 )->setEnableMerge( true );
agxSDK::MergeSplitHandler::getOrCreateProperties( rb2 )->setEnableMerge( true );

32.2.6.1. Resting contact

The contacts points carries three directions (an orthonormal basis), namely the normal N, the tangent U and the tangent V directions. U is the primary friction direction and V the secondary friction direction. Given two rigid bodies

\(rb_1={p_1^{cm},v_1,\omega_1 },\)

\(rb_2={p_2^{cm},v_2,\omega_2 },\)

where \(p_i^{cm}\) is center of mass position, \(v_i\) the linear velocity and \(\omega_i\) the angular velocity. The speed \(s_d\) along a given direction \(d\) at point \(p_d\) is given by

\(s_d=d*[v_1+ \omega_1\times(p_d-p_1^{cm} )-[v_2+\omega_2\times(p_d-p_2^{cm} )]].\)

Given the speeds \(s_N\), \(s_U\) and \(s_V\) along each direction of the contact points \(p_i\), the two objects may merge if:

\(\underset{p_i}{\max} \mid s_N \mid \le \epsilon_N\)

\(\underset{p_i}{\max} \mid s_U \mid \le \epsilon_{UV}\)

\(\underset{p_i}{\max} \mid s_V \mid \le \epsilon_{UV}\)

Where \(\epsilon_N\) is called MAX_RELATIVE_NORMAL_SPEED and \(\epsilon_{UV}\) MAX_RELATIVE_TANGENT_SPEED. Since the conditions above recovers the rolling condition there’s a third condition regarding rolling,

\(\parallel \omega_1 - \omega_2 \parallel \le \epsilon_\omega\)

and \(\epsilon_\omega\) is named MAX_ROLLING_SPEED.

auto contactThresholds = objProperties->getOrCreateContactThresholds();

// Assign maximum speed along a contact normal for a contact
// to be considered resting. Default: 0.01.
contactThresholds->setMaxRelativeNormalSpeed( nS );

// Assign maximum (sliding) speed along a contact tangent
// for a contact to be considered resting. Default 0.01.
contactThresholds->setMaxRelativeTangentSpeed( tS );

// Assign maximum (rolling) speed for a contact to be considered
// resting. Default 0.01.
contactThresholds->setMaxRollingSpeed( rS );

When two objects merge due to a resting contact state, the current contact data (such as contact point, normal, normal- and friction forces) is stored and is used when the objects splits and to determine if they should split due to external interactions.

32.2.6.2. Constraint in a steady state

Similar to contact points, constraints keeps track of the speeds at the anchor point of the constraint. Since constraints (like Hinge, Prismatic, LockJoint etc.) are persistent on a different level compared to contacts, it’s possible to include ‘time’ dependent data. It’s not time in seconds, rather “this constraint has been under constant load for some time - merge!”. The ‘time’ concept is introduced by using an Exponential Moving Average (EMA) operation on the relative speeds.

Given a statistic S and an i’th observation \(O_i\) the i’th EMA statistic is given by

\(S_i= \alpha O_i + ( 1 - \alpha ) S_{i-1}\)

where \(0 \le \alpha \le 1\) is a smoothing factor, controlling the sensitivity of the new observations. When \(\alpha\) is small, the relative speeds has to be small for a (much) longer time than when \(\alpha\) is close to one.

\(\alpha\) isn’t exposed to be manipulated by the user. Instead the more intuitive threshold of maximum relative speed may be changed to make the constrained system merge more or less aggressively.

auto constraintThresholds = objProperties->getOrCreateContactThresholds();

// Assign maximum relative speed between the constrained objects
// for the system to be considered at rest. I.e., when the relative
// motion between the objects is less than this threshold, the
// objects may merge. Default: 0.005.
constraintThresholds->setMaxRelativeSpeed( maxRelSpeed );

32.2.6.3. Splitting Wires

Wires contains bodies, geometries and constraints so when a wire object is merging, the specific objects state and properties are checked. E.g., when a mass node is in contact with another object, the contact thresholds and algorithms are used to determine if the node should be merged. Similar for the wire attachments - the constraint thresholds and algorithms are used.

auto wireProperties = agxSDK::MergeSplitHandler::getOrCreateProperties( wire );

// Thresholds controlling the wire attachments.
auto wireAttachmentsThresholds = wireProperties->getOrCreateConstraintThresholds();

// Thresholds controlling the merging and splitting of
// mass/lumped nodes relative other objects.
auto wireContactThresholds = wireProperties->getOrCreateContactThresholds();

The wires has its own thresholds as well but they are currently classified as split thresholds.

32.2.6.4. Merge ignore filters

A merge ignore filter is used to prevent merges between bodies that would otherwise have merged. The filtering is done based on groups which are added to MergeSplitProperties. A group is specified by either a group ID, which is an agx::UInt32, or a group name:

agx::UInt32 groupById = agx::UInt32(4);
agx::Name groupByName = "group";
properties->addGroup(groupById);
properties->addGroup(groupByName);

A filter is a pair of such groups and is registered with the MergeSplitHandler:

simulation->getMergeSplitHandler()->setEnableMergePair(groupById, groupByName, false);

After this the MergeSplitHandler will not automatically merge any pair of bodies where one body contains the group groupById and the other contains the group groupByName. Constraints with merge split enabled is a special case. In this case the constraint’s MergeSplitProperties takes precedence and its set of groups is used when comparing against the registered filters.

The filters are checked only for the groups of the two bodies that are currently being considered for merging. If either body has already been merged with something else then there may be other merge ignore group within that merged body, but those are not considered. This means that two bodies for which there is a merge ignore filter may still become part of the same merged body if they both merge with some third body against which neither of the two has a merge ignore filter.

Filters cannot be used to enable merging between pairs of bodies, it is an ignore filter only. Passing true to setEnableMergePair will only remove the ignore filter for the given pair and restore the default behavior.

Named groups are given an ID as well. In order to separate user specified group IDs from IDs generated from group names, a user specified group ID must be less than MergeIgnoreFilter::MAX_UNAMED_GROUP_ID. The collection of IDs return by MergeSplitProperties::getGroupIds will include both user specified group IDs and the group IDs generated from the named groups added to the MergeSplitProperties. The ID for a named group can be added only by adding the name, not by adding the ID directly. However, removal based on named group ID is allowed and results in both the ID and the name being removed.

The relationship between merge ignore groups on geometries and the owning rigid body is the same as for any MergeSplitProperties property: the one in the geometry, if any, takes precedence over the one in the rigid body.

32.2.7. Split conditions

An object may split, i.e., leave a merged state, if it has merge split properties and property split is enabled.

agxSDK::MergeSplitHandler::getOrCreateProperties( rb )->setEnableSplit( true );

32.2.7.1. Impacting contact

There are two different concepts to choose from:
  • Physical impact

  • Logical impact

The logical impact is determined by the state of the contact. I.e., if the state were “no contact” and the geometries are overlapping, the state becomes “impact”. It’s at this state the agxSDK::ContactEventListener::impact callbacks are executed.

The physical impact condition is tied to a speed threshold, preventing objects from splitting too often.

The logical impact concept is disabled by default. If enabled, the merged object will split when the state of the contact is “impact”. If the state is different from “impact” the physical impact approach is testing the contact

The impact approach uses the relative speed \(s_N\) (from the Resting contact section), considering the sign of the speed as well:

\(\underset{p_i}{\max} s_N \le - \epsilon_{impact}\)

Note that \(s_N\) is positive when the objects are separating and negative when they’re approaching each other. The latter is the one we’re catching and \(\epsilon_{impact}\ge0\) is a threshold called MAX_IMPACT_SPEED.

auto contactThresholds = objProperties->getOrCreateContactThresholds();

// Assign maximum impact speed (along a contact normal) a merged
// object can resist without being split. Default 0.01.
contactThresholds->setMaxImpactSpeed( value );

// True to split when geometry contact state is agxCollide::GeometryContact::IMPACT_STATE,
// i.e., the first time the objects collide. Default is false and "max impact speed" will
// be used instead.
contactThresholds->setSplitOnLogicalImpact( flag );
// Disable physical impacts completely when using logical impacts.
// This is optional and it's valid to combine the two.
if ( flag )
  contactThresholds->setMaxImpactSpeed( agx::Infinity );

32.2.7.2. Constraints

32.2.7.2.1. Changing state

Constraints can split bodies when the internal state of the constraint has been changed in a way that the dynamics may be affected. Currently, this functionality is focused to the secondary constraints/controllers and the constraint will split the bodies involved when:

  • The motor is enabled with speed different from zero.

  • The position of the lock has been changed (i.e., by calling constraint->getLock1D()->setPosition( newPosition )).

  • The range of the range controller has been changed.

  • When a controller state goes from inactive to active, e.g., when a range is hit or a lock is enabled.

It’s, unfortunately, often not enough to just split the two bodies in the constraint. Consider an articulated, hydraulic crane where the whole structure is merged. To rise the boom the prismatic motor is activated, but nothing will move since it’s only the hydraulic cylinder and piston that are free to move.

../_images/agxsdk__mergesplithandler_1.png

Fig. 32.1 Articulated crane where a prismatic motor is used to rise the boom.

What happens is that the split algorithm first splits the two bodies directly involved in the constraint. The structure is now in a “jammed state” where it still cannot move. The split algorithm continues to traverse the merged structure, splitting all constrained bodies it finds. The split-traversal stops when it reaches a body that may not be split or when the edges connecting two bodies aren’t related to constraints (i.e., different from agx::MergedBody::BinaryConstraintEdgeInteraction).

If, for example, hundreds of these cranes were to stand on a static ground or a dynamic ship, it’s important that the ground or the ship doesn’t have merge split property split enabled. In general there aren’t any reasons to have that property enabled for “parent” objects.

32.2.7.2.2. Applying forces

When a constraint is applying forces on a merged object, it may split that object if that object has merge split property split enabled and is merged due to resting contacts.

Mentioned in the Resting contact section – contact and interaction data is saved when two objects merge. This data can be used to approximately determine “how much” external force is needed for the object to start to move, and we can call it the strength of a contact edge.

When the force exceeds the contact edge strength, the edge can be considered removed and if all edges are removed, the objects splits.

../_images/agxsdk__mergesplithandler_2.png

There are some thresholds in the contact thresholds that controls the strength of a merged contact. In general contacts splits due to impacting objects but when a constraint is applying forces it’s possible to add some adhesiveness to the merged objects - preventing them from splitting at the default moment.

The merged contact adhesive forces are given in unit of force:

auto contactThresholds = objProperties->getOrCreateContactThresholds();

// Adhesive force in the normal directions preventing the object to split (if > 0)
// when the object is subject to external interactions (e.g., constraints).
// Default: 0.0
contactThresholds->setNormalAdhesion( value );

// Adhesive force in the tangential directions preventing the object to split (if > 0)
// when the object is subject to external interactions (e.g., constraints).
// Default: 0.0
contactThresholds->setTangentialAdhesion( value );
32.2.7.2.3. Split when applying external forces

As mentioned in the previous section, only external forces from constraints may split merged objects. I.e., rb->addForce( largeForce ); will by default not split rb given no constraints involved. Disabled by default, but possible to enable, is a flag called may split in gravity field, meaning monitor this objects added external forces and interpret them in the same way as if a constraint was interacting with it.

auto contactThresholds = objProperties->getOrCreateContactThresholds();

// Check split given external forces for all objects merged (i.e., rb->getForce()
// the sum of rb->addForce(), including the gravity force). Performance warning,
// disabled by default.
contactThresholds->setMaySplitInGravityField( flag );

Note

Use this feature with as few objects as possible (if needed to begin with), since merged objects with this feature enabled are going through split tests each time step.

32.2.7.3. Merging Wires

Wire nodes merge when the contact between the wire node and another object is considered resting (thresholds in agxSDK::GeometryContactMergeSplitThresholds). When the contact is in a resting state we store a value of the current tension round the node, giving a measure of how the wire is looking before the merge, then we merge the node. Due to state changes in the simulation, the tension may change, resulting in a split of the wire node.

32.2.7.3.1. Merge tension scale

The merge tension scale threshold (default: 1.0) says something about how much the tension must change before the wire node will split. It’s a scale of the stored merge tension meaning value < 1 it’s more likely, and > 1 less likely for the wire node to split. For system with (locally) low tension but large objects interacting with them, this value could (for example) be round 100. I.e., when the wire/chain should split, the tension is likely to be 100 times larger than when it merged.

auto wireThresholds = wireProperties->getOrCreateWireThresholds();

// When a node is merged the tension is stored and monitored
// to perform split. This threshold scales the merge tension
// making split more likely when < 1 and less likely > 1. When
// this scale is 0 the only force keeping the node merged is
// from the contact - which in most cases isn't enough.
// Default: 1.0
wireThresholds->setMergeTensionScale( value );
32.2.7.3.2. Split propagation decay scale

The split propagation decay scale threshold (default: 1.0) says something about how far splits, due to external forces, propagates along a merged wire segment.

When a node splits, x amount of energy is consumed, and this threshold scales this energy consumption. So if this threshold is 0.0, all nodes will feel the same interaction forces as the first/last node did (probably splitting the whole wire when one splits). When this threshold is 1.0, the external interactions are receiving a measure of how heavy the merged wire is to pull/push, but not much more. When you’re experiencing undesirable splits back and forth along the wire, increase this value slightly (1.0 - 20.0). To completely disable split propagation, set it to some value >> 1 (1.0E4 on the safe side and infinity to completely disable).

auto wireThresholds = wireProperties->getOrCreateWireThresholds();

// When external forces are acting on a partially merged wire,
// the force will propagate and split several nodes at once.
// This threshold controls the amout of force (of the total
// external force) that is used to split each node. If this
// value is high (> 1), the force will not propagate 'too'
// long, keeping the wire merged. Default: 1.0.
wireThresholds->setForcePropagationDecayScale( value );

32.2.7.4. Split on separation

There are cases where bodies remain merged after a contact that held the bodies in place disappears. Some of those cases can be handled by enabling splitting on separation events.

simulation->getMergeSplitHandler()->setEnableSplitOnSeparation(true);

The effect of split on separation is that when two bodies separate then they are split from their respective agx::MergedBody, if any. A side effect of this more aggressing splitting is that it becomes more difficult to form stable piles of merged bodies.

32.2.8. agx::MergedBody with agxSDK::MergeSplitHandler

As mentioned earlier, the agxSDK::MergeSplitHandler manages agx::MergedBody objects. If the agxSDK::MergeSplitHandler encounter a merged rigid body, but the rigid body wasn’t merged by the handler – the rigid body is ignored.

This enables the possibility for the user to manage their own merged bodies. It can, for example, be used to initialize big scenes in a merged state.

For example – create a pile of boxes, enable merge and split and merge them in an agx::MergedBody:

// Create and add a merged body.
agx::MergedBodyRef mergedBody = new agx::MergedBody();
simulation->add( mergedBody );

agx::RigidBodyRef prevBox = nullptr;
for ( agx::UInt i = 0; i < numBoxes; ++i ) {
  // Create, position and add the box to the simulation.
  agx::RigidBodyRef box = createBoxInPile( i, simulation );
  // Enable merge+split for the box.
  agxSDK::MergeSplitHandler::getOrCreateProperties( box )->setEnableMergeSplit( true );
  // Merge with previous box.
  if ( prevBox != nullptr )
    mergedBody->add( new agx::MergedBody::ContactGeneratorEdgeInteraction( prevBox, box ) );
  prevBox = box;
}

Running this simulation, given a ground object with property merge enabled, the set of merged boxes will interact with the ground object, but not merge nor split.

For the agxSDK::MergeSplitHandler to be able to merge and/or split these explicitly merged objects, their agx::MergedBody has to be registered:

// Register 'mergedBody' to the handler. The objects continues to
// be merged after this call.
simulation->getMergeSplitHandler()->registerMergedBody( mergedBody );

Note: After an agx::MergedBody has been registered to the agxSDK::MergeSplitHandler, it’s undefined behavior to add/remove edge interactions/bodies from the agx::MergedBody object.