Twin Activate Custom Blocks Fundamentals

RSGarciarivas
RSGarciarivas
Altair Employee
edited July 15 in Altair HyperWorks

Altair Twin Activate is a very powerful tool for modeling multidomain systems, including digital twins, in 0D (programming languages), 1D (block diagrams) and 3D in cosimulation with domain specific tools. As such, users might often have the need to recreate subsystems with very specific behaviors.

 

Custom blocks are quite useful in this regard, as they allow users to represent any system using either OML, Python or C code. I’ve put together this short guide on things you should know to get the most out of custom blocks in your Twin Activate models.

 

Custom Blocks Parameters and Settings

 

When you include a custom block in a block diagram, there are several things you will have to define regardless of the modeling language and application, namely:

 

  • Inputs and outputs:

 

Custom blocks can have zero or more input and output ports, and these can be either:

    • Regular (taking in or producing signals that represent any quantity)
    • Activation type (discrete signals that will trigger the execution of a block)

 

Regular ports can be assigned a name and a datatype (double, integer, Boolean, etc.). For output activation ports, an initial activation can be declared.

 

All these details are defined in the custom block’s parameter menu, under the Ports tab. A custom OML block with two regular input ports, two activation ports, and one regular and activation port each, would be defined like this:

 

image

 

  • Parameters and states:

 

A custom block’s dynamics are defined by its:

    • Internal states: Continuous (e.g. velocity, acceleration, etc.) or discrete (e.g. an internal counter). Its initial value is declared and the block’s underlying code updates them at each call.
    • Parameters: Real (e.g. mass, internal resistance, etc.) or integer (e.g., length of a queue). These remain constant throughout the simulation and are useful for characterizing different instances of the same system.

 

In a hypothetical system represented in a custom block, setting up position x0, velocity v0 and number of occurrences N as initial states on a custom block, along with parameters for mass m and number of segments nS, would look like this:

 

image

 

Note: Object states and parameters are also available. These feature other datatypes and more complex structures.

 

  • Code template:

 

You don’t have to build all the code for your block from scratch. Even more so, writing everything from scratch would prove difficult as there are necessary function calls from the different bridging APIs that make a custom block work seamlessly.

 

Luckily, once you have defined inputs, outputs, states and parameters, you can go over to the SimFunction tab and click on “generate skeleton”:

 

A screenshot of a computerDescription automatically generated

 

For the custom block described so far, the template code looks like this:

 

image

 

 

There are many important things to notice from this template:

 

    • Inputs (Vin, Win) the activated port (nevprt), continuous and discrete states (x and z) and parameters are read using bridging functions.
    • In the commented lines, all block updates are handled using commands like vssSetOutputData, vssSetDerState and so on.
    • Most of the template is an if statement dependent on a flag. This flag is very important as it will orchestrate all block updates throughout the simulation. The most important flags are: 
      • Output update flag: Statements for updating all output signals goes here.
      • Derivative flag: Here, you can set the rate of change of internal states.
      • State update flag: This one is useful for updating discrete states or for setting discontinuities in continuous states.
      • Event scheduling flag: The setting of output activation signals is handled here.

 

All these details will be explored with some examples. Note that, most times, we won’t use all the flags and variables created by the code template and erasing them enhances readability of the script.

 

  • Activation:

 

The last relevant detail lies in the Advanced tab. Here, you can set the activation mode, which can be:

    • Standard: The block requires an activation in any of its activation inputs to execute its code.
    • Always active: The block is active at each simulation timestep (use this if you have continuous states).
    • Initially active: This is same as the standard mode but automatically executes its code at the first timestep.

 

Different activation modes can cause different block behaviors, so make sure you choose accordingly.

 

Now, let’s go over some simple examples that will illustrate the features we just discussed.

 

Examples:

Mass-spring-damper system 

Modeling a mass-spring-damper system in a custom block is straightforward using its state matrices. This is the system and its state space representation:

 

A diagram of a circuitDescription automatically generated     image

 

This is a straightforward example as it only requires derivative and output updates (each handled inside its corresponding flag). M, b and k are defined as real parameters and initial values for both states (position and velocity) are defined as well.

 

The activation mode is set to always active because of the continuous states, and a single input and output (no activation ports) are required to model the block. You can try out this implementation of OML, Python and C custom blocks in the attached model. It looks something like this:

 

A diagram of a functionDescription automatically generated

 

After cleaning up the unused parts of the created template, we end up with something like this:

 

  • OML:

 

function OmlBlockFunction(block, flag)

F = vssGetInputData(block, 1);

states = vssGetState(block);

