Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lab1: Introduction to Python

Department of Mechanical Engineering, Colorado State University
Open In Colab

Introduction:

Python is a powerful programming language that has become a standard tool in machine learning, data science, and scientific computing. With excellent libraries like NumPy, SciPy, and Matplotlib, Python is ideal for engineering applications. In this course, we will explore how to use Python for simulating and controlling robotic systems.

If your experience is mainly with Arduino or MATLAB, don’t worry—many concepts will feel familiar, and Python’s syntax is straightforward and beginner-friendly. With the support of modern tools and resources such as AI-assisted coding (e.g., Copilot), you’ll find that learning Python is both accessible and rewarding. Every mechanical engineer can become a confident Python coder!

This lab will only provide the basics for Python. You can try to find other resources online to learn more. For instance, this webpage provide more examples in the context of numerical methods: https://numericalmethodssullivan.github.io/ch-python.html Here is a more thorough tutorial from Python https://docs.python.org/3/tutorial/index.html

Learning Objectives:

  • Learn the structures of data and commands in Python with NumPy

  • Gain knowledge and familiarity with Jupyter notebooks and Python scientific libraries

  • Learn how to get help with Python and its libraries

  • Solve mathematical problems with Python and plot the solutions

  • Work with arrays, functions, and visualization tools

  • Using Python to implement basic concepts in robotics such as rotation matrix, homeogenenous transformation matrix, etc.

Task 1 – Basic Python Syntax

Before diving into NumPy and scientific computing, let’s cover some essential Python syntax that every engineer should know. Python’s syntax is clean and readable, making it an excellent choice for engineering applications.

Key Python Concepts:

  • Variables and Data Types: Python automatically determines data types based on the value assigned, so you don’t need to declare types explicitly. This makes it easy to work with numbers, strings, booleans, and even complex numbers.

  • Lists and Dictionaries: Lists are ordered, mutable collections ideal for storing sequences of data, while dictionaries store key-value pairs for fast lookups and structured information. Both are fundamental for organizing and manipulating engineering data.

  • Control Flow: if/else statements and loops (for, while) allow you to make decisions and repeat actions in your code. These constructs are essential for automating calculations and processing data sets.

  • Functions: Functions let you encapsulate reusable code blocks, making your programs modular and easier to maintain. Well-designed functions improve clarity and enable you to test engineering calculations independently.

  • String Formatting: Modern f-string syntax (e.g., f"Value: {variable}") allows you to embed variables directly in strings for clear, readable output. This is especially useful for reporting results and debugging.

# Variables and Basic Data Types
# Python automatically determines the type based on the value assigned

# Numbers
integer_var = 50
float_var = 3.14159
complex_var = 3 + 4j

# Strings
name = "Mechanical Engineer"
course = 'MECH 564 Fundamentals of Robot mechanics'

# Boolean
is_python_awesome = True

# print the result: the 'f' before the string enables f-string formatting for variable substitution
print(f"Integer: {integer_var}, type: {type(integer_var)}")
print(f"Float: {float_var}, type: {type(float_var)}")
print(f"Complex: {complex_var}, type: {type(complex_var)}")
print(f"String: {course}, type: {type(course)}")
print(f"Boolean: {is_python_awesome}, type: {type(is_python_awesome)}")
Integer: 50, type: <class 'int'>
Float: 3.14159, type: <class 'float'>
Complex: (3+4j), type: <class 'complex'>
String: MECH 564 Fundamentals of Robot mechanics, type: <class 'str'>
Boolean: True, type: <class 'bool'>
# Lists - ordered, mutable collections (like MATLAB cell arrays)
forces = [100, 250, 75, 500]  # List of forces in Newtons
materials = ["Steel", "Aluminum", "Titanium", "Carbon Fiber"]

print(f"Forces: {forces}")
print(f"Second force: {forces[1]} N")  # Python uses 0-based indexing
print(f"Last force: {forces[-1]} N")  # Negative indexing from the end

# Adding elements
forces.append(150)  # Add to end
print(f"After adding 150 N: {forces}")

# List slicing (very powerful!)
print(f"First three forces: {forces[0:3]}")  # Elements 0, 1, 2
print(f"All forces: {forces[:]} or {forces}")
Forces: [100, 250, 75, 500]
Second force: 250 N
Last force: 500 N
After adding 150 N: [100, 250, 75, 500, 150]
First three forces: [100, 250, 75, 500]
All forces: [100, 250, 75, 500, 150] or [100, 250, 75, 500, 150]
# Dictionaries - key-value pairs (like MATLAB structures)
material_properties = {
    "Steel": {"density": 7850, "yield_strength": 250},  # kg/m³, MPa
    "Aluminum": {"density": 2700, "yield_strength": 70},
    "Titanium": {"density": 4500, "yield_strength": 880}
}

print("Material Properties:")
for material, props in material_properties.items():
    density = props["density"]
    strength = props["yield_strength"]
    print(f"  {material}: ρ = {density} kg/m³, σy = {strength:.0f} MPa")
Material Properties:
  Steel: ρ = 7850 kg/m³, σy = 250 MPa
  Aluminum: ρ = 2700 kg/m³, σy = 70 MPa
  Titanium: ρ = 4500 kg/m³, σy = 880 MPa
# Control Flow: if/else statements
force = 400  # Newtons, you can change this value to test the if/else statements
max_allowable = 400  # Newtons

if force > max_allowable:
    safety_factor = force / max_allowable
    print(f"⚠️  Force {force} N exceeds limit of {max_allowable} N")
    print(f"   Safety factor: {safety_factor:.2f}")
elif force == max_allowable:
    print(f"✓ Force exactly at limit: {force} N")
else:
    margin = max_allowable - force
    print(f"✓ Force {force} N is safe (margin: {margin} N)")
✓ Force exactly at limit: 400 N
# Loops: for and while
print("For loop example - analyzing different loads:")
loads = [100, 250, 75, 450, 200]  # Newtons

for i, load in enumerate(loads):  # enumerate gives both index (i) and value
    status = "SAFE" if load < 400 else "OVER LIMIT" # check if a given load is safe or not
    print(f"  Load {i+1}: {load} N - {status}")

print("\nWhile loop example - finding critical load:")
load = 100
increment = 50
limit = 400

while load <= limit:
    print(f"  Testing load: {load} N")
    load += increment

print(f"  Critical load exceeded at: {load} N")
For loop example - analyzing different loads:
  Load 1: 100 N - SAFE
  Load 2: 250 N - SAFE
  Load 3: 75 N - SAFE
  Load 4: 450 N - OVER LIMIT
  Load 5: 200 N - SAFE

While loop example - finding critical load:
  Testing load: 100 N
  Testing load: 150 N
  Testing load: 200 N
  Testing load: 250 N
  Testing load: 300 N
  Testing load: 350 N
  Testing load: 400 N
  Critical load exceeded at: 450 N
# Functions - essential for reusable codes
def calculate_stress(force, area):
    """
    This function calculates stress given force and area.

    Parameters:
    force (float): Applied force in Newtons
    area (float): Cross-sectional area in m²

    Returns:
    float: Stress in Pascals
    """
    if area <= 0:
        raise ValueError("Area must be positive")

    stress = force / area
    return stress

def safety_check(stress, yield_strength, safety_factor=2.0):
    """Check if design meets safety requirements"""
    allowable_stress = yield_strength / safety_factor
    is_safe = stress <= allowable_stress  # True if stress is less than or equal to allowable

    return {
        "is_safe": is_safe,
        "stress": stress,
        "allowable": allowable_stress,
        "utilization": stress / allowable_stress
    }

# Example usage
force = 10000  # N
diameter = 0.02  # m
area = 3.14159 * (diameter/2)**2  # m²

