Vortex Studio SDK - Python 2 Scripting


Vortex Studio SDK in python

Vortex Studio SDK is partially available in python scripting.

Python scripting can be used

  • As a dedicated python application that loads, renders or interacts with Vortex assets during a simulation
  • Within Vortex, as a dynamics python extension to change the behavior of a scene or a mechanism.

In the following paragraphs, the reader is expected to be familiar with python scripting language.

This section looks into the VxSim library in python, for some specific dynamics operations, like accessing contacts during the simulation, pyvx usage is required since it exposes the VxCore API.

Python Usage and Environment Configuration

You can use your preferred integrated development environment (IDE) to work with Python.

Please note that due to the way VxSim Python is generated from C++, automatic completion feature of IDEs may not always work with some Vortex Objects.

Embedded Python Scripts

To interpret embedded scripts in the content, a python package 2.7.13 comes bundled with Vortex Studio.

Script dynamics extensions can be added to the content to modify dynamics behavior and will use by default the bundled python modules.

Scripting Vortex Application

Python can also be used to script an external application that loads and simulates content.

Content could have been created by the Vortex Studio Editor or by code, be it c++ or even python

In order to do that, an external python distribution is required.

The current recommended system Python interpreter is Anaconda 4.3.0.1 - Python 64-bit 2.7.13, but any distribution should be compatible as long as it uses Python 64-bit 2.7.13.

(warning) However, there may be incompatibilities between Vortex Studio and some python modules present in your python distribution.

  • matplotlib uses Qt libraries of a different version than Vortex Studio. Some python modules or some Vortex Plugins may not load properly.
  • Anaconda on Windows is usually built with VC9. Some python modules, notably numpy, contain pyd files that might be incompatible with Vortex Studio, which is built with VC14

Ensure that the \bin path of your Vortex Studio installation is in the PATH and the PYTHONPATH environment variables.

To use Vortex SDK in Python, there is only one python module: VxSim.py. Developer simply needs to import the VxSim library.

  • Everything is VxSim in python e.g. C++ VxContent::Scene becomes VxSim.Scene, VxMath::Matrix44 becomes VxSim.Matrix44 etc...
  • The command dir() or dir(object) will list the module content and the object methods.
  • Help(object) will provide more information on one object in particular.
  • Python html doc generation using pydocpython -m pydoc -w VxSim wrote VxSim.html

  • Vortex Studio SDK container-like classes supports len() and operator[]

Import Vxsim
import VxSim
 
s = VxSim.Scene.create()

m = VxSim.Matrix44() 
r = len(m) # r = 4
c = len(m[0]) # c = 4

System Python Modules and Interpreter

If a python distribution is installed on the system, embedded scripts won't use by default the installed modules but the bundled ones within Vortex Studio.

In order to force the use of the installed python distribution, the following environment variable must be defined : VX_PYTHON_USE_SYSTEM=1

Vortex Studio will then use the installed modules if the Python version matches the one distributed with Vortex, i.e. 2.7.13.

Note that if the system python distribution python27.dll is not compiled with the same compiler or some modules use libraries not compiled with the same compiler as Vortex Studio, some run-time exceptions might occur.

In that case, python27.dll should be removed from the Vortex Studio installation bin directory and the python27.dll of the installed distribution will use if found using the PATH variable.

Information in the logging system is written when loading content to indicate which python version and which python modules are currently being used.

Vortex SDK - Python specificities

Property .value

In extensions, the inputs, outputs and parameters are object of type FieldBase, commonly called fields. 

Fields can be accessed in several ways:

  1. In a generic way, via methods getInput/getOutput/getParameter of any VxExtension. e.g. extension.getInput['Int'].value
  2. In a specific way via the interface. e.g part.inputControlType.value
  3. In a specific way within a dynamics python extension script, using self. e.g. self.inputs.myInt.value

The value of a Field can be read or written using the python property .value, a generic accessor that implicitly performs type conversion, which was added in the Vortex python API. 

If the field is an integer, property .value will be an integer. If the field is a reference to an Interface e.g Field<Assembly>, property .value will return that interface e.g. AssemblyInterface

.value Property
# The input is an int
extension.getInput['Int'].value = 8
 
# The output is a VxVector3, extract x from VxVector3
x = extension.getOutput['Vector3'].value.x
 
# The Parameter is an Field<Assembly>
extension.getParameter['Assembly'].value.addPart(part)

Section Set/Get Values on Inputs, Outputs and Parameters of a python dynamics extension below contains additional examples.

Setting value on vector using accessors

.value cannot be used in combination with any accessor to assigned a value. E.g., input.value.x = 1 won't work, one should use input.value = VxVector3(1,1,1)

Object Interfaces

The Vortex API exposes each Content Objects in Python using either their base type or their specialized types:

  • Base Content type objects: 
    • Scene -> SceneInterface
    • Configuration -> ConfigurationInterface
    • ConnectionContainer-> ConnectionContainerInterface
    • VxVHLInterface -> VxVHLInterfaceInterface
    • Mechanism -> MechanismInterface
    • Assembly -> AssemblyInterface

    • Part -> PartInterface
    • Constraint -> ConstraintInterface
    • CollisionGeometry -> CollisionGeometryInterface

  • Specialized Content objects that inherit from one of the base content types: 
    • prismatic constraint -> PrismaticInterface
    • hinge constraint -> HingeInterface
    • sphere cg -> SphereInterface
Converting Interfaces

Interface conversion is explicit. Base class objects can be converted to specialized objects using the right interface constructor.

Explicit Conversion
# Getting the HingeInterface from a ConstraintInterface
hinge = VxSim.HingeInterface(constraint.getExtension())  #constraint is a ConstraintInterface of an hinge.
  • Base Constraints Interface (or VxExtension of constraints) can also be converted to the right Interface using Constraint.getConstraintInterface(constraint)
  • Base Collision Geometry (or VxExtension of collision geometry) can also be converted to the right Interface using CollisionGeometry.getCollisionGeometryInterface(cg)