rpar = vssGetRpar(block);

 

if flag == vssBlockDerivativeFlag

x = states(1);

v = states(2);

 

m = rpar(1);

b = rpar(2);

k = rpar(3);

 

% Define state matrices A & B

A = [0 1; -k/m -b/m];

B = [0; 1/m];

% Get state derivatives

dx = A*[x;v] + B*F;

vssSetDerState(block, dx);

 

elseif flag == vssBlockOutputUpdateFlag

x = states(1);

vssSetOutputData(block, 1, x, vssGetOutputDataType(block, 1));

 

  end

end

 

  • Python:

 

import hwx.activate.apis

import numpy

def PyBlockFunction(block, flag):

apis = hwx.activate.apis

F = apis.vssGetInputData(block, 1)

states = apis.vssGetState(block)

rpar = apis.vssGetRpar(block)

 

if flag == apis.vssBlockDerivativeFlag():

x = numpy.squeeze(states[0])

v = numpy.squeeze(states[1])

 

m = numpy.squeeze(rpar[0])

b = numpy.squeeze(rpar[1])

k = numpy.squeeze(rpar[2])

 

# Define state matrices A & B

A = numpy.array([[0, 1],[-k/m, -b/m]], dtype = 'float64')

B = numpy.array([[0],[1/m]], dtype = 'float64')

 

# Get state derivatives

dx = numpy.dot(A, numpy.array([[x],[v]], dtype = 'float64')) + numpy.dot(B, F)

apis.vssSetDerState(block, dx)

 

elif flag == apis.vssBlockOutputUpdateFlag():

x = states[0]

apis.vssSetOutputData(block, 1, x, apis.vssGetOutputDataType(block, 1))

 

Notice that the only difference between this an the OML block is that, here, we need to import some Python modules.

 

  • C:

 

#include "vss_block4.h"

VSS_EXPORT void CBlockFunction(vss_block *block, int flag)

{

SCSREAL_COP *F = GetRealInPortPtrs(block,1);

SCSREAL_COP *x = GetRealOutPortPtrs(block,1);

SCSREAL_COP *states = GetState(block);

SCSREAL_COP *derstates = GetDerState(block);

SCSREAL_COP *rpar = GetRparPtrs(block);

 

switch (flag){

case VssFlag_Derivatives:

float _x = states[0];

float _v = states[1];

 

float m = rpar[0];

float b = rpar[1];

float k = rpar[2];

 

// Define matrices A, B

double A[2][2] = { {0, 1}, {-k/m, -b/m} };

double B[2] = { 0, 1/m };

 

// Get state derivatives: dx = A * [x; v] + B * F

derstates[0] = A[0][0] * _x + A[0][1] * _v + B[0] * F[0];

derstates[1] = A[1][0] * _x + A[1][1] * _v + B[1] * F[0];

break;

 

case VssFlag_OutputUpdate:

x[0] = states[0];

break;

}

}

 

            Using C custom blocks, you will notice that update commands do not exist and, instead, we use variable names directly.

 

Counter with reset

In this next example, the main highlight is the logic for handling activations from different activation ports and the usage of a discrete state to represent a very simple counter. This block only has two activation input ports. When activated by the one on the left, the internal counter will increase by one. When activated by the one on the right, the counter will reset to zero.

 

As the counter is initialized at zero, the appropriate activation method for this block is initially active. That way, if the initial value of the discrete state is set to zero, that is going to be the block’s output from the beginning. Otherwise, there would be no output until the first activation.

 

In the attached example, two sets of periodic activation signals are used. For increasing the counter, a periodic signal every two seconds starting at t = 1 is used. For resetting the counter, a periodic signal every six seconds starting at t = 1.5 is used. We end up with something like this:

 

A screenshot of a computerDescription automatically generated

 

The underlying code is very simple and, for each language, looks like this:

 

  • OML:

 

function OmlBlockFunction(block, flag)

nevprt = vssGetEventCode(block);

z = vssGetDstate(block);

 

% If activated on first port, add 1 to counter

% If activated on second port, restart counter

      if flag == vssBlockOutputUpdateFlag

if nevprt == 1

N = z + 1;

            else

                  N = 0;

            end

            vssSetOutputData(block, 1, N, vssGetOutputDataType(block, 1));

           

      elseif flag == vssBlockStateUpdateFlag

            if nevprt == 1

                  z = z + 1;

            else

                  z = 0;

            end

            vssSetDstate(block, z);

           

      end

end

 

  • Python:

 

import hwx.activate.apis

import numpy

def PyBlockFunction(block, flag):