stress = calculate_stress(force, area)
result = safety_check(stress, 250e6)  # Steel yield strength

print(f"Engineering Analysis:")
print(f"  Applied force: {force} N")
print(f"  Cross-sectional area: {area*1e6:.1f} mm²")
print(f"  Calculated stress: {stress/1e6:.1f} MPa")
print(f"  Safety status: {'✓ SAFE' if result['is_safe'] else '⚠️ UNSAFE'}")
Engineering Analysis:
  Applied force: 10000 N
  Cross-sectional area: 314.2 mm²
  Calculated stress: 31.8 MPa
  Safety status: ✓ SAFE

Key Takeaways:

  • Python is readable: Code should be clear and self-documenting

  • Indentation matters: Python uses indentation instead of braces {}

  • Zero-based indexing: Arrays/lists start at index 0, not 1 (unlike MATLAB)

  • Dynamic typing: Variables can change type, but be careful in engineering calculations

  • F-strings: Use f"Value: {variable}" for modern string formatting

  • Functions: Always document with docstrings for engineering work (docstrings are multi-line string comments placed right after the function definition to describe its purpose, inputs, and outputs; this helps others understand and maintain your code)

Error Handling and Debugging

Engineers need robust code that handles unexpected situations:

# Error handling with try/except blocks
def safe_divide(a, b):
    """Safely divide two numbers with error handling"""
    try:
        result = a / b
        return result
    except ZeroDivisionError: # Handle division by zero error
        print(f"Error: Cannot divide {a} by zero!")
        return None
    except TypeError: # Handle invalid input types
        print(f"Error: Invalid input types - need numbers, got {type(a)} and {type(b)}")
        return None

# Test error handling
print("Testing error handling:")
print(f"10 / 2 = {safe_divide(10, 2)}")
print(f"10 / 0 = {safe_divide(10, 0)}")
print(f"'10' / 2 = {safe_divide('10', 2)}")

# Common debugging technique: assertions
def calculate_beam_deflection(force, length, E, I):
    """Calculate beam deflection with input validation"""
    assert force > 0, "Force must be positive"
    assert length > 0, "Length must be positive"
    assert E > 0, "Young's modulus must be positive"
    assert I > 0, "Moment of inertia must be positive"

    # Simply supported beam, center load: δ = FL³/(48EI)
    deflection = (force * length**3) / (48 * E * I)  # '**' is the exponentiation operator in Python (length cubed)
    return deflection

# Test with valid inputs
try:
    # Steel beam, you may change the values to negative in the following line to oberserve the output
    delta = calculate_beam_deflection(1000, 2, 200e9, 8.33e-6)
    print(f"Beam deflection: {delta*1000:.2f} mm")
except AssertionError as e:
    print(f"Input validation failed: {e}")
Testing error handling:
10 / 2 = 5.0
Error: Cannot divide 10 by zero!
10 / 0 = None
Error: Invalid input types - need numbers, got <class 'str'> and <class 'int'>
'10' / 2 = None
Input validation failed: Length must be positive

Task 2 – Data Structures and Basic Operations

NumPy is a fundamental Python library for numerical computing. It provides support for large, multi-dimensional arrays and matrices, along with a collection of mathematical functions to operate on these arrays efficiently. NumPy is widely used in scientific computing, data analysis, and engineering applications. Learn more: https://numpy.org/

Matplotlib is a powerful Python library for creating static, animated, and interactive visualizations. It is widely used for plotting data, generating figures, and customizing charts in scientific and engineering applications. Learn more: https://matplotlib.org/. Here is a link with very useful cheetsheets for Matplotlib: https://matplotlib.org/cheatsheets/

Let’s start by importing these three libraries.

# Import necessary libraries
import numpy as np
import matplotlib.pyplot as plt
#import math

""""""
# Set up matplotlib for inline plotting,
# "%" is Jupyter magic command:
# a special command that provides convenient shortcuts for common operations.
# it is not a standard part of Python, but only for Jupyter notebooks,
# which is how this lab is written with
""""""
%matplotlib inline

Basic Mathematical Operations

You can use Python like a calculator:

# Basic calculations
result1 = -1 - 1
result2 = 2**2
print(f"-1 - 1 = {result1}")
print(f"2^2 = {result2}")
-1 - 1 = -2
2^2 = 4

Creating Arrays (equivalent to MATLAB matrices)

You can save answers by assigning them to variables and create arrays:

# Create 1D arrays (equivalent to MATLAB row vectors)
x1 = np.array([1, 2, 3])  # Creates a 1D array with 3 elements
x2 = np.array([2j, 1+4j, np.pi, -1])  # Complex numbers and pi
x3 = np.array([1+1, 2+2, 3+3])  # Array with calculations

print("x1:", x1)
print("x2:", x2)
print("x3:", x3)
x1: [1 2 3]
x2: [ 0.        +2.j  1.        +4.j  3.14159265+0.j -1.        +0.j]
x3: [2 4 6]
# Create 2D arrays (equivalent to MATLAB matrices)
y1 = np.array([[1, 2, 3], [4, 5, 6]])  # 2x3 matrix
y2 = np.array([[1, 2, 3], [4, 5, 6]])  # Same as y1
y3 = np.array([[1, 2, 3.0], [-4.0, 2-5, 6.00]])  # 2x3 matrix with mixed operations

print("y1:")
print(y1)
print("\ny2:")
print(y2)
print("\ny3:")
print(y3)
y1:
[[1 2 3]
 [4 5 6]]

y2:
[[1 2 3]
 [4 5 6]]

y3:
[[ 1.  2.  3.]
 [-4. -3.  6.]]

Matrix operations

Now let’s do some basic matrix operations. NumPy provides essential matrix operations for engineering calculations.

Key Matrix Operations:

  • Matrix multiplication: A @ B

  • Element-wise multiplication: A * B

  • Transpose: A.T

  • Inverse: np.linalg.inv(A)

  • Determinant: np.linalg.det(A)

# Basic Matrix Operations
A = np.array([[1, 2], [3, 4]])
B = np.array([[2, 0], [1, 3]])
vector = np.array([1, 2])

print("Matrix A:")
print(A)
print("\nMatrix B:")
print(B)

# Matrix multiplication (use @ operator)
print("\nA @ B (matrix multiplication):")
print(A @ B)

# Element-wise multiplication (use * operator)
print("\nA * B (element-wise multiplication):")
print(A * B)

# Matrix-vector multiplication
print("\nA @ vector:")
print(A @ vector)

# Transpose
print("\nA transpose (A.T):")
print(A.T)

# Matrix inverse and determinant
print(f"\nDeterminant of A: {np.linalg.det(A):.2f}")
A_inv = np.linalg.inv(A)
print(f"Inverse of A:")
print(A_inv)

# Dot and cross products with 3D vectors
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])

# Dot product (scalar result)
dot_product = np.dot(v1, v2)
print(f"\nDot product of v1 and v2: {dot_product}")
print(f"  v1 · v2 = (1)(4) + (2)(5) + (3)(6) = {dot_product}")

# Cross product (vector result)
cross_product = np.cross(v1, v2)
print(f"\nCross product of v1 and v2: {cross_product}")
print(f"  v1 × v2 = {cross_product}")
Matrix A:
[[1 2]
 [3 4]]

Matrix B:
[[2 0]
 [1 3]]

A @ B (matrix multiplication):
[[ 4  6]
 [10 12]]

A * B (element-wise multiplication):
[[ 2  0]
 [ 3 12]]

A @ vector:
[ 5 11]

A transpose (A.T):
[[1 3]
 [2 4]]