Most content objects have an interface version in Python. When they are not available, just use the VxExtensionFactory and work with the VxExtension in a generic way.

Custom Interfaces developed by clients will not have python Interface objects.

code snippet - creating an object
from VxSim import *
...
# Create and setup the application
...
# using create function from the Part class
part= Part.create() # part type is PartInterface
part.setName(partName)

# update partDef
box = Box.create() # box type is BoxInterface
 
# Part.addCollisionGeometry accepts BoxInterfaces
part.addCollisionGeometry(box) #part.addCollisionGeometry accepts CollisionGeometryInterface as well as all <cg>Interface objects
 
# Contrary to c++, python VxApplication.add() accepts VxExtension rather than Interfaces. Use getExtension() on the interface object to get it
application.add(part.getExtension())

...
# Working with CGs
# Part.getCollisionGeometries() returns an array of CollisionGeometryInterface, getting the proper cg type require a conversion
cg_0 = part.getCollisionGeometries()[0]
 
# To use the cg_0 as a Box, convert to a BoxInterface with getCollisionGeometryInterface using the VxExtension of the CollisionGeometryInterface
box = CollisionGeometry.getCollisionGeometryInterface( cg_0 ) # Since cgs_0 is a box, getCollisionGeometryInterface() returns a BoxInterface
 
# This is equivalent of doing
# box = BoxInterface( cg_0.getExtension() ) 

IMobile and transformation Matrix

Content objects that can be moved are derived from the IMobile interface. IMobileInterface objects can be moved with using method setLocalTransform, which takes VxSim.Matrix44 object describing the transformation in term of scale, rotation and translation.

IMobile
# get the output transform from a PartInterface object
output_transform = part.outputWorldTransform.value
 
# get position and orientation of the part as VxSim.VxVector3
position = VxSim.getTranslation(output_transform.value)
orientation = VxSim.getRotation(output_transform.value)
 
# move the part
# set the input local transform
new_position = VxSim.VxVector3(1,1,1)
part.setLocalTransform( VxSim.translateTo(output_transform.value, new_position) ) # prefer using setLocalTransform over Field inputLocalTransform.value

VxMath::Transformation

A series of global helpers exists at the VxSim level to simplify matrix computation. They are the equivalent of the C++ Global helpers in namespace VxMath::Transformation.

Transformation helpers
# Scale, Rotation and Translation Matrix Constructor
m = VxSim.createScale(x,y,z) # Create a scale matrix.
m = VxSim.createScale(scale) # Create a scale matrix from VxVector3.
m = VxSim.createRotation(rx, ry, rz) # Creates a rotation matrix. rx, ry and rz are Euler angles given in radian, using the default Euler order
m = VxSim.createRotation(axis, angle) # Creates a rotation matrix from an axis (VxVector3) and angle (radian).
m = VxSim.createRotation(quat) # Creates a rotation matrix from a quaternion
m = VxSim.createRotationFromOuterProduct(v, w) # Creates a rotation matrix from the outer product of two 3 dimensions vectors.
m = VxSim.createTranslation(tx, ty, tz) # Creates a translation matrix.
m = VxSim.createTranslation(translation) # Creates a translation matrix from VxVector3.

# Creates a transform matrix with a position sets to the eye and oriented to the center. 
# The first component of the matrix is the forward vector, the second one is the side vector and the third is the up.
m = VxSim.createObjectLookAt(eye, center, up)
 
# The first component of the matrix is the side-way vector, the second one is the up vector and the third is pointing backward to the center.
m = VxSim.createCameraLookAt(eye, center, up)

# Creates an orthographic projection matrix.
m = VxSim.createOrthographic(left, right, bottom, top, zNear, zFar)
 
# Creates a non-regular projection frustum.
m = VxSim.createFrustum(left, right, bottom, top, zNear, zFar)
 
# Creates a regular perspective projection frustum.
m = VxSim.createPerspective(double fovy, double aspectRatio, double zNear, double zFar)

# Extraction helpers
s = VxSim.getScale(m) # Get Scale VxVector3 from Matrix44
r = VxSim.getRotation(m) # Get Rotation VxVector3 from Matrix44
t = VxSim.getTranslation(m) # Get Translation VxVector3 from Matrix44

# Checks whether this matrix includes a perspective projection.
b = VxSim.isPerspectiveProjection(m)

# Affine Matrix operation
mt = VxSim.translateTo(m, translation) # Sets translation VxVector3 on m
mr = VxSim.rotateTo(m, rotation) # Sets rotation VxVector3 on m
ms = VxSim.scaleTo(m, scale) # Sets scale VxVector3 on m
m = VxSim.compose(scale, rotation, translation, flip) #C reates a matrix by composition with a VxVector3 scaling, then a VxVector3 rotation and a VxVector3 translation.
scale, rotation, translation, flip = VxSim.decompose(m) # Decomposes the affine matrix m into a scale, rotation and translation matrix. Rotation are given in the range [-pi, pi] for x, [-pi/2, pi/2] for y, [-pi, pi] for z.

Accessing Application Context

To get information relative to the application context, use getApplicationContext(). The method is available via a VxApplication, any VxExtension or self in the case of a dynamics python extension. Application context API is relatively simple and can be used for time/frame based related logic.

  • <obj>.getApplicationContext().getSimulationFrameRate() # Frame rate e.g. 60 means 60 fps
  • <obj>.getApplicationContext().getSimulationTimeStep() # Time of a step. Is the inverse of frame rate e.g. Frame Rate = 60, Time Step = 1/60 = 0.016666
  • <obj>.getApplicationContext().getSimulationTime() # Current Time of the simulation. Time increases by Time step every frame.
  • <obj>.getApplicationContext().getFrame() # Current Frame of the simulation
  • <obj>.getApplicationContext().getApplicationMode() # Current Simulation mode i.e. KEditingMode, kSimulatingMode or kPlaybackMode
  • <obj>.getApplicationContext().isPaused() # Indicates if the simulation is paused
  • <obj>.getApplicationContext().isSimulationRunning() # Indicates if the Simulation is running i.e. ApplicationMode is kSimulatingMode And isPaused == False

