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:
Custom blocks can have zero or more input and output ports, and these can be either:
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:
A custom block’s dynamics are defined by its:
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:
Note: Object states and parameters are also available. These feature other datatypes and more complex structures.
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”:
For the custom block described so far, the template code looks like this:
There are many important things to notice from this template:
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.
The last relevant detail lies in the Advanced tab. Here, you can set the activation mode, which can be:
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:
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:
After cleaning up the unused parts of the created template, we end up with something like this:
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
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.
#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:
The underlying code is very simple and, for each language, looks like this:
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
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)
#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:
This is how it looks like in OML, Python and C:
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
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))
#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.