Determinant of A: -2.00
Inverse of A:
[[-2.   1. ]
 [ 1.5 -0.5]]

Dot product of v1 and v2: 32
  v1 · v2 = (1)(4) + (2)(5) + (3)(6) = 32

Cross product of v1 and v2: [-3  6 -3]
  v1 × v2 = [-3  6 -3]

Task 3 – Built-in Functions and Plotting

Python with NumPy has many built-in functions similar to MATLAB. The general structure is: result = function_name(input1, input2, ...)

# Array operations
a = np.array([4, 9, 7])
y = np.sqrt(a)  # Square root
z = np.array([np.max(a), np.min(a), np.mean(a)])  # Max, min, mean

print(f"a = {a}")
print(f"sqrt(a) = {y}")
print(f"[max, min, mean] = {z}")
print(f"Size of a: {a.shape}")
a = [4 9 7]
sqrt(a) = [2.         3.         2.64575131]
[max, min, mean] = [9.         4.         6.66666667]
Size of a: (3,)

Common Mathematical Functions

Here are some commonly used functions:

# Arc tangent
angle = np.arctan(1)  # arctan(1) = π/4
print(f"arctan(1) = {angle} radians = {np.degrees(angle)} degrees")

# Random numbers
random_num = np.random.rand()  # Single random number between 0 and 1
random_array = np.random.rand(3, 3)  # 3x3 array of random numbers
print(f"Random number: {random_num}")

# To print a numpy array with specific precision, use np.array2string
print("Random 3x3 array:")
print(np.array2string(random_array, formatter={'float_kind':lambda x: "%.2f" % x}))
arctan(1) = 0.7853981633974483 radians = 45.0 degrees
Random number: 0.7579251502056656
Random 3x3 array:
[[0.85 0.30 0.42]
 [0.99 0.97 0.11]
 [0.20 0.50 0.24]]

2D Plotting

# Create time array and plot sine function
t = np.linspace(0, 10, 100) # Creates exactly 100 points from 0 to 10 (inclusive)
y = 2 * np.sin(t)

# Create a new figure with a specific size (10 inches wide by 6 inches tall)
plt.figure(figsize=(10, 6))
plt.plot(t, y)
# plt.plot(t,y,'b-', linewidth=2)
plt.xlabel('Time (t)')
plt.ylabel('y = 2*sin(t)')
plt.title('Sine Wave Plot')
# Add a text annotation to the plot showing the equation
plt.text(5, 1.5, r'y(t) = 2sin(t)', fontsize=12,
         bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
plt.grid(True)
<Figure size 1000x600 with 1 Axes>

3D Plotting

# 3D plotting example
# Import 3D plotting toolkit for matplotlib (required for 3D surface plots)
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(10, 8)) # Create a new figure with a specific size
ax = fig.add_subplot(111, projection='3d')  # Add a 3D subplot to the figure

# Create meshgrid for 3D surface
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

# Create 3D surface plot
# Plot the 3D surface; cmap sets the color map, alpha sets transparency
# cmap='viridis' uses a blue-green-yellow color gradient for the surface
# alpha=0.8 makes the surface slightly transparent (1.0 is fully opaque)
surf = ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.8)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('3D Surface Plot: Z = sin(√(X² + Y²))')
# Add color bar to indicate the mapping of colors to Z values
plt.colorbar(surf)
<Figure size 1000x800 with 2 Axes>

Plot with Custom Functions

In Python, you can define custom functions easily:

def ME564_fun(fxn_input):
    """Custom function that combines sine wave with random noise"""
    return 10 * np.sin(fxn_input * 10) + 5 * np.random.rand(*fxn_input.shape)

# Use the custom function
y_564 = ME564_fun(t)

plt.figure(figsize=(10, 6))
plt.plot(t, y_564, 'r-', linewidth=1.5)
plt.xlabel('Time (t), sec')
plt.ylabel('Function, f(t), -')
plt.title('Custom Function: 10*sin(10t) + 5*random')
plt.grid(True, alpha=0.3)
plt.show()
<Figure size 1000x600 with 1 Axes>

Task 4 – Debugging Techniques in Python

Effective debugging is crucial for engineering applications where accuracy is paramount. Python offers several powerful debugging tools and techniques that every engineer should know. We will go through two methods.

Method 1: Print statements and logging

# Method 1: Strategic Print Statements and Logging
# Example: Rotation Matrix Analysis in Robotics

def create_rotation_matrix(theta_deg, axis='z'):
    """
    Create a 3D rotation matrix with debugging prints

    Parameters:
    theta_deg: rotation angle in degrees
    axis: rotation axis ('x', 'y', or 'z')

    Returns:
    numpy.ndarray: 3x3 rotation matrix
    """
    print(f"DEBUG: Creating rotation matrix - angle={theta_deg}°, axis='{axis}'")

    # Convert angle to radians
    theta_rad = np.radians(theta_deg)
    print(f"DEBUG: Angle in radians: {theta_rad:.4f}")

    c = np.cos(theta_rad)
    s = np.sin(theta_rad)
    print(f"DEBUG: cos(θ)={c:.4f}, sin(θ)={s:.4f}")

    if axis == 'x':
        # 3D rotation about X-axis
        R = np.array([[1, 0, 0],
                      [0, c, -s],
                      [0, s, c]])
        print(f"DEBUG: Created 3D rotation matrix (X-axis)")
    elif axis == 'y':
        # 3D rotation about Y-axis
        R = np.array([[c, 0, s],
                      [0, 1, 0],
                      [-s, 0, c]])
        print(f"DEBUG: Created 3D rotation matrix (Y-axis)")
    elif axis == 'z':
        # 3D rotation about Z-axis
        R = np.array([[c, -s, 0],
                      [s, c, 0],
                      [0, 0, 1]])
        print(f"DEBUG: Created 3D rotation matrix (Z-axis)")
    else:
        raise ValueError("Axis must be 'x', 'y', or 'z'")

    print(f"DEBUG: Rotation matrix determinant: {np.linalg.det(R):.6f}")
    print(f"DEBUG: Matrix is orthogonal: {np.allclose(R @ R.T, np.eye(R.shape[0]))}")

    return R

# Test with a robot arm rotating 45 degrees about Z-axis
print("EXAMPLE: Rotating robot end-effector point about Z-axis\n")
R_z45 = create_rotation_matrix(45, axis='z')

# Original point in robot frame (100 mm, 50 mm, 0 mm)
point = np.array([100, 50, 0])
print(f"\nDEBUG: Original point: {point}")

# Rotate the point
rotated_point = R_z45 @ point
print(f"DEBUG: Rotated point: {rotated_point}")
print(f"DEBUG: Distance preserved: {np.linalg.norm(point):.2f} → {np.linalg.norm(rotated_point):.2f}")

print(f"\nFINAL RESULT: Point rotated 45° CCW about Z-axis")
print(f"  Before: ({point[0]:.1f}, {point[1]:.1f}, {point[2]:.1f}) mm")
print(f"  After:  ({rotated_point[0]:.1f}, {rotated_point[1]:.1f}, {rotated_point[2]:.1f}) mm")
EXAMPLE: Rotating robot end-effector point about Z-axis

DEBUG: Creating rotation matrix - angle=45°, axis='z'
DEBUG: Angle in radians: 0.7854
DEBUG: cos(θ)=0.7071, sin(θ)=0.7071
DEBUG: Created 3D rotation matrix (Z-axis)
DEBUG: Rotation matrix determinant: 1.000000
DEBUG: Matrix is orthogonal: True

DEBUG: Original point: [100  50   0]
DEBUG: Rotated point: [ 35.35533906 106.06601718   0.        ]
DEBUG: Distance preserved: 111.80 → 111.80