Application Scripting

Python can be used to prototypes, tests (some of Vortex internal tests are written with python) or a complete Vortex Application. The concept is no different than what is described in sections Integrating the Application and Creating an Application. The difference is that the application is started in python.

Creating a VxApplication

The first step is to create an application and setting it up. Use the Vortex Studio Editor to create its Application Setup. Then it is simply a matter of loading the setup to apply it.

Setting up an application
from VxSim import *
application = VxApplication()
serializer = ApplicationConfigSerializer()
 
serializer.load('myconfig.vxc')

# Extract the ApplicationConfig
config = serializer.getApplicationConfig()
 
# Apply it
config.apply(application)
...
# Load Content
...
# Run the application
... 

Note that like in C++, it is possible to insert and remove modules and extensions manually to the application as well as setting up some application parameters.

Likewise, it is possible to create an ApplicationConfig object in python. However, not all modules and extensions factory keys and the VxID to its parameters are exposed in python. Although it is technically possible to create a valid application setup in python, the user in encouraged to use the Vortex Studio Editor to create its Application Setup.

Loading Content

Loading content created using the Vortex Studio Editor should be done with the VxSimulationFileManager provided by the VxApplication. The object loaded is distributed across the network. Object loaded this way are not meant to be edited, most changes will not be distributed. To edit content using python, see section Content Creation.

Content gets unloaded on its own when the application is destroyed, but should you need to remove content, the simulation file manager can also be used to remove content via unloadObject().

Example:Loading a scene
...
application = VxApplication()
...
# Setup the application
...
# Get the file manager to load content
fileManager = application.getSimulationFileManager()

# Load the file, the object returned is the root object, in this case, a scene. The Reference should be kept to inspect the content during the simulation
# The object loaded is already added to the application
scene = SceneInterface( fileManager.loadObject("myScene.vxscene") )
if (scene.valid()):
	# Work with scene
	...
	# run application
	...
	# Done with content
	fileManager.unloadObject("myScene.vxscene")
	...
	# Load new content and continue...

Running the Application

Once the application has some content loaded or created, it can be updated.

Function update() should be called once per frame, no matter the simulation mode and also when the simulation is paused, as modules requires updates no matter the mode or the pause state. The application context is available like in c++ and the mode should be set properly depending on what you are doing, 

If you only need to run the application until it ends, use function VxApplication.run().

Inspecting and updating content

To look into content, a user can browse from the VxObject returned by the simulation file manager. It is possible to use <content_object>Interface to get to the object of interest, as explained in the Content Creation in python section above.

The following is an example of how to use an hinge constraint retrieved from a scene object in Python:

Browsing content
...
# Create and setup the application
...
#Load content
# loadObject returns a VxObject type, it needs to be converted to a SceneInterface (C++ equivalent of VxSmartInterface<Scene>)
scene = SceneInterface( fileManager.loadObject("myScene.vxscene") )
if scene.valid():
	# Browse to my hinge
    # scene.getMechanisms returns an array of MechanismInterface, no conversion needed
    mechanism = scene.getMechanisms()[0]
	# mechanism.getExtensions returns an array of IExtensionInterface
	iextension = mechanism.getExtensions ()[0]
	connectionContainer = ConnectionContainerInterface(iextension .getExtension())
 
    # mechanism.getAssemblies returns an array of AssemblyInterface, no conversion needed    
	assembly = mechanism.getAssemblies()[0]
    # assembly.getConstraints() returns an array of ConstraintInterface, getting the proper constraint type require a conversion
    constraint = assembly.getConstraints[0]
     
    # To use the constraint as a hinge, create the HingeInterface using the VxObject of the ContraintInterface
    hinge = ConstraintInterface.getConstraintInterface(constraint) #since contraint is an Hinge, getConstraintInterface() returns an HingeInterface
	# this is the equivalent of doing
	# hinge = HingeInterface(constraint.getExtension())
 

Once the references to the required objects have been established, the external application basically need to update the inputs, execute an update and read the values from the outputs. Data should not be written into outputs. Parameters value should not change during simulation. In python, when you have access to the interface API, it is preferable to use it to get access to the fields rather than using function getInput()/getOutputs()/getParameters() on the VxExtension, as the second option requires knowledge of the field's VxID and is less efficient.

Getting data from content
...
# Create and setup the application
...
#Load content
...
# Set the input Velocity, with is a Field<VxVector3>, property value is a VxVector3
part.inputLinearVelocity.value = VxSim.VxVector3(1,1,1)

# Updates the application's modules
application.update()

# Get the mechanism position from an custom extension with a field<Mechanism>, using getParameter since the Interface is not available in python
# property value is a MechanismInterface
mechanism = myCustomExtension.getParameter('Mechanism').value
mechPos = VxSim.getTranslation( mechanism.outputWorldTranform.value )
...
 

Using Keyframes to restore the initial positions

Keyframes can be used to restore the values of content objects taken at some point in time, e.g. when starting the simulation.

First thing to do is to create a keyframe list and then use it to save and restore keyframes.

The following provides an example saving one keyframe and restoring it later.

Save and Restore Keyframe
        # init key frame
        application.update()
        keyFrameList = application.getContext().getKeyFrameManager().createKeyFrameList("KeyFrameList", False)
        application.update()
        key_frames_array = keyFrameList.getKeyFrames()
        # len(key_frames_array) should be 0

        # first key frame
        frameId1 = keyFrameList.saveKeyFrame()
        waitForNbKeyFrames(1, application, keyFrameList)
        key_frames_array = keyFrameList.getKeyFrames()
        # len(key_frames_array) should be 1
        # key_frames_array[0] should not be None

        # wait a bit, do something...
        counter = 0
        while(self.application.update() and counter < 60):
            counter += 1

        # restore first key frame       
        keyFrameList.restore(key_frames_array[0])
        self.application.update()

In contrary to the c++ implementation, there is no callback implemented to know when a keyframe is ready, therefore a small pull function must be implemented

