This guide covers the
generateVelocities
,
generatePositions
and
generateVelocitiesAndTimes
functions of the
PvtSequence
class.
If you are new to PVT, please refer to our introductory guide on the basics of setting up sequences and loading points.
PVT is a very useful feature of Zaber devices. It allows for precise control of timing and movement over continuous paths with many points, but path planning can involve solving for some unknowns. For some applications, a user may know the specific time and position for a point in a sequence, but not the velocity. In others, the user may know the times and velocities but not the positions. There may also be situations where the user knows only the positions. In any case, finding the best values to specify the desired motion requires either some fortuitous guess-work or a fairly comprehensive understanding of the math behind PVT.
This sequence generation API aims to simplify PVT path-planning and solve for these unknowns in a sensible way.
All of the sequence generation functions return an array of
PvtSequenceItem
objects, which
can either be saved as a CSV file using
saveSequenceData
, or used directly with the
PvtSequence
submitSequenceData
method. Typically most of the
items in the array are of type
PvtPoint
, but they are held in or represented
by (depending on programming language)
PvtSequenceItem
because you can also
interleave non-point PVT actions such as output port operations in the array, and they will be preserved.
For example:
# Save as CSV
PvtSequence.save_sequence_data(generated, "my_pvt_sequence.csv")
# Add points (sequence must be in live or store mode)
pvt_sequence.submit_sequence_data(generated)
pvt_sequence.wait_until_idle()
Points input to the sequence generation functions are a different type,
PvtPartialPoint
, which
allows for missing elements (or missing columns in the CSV format). These can be interleaved with the same non-point PVT action types if needed.
For the sake the consistency of the trajectory and path plots, the example inputs for
generateVelocities
and
generatePositions
both have initial times set to 0.
The
generateVelocitiesAndTimes
function will also return an initial time of zero.
If the initial point in a sequence has its time set to zero, the library treats it as a special "start position". Start positions must have an absolute position and zero velocity, and the device must already be at the start position when the sequence is submitted. The start position will not be submitted to the device as part of the PVT sequence.
If that behavior is undesirable, you must set the time of the first point to something greater than zero, giving the device enough time to move to the first point.
Here's an example showing how to move a two-axis device to the start position:
device.get_axis(1).move_absolute(generated[0].positions[0].value, generated[0].positions[0].unit)
device.get_axis(2).move_absolute(generated[0].positions[1].value, generated[0].positions[1].unit)
Generally, the Zaber Motion Library PVT functions expect the point times to be relative. That is, relative to the previous point. The opposite condition, called absolute time, is when each point's time value is relative to the beginning of the sequence. There are functions to convert times between relative and absolute, for example:
points = PvtSequence.convert_time_absolute_to_relative_partial(points)
generateVelocities
)This function works by generating velocities such that acceleration is continuous over the entire sequence. It will also set the
start and end velocities to zero unless otherwise specified (see With Partially Defined Velocities section below).
In this example we are generating the velocities for a 2-dimensional spiral path. The inputs to the sequence generation functions
are always arrays of
PvtPartialPoint
, which allows the positions and velocities to be empty
arrays or to contain null measurements, and allows the time measurement to be null. Only some combinations of missing
measurements are supported by the sequence generation functions.
points = [
PvtPartialPoint(positions=[Measurement(14.5, "mm"), Measurement(7.5, "mm")], velocities=[], time=Measurement(0, "s")),
PvtPartialPoint(positions=[Measurement(15.943, "mm"), Measurement(6.666, "mm")], velocities=[], time=Measurement(3.333, "s")),
PvtPartialPoint(positions=[Measurement(11.613, "mm"), Measurement(5.833, "mm")], velocities=[], time=Measurement(6.667, "s")),
PvtPartialPoint(positions=[Measurement(14.5, "mm"), Measurement(12.5, "mm")], velocities=[], time=Measurement(10, "s")),
PvtPartialPoint(positions=[Measurement(20.274, "mm"), Measurement(4.167, "mm")], velocities=[], time=Measurement(13.333, "s")),
PvtPartialPoint(positions=[Measurement(7.283, "mm"), Measurement(3.333, "mm")], velocities=[], time=Measurement(16.667, "s")),
PvtPartialPoint(positions=[Measurement(22, "mm"), Measurement(15, "mm")], velocities=[], time=Measurement(20, "s")),
]
Then make the call to generate the velocities for the sequence. Note that while you can specify the data with either relative or absolute time,
Then make the call to generate the velocities for the sequence. Note that while you can specify the data with either relative or absolute time,
the sequence generation functions expect input times to be relative. Additionally, the time sequences in the output data are always relative.
This is because
submitSequenceData
expects times to be relative. This example includes
a call to convert to relative time:
points = PvtSequence.convert_time_absolute_to_relative_partial(points)
generated = PvtSequence.generate_velocities(points)