FINAL RESULT: Point rotated 45° CCW about Z-axis
  Before: (100.0, 50.0, 0.0) mm
  After:  (35.4, 106.1, 0.0) mm

Method 2: Python Debugger (pdb) - Interactive Debugging

The Python debugger allows you to pause execution, inspect variables, and step through code line by line.

Key pdb commands:

  • n (next): Execute next line

  • s (step): Step into function calls

  • l (list): Show current code

  • p variable_name: Print variable value

  • pp variable_name: Pretty print variable

  • c (continue): Continue execution

  • q (quit): Quit debugger

# Method 2: Using pdb for interactive debugging
import numpy as np
import pdb

def rotate_point_2d(x, y, angle_deg):
    """
    Rotate a 2D point counterclockwise by angle_deg degrees around origin.
    Simple example to demonstrate pdb debugging.

    Parameters:
    x, y: point coordinates
    angle_deg: rotation angle in degrees

    Returns:
    tuple: (x_rotated, y_rotated)
    """

    # Uncomment the next line to pause execution and debug interactively


    # Step 1: Convert angle to radians
    angle_rad = np.radians(angle_deg)
    print(f"Step 1: angle_deg = {angle_deg}° → angle_rad = {angle_rad:.4f}")

    pdb.set_trace()  # Debugger will start here

    # Step 2: Calculate rotation components
    cos_a = np.cos(angle_rad)
    sin_a = np.sin(angle_rad)
    print(f"Step 2: cos(θ) = {cos_a:.4f}, sin(θ) = {sin_a:.4f}")

    # Step 3: Apply rotation matrix
    # [x']   [cos(θ)  -sin(θ)] [x]
    # [y'] = [sin(θ)   cos(θ)] [y]
    x_rotated = cos_a * x - sin_a * y
    y_rotated = sin_a * x + cos_a * y
    print(f"Step 3: ({x}, {y}) → ({x_rotated:.2f}, {y_rotated:.2f})")

    return x_rotated, y_rotated

# Example: Robot arm end-effector position
print("=== 2D Point Rotation (Robot End-Effector) ===\n")
print("Original point: (1.0, 0.0) - on X-axis")
print("Rotation: 90° counterclockwise\n")

x_new, y_new = rotate_point_2d(1.0, 0.0, 90)

print(f"\n=== RESULT ===")
print(f"After 90° rotation: ({x_new:.2f}, {y_new:.2f})")
print(f"Expected: (0.0, 1.0) - now on Y-axis")

# Try uncommenting pdb.set_trace() above to:
# - Type 'p x' to print x value
# - Type 'p angle_rad' to print angle_rad value
# - Type 'n' to execute next line
# - Type 'l' to list code around breakpoint
# - Type 'c' to continue execution
=== 2D Point Rotation (Robot End-Effector) ===

Original point: (1.0, 0.0) - on X-axis
Rotation: 90° counterclockwise

Step 1: angle_deg = 90° → angle_rad = 1.5708
> /tmp/ipython-input-2080816695.py(28)rotate_point_2d()
     26 
     27     # Step 2: Calculate rotation components
---> 28     cos_a = np.cos(angle_rad)
     29     sin_a = np.sin(angle_rad)
     30     print(f"Step 2: cos(θ) = {cos_a:.4f}, sin(θ) = {sin_a:.4f}")

ipdb> p angle_rad
np.float64(1.5707963267948966)
--KeyboardInterrupt--

KeyboardInterrupt: Interrupted by user
Step 2: cos(θ) = 0.0000, sin(θ) = 1.0000
Step 3: (1.0, 0.0) → (0.00, 1.00)

=== RESULT ===
After 90° rotation: (0.00, 1.00)
Expected: (0.0, 1.0) - now on Y-axis

Task 5: Matrix Exponential and Logarithm for Rotation and Homogeneous Transformation Matrices

After understanding the basics for Python, we can now practice what we learned in lectures. In robot mechanics and control, we often need to convert between different representations of rotations and transformations. The matrix exponential and matrix logarithm provide powerful tools for this conversion.

1. Matrix Exponential and Logarithm for Rotation Matrices

For a 3×3 rotation matrix R, we can use the exponential map to generate rotations from a skew-symmetric matrix (rotation vector):

  • Exponential Map: from so(3)so(3) to SO(3)SO(3), which can be obtained by R=exp([ω^]θ)=I+sinθ[ω^]+(1cosθ)[ω^]2\mathbf{R} = \exp([\hat{\boldsymbol{\omega}}]\theta) = \mathbf{I} + \sin\theta[\hat{\boldsymbol{\omega}}] + (1-\cos\theta)[\hat{\boldsymbol{\omega}}]^2

    • Input: Skew-symmetric matrix from unit rotation axis ω^\hat{\boldsymbol{\omega}} with rotation angle θ\theta

    • Output: Three by three Rotation matrix R

In this task, we will implement Python functions to compute these maps for both rotation matrices and homogeneous transformation matrices, providing practical tools for robot mechanics calculations.

import numpy as np
# Helper function: Create skew-symmetric matrix from a 3D vector
def skew_symmetric(omega):
    """
    Create a 3x3 skew-symmetric matrix from a 3D vector.

    For a vector omega = [w1, w2, w3], creates:
    [omega] = [  0  -w3   w2]
              [ w3    0  -w1]
              [-w2   w1    0]

    Parameters:
    omega: 3D vector (numpy array or list)

    Returns:
    3x3 skew-symmetric matrix (numpy array)
    """
    omega = np.array(omega).flatten()
    if len(omega) != 3:
        raise ValueError("Input must be a 3D vector")

    return np.array([
        [0,         -omega[2],  omega[1]],
        [omega[2],   0,        -omega[0]],
        [-omega[1],  omega[0],  0]
    ])


def matrix_exp_so3(omega_hat_skew, theta):
    """
    Exponential map from so(3) to SO(3) using Rodrigues' formula.

    Computes: R = exp([omega_hat]*theta) = I + sin(theta)*[omega_hat] + (1-cos(theta))*[omega_hat]^2

    Parameters:
    omega_hat_skew: 3x3 skew-symmetric matrix of UNIT rotation axis (numpy array)
    theta: rotation angle in radians (scalar)

    Returns:
    R: 3x3 rotation matrix in SO(3)

    Example:
    >>> omega_hat = np.array([0, 0, 1])  # Unit vector along z-axis
    >>> omega_hat_skew = skew_symmetric(omega_hat)
    >>> R = matrix_exp_so3(omega_hat_skew, np.pi/2)  # 90 degree rotation about z
    """
    # Verify input is 3x3
    if omega_hat_skew.shape != (3, 3):
        raise ValueError("Input must be a 3x3 skew-symmetric matrix")

    # Check if matrix is skew-symmetric (within numerical tolerance)
    if not np.allclose(omega_hat_skew, -omega_hat_skew.T):
        raise ValueError("Input matrix must be skew-symmetric: [omega]^T = -[omega]")

    # Identity matrix
    I = np.eye(3)

    # Apply Rodrigues' formula
    # R = I + sin(theta)*[omega_hat] + (1-cos(theta))*[omega_hat]^2
    R = I + np.sin(theta) * omega_hat_skew + (1 - np.cos(theta)) * (omega_hat_skew @ omega_hat_skew)

    return R