waitForNbKeyFrames
def waitForNbKeyFrames(expectedNbKeyFrames, application, keyFrameList):
    maxNbIter = 100
    nbIter = 0
    while len(keyFrameList.getKeyFrames()) != expectedNbKeyFrames and nbIter < maxNbIter:
        if not application.update():
            break
        ++nbIter

Content Creation

Content can also be created, or updated after being loaded.

Please read Vortex Studio SDK - Creating Content for the fundamentals of content creation. The concepts describes in that section applies in python as well and will not be repeated here.

Working with definitions and instances

Use the VxObjectSerializer to load and save your document definition. Children in document are instances and must be instantiated like in C++.

code snippet - I/O with an object
# loading a definition
serializer = VxObjectSerializer()
serializer.load(fileName)
partDef = PartInterface(serializer.getObject()) #getObject() returns a VxObject so the PartInterface must be created

...
#Modify the definition
...
#saving a definition
serializer = VxObjectSerializer(partDef)
serializer.save(fileName)
 
Clone, instantiate and sync

The C++ global functions to perform those operations are not available in python. However, all python Smart Interfaces have additional functions that their C++ counterpart do not have: clone, instantiate and sync

code snippet - sync operations
# Clone the part since I need a part with a box CG, but lighter
partDefClone = partDef.clone()
partDefClone.parameterMassPropertiesContainer.mass.value = lighter
 
# Instantiate a definition
partInstance = partDef.instantiate()

# add it to a previously created assembly
assembly.addPart(partInstance)

# move the instance
partInstance.setLocalTransform(transform) #Instance data
partInstance.inputControlType.value = Part.kControlDynamic #Instance Data

...
# Modify partDef by changing the mass and adding a sphere in place of a box
partDef .parameterMassPropertiesContainer.mass.value = heavier # Definition data
partDef .removeCollisionGeometry(box)
sphere = Sphere.create()
partDef .addCollisionGeometry(sphere)
...
# Sync to get the definition data
partInstance.sync()
# After the sync, part instance mass is heavier, have a sphere in its CG

Content Creation examples

These examples reproduce the C++ examples found in Vortex Studio SDK - Creating Content

See python tutorial Content Creation for additional examples.

The following code snippets are not intended to produce a complete, runnable example when combined as-is.

Collision Geometries


Collision Geometries
# create a box
box = VxSim.Box.create()
 
# Set the box dimensions (x,y,z) to (1 meters, 2 meters, 3 meters)
box.parameterDimension.value = VxSim.VxVector3(1.0,2.0,3.0)

Parts

Part
# create a part
part = VxSim.Part.create()
# set the mass
part.parameterMassPropertiesContainer.mass.value = 1.0
 # Adding a CG
part.addCollisionGeometry(box)

Assemblies

Assemblies
# create an assembly
assembly = VxSim.Assembly.create()

# create an instance from the part definition
partInstance = part.instantiate()   

# Sets Position and Control type
partInstance.setLocalTransform( VxSim.createTranslation(1.0,2.0,3.0) )
partInstance.inputControlType.value = VxSim.Part.kControlDynamic
 
#Add the part instance to the assembly
assembly.addPart(partInstance)

Constraints

Constraints
# create an hinge
hinge = VxSim.Hinge.create()
 
# set the attachment parts
hinge.inputAttachment1.part.value = part1
hinge.inputAttachment1.part.value = part2
 
# set the attachment positions
hinge.inputAttachment1.position.value = VxSim.VxVector3(1.0,0.0,0.0)
hinge.inputAttachment2.position.value = VxSim.VxVector3(1.0,0.0,0.0)
 
# set the attachment primary axis
hinge.inputAttachment1.primaryAxis = primaryAxis
hinge.inputAttachment2.primaryAxis = primaryAxis
 
# Add the hinge to the assembly
assembly.addConstraint(hinge)

Attachments

Attachments
# create a first attachment point
attPt1 = VxSim.AttachmentPoint.create()
attPt1.parameterParentPart.value = partInstance1
assembly.addAttachmentPoint(attPt1)

# create a second attachment point and add it 
attPt2 = VxSim.AttachmentPoint.create()
attPt2.parameterParentPart.value = partInstance2
assembly.addAttachmentPoint(attPt2)

# create an attachment using both attachment points
attachment = VxSim.Attachment.create()
attachment.setAttachmentPoints(attPt1, attPt2)

# add the attachment to the assembly
assembly.addAttachment(attachment)
 
# attach 
attachment.inputAttach = True

Mechanisms

Mechanisms
# create a mechanism
mechanism = VxSim.Mechanism.create()
 
# create an instance from the assembly definition
assemblyInstance = assembly.instantiate()    

# Position
assemblyInstance.setLocalTransform( VxSim.createTranslation(1.0,2.0,3.0) )

# Add the assembly instance to the mechanism
mechanism.addAssembly(assemblyInstance)

Scenes

Scenes
scene = VxSim.Scene.create()

# create an instance from the mechanism definition
mechanismInstance = mechanism.instantiate()

# Position
mechanismInstance.setLocalTransform( VxSim.createTranslation(1.0,2.0,3.0) )

# Add the mechanism instance to the scene
scene.addMechanism(mechanismInstance)

VHL Interface

VHL
# Create the VHL extension
vhlInterface = VxSim.VxVHLInterface.create()

# Add a hinge control field as input of VHL. Make sure to use the correct hinge extension (i.e. from the assembly instance)
vhlInterface.addInput('Input Value', hingeFromAssemblyInstance.inputAngularCoordinate.control)        

# Add the VHL extension to the mechanism
mechanism.addExtension(vhlInterface.getExtension()) 

Connection Container

Connections Container
# Create the connection container extension
connectionContainer = VxSim.ConnectionContainerExtension.create()

# Add a connection between two fields : my custom extension output and a part input control
# Types have to be compatible for the connection creation to succeed
connectionIndex = connectionContainer.create(myCustomExt.getOutput('CustomOutput'), part.inputControlType)

# Add the connection container to the mechanism
mechanism.addExtension(connectionContainer.getExtension())

Python Scripting Extension

Python extension do not have a dedicated interface, so a VxExtension is used directly.