apis = hwx.activate.apis

      nevprt = apis.vssGetEventCode(block)

      z = apis.vssGetDstate(block)

 

# If activated on first port, add 1 to counter

# If activated on second port, restart counter

      if flag == apis.vssBlockOutputUpdateFlag():

if nevprt == 1:

                  N = z + 1

            else:

                  N = 0

            apis.vssSetOutputData(block, 1, N, apis.vssGetOutputDataType(block, 1))

           

      elif flag == apis.vssBlockStateUpdateFlag():

            if nevprt == 1:

z = z + 1

            else:

                  z = 0

            apis.vssSetDstate(block, z)

 

  • C:

 

#include "vss_block4.h"

VSS_EXPORT void CBlockFunction(vss_block *block, int flag)

{

      SCSREAL_COP *N = GetRealOutPortPtrs(block, 1);

      int nevprt = GetEventCode(block);

      SCSREAL_COP *z = GetDstate(block);

     

      switch (flag){

// If activated on second port, restart counter

      // If activated on second port, restart counter

            case VssFlag_OutputUpdate:

                  if (nevprt == 1){

                        N[0] = z[0] + 1;

                  }

                  else {

                        N[0] = 0;

                  }

                  break;

                 

            case VssFlag_StateUpdate:

                  if (nevprt == 1){

                        z[0] = z[0] + 1;

                  }

                  else {

                        z[0] = 0;

                  }

                  break;

      }

}

 

Variable delay block

 

The last example mainly highlights event scheduling. Its activation mode is standard and it doesn’t contain any states or parameters.

 

When activated through its first activation port, this variable delay block will schedule an activation event from its activation output with a delay equal to the value at its second input. An activation at its second activation input will update its output to the value at its first input. Connecting its activation output to its second input, we end up with a variable delay block:

A diagram of a programDescription automatically generated

 

This is how it looks like in OML, Python and C:

 

  • OML:

 

function OmlBlockFunction(block, flag)

      in = vssGetInputData(block, 1);

      D = vssGetInputData(block, 2);

      nevprt = vssGetEventCode(block);

     

      if flag == vssBlockEventSchedulingFlag

% If activated on first port, set firing

% on output activation after D seconds

            if log2(nevprt) + 1 == 1

vssSetFiring(block, D);

            end

           

      elseif flag == vssBlockOutputUpdateFlag

% If activated on second port, set output

% to be the same as current input

            if log2(nevprt) + 1 == 2

                  vssSetOutputData(block, 1, in, vssGetOutputDataType(block, 1));

            end

      end

end

 

  • Python:

 

import hwx.activate.apis

import numpy

def PyBlockFunction(block, flag):

      apis = hwx.activate.apis

      In = apis.vssGetInputData(block, 1)[0,0]

      D = apis.vssGetInputData(block, 2)[0,0]

      nevprt = apis.vssGetEventCode(block)

     

      if flag == apis.vssBlockEventSchedulingFlag():

# If activated on first port, set firing

# on putput activation after D seconds

            if numpy.log2(nevprt) + 1 == 1:

                  apis.vssSetFiring(block, D)

           

      elif flag == apis.vssBlockOutputUpdateFlag():

# If activated on second port, set output

# to be the same as current input

            if numpy.log2(nevprt) + 1 == 2:

                  apis.vssSetOutputData(block, 1, In, apis.vssGetOutputDataType(block, 1))

 

  • C:

 

#include "vss_block4.h"

VSS_EXPORT void CBlockFunction(vss_block *block, int flag)

{

      SCSREAL_COP *in = GetRealInPortPtrs(block, 1);

      SCSREAL_COP *D = GetRealInPortPtrs(block, 2);

      SCSREAL_COP *out = GetRealOutPortPtrs(block, 1);

      int nevprt = GetEventCode(block);

      SCSREAL_COP *evout = GetNevOutPtrs(block);

     

      switch (flag){

            case VssFlag_EventScheduling:

      // If activated on first port, set firing

      // on output activation after D seconds

                  if (nevprt == 1) {

                        evout[0] = D[0];

                        }

                  break;

                 

            case VssFlag_OutputUpdate:

      // If activated on second port, set output

      // to be the same as current input

                  if (nevprt == 2) {

                        out[0] = in[0];

                        }

                  break;

      }

}

 

With these simple examples, you are ready to start leveraging the different custom blocks available in Twin Activate. All the examples shown here have been attached for you to test and explore further.

 

Reach out in the community product forums if you are having issues leveraging Twin Activate in this way. Alternatively, email hwsupport@altair.com or reach out for support from your Altair One account if you need assistance from experts.