def matrix_exp_so3_from_vector(omega_theta):
    """
    Exponential map from so(3) to SO(3) using exponential coordinates.

    This is a convenient wrapper that takes the rotation vector directly
    (where the magnitude is the angle and direction is the axis).

    Parameters:
    omega_theta: 3D rotation vector where ||omega_theta|| = theta (rotation angle)
                 and omega_theta/||omega_theta|| = omega_hat (unit rotation axis)

    Returns:
    R: 3x3 rotation matrix in SO(3)

    Example:
    >>> omega_theta = np.array([0, 0, np.pi/2])  # 90 deg rotation about z-axis
    >>> R = matrix_exp_so3_from_vector(omega_theta)
    """
    omega_theta = np.array(omega_theta).flatten()

    # Calculate rotation angle (magnitude of vector)
    theta = np.linalg.norm(omega_theta)

    # Handle special case: zero rotation
    if theta < 1e-10:
        return np.eye(3)

    # Extract unit rotation axis
    omega_hat = omega_theta / theta

    # Create skew-symmetric matrix
    omega_hat_skew = skew_symmetric(omega_hat)

    # Apply Rodrigues' formula
    return matrix_exp_so3(omega_hat_skew, theta)


# Test Example 1: Rotation about Z-axis by 90 degrees
print("=" * 60)
print("Example 1: Rotation about Z-axis by 90 degrees")
print("=" * 60)

# Define unit rotation axis (z-axis)
omega_hat = np.array([0, 0, 1])
theta = np.pi / 2  # 90 degrees in radians

# Create skew-symmetric matrix
omega_hat_skew = skew_symmetric(omega_hat)
print(f"\nUnit rotation axis (omega_hat): {omega_hat}")
print(f"Rotation angle (theta): {theta:.4f} rad = {np.degrees(theta):.1f}°")
print(f"\nSkew-symmetric matrix [omega_hat]:")
print(omega_hat_skew)

# Compute rotation matrix using exponential map
R_z90 = matrix_exp_so3(omega_hat_skew, theta)
print(f"\nRotation matrix R (from exponential map):")
print(R_z90)

# Verify it's a valid rotation matrix
print(f"\nVerification:")
print(f"  det(R) = {np.linalg.det(R_z90):.6f} (should be 1)")
print(f"  R*R^T = I? {np.allclose(R_z90 @ R_z90.T, np.eye(3))}")

# Test rotation on a point
point = np.array([1, 0, 0])  # Point on x-axis
rotated_point = R_z90 @ point
print(f"\nRotating point {point} by 90° about z-axis:")
print(f"  Result: [{rotated_point[0]:.4f}, {rotated_point[1]:.4f}, {rotated_point[2]:.4f}]")
print(f"  Expected: [0, 1, 0] (on y-axis)")


# Test Example 2: Using the vector wrapper function
print("\n" + "=" * 60)
print("Example 2: Using exponential coordinates (vector form)")
print("=" * 60)

# Rotation vector: direction = axis, magnitude = angle
omega_theta = np.array([0, np.pi/4, 0])  # 45° rotation about y-axis
print(f"\nRotation vector (omega*theta): {omega_theta}")
print(f"  Magnitude (angle): {np.linalg.norm(omega_theta):.4f} rad = {np.degrees(np.linalg.norm(omega_theta)):.1f}°")
print(f"  Direction (axis): {omega_theta / np.linalg.norm(omega_theta)}")

R_y45 = matrix_exp_so3_from_vector(omega_theta)
print(f"\nRotation matrix R:")
print(R_y45)

# Verify
print(f"\nVerification:")
print(f"  det(R) = {np.linalg.det(R_y45):.6f}")
print(f"  Orthogonal? {np.allclose(R_y45 @ R_y45.T, np.eye(3))}")
============================================================
Example 1: Rotation about Z-axis by 90 degrees
============================================================

Unit rotation axis (omega_hat): [0 0 1]
Rotation angle (theta): 1.5708 rad = 90.0°

Skew-symmetric matrix [omega_hat]:
[[ 0 -1  0]
 [ 1  0  0]
 [ 0  0  0]]

Rotation matrix R (from exponential map):
[[ 1.11022302e-16 -1.00000000e+00  0.00000000e+00]
 [ 1.00000000e+00  1.11022302e-16  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  1.00000000e+00]]

Verification:
  det(R) = 1.000000 (should be 1)
  R*R^T = I? True

Rotating point [1 0 0] by 90° about z-axis:
  Result: [0.0000, 1.0000, 0.0000]
  Expected: [0, 1, 0] (on y-axis)

============================================================
Example 2: Using exponential coordinates (vector form)
============================================================

Rotation vector (omega*theta): [0.         0.78539816 0.        ]
  Magnitude (angle): 0.7854 rad = 45.0°
  Direction (axis): [0. 1. 0.]

Rotation matrix R:
[[ 0.70710678  0.          0.70710678]
 [ 0.          1.          0.        ]
 [-0.70710678  0.          0.70710678]]

Verification:
  det(R) = 1.000000
  Orthogonal? True
  • Logarithm Map: from SO(3)SO(3) to so(3)so(3), which can be obtained [ω]^θ=log(R)[\hat{\boldsymbol{\omega}]}\theta = \log(\mathbf{R}) using the Algorithm on Page 87 of Mordern Robotics Textbook.

    • Input: Rotation matrix R

    • Output: Skew-symmetric matrix from unit rotation axis ω^\hat{\boldsymbol{\omega}} with rotation angle θ\theta

def matrix_log_so3(R):
    """
    Logarithm map from SO(3) to so(3) using the algorithm from Modern Robotics textbook.

    Given R ∈ SO(3), find θ ∈ [0, π] and unit rotation axis ω ∈ ℝ³, ||ω|| = 1,
    such that R = exp([ω]θ).

    Algorithm:
    (a) If R = I, then θ = 0 and ω is undefined.
    (b) If tr(R) = -1, then θ = π. Use formula (3.60): ω = (1/√(2(1+r₁₁))) [1+r₁₁, r₂₁, r₃₁]ᵀ
    (c) Otherwise, θ = arccos((tr(R)-1)/2) and [ω] = (1/(2sinθ))(R - Rᵀ)

    Parameters:
    R: 3x3 rotation matrix (numpy array)

    Returns:
    omega_hat_skew: 3x3 skew-symmetric matrix [ω] = [ω]θ where ω is unit rotation axis
    theta: scalar rotation angle in radians
    omega_hat: 3D unit rotation axis vector (None if R=I)

    Example:
    >>> R = matrix_exp_so3_from_vector(np.array([0, 0, np.pi/2]))
    >>> omega_hat_skew, theta, omega_hat = matrix_log_so3(R)
    >>> print(f"Angle: {theta}, Axis: {omega_hat}")
    """

    # Verify input is 3x3
    if R.shape != (3, 3):
        raise ValueError("Input must be a 3x3 rotation matrix")

    # Verify it's a valid rotation matrix
    if not np.allclose(np.linalg.det(R), 1.0, atol=1e-6): #allclose checks if the determinant is close to 1 within a small tolerance
        raise ValueError(f"Input matrix determinant is {np.linalg.det(R):.6f}, not 1. Not a valid rotation matrix.")

    if not np.allclose(R @ R.T, np.eye(3), atol=1e-6):
        raise ValueError("Input matrix is not orthogonal (R @ R.T ≠ I). Not a valid rotation matrix.")

    # Case (a): If R = I, then θ = 0
    if np.allclose(R, np.eye(3), atol=1e-6):
        print("Case (a): R = I, so θ = 0 and ω is undefined")
        omega_hat_skew = np.zeros((3, 3))
        theta = 0.0
        omega_hat = None
        return omega_hat_skew, theta, omega_hat

    # Calculate trace of R, i.e., trace(r11 + r22 + r33)
    trace_R = np.trace(R)

    # Case (b): If tr(R) = -1, then θ = π
    if np.isclose(trace_R, -1.0, atol=1e-6):
        print("Case (b): tr(R) = -1, so θ = π")
        theta = np.pi

        # Use formula (3.60): ω = (1/√(2(1+r₁₁))) [1+r₁₁, r₂₁, r₃₁]ᵀ
        # where r₁₁ is R[0,0], r₂₁ is R[1,0], r₃₁ is R[2,0]
        r11 = R[0, 0]
        r21 = R[1, 0]
        r31 = R[2, 0]

        denominator = np.sqrt(2 * (1 + r11))

        if abs(denominator) < 1e-10:
            # Try formula (3.59) if (3.60) has numerical issues
            r12 = R[0, 1]
            r22 = R[1, 1]
            r32 = R[2, 1]
            denominator = np.sqrt(2 * (1 + r22))
            omega_hat = np.array([r12, 1 + r22, r32]) / denominator
        else:
            omega_hat = np.array([1 + r11, r21, r31]) / denominator

        omega_hat_skew = skew_symmetric(omega_hat)

        print(f"  Using formula (3.60): ω = {omega_hat}")
        return omega_hat_skew, theta, omega_hat

    # Case (c): Otherwise, use general formula
    print("Case (c): General case")

    # θ = arccos((tr(R) - 1) / 2)
    theta = np.arccos((trace_R - 1) / 2)

    # [ω] = (1/(2sinθ)) * (R - Rᵀ)
    sin_theta = np.sin(theta)

    if abs(sin_theta) < 1e-6:
        raise ValueError("sin(θ) is too close to zero. Numerical issue in logarithm computation.")

    omega_hat_skew = (1 / (2 * sin_theta)) * (R - R.T)

    # Extract ω from skew-symmetric matrix
    # For skew-symmetric matrix [ω] = [  0  -w3   w2]
    #                                 [ w3    0  -w1]
    #                                 [-w2   w1    0]
    # ω = [w1, w2, w3]
    omega_hat = np.array([omega_hat_skew[2, 1], omega_hat_skew[0, 2], omega_hat_skew[1, 0]])

    print(f"  θ = arccos((tr(R)-1)/2) = {theta:.6f} rad = {np.degrees(theta):.2f}°")
    print(f"  ω = {omega_hat}")

    return omega_hat_skew, theta, omega_hat