Python Extension
# Create the python scripting extension
pythonExt = VxSim.VxExtensionFactory.create(VxSim.VxSimPythonDynamicsICD.kFactoryKey)

# Add the scripting file as parameter - the script can also be added as a string
pythonExt.getParameter(VxSim.VxSimPythonDynamicsICD.kScriptFile)value = '/mypythonscript.py'

# Add a parameter of type Part and set its reference
pythonExt.addParameter('ParamPart', VxSim.Types.Type_Part)
pythonExt.getParameter('ParamPart').value = part

# Add an input of type boolean and set its value
pythonExt.addInput('Switch', VxSim.Types.Type_Bool)
pythonExt.getInput('Switch').value = False

# Add an output of type integer and set its value
Vx::VxID kOutputControl("Control Mode")
pythonExt.addOutput('Control Mode', VxSim.Types.Type_Int)
pythonExt.getOutput('Control Mode').value = 0

# Add the connection container to the mechanism
mechanism.addExtension(pythonExt)

Configuration

Configuration can also be edited in python, but is is preferable to use the Vortex Studio Editor, as it is easy to make mistakes in code.

Configuration
...
# Add 4 extensions to mechanism
myExtension1 = VxSim.VxExtensionFactory.create(MyFactoryKey)
myExtension2 = VxSim.VxExtensionFactory.create(MyFactoryKey)
myExtension3 = VxSim.VxExtensionFactory.create(MyFactoryKey)
myExtension4 = VxSim.VxExtensionFactory.create(MyFactoryKey)
 
# Sets default value
myExtension1.parameterA.value = 1
myExtension1.parameterB.value = 2
myExtension1.parameterC.value = 3

myExtension2.parameterA.value = 4
myExtension2.parameterB.value = 5
myExtension2.parameterC.value = 6

myExtension3.parameterA.value = 7
myExtension3.parameterB.value = 8
myExtension3.parameterC.value = 9

myExtension4.parameterA.value = 10
myExtension4.parameterB.value = 11
myExtension4.parameterC.value = 12

mechanism.addExtension(myExtension1)
mechanism.addExtension(myExtension2)
mechanism.addExtension(myExtension3)
mechanism.addExtension(myExtension4)
...
# Create a configuration
configuration = VxSim.Configuration.create()
configuration.setName('Configuration1')
mechanism.addExtension(configuration.getExtension())
 
# Configuration will modify Extension 1, remove extension 2 and add extension 4
configuration.addReference(myExtension1, VxSim.kModify)
configuration.addReference(myExtension2, VxSim.kRemoveOnActivation)
configuration.addReference(myExtension4, VxSim.kAddOnActivation) # Because the flag is AddOnActivation, myExtension4 is immediately removed from the content. It still exist with mechanism but no module will received it.
 
# Start edition.
if(configuration.canActivate()[0]): # just to make sure there is no conflict, canActivate has 2 return values, the first being True are False, the second being the conflicts
{
	configuration.inputActivate.value = True # Activation will remove myExtension2 and add myExtension4.
    mApplication.update() # activation requires an application update
 
	if(configuration.outputActivated.value):
	{
		# Edit the extension 1 and 4
		myExtension1.parameterA.value = -1
		myExtension1.parameterC.value = -10
 
		myExtension4.parameterB.value = 42
		
        # Done editing
		mConfiguration.inputActivate.value = False
		mApplication.update() # deactivation requires an application update
	}
	else:
	{
		# Handles errors
		errors = mConfiguration.getRuntimeErrors()
	}
}
   
... # Continue edition
 
# Create a scene
scene = VxSim.Scene.create()
# instantiate a mechanism
mechInstance = mechanism.instantiate()
 
# myExtension1 is in the content, parameterA value is 1, parameterB is 2 and parameterC is 3
# myExtension2 is in the content, parameterA value is 4, parameterB is 5 and parameterC is 6
# myExtension3 is in the content, parameterA value is 7, parameterB is 8 and parameterC is 9
# myExtension4 is NOT in the content
scene.addMechanism(mechInstance)
 
# Activate the mechanism configuration in the scene
confInstance = VxSim.ConfigurationInstance( mechInstance.getObject().findExtensionByName('configuration1', False) )
confInstance.inputActivate.value = True
mApplication.update() # requires an application update
 
# myExtension1 is in the content, parameterA value is 11, parameterB is 2 and parameterC is -10
# myExtension2 is NOT in the content
# myExtension3 is in the content, parameterA value is 7, parameterB is 8 and parameterC is 9
# myExtension4 is in the content, parameterA value is 10, parameterB is 42 and parameterC is 12

Vortex Automated Test Platform

A module containing tools to write test in python with Vortex is made available in the binary directory.

This module is called vxatp.

Structure of vxatp folder

vxatp has the following directory structure:

  • vortex \bin\vxatp : the python package
  • vortex \bin\vxatp_*.bat : batch helpers to run tests from the command line
  • vortex \resources\vxatp\ : resources used by vxatp, e.g. config files

Setting the environment to run vxatp

The batch command vxatp_set_env.bat details how to set properly set the environment when starting from the binary installation directory.

vxatp_set_env
set VXTK=%~dp0..\
set PATH=%VXTK%\bin;%VXTK%\plugins;%VXTK%\bin\osgPlugins-3.2.0;%PATH%
set OSG_FILE_PATH=%VXTK%\resources\images;%VXTK%\resources\shaders
set PYTHONPATH=%VXTK%\bin;%PYTHONPATH%

Any IDE or python interpreter would have to setup or inherit the same environment.

How to script a test

The vxatp test scripts are similar to the unittest ones : unittest#basic-example.

From the configuration given, a VxApplication can be created to perform operations on assets.

An helper is provided in VxATPConfig that return a setup application ready to be tested : VxATPConfig.createApplication(self, prefix_name, config_name).

In addition, VxATPConfig.createApplication sets to the test case a configuration member that contains useful information in the context of vxatp.

The configuration can be accessed with self._config .

