Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 34 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: Tests

on:
pull_request:
push:
branches: [master]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-22.04

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install package and test dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
python -m pip install --no-deps -e .

- name: Run tests
env:
MPLBACKEND: Agg
run: |
python -m pytest
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ eggs/
.eggs/
lib/
lib64/
parts/
/parts/
sdist/
var/
.installed.cfg
Expand All @@ -18,6 +18,8 @@ MANIFEST
*.spec
.cache
.ipynb_checkpoints
.pytest_cache/
pytest-cache-files-*/
.python-version
.vscode/
env/
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ then
pip install ir-support
```

Currently, it gets tested on Python 3.8 - 3.11
It is currently tested on Python 3.8 - 3.11

# Contact
- via 41013 subject teams site
Expand Down
9 changes: 8 additions & 1 deletion ir_support/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from . import classes, functions, plyclasses, plyprocess, robots
from .classes import *
from .functions import *
from .plyclasses import *
from .plyprocess import *
from .robots import *

__all__ = classes.__all__ + functions.__all__ + plyclasses.__all__ + plyprocess.__all__ + robots.__all__
__all__ = (
classes.__all__
+ functions.__all__
+ plyclasses.__all__
+ plyprocess.__all__
+ robots.__all__
)
18 changes: 12 additions & 6 deletions ir_support/classes/DHRobotPlotter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## @file
# @brief A class to create a 3D Robot defined by standard DH parameters, that can be displayed in the `Swift` simulator.
#
# @author Ho Minh Quang Ngo, Gavin Paul
# @date May 29, 2026

import roboticstoolbox as rtb
import numpy as np
from spatialgeometry import Cylinder
Expand All @@ -15,17 +21,17 @@ def __init__(self, robot, cylinder_radius=0.02, color="#3478f6", multicolor=Fals
self.cylinder_radius = cylinder_radius
self.multicolor = multicolor

# Handle color options
# Handle colour options
if multicolor:
self.colors = self._generate_color_palette(len(robot.links))
elif isinstance(color, list):
self.colors = color
else:
self.colors = [color] # Single color for all links
self.colors = [color] # Single colour for all links


def _generate_color_palette(self, num_colors):
"""Generate a distinct color palette for multi-colored links"""
"""Generate a distinct colour palette for multicoloured links"""
import colorsys

colors = []
Expand All @@ -34,7 +40,7 @@ def _generate_color_palette(self, num_colors):
saturation = 0.8
value = 0.9
rgb = colorsys.hsv_to_rgb(hue, saturation, value)
# Convert to hex or named color for Swift
# Convert to hex or named colour for Swift
hex_color = '#{:02x}{:02x}{:02x}'.format(
int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255)
)
Expand Down Expand Up @@ -133,7 +139,7 @@ def _create_cylinder_for_link(self, start_frame, end_frame, link_frame, color=No
# Create transformation matrix in local coordinates
cylinder_pose = SE3.Rt(rotation, center)

# Use provided color or default
# Use provided colour or default
if color is None:
color = self.colors[0] if self.colors else "#3478f6"

Expand Down Expand Up @@ -270,4 +276,4 @@ def create_enhanced_geometry_robot(robot, cylinder_radius=0.02, joint_radius=0.0


if __name__ == "__main__":
demo_geometry_robot()
demo_geometry_robot()
32 changes: 19 additions & 13 deletions ir_support/classes/EllipsoidRobot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Require libraries
## @file
# @brief Generate and animate link ellipsoids for DH robots.
# @author 41013 Teaching Team, Gavin Paul
# @date May 29, 2026

from typing import Optional, Union
from roboticstoolbox.backends import PyPlot
from scipy import linalg
from ir_support.functions import make_ellipsoid
import numpy as np
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -40,11 +43,11 @@ def __init__(self, robot: rtb.DHRobot,
self.robot = robot
else:
raise TypeError('Invalid input robot type. Requires DHRobot!')
if q == None:
if q is None:
self.q = robot.q
else:
self.q = q
if fig == None or not isinstance(fig, PyPlot.PyPlot):
if fig is None or not isinstance(fig, PyPlot.PyPlot):
warnings.warn("No input figure or invalid input figure. Use the robot's plot method instead")
self.fig = robot.plot(self.q)
else:
Expand Down Expand Up @@ -80,13 +83,16 @@ def ellipsoid_for_robot_links(self,q):
ax2 = ellipsoid['ax2']
ax3 = ellipsoid['ax3']

# Normalize ax1, ax2, ax3 if their norms are not zero
if linalg.norm(ax1) != 0:
ax1 = ax1 / linalg.norm(ax1)
if linalg.norm(ax2) != 0:
ax2 = ax2 / linalg.norm(ax2)
if linalg.norm(ax3) != 0:
ax3 = ax3 / linalg.norm(ax3)
# Normalise ax1, ax2, ax3 if their norms are not zero
ax1_norm = np.linalg.norm(ax1)
ax2_norm = np.linalg.norm(ax2)
ax3_norm = np.linalg.norm(ax3)
if ax1_norm != 0:
ax1 = ax1 / ax1_norm
if ax2_norm != 0:
ax2 = ax2 / ax2_norm
if ax3_norm != 0:
ax3 = ax3 / ax3_norm

axes_matrix = np.column_stack((ax1, ax2, ax3))
diagonal_matrix = np.diag(np.square([np.linalg.norm(ellipsoid['ax1']/2),
Expand Down Expand Up @@ -184,7 +190,7 @@ def on_enter_press(event):
# ---------------------------------------------------------------------------------------#
def get_ellipsoid_parameters(self, link_index: int):
"""
Get center point, orientation matrix, and radii for a given link's ellipsoid.
Get centre point, orientation matrix, and radii for a given link's ellipsoid.

Parameters
----------
Expand All @@ -194,7 +200,7 @@ def get_ellipsoid_parameters(self, link_index: int):
Returns
-------
center_point : np.ndarray
Translation vector [xc, yc, zc]
Centre translation vector [xc, yc, zc]
R : np.ndarray
3x3 orientation matrix (columns are principal axes)
radii : np.ndarray
Expand Down
49 changes: 25 additions & 24 deletions ir_support/classes/RectangularPrism.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## @file
# @brief Utility class for rectangular-prism collision geometry.
# @author 41013 Teaching Team, Gavin Paul
# @date May 29, 2026

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
Expand All @@ -23,7 +28,7 @@ def __init__(self, width:float=1, length:float=1, height:float=1, color=[0, 0, 1
self.width = width
self.length = length
self.height = height
self.center = center
self.center = np.asarray(center, dtype=float)
self.color = color
self.options = []

Expand All @@ -36,22 +41,18 @@ def __init__(self, width:float=1, length:float=1, height:float=1, color=[0, 0, 1
self._plot_prism()

def _calculate_vertices(self):
vertices = np.zeros((8, 3))
vertices[1] = [self.width, 0, 0]
vertices[2] = [self.width, self.length, 0]
vertices[3] = [0, self.length, 0]
vertices[4] = [0, 0, self.height]
vertices[5] = [self.width, 0, self.height]
vertices[6] = [self.width, self.length, self.height]
vertices[7] = [0, self.length, self.height]

# Shift vertices based on the center point
vertices -= np.array([self.width/2, self.length/2, self.height/2]) # Subtract half of width, length, and height

# Translate the vertices to the specified center
vertices += np.array(self.center)

return vertices
extents = np.array([self.width, self.length, self.height], dtype=float)
vertices = np.array([
[0, 0, 0],
[self.width, 0, 0],
[self.width, self.length, 0],
[0, self.length, 0],
[0, 0, self.height],
[self.width, 0, self.height],
[self.width, self.length, self.height],
[0, self.length, self.height],
], dtype=float)
return vertices - extents / 2 + self.center

def _calculate_faces(self):
faces = [
Expand All @@ -65,13 +66,13 @@ def _calculate_faces(self):
return faces

def _calculate_normals(self):
normals = []
for face in self._faces:
v1 = self._vertices[face[1]] - self._vertices[face[0]]
v2 = self._vertices[face[2]] - self._vertices[face[1]]
normal = np.cross(v1, v2)
normals.append(normal)
return normals
return [
np.cross(
self._vertices[face[1]] - self._vertices[face[0]],
self._vertices[face[2]] - self._vertices[face[1]]
)
for face in self._faces
]

def _plot_prism(self):
existing_axes = plt.gcf().axes
Expand Down
10 changes: 8 additions & 2 deletions ir_support/functions/draw_possible_ellipse_given_foci.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
## @file
# @brief A function to draw an ellipse given its foci
#
# @author Ho Minh Quang Ngo, Gavin Paul
# @date May 29, 2026

import numpy as np
import matplotlib.pyplot as plt

def draw_possible_ellipse_given_foci(focus1:np.ndarray, focus2:np.ndarray, a=None, ax=None):
"""
Simple custom function to draw an ellipse with 2 given focii
Simple custom function to draw an ellipse with 2 given foci

Parameters
----------
Expand All @@ -12,7 +18,7 @@ def draw_possible_ellipse_given_foci(focus1:np.ndarray, focus2:np.ndarray, a=Non
focus2 : np.ndarray
Second focus of the ellipse
a : float, optional
Default length for semi-major axis of the ellipse, by default 0.75 * distance between focii
Default length for semi-major axis of the ellipse, by default 0.75 * distance between foci
ax : matplotlib.axes.Axes, optional
Axes to plot the ellipse, by default None
"""
Expand Down
30 changes: 20 additions & 10 deletions ir_support/functions/line_plane_intersection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## @file
# @brief Find the intersection between a line segment and a plane.
# @author Gavin Paul
# @date May 29, 2026

import numpy as np
from typing import Union, List, Tuple

Expand All @@ -24,25 +29,30 @@ def line_plane_intersection(plane_normal:Union[np.ndarray, List[float]],
Second point on the line
"""

intersection_point = [0,0,0]
u = np.array(point2_on_line) - np.array(point1_on_line)
w = np.array(point1_on_line) - np.array(point_on_plane)
D = np.dot(np.array(plane_normal), u)
N = -np.dot(np.array(plane_normal), w)
plane_normal = np.asarray(plane_normal, dtype=float)
point_on_plane = np.asarray(point_on_plane, dtype=float)
point1_on_line = np.asarray(point1_on_line, dtype=float)
point2_on_line = np.asarray(point2_on_line, dtype=float)

intersection_point = np.zeros(3)
line_vector = point2_on_line - point1_on_line
plane_to_line = point1_on_line - point_on_plane
denominator = np.dot(plane_normal, line_vector)
numerator = -np.dot(plane_normal, plane_to_line)
check = 0

if np.abs(D) < pow(10,-7): # The segment is parallel to plane
if N == 0: # The segment lies in plane
if np.abs(denominator) < 1e-7: # The segment is parallel to plane
if np.isclose(numerator, 0): # The segment lies in plane
check = 2
return intersection_point, check
else:
return intersection_point, check # No intersection

# Compute the intersection parameter
sI = N/D
intersection_point = point1_on_line + sI * u
segment_fraction = numerator / denominator
intersection_point = point1_on_line + segment_fraction * line_vector

if sI < 0 or sI > 1:
if segment_fraction < 0 or segment_fraction > 1:
check = 3 # The intersection point lies outside the segment, so there is no intersection
else:
check = 1
Expand Down
Loading
Loading