Skip to content

Commit

Permalink
test(ot3): add Hypothesis motion test (#9328)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahiuchingau committed Feb 3, 2022
1 parent c377e98 commit 7889bee
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def apply_constraint(constraint: np.float64, input: np.float64) -> np.float64:
return np.copysign(np.minimum(abs(constraint), abs(input)), input)


def check_less_or_close(constraint: np.float64, input: np.float64) -> bool:
"""Evaluate whether the input value equals to or less than the constraint."""
return abs(input) <= constraint or bool(np.isclose(input, constraint))


def get_unit_vector(
initial: Coordinates, target: Coordinates
) -> Tuple[Coordinates, np.float64]:
Expand Down Expand Up @@ -142,7 +147,7 @@ def find_initial_speed(
)
initial_speed = np.minimum(axis_constrained_speed, initial_speed)

log.debug(f"Initial speed: {initial_speed}")
log.info(f"Initial speed: {initial_speed}")
return initial_speed


Expand Down Expand Up @@ -216,7 +221,7 @@ def find_final_speed(
)
final_speed = np.minimum(axis_speed_limit, final_speed)

log.debug(f"Final speed: {final_speed}")
log.info(f"Final speed: {final_speed}")
return final_speed


Expand Down Expand Up @@ -252,8 +257,8 @@ def achievable_final(
)
# take the smaller of the aboslute value
final_speed = apply_constraint(max_axis_final_velocity, final_speed)
log.debug(f"final: {final_speed}")

log.info(f"final: {final_speed}")
return final_speed


Expand All @@ -275,11 +280,11 @@ def build_blocks(
- have at most one 0 acceleration coast phase at our max speed
"""
log = logging.getLogger("build_blocks")
assert (
initial_speed <= max_speed
assert abs(initial_speed) <= max_speed or np.isclose(
abs(initial_speed), max_speed
), f"initial speed {initial_speed} exceeds max speed {max_speed}"
assert (
final_speed <= max_speed
assert abs(final_speed) <= max_speed or np.isclose(
abs(final_speed), max_speed
), f"final speed {final_speed} exceeds max speed {max_speed}"

constraint_max_speed = max_speed
Expand Down Expand Up @@ -375,15 +380,19 @@ def blended(constraints: SystemConstraints, first: Move, second: Move) -> bool:
"""Check if the moves are blended."""
log = logging.getLogger("blended")
# have these actually had their blocks built?
fist_dist_sum = sum(b.distance for b in first.blocks)
if abs(fist_dist_sum - first.distance) > FLOAT_THRESHOLD:
first_dist_sum = sum(b.distance for b in first.blocks)
if (abs(first_dist_sum - first.distance) > FLOAT_THRESHOLD) or not np.isclose(
first_dist_sum, first.distance
):
log.debug(
f"Sum of distance for first move blocks {fist_dist_sum} does not match "
f"Sum of distance for first move blocks {first_dist_sum} does not match "
f"{first.distance}"
)
return False
second_dist_sum = sum(b.distance for b in second.blocks)
if abs(second_dist_sum - second.distance) > FLOAT_THRESHOLD:
if abs(second_dist_sum - second.distance) > FLOAT_THRESHOLD or not np.isclose(
second_dist_sum, second.distance
):
log.debug(
f"Sum of distance for second move blocks {second_dist_sum} does not match "
f"{second.distance}"
Expand All @@ -393,26 +402,37 @@ def blended(constraints: SystemConstraints, first: Move, second: Move) -> bool:
# do their junction velocities match constraints?
for axis in Axis.get_all_axes():
final_speed = first.blocks[-1].final_speed * first.unit_vector[axis]
log.debug(f"final_speed: {final_speed}")
log.debug(f"{axis} final_speed: {final_speed}")
initial_speed = second.blocks[0].initial_speed * second.unit_vector[axis]
log.debug(f"initial_speed: {initial_speed}")
log.debug(f"{axis} initial_speed: {initial_speed}")
if first.unit_vector[axis] * second.unit_vector[axis] > 0:
# if they're in the same direction, we can check that either the junction
# speeds exactly match, or that they're both under the discontinuity limit
discont_limit = constraints[axis].max_speed_discont
if not (abs(initial_speed - final_speed) < FLOAT_THRESHOLD):
if (
abs(final_speed) > discont_limit
or abs(initial_speed) > discont_limit
if not (
check_less_or_close(discont_limit, final_speed)
or check_less_or_close(discont_limit, initial_speed)
):
log.debug(
f"Final speed: {final_speed}, initial speed: {initial_speed}, "
f"discont: {discont_limit}"
)
return False
else:
# if they're in different directions, then the junction has to be at or
# under the speed change discontinuity
discont_limit = constraints[axis].max_direction_change_speed_discont
if abs(final_speed) > discont_limit or abs(initial_speed) > discont_limit:
if not (
check_less_or_close(discont_limit, final_speed)
or check_less_or_close(discont_limit, initial_speed)
):
log.debug(
f"Final speed: {final_speed}, initial speed: {initial_speed}, "
f"discont: {discont_limit}"
)
return False
log.debug("Successfully blended.")
log.info("Successfully blended.")
return True


Expand Down
166 changes: 166 additions & 0 deletions hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Tests for motion planning."""
import numpy as np # type: ignore[import]
from hypothesis import given, assume, strategies as st
from hypothesis.extra import numpy as hynp
from typing import Iterator, List

from opentrons_hardware.hardware_control.motion_planning import move_manager
from opentrons_hardware.hardware_control.motion_planning.types import (
Axis,
AxisConstraints,
Coordinates,
MoveTarget,
)


@st.composite
def generate_axis_constraint(draw: st.DrawFn) -> AxisConstraints:
"""Create axis constraint using Hypothesis."""
acc = draw(st.integers(min_value=500, max_value=5000))
speed_dist = draw(st.integers(min_value=100, max_value=500))
dir_change_dist = draw(st.integers(min_value=10, max_value=100))
assume(speed_dist > dir_change_dist)
return AxisConstraints.build(
max_acceleration=acc,
max_speed_discont=speed_dist,
max_direction_change_speed_discont=dir_change_dist,
)


@st.composite
def generate_coordinates(draw: st.DrawFn) -> Coordinates:
"""Create coordinates using Hypothesis."""
coord = [
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0, max_value=500)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0, max_value=490)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0, max_value=300)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0, max_value=300)),
]
formatted: Iterator[np.float64] = (np.float64(i) for i in coord)
return Coordinates.from_iter(formatted)