The configuration contains the following:

  • self._config.app_config_file : the setup file. It can be overridden by the test itself to add or remove specific vortex modules.
  • self._config.app_log_prefix : the prefix for the log file. It can be overridden by the test to specify the name of the test case in it.
  • self._config.output_directory : the output directory desired by the test suite. It should be used by the test to output data in a safe place (SVN and other versioned locations must be avoided).

VxSim python module must also be imported in order to use the vortex python binding of the SDK.

Example from unittest documentation adapted to vxatp
import os
import unittest

from VxSim import *
from vxatp import * 

class TestMethods(unittest.TestCase):

  directory = os.path.dirname(os.path.realpath(__file__))
 
  def test_upper(self):
     self.assertEqual('foo'.upper(), 'FOO')

  def test_isupper(self):
     self.assertTrue('FOO'.isupper())
     self.assertFalse('Foo'.isupper())

  def test_loading(self):
     # create a VxApplication while applying a setup file contained in the test directory 
     application = VxATPConfig.createApplication(self, setup='%s/config.vxc' % self.directory)

     # load content and verify it has been correctly loaded
     file = '%s/MyAsset.vxscene' % self.directory
     object = application.getSimulationFileManager().loadObject(file)
     self.assertIsNotNone(object)

if __name__ == '__main__':
    vxatp_run_thisfile('my_vxatp_output_directory')

Helpers for default setups

VxATPConfig provides optional helpers to get default pre defined setup of the application :

  • VxATPConfig.getAppConfigWithoutGraphics() return absolute path to default config not containing graphics module
  • VxATPConfig.getAppConfigWithGraphics() return absolute path to default config containing graphics module

Helper for tests parametrization

vxatp provides a decorator that allow tests to be parametrized.

The following gives an example of its usage.

Example using VxATPUtils.parametrize
import os
import unittest

from VxSim import *
from vxatp import * 

class TestMutipleDiff(unittest.TestCase):

  directory = os.path.dirname(os.path.realpath(__file__))
 
  @VxATPUtils.parametrized([['diff_one', 3, 5, -2], ['diff_two', 13, 8, 6], ['diff_three', 13, 8, 5]])
  def test_diff(self, test_name, a, b, expected):
     self.assertTrue(a-b == expected, '%d-%d does not match expected value %d.' % (a,b,expected))

if __name__ == '__main__':
    vxatp_run_thisfile('.\\results\\')

The following is the output generated by the test.

example of parametrized test output
-------------------------------------------------------------------------
Running Test : test_difference
-------------------------------------------------------------------------

======================================================================
FAIL [0.000s]: test_diff_diff_two (test_difference.TestMutipleDiff)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\CM Labs\Vortex Studio 2018b\bin\vxatp\vxatp_utils.py", line 108, in parametrized_func_wrapper
    return fn(*a, **kwargs)
  File "S:\scripts\test_difference.py", line 13, in test_diff
    self.assertTrue(a-b == expected, '%d-%d does not match expected value %d.' % (a,b,expected))
AssertionError: 13-8 does not match expected value 6.

test_difference.TestMutipleDiff - test_diff_diff_one:           Pass
test_difference.TestMutipleDiff - test_diff_diff_three:         Pass
test_difference.TestMutipleDiff - test_diff_diff_two:           Fail

Ran 3 test cases in 0.000s

FAILED (failures=1, errors=0)


How to call a vxatp test from the command line

There are two ways a vxatp test can be called from the command line :

Calling a vxatp test like any unittest.TestCase

See unittest#command-line-interface (e.g. python my_test.py).

A convenience batch file is also provided to set up all Vortex required environment variables.

running one vxatp test script
vxatp_run_onetest.bat my_test.py

Calling vxatp test using vxatp_launcher

vxatp_launcher can be used to run all tests in a given directory or recursively or interactively

running all vxatp test scripts in a given directory
vxatp_launcher.py -p my_test_directory -o my_output_directory_for_results_and_logs
running all vxatp test scripts recursively
vxatp_launcher.py -p my_test_directory -o my_output_directory_for_results_and_logs -r
running one vxatp test script interactively
vxatp_launcher.py -p my_test_directory -o my_output_directory_for_results_and_logs -i -r
running all vxatp test scripts recursively with forcing graphics
vxatp_launcher.py -p my_test_directory -o my_output_directory_for_results_and_logs -g -r

Naming Convention

To be found by discovery, test scripts must follow the default pattern of the unittest package : "test*.py".

"test_my_test_name.py" is the recommended syntax.


In addition to running all test cases, the runner outputs xml formatted logs that can be used by any JUnit compatible parser.

example of console output
-------------------------------------------------------------------------
Running Test : content
-------------------------------------------------------------------------

test_content_rover.TestContentRover - test_validate_content_rover_assembly:             Pass
test_content_rover.TestContentRover - test_validate_content_rover_mechanism:            Pass
test_content_rover.TestContentRover - test_validate_content_rover_scene:                Pass

Ran 3 test cases in 1.285s

SUCCESS
an example of xml output
<testsuite duration="1.285" errors="0" failures="0" name="content" tests="3">
  <testcase classname="test_content_rover.TestContentRover" duration="0.400" name="test_validate_content_rover_assembly" start_time="2016-01-07 14:30:25"/>
  <testcase classname="test_content_rover.TestContentRover" duration="0.376" name="test_validate_content_rover_mechanism" start_time="2016-01-07 14:30:26"/>
  <testcase classname="test_content_rover.TestContentRover" duration="0.509" name="test_validate_content_rover_scene" start_time="2016-01-07 14:30:26"/>
</testsuite>

More batch helpers

As convenience, the following batch files are provided in the bin directory

set environment variables to use vxatp_launcher.py
vxatp_set_env.bat
running one vxatp test script
vxatp_run_onetest.bat my_test.py
running all vxatp test scripts recursively from the test_directory
vxatp_run_tests.bat test_directory output_directory
running one vxatp test script interactively discovered recursively from the test_directory
vxatp_run_tests_interactive.bat test_directory output_directory
running one vxatp test script with graphics discovered recursively from the test_directory
vxatp_run_tests_interactive.bat test_directory output_directory

