Skip to content

Commit 531fbb3

Browse files
authored
❗Add more generalized algorithm to compute ground contacts + chores (#78)
* Add a new the more general algorithm to computer orientation properties - Incorporate both ground contact solver algorithms in updating hexapod pose depending on situation - Use the more general algorithm for the kinematics page - Incorporate new algorithm in test * Fix ground contact solver module - Move ground contact solver to own module - Replace check stability with function that accounts for all cases - Simplify and refactor ground contact solver, remove duplicate operations * Do pylint chores - Remove unused ignore declarations on pylint file - Dont ignore tests directory since it's not pylint isn't ignoring directories anyway * Update README
1 parent 45acb5a commit 531fbb3

13 files changed

Lines changed: 249 additions & 99 deletions

File tree

.pylintrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ extension-pkg-whitelist=
77

88
# Add files or directories to the blacklist. They should be base names, not
99
# paths.
10-
#ignore=CVS, ik_solver.py, page_kinematics.py
10+
#ignore=
1111

1212
# Add files or directories matching the regex patterns to the blacklist. The
1313
# regex matches against base names, not paths.
14-
#ignore-patterns=case*.py
14+
#ignore-patterns=
1515

1616
# Python code to execute, usually for sys.path manipulation such as
1717
# pygtk.require().

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ script:
88
- python -m pytest
99
- python -m py_compile ./*/*.py ./*.py ./*/*/*.py
1010
- flake8 ./*/*.py ./*.py ./*/*/*.py
11-
- pylint ./*.py */*.py */*/*.py --reports=y --ignore=page_kinematics.py,ik_solver.py,tests --disable=broad-except,too-few-public-methods,attribute-defined-outside-init,too-many-locals,too-many-instance-attributes,too-many-arguments,bad-continuation,missing-class-docstring,missing-module-docstring,missing-function-docstring,invalid-name,duplicate-code
11+
- pylint ./*.py */*.py */*/*.py --reports=y --ignore=page_kinematics.py,ik_solver.py --disable=broad-except,too-few-public-methods,attribute-defined-outside-init,too-many-locals,too-many-instance-attributes,too-many-arguments,bad-continuation,missing-class-docstring,missing-module-docstring,missing-function-docstring,invalid-name,duplicate-code

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
| | | | |
99
|---------|---------|---------|---------|
10-
|![Twisting turning and tilting](https://mithi.github.io/robotics-blog/robot-only-x1.gif)|<img src="https://mithi.github.io/robotics-blog/v2-hexapod-1.gif" width="550"/>|<img src="https://mithi.github.io/robotics-blog/v2-hexapod-2.gif" width="500"/>|![Adjusting camera view](https://mithi.github.io/robotics-blog/robot-only-x2.gif)|
10+
|![Twisting turning and tilting](https://mithi.github.io/robotics-blog/robot-only-x1.gif)|<img src="https://mithi.github.io/robotics-blog/v2-hexapod-1.gif" width="550"/>|<img src="https://mithi.github.io/robotics-blog/v2-hexapod-2.gif" width="500"/>|![Adjusting camera view](https://mithi.github.io/robotics-blog/robot-only-x3.gif)|
1111

1212
| STATUS | FEATURE | DESCRIPTION |
1313
|---|-----------|--------------|
@@ -24,9 +24,9 @@
2424

2525
## 🕷️ Preview
2626

27-
|![IK](https://mithi.github.io/robotics-blog/v2-ik-ui.gif)|![Kinematics](https://mithi.github.io/robotics-blog/v2-kinematics-ui.gif)|
27+
|![image](https://mithi.github.io/robotics-blog/v2-ik-ui.gif)|![image](https://mithi.github.io/robotics-blog/v2-kinematics-ui.gif)|
2828
|----|----|
29-
| ![IK](https://mithi.github.io/robotics-blog/UI-1.gif) | ![Kinematics](https://mithi.github.io/robotics-blog/UI-2.gif) |
29+
| ![image](https://mithi.github.io/robotics-blog/UI-1.gif) | ![image](https://mithi.github.io/robotics-blog/UI-2.gif) |
3030

3131
## 🕷️ Requirements
3232

@@ -57,13 +57,15 @@ Running on http://127.0.0.1:8050/
5757

5858
- Definitions
5959
- [`Linkage`](./hexapod/linkage.py)
60-
- [`VirtualHexapod`](./hexapod/models.py#L94)
60+
- [`VirtualHexapod`](./hexapod/models.py)
6161
- The [Inverse Kinematics Algorithm](./hexapod/ik_solver/README.md) used for this project
62-
- [How to find the ground contact points, tilt, and height of the hexapod](./hexapod/ground_contact_solver.py#L43)
63-
- [How to make the hexapod step on the correct target ground contacts](./hexapod/ik_solver/recompute_hexapod.py#L15)
62+
- How to find the orientation of the hexapod with respect to the ground given we know all the orientations of the six legs with respect to the robot's body.
63+
- [Algorithm 1](./hexapod/ground_contact_solver/ground_contact_solver.py) when we know which of the three points of each leg could contact the ground
64+
- [Algorithm 2](./hexapod/ground_contact_solver/ground_contact_solver2.py) when we **don't** know which of points of which legs could be in contact with the ground
65+
- [How to make the hexapod step on the correct target ground contacts](./hexapod/ik_solver/recompute_hexapod.py)
6466
- How to determine if the hexapod should twist and by how much
65-
- [`find_if_might_twist`](./hexapod/models.py#L242)
66-
- [`find_twist_frame`](./hexapod/models.py#L267)
67+
- [`find_if_might_twist`](./hexapod/models.py#L248)
68+
- [`find_twist_frame`](./hexapod/models.py#L273)
6769

6870
## 🕷️ Screenshots
6971

@@ -73,15 +75,18 @@ Running on http://127.0.0.1:8050/
7375

7476
## 🕷️ Notes
7577

76-
- Now live on https://hexapod-robot-simulator.herokuapp.com ! **BUT** (and a big one) I highly suggest that you run it locally. When run locally, it's pretty speedy! On the other hand, the link above is barely usable. Might convert this to to be a fully client-side Javascript app later, maybe?
78+
- Now live on https://hexapod-robot-simulator.herokuapp.com ! **BUT** (and a big one) I highly suggest that you run it locally. When run locally, it's pretty speedy! On the other hand, the link above is barely usable. Might convert this to to be a fully client-side Javascript app later, maybe?
7779

78-
- This implementation uses matrices, **NOT** quaternions. I'm aware that quaternions is far superior in every single way. In the (un)forseeable future, maybe?
80+
- This implementation uses matrices, **NOT** quaternions. I'm aware that quaternions is far superior in every single way. In the (un)forseeable future, maybe?
7981

80-
- ❗Frankly, [My IK algorithm](https://github.com/mithi/hexapod-robot-simulator/blob/master/hexapod/ik_solver/README.md) isn't all that great, it's just something I came up with based on what I remember back in college plus browsing through the [Mathematics Stack Exchange](https://math.stackexchange.com/). It might not be the best, but it's the most intuitive that I can think of. If you want something closer to the state-of-the-art, maybe checkout [Unity's Fast IK](https://assetstore.unity.com/packages/tools/animation/fast-ik-139972) or [ROS IKFast](http://wiki.ros.org/Industrial/Tutorials/Create_a_Fast_IK_Solution).
82+
- Honestly, [My IK algorithm](./hexapod/ik_solver/README.md) is just something I came up with based on what I remember back in college plus browsing through the [Mathematics Stack Exchange](https://math.stackexchange.com/). It's just the most intuitive that I can think of. I'm open to hearing other ways to approach this. If you want something closer to the state-of-the-art, maybe checkout [Unity's Fast IK](https://assetstore.unity.com/packages/tools/animation/fast-ik-139972) or [ROS IKFast](http://wiki.ros.org/Industrial/Tutorials/Create_a_Fast_IK_Solution).
8183

82-
- I believe that the idea that it's best to be kind to one another shouldn't be controversial. And I shouldn't be afraid to say that. [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://www.contributor-covenant.org/)
84+
- I would also love to here new ideas on how to improve the [algorithms currently used](./hexapod/ground_contact_solver/) to find the orientation of the hexapod given we know the orientation of the legs with respect to the body.
85+
86+
- I believe that the idea that it's best if we are kind to one another shouldn't be controversial. And I shouldn't be afraid to say that. [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://www.contributor-covenant.org/)
8387

8488
## 🤗 Contributors
8589

8690
- [@mithi](https://github.com/mithi/)
8791
- [@philippeitis](https://github.com/philippeitis/)
92+
- [@mikong](https://github.com/mikong/)

checker

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ black .
33
python -m pytest
44
python -m py_compile ./*/*.py ./*.py ./*/*/*.py
55
flake8 ./*/*.py ./*.py ./*/*/*.py
6-
pylint ./*.py */*.py */*/*.py --reports=y --ignore=page_kinematics.py,ik_solver.py,tests --disable=broad-except,too-few-public-methods,attribute-defined-outside-init,too-many-locals,too-many-instance-attributes,too-many-arguments,bad-continuation,missing-class-docstring,missing-module-docstring,missing-function-docstring,invalid-name,duplicate-code
6+
pylint ./*.py */*.py */*/*.py --reports=y --ignore=page_kinematics.py,ik_solver.py --disable=broad-except,too-few-public-methods,attribute-defined-outside-init,too-many-locals,too-many-instance-attributes,too-many-arguments,bad-continuation,missing-class-docstring,missing-module-docstring,missing-function-docstring,invalid-name,duplicate-code

hexapod/ground_contact_solver.py renamed to hexapod/ground_contact_solver/ground_contact_solver.py

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
"""
2+
❗❗❗A more general algorithm for computing the robot's orientation.
3+
This algorithm rests upon the assumption that it
4+
knows which point of the each leg is in contact with the ground.
5+
This assumption seems to be true for all possible cases for
6+
leg-patterns page and inverse-kinematics page.
7+
8+
But this is not true for all possible
9+
angle combinations (18 angles) that can be defined in
10+
the kinematics page.
11+
12+
This module is used for the leg-patterns page,
13+
and the inverse-kinematics page.
14+
15+
The other module will be used for the kinematics page.
16+
❗❗❗
17+
218
This module is responsible for the following:
319
1. determining which legs of the hexapod is on the ground
420
2. Computing the normal vector of the triangle defined by at least three legs on the ground
@@ -9,12 +25,8 @@
925
"""
1026
from math import isclose
1127
from itertools import combinations
12-
from hexapod.points import (
13-
Point,
14-
dot,
15-
get_normal_given_three_points,
16-
is_point_inside_triangle,
17-
)
28+
from hexapod.points import dot, get_normal_given_three_points
29+
from hexapod.ground_contact_solver.helpers import is_stable
1830

1931

2032
def compute_orientation_properties(legs, tol=1):
@@ -24,20 +36,13 @@ def compute_orientation_properties(legs, tol=1):
2436
- Normal vector of the plane defined by these legs
2537
- Distance of this plane to center of gravity
2638
"""
27-
trio = three_ids_of_ground_contacts(legs)
39+
trio, n, height = find_ground_plane_properties(legs)
2840

2941
# This pose is unstable, The hexapod has no balance
3042
if trio is None:
3143
return [], None, None
3244

33-
p0, p1, p2 = get_corresponding_ground_contacts(trio, legs)
34-
n = get_normal_given_three_points(p0, p1, p2)
35-
36-
# Note: using p0, p1 or p2 should yield the same result
37-
# height from cog to ground
38-
height = -dot(n, p0)
39-
40-
# Get all contacts of the same height
45+
# get all the legs on the ground
4146
legs_on_ground = []
4247

4348
for leg in legs:
@@ -48,17 +53,15 @@ def compute_orientation_properties(legs, tol=1):
4853
return legs_on_ground, n, height
4954

5055

51-
def three_ids_of_ground_contacts(legs, tol=1):
56+
def find_ground_plane_properties(legs, tol=1):
5257
"""
5358
Return three legs forming a stable position from legs,
5459
or None if no three legs satisfy this requirement.
55-
56-
This function takes the legs of the hexapod
57-
and finds three legs on the ground that form a stable position
58-
returns the leg ids of those three legs
59-
return None if no stable position found
60+
It also returns the normal vector of the plane
61+
defined by the three ground contacts, and the
62+
computed distance of the hexapod body to the ground plane
6063
"""
61-
trios = set_of_trios_from_six()
64+
trios = list(combinations(range(6), 3))
6265
ground_contacts = [leg.ground_contact() for leg in legs]
6366

6467
# (2, 3, 5) is a trio from the set [0, 1, 2, 3, 4, 5]
@@ -67,7 +70,7 @@ def three_ids_of_ground_contacts(legs, tol=1):
6770
for trio, other_trio in zip(trios, reversed(trios)):
6871
p0, p1, p2 = [ground_contacts[i] for i in trio]
6972

70-
if not check_stability(p0, p1, p2):
73+
if not is_stable(p0, p1, p2):
7174
continue
7275

7376
# Get the vector normal to plane defined by these points
@@ -94,43 +97,18 @@ def three_ids_of_ground_contacts(legs, tol=1):
9497
# height should be the most largest since
9598
# the plane defined by this trio is on the ground
9699
# the other legs ground contact cannot be lower than the ground
97-
condition_violated = False
98-
for i in other_trio:
99-
_height = -dot(n, ground_contacts[i])
100-
if _height > height + tol:
101-
# Wrong leg combination, check another
102-
condition_violated = True
103-
break
104-
105-
if not condition_violated:
106-
return trio # Found one!
100+
other_points = [ground_contacts[i] for i in other_trio]
101+
if no_other_legs_lower(n, height, other_points, tol):
102+
# Found one!
103+
return [p0, p1, p2], n, height
107104

108105
# Nothing met the condition
109106
return None
110107

111108

112-
def get_corresponding_ground_contacts(ids, legs):
113-
"""
114-
Given three leg ids and the list of legs get the points
115-
contacting the ground of those three legs.
116-
"""
117-
return [legs[i].ground_contact() for i in ids]
118-
119-
120-
def set_of_trios_from_six():
121-
"""
122-
Get all combinations of a three-item-group given six items.
123-
20 combinations total
124-
"""
125-
return list(combinations(range(6), 3))
126-
127-
128-
def check_stability(a, b, c):
129-
"""
130-
Check if the points a, b, c form a stable triangle.
131-
132-
If the center of gravity p (0, 0) on xy plane
133-
is inside projection (in the xy plane) of
134-
the triangle defined by point a, b, c, then this is stable
135-
"""
136-
return is_point_inside_triangle(Point(0, 0, 0), a, b, c)
109+
def no_other_legs_lower(n, height, other_points, tol):
110+
for point in other_points:
111+
_height = -dot(n, point)
112+
if _height > height + tol:
113+
return False
114+
return True
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
❗❗❗This is a more general algorithm to account for the cases
3+
that are not handled correctly by the other ground_contact_solver.
4+
This will only be used by the kinematics-page of the app.
5+
This algorithm can be optimized or replaced if a more elegant
6+
algorithm is available.
7+
❗❗❗
8+
9+
We have 18 points total.
10+
(6 legs, three possible points per leg)
11+
12+
We have a total of 540 combinations
13+
- get three legs out of six (20 combinations)
14+
- we have three possible points for each leg, that's 27 (3^3)
15+
- 27 * 20 is 540
16+
17+
For each combination:
18+
1. Check if stable if not, next
19+
- Check if the projection of the center of gravity to the plane
20+
defined by the three points lies inside the triangle, if not, next
21+
2. Get the height and normal of the height and normal of the triangle plane
22+
(We need this for the next part)
23+
3. For each of the three leg, check if the two other points on the leg is not a
24+
lower height, if condition if broken, next. (6 points total)
25+
4. For each of the three other legs, check all points (3 points of each leg)
26+
if so, next. (9 points total)
27+
5. If no condition is violated, then this is good, return this!
28+
(height, n_axis, 3 ground contacts)
29+
"""
30+
from math import isclose
31+
from itertools import combinations
32+
from hexapod.ground_contact_solver.helpers import is_stable
33+
from hexapod.points import get_normal_given_three_points, dot
34+
35+
TOL = 1.0
36+
37+
38+
def all_joint_id_combinations():
39+
joints_combination_list = []
40+
# joint coxia point 1, femur point 2, foot_tip 3
41+
for i in range(3, 0, -1):
42+
for j in range(3, 0, -1):
43+
for k in range(3, 0, -1):
44+
joints_combination_list.append([i, j, k])
45+
46+
return joints_combination_list
47+
48+
49+
OTHER_POINTS_MAP = {1: [2, 3], 2: [3, 1], 3: [1, 2]}
50+
51+
52+
def compute_orientation_properties(legs):
53+
leg_trios = list(combinations(range(6), 3))
54+
joint_trios = all_joint_id_combinations()
55+
56+
for leg_trio, other_leg_trio in zip(leg_trios, reversed(leg_trios)):
57+
58+
three_legs = [legs[i] for i in leg_trio]
59+
other_three_legs = [legs[i] for i in other_leg_trio]
60+
61+
for joint_trio in joint_trios:
62+
63+
p0, p1, p2 = [legs[i].get_point(j) for i, j in zip(leg_trio, joint_trio)]
64+
65+
if not is_stable(p0, p1, p2):
66+
continue
67+
68+
n = get_normal_given_three_points(p0, p1, p2)
69+
height = -dot(n, p0)
70+
71+
if same_leg_joints_break_condition(three_legs, joint_trio, n, height):
72+
continue
73+
74+
if other_leg_joints_break_condition(other_three_legs, n, height):
75+
continue
76+
77+
legs_on_ground = find_legs_on_ground(legs, n, height)
78+
return legs_on_ground, n, height
79+
80+
return [], None, None
81+
82+
83+
def other_leg_joints_break_condition(other_three_legs, n, height):
84+
for leg in other_three_legs:
85+
for i in range(1, 4):
86+
height_to_test = -dot(n, leg.get_point(i))
87+
if height_to_test > height + TOL:
88+
return True
89+
return False
90+
91+
92+
def same_leg_joints_break_condition(three_legs, three_point_ids, n, height):
93+
for leg, point_id in zip(three_legs, three_point_ids):
94+
for other_point_id in OTHER_POINTS_MAP[point_id]:
95+
other_point = leg.get_point(other_point_id)
96+
height_to_test = -dot(n, other_point)
97+
if height_to_test > height + TOL:
98+
return True
99+
return False
100+
101+
102+
def find_legs_on_ground(legs, n, height):
103+
legs_on_ground = []
104+
for leg in legs:
105+
for point in reversed(leg.all_points[1:]):
106+
_height = -dot(n, point)
107+
if isclose(height, _height, abs_tol=TOL):
108+
legs_on_ground.append(leg)
109+
break
110+
111+
return legs_on_ground
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from hexapod.points import (
2+
Point,
3+
dot,
4+
cross,
5+
vector_from_to,
6+
)
7+
8+
9+
# math.stackexchange.com/questions/544946/
10+
# determine-if-projection-of-3d-point-onto-plane-is-within-a-triangle
11+
# gamedev.stackexchange.com/questions/23743/
12+
# whats-the-most-efficient-way-to-find-barycentric-coordinates
13+
# en.wikipedia.org/wiki/Barycentric_coordinate_system
14+
def is_stable(p1, p2, p3, tol=0.001):
15+
"""
16+
Determines stability of the pose.
17+
Determine if projection of 3D point p
18+
onto the plane defined by p1, p2, p3
19+
is within a triangle defined by p1, p2, p3.
20+
"""
21+
p = Point(0, 0, 0)
22+
u = vector_from_to(p1, p2)
23+
v = vector_from_to(p1, p3)
24+
n = cross(u, v)
25+
w = vector_from_to(p1, p)
26+
n2 = dot(n, n)
27+
beta = dot(cross(u, w), n) / n2
28+
gamma = dot(cross(w, v), n) / n2
29+
alpha = 1 - gamma - beta
30+
# then coordinate of the projected point (p_) of point p
31+
# p_ = alpha * p1 + beta * p2 + gamma * p3
32+
min_val = -tol
33+
max_val = 1 + tol
34+
cond1 = min_val <= alpha <= max_val
35+
cond2 = min_val <= beta <= max_val
36+
cond3 = min_val <= gamma <= max_val
37+
return cond1 and cond2 and cond3

0 commit comments

Comments
 (0)