|
To fully understand the following section, the reader is expected to be familiar with the Python scripting language.
Vortex Studio SDK is partially available in Python scripting.
Python scripting can be used:
VxSim
library in Python, although for advanced operations, like accessing contacts during the simulation, PyVX usage is required since it exposes the VxCore API.You can use your preferred integrated development environment (IDE) to work with Python. Please note that due to the way we generate Python from C++, automatic completion features of IDEs may not always work with some Vortex objects.
To interpret embedded scripts in the content, 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 the bundled Python modules by default.
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.
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
. Developers simply need to import the VxSim
library.
VxSim
in Python, e.g., C++ VxContent::Scene becomes VxSim.Scene, VxMath::Matrix44 becomes VxSim.Matrix44.dir()
or dir(object)
will list the module content and the object methods.Help(object)
will provide more information on one object in particular.pydoc
: python -m pydoc -w VxSim wrote VxSim.html
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 |
If a Python distribution is installed on the system, embedded scripts will not use the installed modules by default but rather 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.
python27.dll
is not compiled with the same compiler or some modules use libraries not compiled with the same compiler as Vortex Studio, some runtime exceptions might occur.python27.dll
should be removed from the Vortex Studio installation bin directory and the python27.dll
of the installed distribution will be used if found using the PATH variable.In extensions, the inputs, outputs and parameters are objects of type FieldBase
, commonly called fields.
Fields can be accessed in several ways:
getInput/getOutput/getParameter
of any VxExtension, e.g., extension.getInput['Int'].value
.
part.inputControlType.value
.self
, e.g., self.inputs.myInt.value
.next(iter(extension.getInputContainer())).value
.iter
and next
, it is understood that these containers can be iterated upon in a Pythonic way. For example, the first field's name in this container can be discovered using extension.getInputContainer()[0].getID().asString()
, and the value using extension.getInputContainer()[0].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, .value
will be an integer. If the field is a reference to an interface, e.g., Field<Assembly>
, .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) |
The section Set/Get Values on Inputs, Outputs and Parameters of a Python Dynamics Extension below contains additional examples.
.value
cannot be used in combination with any accessor to assign a value. For example, input.value.x = 1
won't work, one should use input.value = VxVector3(1,1,1)
instead.The Vortex API exposes each Content Object in Python using either their base type or their specialized types:
SceneInterface
ConfigurationInterface
ConnectionContainerInterface
VxVHLInterfaceInterface
MechanismInterface
AssemblyInterface
PartInterface
ConstraintInterface
CollisionGeometryInterface
PrismaticInterface
HingeInterface
SphereInterface
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 a hinge. |
VxExtension
of constraints) can also be converted to the right Interface using Constraint.getConstraintInterface(constraint)
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.
Creating an Object |
---|
from VxSim import * ... # Create and set up 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 requires 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 the equivalent of doing # box = BoxInterface( cg_0.getExtension() ) |
Content objects that can be moved are derived from the IMobile
interface. IMobileInterface
objects can be moved by using method setLocalTransform
, which takes a VxSim.Matrix44
object describing the transformation in terms 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 |
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. |
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., kModeEditing, kModeSimulating or kModePlayingBack
<obj>.getApplicationContext().isPaused() # Indicates if the simulation is paused
<obj>.getApplicationContext().isSimulationRunning() # Indicates if the Simulation is running i.e., ApplicationMode is kModeSimulating And isPaused == False
Python can be used to prototype, test (some Vortex internal tests are written with Python) or complete a Vortex application. The concept is no different than what is described in sections Integrating the Application and Creating an Application. The only difference is that the application is started in Python.
The first step is to create an application and set 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 manually insert and remove modules and extensions to and from 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 created using the Vortex Studio Editor should be done with the VxSimulationFileManager provided by VxApplication
. The loaded object is distributed across the network. Objects loaded this way are not meant to be edited; most changes will not be distributed. To edit the content using Python, see Content Creation in Python.
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() ... # Set up 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... |
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 require updates regardless of the mode or the pause state. The application context is available as 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()
.
To look into content, a user can browse 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.
The following is an example of how to use a 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 needs to update the inputs, execute an update and read the values from the outputs. Data should not be written into outputs. Parameter values 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 ) ... |
Key frames can be used to restore the values of content objects taken at some point in time, e.g., when starting the simulation. The first step is to create a key frame list and then use it to save and restore key frames.
The following provides an example of saving one key frame and restoring it later.
Save and Restore Key Frame |
---|
# 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 key frame 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 can also be created, or updated after being loaded.
Please read Creating Content for the fundamentals of content creation. The concepts described in that section apply in Python as well and will not be repeated here.
Use the VxObjectSerializer to load and save your document definition. Children in documents are instances and must be instantiated like in C++.
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) |
The C++ global functions to perform these operations are not available in Python. However, all Python Smart Interfaces have additional functions that their C++ counterparts do not have: clone
, instantiate
and sync
.
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 |
These examples reproduce the C++ examples found in Creating Content.
See Python tutorial pyContentCreation.py for additional examples.
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 |
---|
# create a part part = VxSim.Part.create() # set the mass part.parameterMassPropertiesContainer.mass.value = 1.0 # Adding a CG part.addCollisionGeometry(box) |
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 |
---|
# create a 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 |
---|
# 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 |
---|
# 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 |
---|
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 |
---|
# Create the VHL extension vhlInterface = VxSim.VxVHLInterface.create() # Add a hinge control field as input of VHL vhlInterface.addInput('Input Value', hinge.inputAngularCoordinate.control) # Add the VHL extension to the mechanism mechanism.addExtension(vhlInterface.getExtension()) |
Connection 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 extensions 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) |
Configurations can also be edited in Python, but it is preferable to use the Vortex Studio Editor, as editing in code is susceptible to mistakes.
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 editing. 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 editing # 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 |
VxATP has the following structure in your Vortex directory:
\bin\vxatp
: the Python package\bin\vxatp_*.bat
: batch helpers to run tests from the command line\resources\vxatp\
: resources used by VxATP, e.g., configuration filesThe batch command vxatp_set_env.bat
details how to 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 set up or inherit the same environment.
The VxATP test scripts are similar to the unittest scripts (see The Python Standard Library's online documentation, "Unit Testing Framework: Basic Example").
From the configuration given, a VxApplication can be created to perform operations on assets.
A helper is provided in VxATPConfig
that returns a setup application ready to be tested: VxATPConfig.createApplication(self, prefix_name, config_name)
.
In addition, VxATPConfig.createApplication
sets a configuration member to the test case 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 for 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') |
VxATPConfig
provides optional helpers to get default predefined setup of the application:
VxATPConfig.getAppConfigWithoutGraphics()
returns absolute path to default configuration not containing graphics moduleVxATPConfig.getAppConfigWithGraphics()
returns absolute path to default configuration containing graphics moduleThere are several ways a VxATP test can be called from the command line, explained below.
See The Python Standard Library's online documentation, "Unit Testing Framework: Command-Line Interface" (e.g., python my_test.py
).
A convenience batch file is also provided to set up all required Vortex environment variables.
Running one VxATP test script |
---|
vxatp_run_onetest.bat my_test.py |
vxatp_launcher
can be used to run all tests in a given directory, 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, forcing graphics |
---|
vxatp_launcher.py -p my_test_directory -o my_output_directory_for_results_and_logs -g -r |
"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 |
---|
-------------------------------------------------------------------------
|
Example of XML output |
---|
<testsuite duration="1.285" errors="0" failures="0" name="content" tests="3"> |
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
|
Visual Studio has Python tools available as add-ons. 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 the binary directory of your Vortex installation, the VxATP path of your binary installation and optionally the path containing your test scripts.
These settings should be enough to run and debug your VxATP-based verification scripts in Visual Studio.
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.
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 standard IDynamics
and IExtension
callbacks.
Function | Method Documentation | Behavior |
---|---|---|
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 when the 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 paused. Use this method to set outputs or get values from dynamics objects. |
def on_state_save(self, data)
|
IExtension::onStateSave | Called 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::onStateRestore | Called 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 comes with these callbacks 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.
For embedded scripts in content, the self
parameter contains accessors to the field value without the need to call 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 <container>
is the name of the container in lowercase (i.e., "inputs", "outputs" or "parameters") and <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.
Example: Drum script extensions from the Mobile Crane demo scene, 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 resource expensive in a real-time simulation, so the Dynamics Script should cache as much as possible and avoid repeating operations.
The following is a series of examples accessing several fields in dynamics objects.
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 |
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 |
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 |
Vortex Demo Scene 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 |
Vortex Mobile Crane Demo Scene 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 |
Vortex EOD Demo Scene 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. |
Tutorials can be found in the Tutorials folder of your Vortex Studio installation directory.
The pyMechanismViewer tutorial recreates the MechanismViewer tutorial in Python. It is an example of a Python application.
This tutorial recreates the ExContentCreation tutorial (see the tutorials under Creating Content) in Python, matching the algorithm, the comments, the functions' purpose and the variables names.
The end result is the same except that the files are named pyRover<Name>.vx<Document Type>
.
Tutorial pyContentParsing shows how to navigate content. It uses VxExtension
to navigate in a generic way and <content_object>Interface
for the specifics.
Most of the C++ interfaces have their equivalent in the Vortex Python API. C++ Smart Interfaces are IExtension
's implementation with their VxExtension
underneath, wrapped into a 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 field's ID.
In Python, it is still possible to work with VxExtension
s, like in C++. They can be created via the VxExtensionFactory
as usual, and still require knowledge of the field's ID.
In order to use an equivalent of a C++ Smart Interface in Python, the Python API has 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:
<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<content_object>Interface.create()
, or by constructing a <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 an explicit conversion.
Given object a
of type <content_object>InterfaceA
, to get an object b
of type <content_object>InterfaceB
:
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 will not 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() ) |
Next topic: Devices