How to use vxATP in Visual Studio 2015

Visual Studio has python tools available as addon.

It includes among others functionalities an editor and a debugger.

To use it with vxatp, you need first to configure your python environment to be one compatible with Vortex.

Next, in your python project properties the 'Working Directory' must be the binary directory of your Vortex installation.

To be able to run and debug, the 'Search Paths' of the 'Debug' property tab must contain your binary directory of your Vortex installation, the vxatp path of your binary installation and optionally the path containing your test scripts.

The settings should be enough to run and debug your vxatp based verification scripts in Visual Studio.

Python Dynamics Scripting

Using python scripting, a user can change the behavior of a scene or a mechanism  by adding a script extension. For example, if a mechanism consists of two parts that are constrained by a hinge, adding tension to that mechanism will not cause it to break. However, a simple script that reads the tension and disables the constraint if it reaches a certain threshold is easy to add. The execution of the script will accurately simulate breaking the hinge. The script is part of the content.

Vortex Studio comes prepackaged with its own version python 2.7.7, but it you have a python installation, Vortex will use that one, allowing you to use external libraries within your scripts.

Inserting a python dynamics extensions

Using the Vortex Studio Editor, a script extension can be added to a mechanism or a scene. The script extension will run a python script, either an external .py file or directly embedded as a string. The script extension is a special type of IDynamics Extensions that implements some of the standards IDynamics and IExtension callbacks:

FunctionMethod DocumentationBehavior
def on_add_to_universe(self, universe)IDynamics::onAddToUniverse

Use this method to define specific dynamics actions that must be taken at initialization. This is called the that application goes in Simulating Mode.

def on_remove_from_universe(self,universe)IDynamics::onRemoveFromUniverse

Use this method to define specific dynamics actions that must be taken at shutdown. This is called when the application goes out of Simulating Mode.

def pre_step(self)IDynamics::preStep

Called before the collision detection and before the dynamic solver.

Use this method to get inputs or set values to dynamics objects.

def post_step(self)IDynamics::postStep

Called after the collision detection and after the dynamic solver.

Use this method to set outputs or get values from dynamics objects.

def paused_update(self)IDynamics::pausedUpdate

Called when the simulation is in Editing or playback Mode, or when is paused

Use this method to set outputs or get values from dynamics objects.

def on_state_save(self, data)IExtension::onStateSaveCalled after the key frame is taken. It is possible to modify the provided data parameter, which is an empty dictionary and store values that will be provided back in the on_state_restore. The following python types are supported: booleans, integers, long integers, floating point numbers, complex numbers, strings, Unicode objects, tuples, lists, sets, frozensets, dictionaries, and code objects, where it should be understood that tuples, lists, sets, frozensets and dictionaries are only supported as long as the values contained therein are themselves supported; and recursive lists, sets and dictionaries should not be written (they will cause infinite loops). The singletons None, Ellipsis and StopIteration can also be saved and restored.
def on_state_restore(self, data)IExtension::onStateRestoreCalled after the key frame is fully restored. Data is a dictionary filled with the values that were provided in the corresponding on_state_save.

The best way to add a python script is to use the Vortex Studio Editor. The user can add inputs, outputs and parameters to the python extension that will be used within the script and they can be connected as normal. The script window come with these callback already defined.

If you want to use your own python IDE to edit your python code, simply update the python extension to use the python file created by your python IDE by setting the Script File Parameter.  The Vortex Studio Editor python property browser will update itself when the python file is saved by your IDE.

Set/Get Values on Inputs, Outputs and Parameters of a python dynamics extension

For embedded script in content, the self parameter contains accessors to the field value without the need of calling getInput/getOutput/getParameter. The value property is used to read and write. The field value can thus be accessed with self.<container>.<field>.value, where the <container> is the name of the container in lowercase (i.e., "inputs", "outputs" or "parameters") and the <field> is the name of the field with non-alphanumeric characters replaced by underscores. You should use these accessors over self.get<FieldType>, they are more efficient as the lookup is already done and cached.

ExampleDrum script extensions from the Mobile Crane Sample, Field Rope Length is accessed with self.inputs.Rope_Length. The concept is the same for Anti-Two Block Warning and Max Rope Length.


Python extensions are expensive in a real-time simulation, so the Dynamics Script should cache as much as possible and avoid repeating operations.

Value access Examples

Here is a series of examples accessing several fields in dynamics objects.

Accessing Parts and Collision Geometries

Fields containing dynamics objects are accessed using the value accessor and the output transform.