@st.composite
def generate_close_coordinates(draw: st.DrawFn, prev_coord: Coordinates) -> Coordinates:
"""Create coordinates using Hypothesis."""
diff = [
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0.1, max_value=1.0)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0.1, max_value=1.0)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0.1, max_value=1.0)),
draw(hynp.from_dtype(np.dtype(np.float64), min_value=0.1, max_value=1.0)),
]
coord = prev_coord.vectorize() + diff
formatted: Iterator[np.float64] = (np.float64(i) for i in coord)
return Coordinates.from_iter(formatted)


def reject_close_coordinates(a: Coordinates, b: Coordinates) -> bool:
"""Reject example if the coordinates are too close.
Consecutive coordinates must be at least 1mm apart in one of the axes.
"""
return any(abs(b.vectorize() - a.vectorize()) > 1.0)


@st.composite
def generate_target_list(
draw: st.DrawFn, elements: st.SearchStrategy[Coordinates] = generate_coordinates()
) -> List[MoveTarget]:
"""Generate a list of MoveTarget using Hypothesis."""
target_num = draw(st.integers(min_value=1, max_value=10))
target_list: List[MoveTarget] = []
while len(target_list) < target_num:
position = draw(elements)
if len(target_list):
assume(reject_close_coordinates(position, target_list[-1].position))
target = MoveTarget.build(
position, draw(st.floats(min_value=10, max_value=500))
)
target_list.append(target)
return target_list


@st.composite
def generate_close_target_list(
draw: st.DrawFn, origin: Coordinates
) -> List[MoveTarget]:
"""Generate a list of MoveTarget using Hypothesis."""
target_num = draw(st.integers(min_value=1, max_value=10))
target_list: List[MoveTarget] = []
prev_coord = origin
while len(target_list) < target_num:
position = draw(generate_close_coordinates(prev_coord))
target = MoveTarget.build(
position, draw(st.floats(min_value=0.1, max_value=10.0))
)
target_list.append(target)
prev_coord = position
return target_list


@given(
x_constraint=generate_axis_constraint(),
y_constraint=generate_axis_constraint(),
z_constraint=generate_axis_constraint(),
a_constraint=generate_axis_constraint(),
origin=generate_coordinates(),
targets=generate_target_list(),
)
def test_move_plan(
x_constraint: AxisConstraints,
y_constraint: AxisConstraints,
z_constraint: AxisConstraints,
a_constraint: AxisConstraints,
origin: Coordinates,
targets: List[MoveTarget],
) -> None:
"""Test motion plan using Hypothesis."""
assume(reject_close_coordinates(origin, targets[0].position))
constraints = {
Axis.X: x_constraint,
Axis.Y: y_constraint,
Axis.Z: z_constraint,
Axis.A: a_constraint,
}
manager = move_manager.MoveManager(constraints=constraints)
converged, blend_log = manager.plan_motion(
origin=origin,
target_list=targets,
iteration_limit=5,
)

assert converged


@given(
x_constraint=generate_axis_constraint(),
y_constraint=generate_axis_constraint(),
z_constraint=generate_axis_constraint(),
a_constraint=generate_axis_constraint(),
origin=generate_coordinates(),
data=st.data(),
)
def test_close_move_plan(
x_constraint: AxisConstraints,
y_constraint: AxisConstraints,
z_constraint: AxisConstraints,
a_constraint: AxisConstraints,
origin: Coordinates,
data: st.DataObject,
) -> None:
"""Test motion plan using Hypothesis."""
targets = data.draw(generate_close_target_list(origin))
constraints = {
Axis.X: x_constraint,
Axis.Y: y_constraint,
Axis.Z: z_constraint,
Axis.A: a_constraint,
}
manager = move_manager.MoveManager(constraints=constraints)
converged, blend_log = manager.plan_motion(
origin=origin,
target_list=targets,
iteration_limit=5,
)

assert converged

0 comments on commit 7889bee

Please sign in to comment.