Note that there is no guarantee that the generated sequence will adhere to your axes' velocity and acceleration constraints, so it is important to provide 'reasonable' times which allow for enough travel time from point to point.
Optionally, you can pass in a partially-defined sequence of velocities. In this case the function will generate velocities with continuous acceleration for all of the undefined segments of the path.
points[3].velocities = [Measurement(5, "mm/s"), Measurement(0, "mm/s")]
generated = PvtSequence.generate_velocities(points)
Since the start and end velocities are set to zero if undefined, the above velocity example is equivalent to:
points[0].velocities = [Measurement(0, "mm/s"), Measurement(0, "mm/s")]
points[3].velocities = [Measurement(5, "mm/s"), Measurement(0, "mm/s")]
points[6].velocities = [Measurement(0, "mm/s"), Measurement(0, "mm/s")]
generated = PvtSequence.generate_velocities(points)
The call to generate velocities produces the following path:

Notice that acceleration is not continuous between the path segments on either side of the defined point. This is because the function actually splits the path into two sections and solves each separately, so while the continuity constraint applies over the first and second sub-paths, it isn't enforced over the entire path.
generatePositions
)Like
generateVelocities
, this function also enforces that acceleration is
continuous from point to point. Additionally, it uses the constraint that acceleration is 0 at the start point.
In this example we are generating the positions for a 1-dimensional rotary path with velocities and times:
velocities = [
PvtPartialPoint(positions=[], velocities=[Measurement(0, "°/s")], time=Measurement(0, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(180, "°/s")], time=Measurement(3, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(-180, "°/s")], time=Measurement(6, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(180, "°/s")], time=Measurement(6, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(360, "°/s")], time=Measurement(3, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(0, "°/s")], time=Measurement(6, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(-180, "°/s")], time=Measurement(3, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(-360, "°/s")], time=Measurement(3, "s")),
PvtPartialPoint(positions=[], velocities=[Measurement(0, "°/s")], time=Measurement(6, "s")),
]
Then, the call to generate positions produces the following trajectory. Notice that in this case we have specified times to be relative (the default value for this parameter is true).
generated = PvtSequence.generate_positions(velocities)

generateVelocitiesAndTimes
)This function works differently than
generateVelocities
and
generatePositions
.
It first fits a geometric spline over the position sequence and then calculates the velocity and time information by
traversing the spline using a trapezoidal motion profile. While it will produce a smooth trajectory, it does not
guarantee that velocity will be continuous over the entire path. It is also only capable of generating trajectories
for single axis linear and rotary trajectories, and multi-axis linear trajectories where the directions of all axes are orthogonal (ie. Cartesian systems).
In this example, we are generating the same 2-dimensional spiral path using only position information.
positions = [
PvtPartialPoint(positions=[Measurement(14.5, "mm"), Measurement(7.5, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(15.943, "mm"), Measurement(6.666, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(11.613, "mm"), Measurement(5.833, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(14.5, "mm"), Measurement(12.5, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(20.274, "mm"), Measurement(4.167, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(7.283, "mm"), Measurement(3.333, "mm")], velocities=[]),
PvtPartialPoint(positions=[Measurement(22, "mm"), Measurement(15, "mm")], velocities=[]),
]
Then we generate the velocities and times for the sequence with a target speed of 10mm/s and target acceleration of 5mm/s².
target_speed = Measurement(10, "mm/s")
target_accel = Measurement(5, "mm/s²")
generated = PvtSequence.generate_velocities_and_times(positions, target_speed, target_accel)

Notice that acceleration and velocity don't adhere perfectly to our target values. The following section on resampling provides a means of keeping the trajectory closer to the specified velocity and acceleration targets.
Resampling allows you to specify the number of sample points along the geometric spline which is fitted to the input positions. For PVT paths with few positions, this can help the function to build a trajectory which adheres better to the velocity and acceleration targets. For PVT paths with many positions, this can help to reduce the size of the final generated sequence.
We will be upsampling the path from above using the same target speed and acceleration values and a resample value of 30, so that there will be 30 PVT points in the returned sequence data object.
generated = PvtSequence.generate_velocities_and_times(positions, target_speed, target_accel, 30)

Notice that after the initial curve in the path, the trajectory stays much closer to the target speed and acceleration than in the previous example.
It is important to note that resampled paths may not pass through the exact positions in the input sequence. This is because the input positions likely won't be included in the generated trajectory. The following plot illustrates this clearly, with the red x's representing the original input positions:

If for your application you require your device to pass through the input positions exactly, then resampling may not be the tool for you. That said, a higher resample value will likely reduce the amount of deviation.
Also note that resampling does not work on sequences that contain non-point actions, because their relative timing would be lost.