Vortex Dynamics Part
hook = self.parameters.Hook.value
pulley = self.parameters.Pulley.value
diff = getTranslation(hook.outputWorldTransform.value) - getTranslation(pulley.outputWorldTransform.value)
distance = math.sqrt(diff.x ** 2 + diff.y ** 2 + diff.z ** 2
Accessing Attachments

Attachment Points and Attachments can be accessed the same way as any other dynamics object.

Vortex Dynamics Attachment
attachment_point1 = self.parameters.AttachmentPoint_A.value
attached_part1 = attachment_point1.parameterParentPart.value

attachment_point2 = self.parameters.AttachmentPoint_B.value
attached_part2 = attachment_point2.parameterParentPart.value

attachment = self.parameters.Attachment.value
attachment.setAttachmentPoints(attachment_point1, attachment_point2)
attachment.inputAttach = True
Accessing Constraints

Base class, e.g., constraints, is still accessible since the object derives from it.

Enums, instead of strings have to be used for some properties, e.g., control of the constraint.

Vortex Dynamics Constraint
drum = self.parameters.Drum.value

# accessing drum as a Hinge
drum_c = drum.inputAngularCoordinate  # Equivalent to drum.inputCoordinates[0]
drum_c.lock.position.value = 0
drum_c.control.value = Constraint.kControlLocked 
Rigging from Offshore Crane Samples 
Vortex Sample Main Rigging
from VxSim import *

def on_add_to_universe(self, universe):
	self.inputs.Connect.value = False

def on_remove_from_universe(self, universe):
	self.inputs.Connect.value = False

def pre_step(self):
	self.outputs.Attach.value = self.inputs.Connect.value
Hydraulic Boom from Mobile Crane Sample 
Vortex Mobile Crane Sample Hydraulic boom
from VxSim import *
import math


filteredHydraulic = 0.0


def on_add_to_universe(self, universe):
    global filteredHydraulic
    filteredHydraulic = 0.0
    Hydraulic = self.parameters.Hydraulic.value.inputLinearCoordinate
    Hydraulic.control.value = Constraint.kControlLocked
def post_step(self):
    global filteredHydraulic
    Throttle = self.inputs.Throttle.value
    Direction = self.inputs.Direction.value
    filteredHydraulic = lowPassFilter( filteredHydraulic, Direction, 0.001 )
    Hydraulic = self.parameters.Hydraulic.value.inputLinearCoordinate
    BoomOut = self.parameters.Boom.value.outputAngularCoordinate
    Hydraulic.lock.velocity.value = filteredHydraulic * Throttle
    self.outputs.Boom_Angle.value = -math.degrees( BoomOut.currentStatePosition.value )
def lowPassFilter( inputSignal, outputSignal, timeConstant ):
    deltaTime = getSimulationTimeStep()
    value = ( (deltaTime * inputSignal) + (timeConstant * outputSignal) ) / (deltaTime + timeConstant )
    return value
    
def on_state_save( self, data ):
    global filteredHydraulic
    
    # Save the filter internal state
    data['filteredHydraulic'] = filteredHydraulic
def on_state_restore( self, data ):
    global filteredHydraulic
    
    # Restore the filter internal state
    if data['filteredHydraulic']:
        filteredHydraulic = data['filteredHydraulic']
    else:
        filteredHydraulic = 0.0
Arm Controller from EOD Sample
Vortex EOD Sample Arm Controller
from VxSim import *

def pre_step(self):
	shoulder_rot = self.inputs.Shoulder_Rotation_Signal.value
	shoulder_piv = self.inputs.Shoulder_Pivot_Signal.value
	elbow1_piv = self.inputs.Elbow_1_Pivot_Signal.value
	elbow2_piv = self.inputs.Elbow_2_Pivot_Signal.value 

	basearm = self.parameters.Base_Arm.value
	basearm_c = basearm.inputAngularCoordinate
	basearm_c.motor.desiredVelocity.value = shoulder_rot * -2. / 10. 

	arm01 = self.getParameter("Arm 01").value
	arm01_c = arm01.inputAngularCoordinate
	arm01_c.motor.desiredVelocity.value = shoulder_piv * 1. / 10. 

	arm02 = self.getParameter("Arm 02").value
	arm02_c = arm02.inputAngularCoordinate
	arm02_c.motor.desiredVelocity.value = elbow1_piv * -2. / 10. 

	arm03 = self.getParameter("Arm 03").value
	arm03_c = arm03.inputAngularCoordinate
	arm03_c.motor.desiredVelocity.value = elbow2_piv * 2. / 10.

Python Tutorials

See Python Tutorials for the list of tutorials

Advanced

C++ Smart Interface vs. python

Most of the C++ interfaces have their have their equivalent in the Vortex Python API.

C++ Smart Interfaces are IExtension's implementation with their VxExtension underneath, wrapped into a nice template class and is the preferred way of working with Vortex objects, rather than using VxExtension in a generic way, which requires the user to know every fields' Id.

In python, it is still possible to work with VxExtension, like in C++. They can be created via the VxExtensionFactory as usual and still requires the knowledge of the field's id.

In order to use an equivalent of a c++ smart interface in python, the Python API have objects named <content_object>Interface, where <content_object> is the name of the c++ interface. e.g. PartInterface python object is the C++ equivalent of VxSmartInterface<Part>.

To use content objects in Python:

  • Methods are accessed via <content_object>Interface.<interface_methods>, e.g. mechanism.getExtensions() where mechanism is a MechanismInterface python object (C++ equivalent of VxSmartInterface<Mechanism>).
  • <content_object>Interface.getObject() returns a VxObject typed instance, none is returned if it is not a VxObject.
  • <content_object>Interface.getExtension() returns the VxExtension typed instance
  • Can be created by calling <content_object>Interface.create(), or by constructing an <content_object>Interface from a VxExtension
Interface conversion
from VxSim import *
 
# create a VxDynamics::Part
part = Part.create()
# This is the equivalent of doing
extension = VxExtensionFactory.create(PartICD.kFactoryKey)
part = PartInterface(extension)
 
part->addCollisionGeometry(cg) # using it has an interface


Since all objects are IExtension or IObject, all objects in python are IExtensionInterface and IObjectInterface. However, passing from one to another, requires and explicit conversion.

Given object a of type <content_object>InterfaceA, to get an object b of type <content_object>InterfaceB : 

  • b = <content_object>InterfaceB ( a.getExtension() ) e.g., ConstraintInterface to HingeInterface or IExtensionInterface object to ConnectionContainerInterface.
Interface conversion
# mechanism.getExtensions returns an array of IExtensionInterface
iextension_0 = mechanism.getExtensions ()[0]
connectionContainer = ConnectionContainerInterface(iextension_0.getExtension())


Note that while in C++ many functions (such as VxApplication::add() ) that take VxSmartInterface in a generic way work because in C++ the conversion is implicit, it won't work in python because conversion is explicit. Some of the Python API was extended to accept VxExtension to simplify general usage. The user simply needs to call getExtension() on the object interface when working in Python.

getExtension()
import VxSim
  
application = VxSim.VxApplication() # VxApplication is not a Smart Interface. Just call the constructor
part = VxSim.Part.create()
# Contrary to c++, python VxApplication.add() accepts VxExtension rather than Smart Interface. Use getExtension() on the interface object 
application.add(part.getExtension())
 
# addExtension on a VxDynamics::Mechanism was modified to accept VxExtension
connectionContainer = ConnectionContainer.create()
mechanism.addExtension( connectionContainer.getExtension() ) 


2.7.13