# Test Example 1: Logarithm of identity matrix
print("=" * 70)
print("Test 1: Logarithm of Identity Matrix (Case a)")
print("=" * 70)

R_identity = np.eye(3)
omega_skew_id, theta_id, omega_id = matrix_log_so3(R_identity)
print(f"Input: R = I₃ (identity matrix)")
print(f"Result: θ = {theta_id}, ω = {omega_id}")


# Test Example 2: Verify exponential-logarithm round trip (General case)
print("\n" + "=" * 70) # Print a separator line for clarity
print("Test 2: Round-trip Verification (exp → log → exp)")
print("=" * 70)

# Start with a rotation vector
omega_theta_original = np.array([0, 0, np.pi/3])  # 60° about z-axis
print(f"Original rotation vector: {omega_theta_original}")
print(f"  Angle: {np.linalg.norm(omega_theta_original):.6f} rad = {np.degrees(np.linalg.norm(omega_theta_original)):.2f}°")

# Compute exponential: rotation vector → rotation matrix
R_test = matrix_exp_so3_from_vector(omega_theta_original)
print(f"\nComputed rotation matrix R via exponential map:")
print(R_test)

# Compute logarithm: rotation matrix → rotation vector
omega_skew_test, theta_test, omega_hat_test = matrix_log_so3(R_test)
print(f"\nLogarithm of R:")
print(f"  Skew-symmetric matrix [ω]:")
print(omega_skew_test)
print(f"  Rotation angle: {theta_test:.6f} rad = {np.degrees(theta_test):.2f}°")
print(f"  Rotation axis: {omega_hat_test}")

# Reconstruct original rotation vector
omega_theta_recovered = omega_hat_test * theta_test
print(f"\nRecovered rotation vector: {omega_theta_recovered}")
print(f"Original rotation vector:  {omega_theta_original}")
print(f"Match? {np.allclose(omega_theta_recovered, omega_theta_original)}")

# Verify by applying exponential again
R_recovered = matrix_exp_so3_from_vector(omega_theta_recovered)
print(f"\nRe-applying exponential map:")
print(f"Original R matches recovered R? {np.allclose(R_test, R_recovered)}")


# Test Example 3: 180-degree rotation (Case b - tr(R) = -1)
print("\n" + "=" * 70)
print("Test 3: 180-Degree Rotation about [1, 0, 0] (Case b)")
print("=" * 70)

# 180° rotation about x-axis
omega_theta_180 = np.array([np.pi, 0, 0])
R_180 = matrix_exp_so3_from_vector(omega_theta_180)
print(f"Input rotation vector: {omega_theta_180}")
print(f"Rotation matrix:")
print(R_180)
print(f"Trace of R: {np.trace(R_180):.6f}")

omega_skew_180, theta_180, omega_hat_180 = matrix_log_so3(R_180)
print(f"\nLogarithm result:")
print(f"  θ = {theta_180:.6f} rad = {np.degrees(theta_180):.2f}°")
print(f"  ω = {omega_hat_180}")
print(f"  Recovered rotation vector: {omega_hat_180 * theta_180}")


# Test Example 4: Arbitrary rotation
print("\n" + "=" * 70)
print("Test 4: Arbitrary Rotation (45° about [1,1,1])")
print("=" * 70)

axis_arbitrary = np.array([1, 1, 1])
omega_hat_arb = axis_arbitrary / np.linalg.norm(axis_arbitrary)
theta_arbitrary = np.pi / 4  # 45°
omega_theta_arb = omega_hat_arb * theta_arbitrary

print(f"Original axis (normalized): {omega_hat_arb}")
print(f"Original angle: {theta_arbitrary:.6f} rad = {np.degrees(theta_arbitrary):.2f}°")

R_arb = matrix_exp_so3_from_vector(omega_theta_arb)
omega_skew_arb, theta_arb_recovered, omega_hat_arb_recovered = matrix_log_so3(R_arb)

print(f"\nRecovered axis: {omega_hat_arb_recovered}")
print(f"Recovered angle: {theta_arb_recovered:.6f} rad = {np.degrees(theta_arb_recovered):.2f}°")
print(f"Axis match? {np.allclose(omega_hat_arb, omega_hat_arb_recovered)}")
print(f"Angle match? {np.allclose(theta_arbitrary, theta_arb_recovered)}")
======================================================================
Test 1: Logarithm of Identity Matrix (Case a)
======================================================================
Case (a): R = I, so θ = 0 and ω is undefined
Input: R = I₃ (identity matrix)
Result: θ = 0.0, ω = None

======================================================================
Test 2: Round-trip Verification (exp → log → exp)
======================================================================
Original rotation vector: [0.         0.         1.04719755]
  Angle: 1.047198 rad = 60.00°

Computed rotation matrix R via exponential map:
[[ 0.5       -0.8660254  0.       ]
 [ 0.8660254  0.5        0.       ]
 [ 0.         0.         1.       ]]
Case (c): General case
  θ = arccos((tr(R)-1)/2) = 1.047198 rad = 60.00°
  ω = [0. 0. 1.]

Logarithm of R:
  Skew-symmetric matrix [ω]:
[[ 0. -1.  0.]
 [ 1.  0.  0.]
 [ 0.  0.  0.]]
  Rotation angle: 1.047198 rad = 60.00°
  Rotation axis: [0. 0. 1.]

Recovered rotation vector: [0.         0.         1.04719755]
Original rotation vector:  [0.         0.         1.04719755]
Match? True

Re-applying exponential map:
Original R matches recovered R? True

======================================================================
Test 3: 180-Degree Rotation about [1, 0, 0] (Case b)
======================================================================
Input rotation vector: [3.14159265 0.         0.        ]
Rotation matrix:
[[ 1.0000000e+00  0.0000000e+00  0.0000000e+00]
 [ 0.0000000e+00 -1.0000000e+00 -1.2246468e-16]
 [ 0.0000000e+00  1.2246468e-16 -1.0000000e+00]]
Trace of R: -1.000000
Case (b): tr(R) = -1, so θ = π
  Using formula (3.60): ω = [1. 0. 0.]

Logarithm result:
  θ = 3.141593 rad = 180.00°
  ω = [1. 0. 0.]
  Recovered rotation vector: [3.14159265 0.         0.        ]

======================================================================
Test 4: Arbitrary Rotation (45° about [1,1,1])
======================================================================
Original axis (normalized): [0.57735027 0.57735027 0.57735027]
Original angle: 0.785398 rad = 45.00°
Case (c): General case
  θ = arccos((tr(R)-1)/2) = 0.785398 rad = 45.00°
  ω = [0.57735027 0.57735027 0.57735027]

Recovered axis: [0.57735027 0.57735027 0.57735027]
Recovered angle: 0.785398 rad = 45.00°
Axis match? True
Angle match? True

2. Matrix Exponential and Logarithm for Homogeneous Transformation Matrices

We can perform the similar transformation for Homogeneous Transformation Matrices. Specifically, for SE(3) (Special Euclidean Group in 3D), a homogeneous transformation matrix combines rotation and translation:

T=[R3×3p3×101×31]\mathbf{T} = \begin{bmatrix} \mathbf{R}_{3\times3} & \mathbf{p}_{3\times1} \\ \mathbf{0}_{1\times3} & 1 \end{bmatrix}
  • Exponential Map: T=exp([S]θ)\mathbf{T} = \exp([\mathcal{S}]\theta) where [S][\mathcal{S}] is a 4×4 matrix from the screw S=[Sω,Sv]\mathcal{S}=[\mathcal{S}_\omega, \mathcal{S}_v]:

    [S]=[[Sω]Sv01×30][\mathcal{S}] = \begin{bmatrix} [\mathcal{S}_\omega] & \mathcal{S}_v \\ \mathbf{0}_{1\times3} & 0 \end{bmatrix}
    • Input: [S]θ[\mathcal{S}]\theta

    • Output: Homogeneous transformation T

def matrix_exp_se3(S, theta):
    """
    Exponential map from se(3) to SE(3) using screw axis formulation.

    Given screw axis S = [S_omega, S_v] and angle theta, compute:
    T = exp([S] theta) = [[R, p], [0, 1]]

    If ||S_omega|| = 0 (pure translation):
        R = I, p = S_v * theta
    Otherwise:
        R = exp([omega_hat] theta)
        p = (I*theta + (1-cos(theta))[omega_hat] + (theta - sin(theta))[omega_hat]^2) S_v

    Parameters:
    S: 6D screw axis array [S_omega (3,), S_v (3,)]
    theta: scalar joint displacement

    Returns:
    T: 4x4 homogeneous transformation matrix in SE(3)
    """
    S = np.array(S).flatten()
    if S.shape[0] != 6:
        raise ValueError("S must be a 6D vector: [S_omega, S_v]")

    S_omega = S[:3]
    S_v = S[3:]

    omega_norm = np.linalg.norm(S_omega)
    I = np.eye(3)

    if omega_norm < 1e-10:
        # Pure translation
        R = I
        p = S_v * theta
    else:
        # Rotation + translation
        omega_hat = S_omega / omega_norm
        omega_hat_skew = skew_symmetric(omega_hat)

        R = matrix_exp_so3(omega_hat_skew, theta)

        V = (
            I * theta
            + (1 - np.cos(theta)) * omega_hat_skew
            + (theta - np.sin(theta)) * (omega_hat_skew @ omega_hat_skew)
        )
        p = V @ S_v

    T = np.eye(4)
    T[:3, :3] = R
    T[:3, 3] = p

    return T


# Test Example 1: Pure translation
print("=" * 70)
print("SE(3) Exponential Map - Test 1: Pure Translation")
print("=" * 70)

S_translation = np.array([0, 0, 0, 1, 2, 3])
theta_translation = 0.5
T_translation = matrix_exp_se3(S_translation, theta_translation)

print(f"S = {S_translation}, theta = {theta_translation}")
print("T =")
print(T_translation)


# Test Example 2: Rotation about z-axis with translation
print("\n" + "=" * 70)
print("SE(3) Exponential Map - Test 2: Rotation + Translation")
print("=" * 70)

S_screw = np.array([0, 0, 1, 1, 0, 0])  # omega = z-axis, v = x-axis

theta_screw = np.pi / 4  # 45 degrees
T_screw = matrix_exp_se3(S_screw, theta_screw)

print(f"S = {S_screw}, theta = {theta_screw:.4f} rad")
print("T =")
print(T_screw)

# Verify rotation part is orthogonal
print(f"\nRotation det(R) = {np.linalg.det(T_screw[:3, :3]):.6f}")
print(f"Rotation orthogonal? {np.allclose(T_screw[:3, :3] @ T_screw[:3, :3].T, np.eye(3))}")
======================================================================
SE(3) Exponential Map - Test 1: Pure Translation
======================================================================
S = [0 0 0 1 2 3], theta = 0.5
T =
[[1.  0.  0.  0.5]
 [0.  1.  0.  1. ]
 [0.  0.  1.  1.5]
 [0.  0.  0.  1. ]]

======================================================================
SE(3) Exponential Map - Test 2: Rotation + Translation
======================================================================
S = [0 0 1 1 0 0], theta = 0.7854 rad
T =
[[ 0.70710678 -0.70710678  0.          0.70710678]
 [ 0.70710678  0.70710678  0.          0.29289322]
 [ 0.          0.          1.          0.        ]
 [ 0.          0.          0.          1.        ]]

Rotation det(R) = 1.000000
Rotation orthogonal? True
  • Logarithm Map: [S]θ=log(T)[\mathcal{S}]\theta = \log(\mathbf{T})

    • Input: Homogeneous transformation T

    • Output: expoential coordinates [S]θ[\mathcal{S}]\theta

The following code is based on the algorithm on page 106 in Modern Robotics textbook.

def matrix_log_se3(T):
    """
    Logarithm map from SE(3) to se(3) using the Modern Robotics algorithm.

    Given T = [[R, p], [0, 1]], find theta in [0, pi] and screw axis S = (S_omega, S_v)
    such that exp([S] theta) = T.

    Algorithm:
    (a) If R = I: set S_omega = 0, S_v = p / ||p||, and theta = ||p||.
    (b) Otherwise: use log on SO(3) to get S_omega (unit) and theta, then
        S_v = G_inv(theta) * p
        G_inv(theta) = (1/theta) I - (1/2) [S_omega] + (1/theta - 1/2 cot(theta/2)) [S_omega]^2

    Parameters:
    T: 4x4 homogeneous transformation matrix (numpy array)

    Returns:
    S_omega: 3D unit rotation axis (or zero vector for pure translation)
    S_v: 3D linear velocity component of screw axis
    theta: scalar magnitude
    S_theta: 6D exponential coordinates = [S_omega, S_v] * theta
    """
    T = np.array(T)
    if T.shape != (4, 4):
        raise ValueError("Input T must be a 4x4 homogeneous transformation matrix")

    R = T[:3, :3]
    p = T[:3, 3]

    # Case (a): Pure translation (R = I)
    if np.allclose(R, np.eye(3), atol=1e-6):
        theta = np.linalg.norm(p)
        if theta < 1e-10:
            # Identity transform
            S_omega = np.zeros(3)
            S_v = np.zeros(3)
            S_theta = np.zeros(6)
            return S_omega, S_v, theta, S_theta

        S_omega = np.zeros(3)
        S_v = p / theta
        S_theta = np.hstack([S_omega, S_v]) * theta
        return S_omega, S_v, theta, S_theta

    # Case (b): General case with rotation
    omega_hat_skew, theta, omega_hat = matrix_log_so3(R)
    S_omega = omega_hat

    # Compute G_inv(theta)
    I = np.eye(3)
    half_theta = 0.5 * theta
    cot_half_theta = np.cos(half_theta) / np.sin(half_theta)

    G_inv = (
        (1.0 / theta) * I
        - 0.5 * omega_hat_skew
        + (1.0 / theta - 0.5 * cot_half_theta) * (omega_hat_skew @ omega_hat_skew)
    )

    S_v = G_inv @ p
    S_theta = np.hstack([S_omega, S_v]) * theta

    return S_omega, S_v, theta, S_theta


# Test Example 1: Pure translation
print("=" * 70)
print("SE(3) Logarithm Map - Test 1: Pure Translation")
print("=" * 70)

T_translation = np.eye(4)
T_translation[:3, 3] = np.array([1.0, 2.0, 3.0])

S_omega, S_v, theta, S_theta = matrix_log_se3(T_translation)
print("T =")
print(T_translation)
print(f"S_omega = {S_omega}")
print(f"S_v = {S_v}")
print(f"theta = {theta:.6f}")
print(f"S_theta = {S_theta}")


# Test Example 2: Rotation + translation (round-trip check)
print("\n" + "=" * 70)
print("SE(3) Logarithm Map - Test 2: Rotation + Translation")
print("=" * 70)

S_test = np.array([0, 0, 1, 1, 0, 0])
theta_test = np.pi / 4
T_test = matrix_exp_se3(S_test, theta_test)

S_omega2, S_v2, theta2, S_theta2 = matrix_log_se3(T_test)
print("T =")
print(T_test)
print(f"S_omega = {S_omega2}")
print(f"S_v = {S_v2}")
print(f"theta = {theta2:.6f}")
print(f"S_theta = {S_theta2}")

# Reconstruct T from recovered screw axis
T_recovered = matrix_exp_se3(np.hstack([S_omega2, S_v2]), theta2)
print(f"\nRecovered T matches original? {np.allclose(T_test, T_recovered)}")
======================================================================
SE(3) Logarithm Map - Test 1: Pure Translation
======================================================================
T =
[[1. 0. 0. 1.]
 [0. 1. 0. 2.]
 [0. 0. 1. 3.]
 [0. 0. 0. 1.]]
S_omega = [0. 0. 0.]
S_v = [0.26726124 0.53452248 0.80178373]
theta = 3.741657
S_theta = [0. 0. 0. 1. 2. 3.]

======================================================================
SE(3) Logarithm Map - Test 2: Rotation + Translation
======================================================================
Case (c): General case
  θ = arccos((tr(R)-1)/2) = 0.785398 rad = 45.00°
  ω = [0. 0. 1.]
T =
[[ 0.70710678 -0.70710678  0.          0.70710678]
 [ 0.70710678  0.70710678  0.          0.29289322]
 [ 0.          0.          1.          0.        ]
 [ 0.          0.          0.          1.        ]]
S_omega = [0. 0. 1.]
S_v = [1.00000000e+00 2.29934717e-17 0.00000000e+00]
theta = 0.785398
S_theta = [0.00000000e+00 0.00000000e+00 7.85398163e-01 7.85398163e-01
 1.80590304e-17 0.00000000e+00]

Recovered T matches original? True

HW Problems

Now that you’ve learned the basics, practice the following problems. Work through them step by step, using the debugging techniques you’ve learned. You need to submit your solutions to these HW problems with your source code.

Problem 1: Basic Python Syntax and Control Flow

In this problem, you will analyze material properties for design selection.

Task: Write a program that:

  1. Creates a dictionary of materials with their properties (density, yield strength, cost per kg)

  2. Finds the material with the highest strength-to-weight ratio

  3. Determines which materials are suitable for a given application (yield strength > 200 MPa, cost < $10/kg)

  4. Uses proper error handling for invalid inputs

Given data:

  • Steel: density=7850 kg/m³, yield=250 MPa, cost=$2/kg

  • Aluminum: density=2700 kg/m³, yield=70 MPa, cost=$8/kg

  • Titanium: density=4500 kg/m³, yield=880 MPa, cost=$30/kg

  • Carbon Fiber: density=1600 kg/m³, yield=600 MPa, cost=$50/kg

# Problem 1 Solution Space
# Write your solution here

# Hint: Start by creating the materials dictionary
materials = {
    # Fill in the material properties
}

# Your code here...

Problem 2: NumPy Arrays and Mathematical Operations

In this problem, you will analyze experimental strain gauge data and plot them.

Task: Given strain measurements from 5 gauges over 10 time steps:

  1. Create a 2D NumPy array (10 time steps × 5 gauges) with realistic strain data

  2. Calculate the maximum, minimum, and average strain for each gauge

  3. Find the time step where the maximum overall strain occurred

  4. Calculate the stress using σ = E × ε (E = 200 GPa for steel)

  5. Plot strain vs time for all gauges on the same plot with different colors

  6. Add proper labels, legend, and title

Hints:

  • Use np.random.normal() to generate realistic strain data around 0.001 (1000 microstrain)

  • Use np.argmax() to find indices of maximum values

  • Remember to convert units properly (strain is dimensionless, stress in Pa)

# Problem 2 Solution Space
import numpy as np
import matplotlib.pyplot as plt

# Create time array
time = np.linspace(0, 10, 10)  # 10 time steps from 0 to 10 seconds

# Generate strain data (your code here)
# Hint: strain_data = np.random.normal(loc=0.001, scale=0.0002, size=(10, 5))

# Your analysis code here...

Problem 3: Matrix Exponential Practice (Task 5)

Use the functions from Task 5 to compute matrix exponentials. Keep the calculations simple and verify results.

  1. Rotation Matrix (SO(3))

    • Given: Unit rotation axis ω^=114[1,2,3]\hat{\omega} = \dfrac{1}{\sqrt{14}}[1, 2, 3] with angle θ=π/2\theta = \pi/2.

    • Step 1: Compute the rotation matrix RR using matrix_exp_so3_from_vector (exponential map).

    • Step 2: Verify that RR is a valid rotation matrix (check det(R)=1\det(R) = 1 and RRT=IR R^T = I).

    • Step 3: Apply the logarithm map using matrix_log_so3 to recover ω^\hat{\omega} and θ\theta.

    • Step 4: Verify that the recovered rotation axis and angle match the original values.

  2. Homogeneous Transformation (SE(3))

    • Given: Screw axis S=[13,13,13,2,1,0.5]\mathcal{S} = \left[\dfrac{1}{\sqrt{3}}, \dfrac{1}{\sqrt{3}}, \dfrac{1}{\sqrt{3}}, 2, -1, 0.5\right] with θ=π/3\theta = \pi/3.

    • Step 1: Compute the transformation matrix TT using matrix_exp_se3 (exponential map).

    • Step 2: Verify that the rotation part is orthogonal and print the translation vector.

    • Step 3: Apply the logarithm map using matrix_log_se3 to recover S\mathcal{S} and θ\theta.

    • Step 4: Verify that the recovered screw axis and angle match the original values (compute Sθ\mathcal{S} \cdot \theta from the recovered values).