Lab
Visualizing Quadratic Forms in \(\mathbb{R}^2\) and \(\mathbb{R}^3\)
This lab explores quadratic forms and their geometric interpretations in 2D and 3D spaces. Quadratic forms are fundamental mathematical structures that appear in many contexts, including linear algebra, optimization, statistics, and computer graphics. In this lab, we will:
- Understand the connection between symmetric matrices and quadratic forms
- Visualize conic sections in \(\mathbb{R}^2\) as level sets of quadratic forms
- Apply the Spectral Theorem to analyze and transform quadratic forms
- Extend these concepts to quadratic surfaces in \(\mathbb{R}^3\)
This lab will help solidify concepts from our Linear Algebra course, particularly:
- Symmetric matrices and their properties
- Eigenvalues and eigenvectors
- The Spectral Theorem
- Linear transformations and change of basis
Required Libraries
import numpy as np
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import sympy as sp
from IPython.display import display, Markdown
Part 1: Quadratic Forms in \(\mathbb{R}^2\)
A quadratic form in \(\mathbb{R}^2\) can be written as:
\(q(\mathbf{x}) = \mathbf{x}^T A \mathbf{x}\)
where \(\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix}\) and \(A\) is a \(2 \times 2\) symmetric matrix.
When expanded, this gives:
\[q(x) = a_{11}x_1^2 + 2a_{12}x_1x_2 + a_{22}x_2^2\]
Make sure you understand the following helper functions:
- The first function creates a random rotation, which is easy in \(\mathbb{R}^2\) and harder in \(\mathbb{R}^3\), and it uses the eigenvalues and the spectral theorem to create a \(2\times 2\) symmetric matrix.
- The second function looks at the eigenvalues to determine the conic type
- The last function returns the quadratic forms in sympy for easy visualization
def create_symmetric_matrix_2d(eigenvalues, random_seed=42):
"""Create a 2×2 symmetric matrix with specified eigenvalues and random orientation"""
np.random.seed(random_seed)
# Create a random rotation matrix (for eigenvectors)
= np.random.uniform(0, 2*np.pi)
theta = np.array([
Q -np.sin(theta)],
[np.cos(theta),
[np.sin(theta), np.cos(theta)]
])
# Create the diagonal matrix of eigenvalues
= np.diag(eigenvalues)
D
# Apply the spectral theorem formula: A = QDQ^T
= Q @ D @ Q.T
A
return A, Q, D, theta
def get_conic_type(eigenvalues):
"""Determine the type of conic section based on eigenvalue signs"""
= sum(1 for ev in eigenvalues if ev > 1e-10)
pos_count = sum(1 for ev in eigenvalues if ev < -1e-10)
neg_count = sum(1 for ev in eigenvalues if abs(ev) <= 1e-10)
zero_count
if pos_count == 2:
return "ellipse"
elif pos_count == 1 and neg_count == 1:
return "hyperbola"
elif pos_count == 1 and zero_count == 1:
return "(degenerate as two parallel lines)"
elif neg_count == 2:
return "empty set (imaginary ellipse)"
else:
return "unknown"
def display_equation(A, c=1):
"""Display the equation of the conic in both original and canonical form"""
# Define symbolic variables
= sp.symbols('x y')
x, y
# Extract matrix elements
= A[0, 0], A[0, 1], A[1, 1]
a, b, d
# Create the quadratic form expression
= a*x**2 + 2*b*x*y + d*y**2
quadratic_form
# Get eigenvalues for canonical form
= np.linalg.eigvalsh(A)
eigenvalues
# Create canonical form expression
= sp.symbols("x' y'")
x_prime, y_prime = eigenvalues[0] * x_prime**2 + eigenvalues[1] * y_prime**2
canonical_form
# Return forms for display
return (quadratic_form,canonical_form)
Graph the conic sections
This function graphs the quadratic forms and you don’t need to understand what it does, but you can.
def plot_conic(A, c=1, title=None, grid_size=2, num_points=1000):
"""Plot the conic section \mathbf{x}^T A \mathbf{x} = c"""
# Extract matrix elements
= A[0, 0], A[0, 1], A[1, 1]
a, b, d
# Get eigenvalues and eigenvectors to determine conic type
= np.linalg.eigh(A)
eigenvalues, eigenvectors = get_conic_type(eigenvalues)
conic_type
# Create a grid of points
= np.linspace(-grid_size, grid_size, num_points)
x = np.linspace(-grid_size, grid_size, num_points)
y = np.meshgrid(x, y)
X, Y
# Calculate the quadratic form
= a*X**2 + 2*b*X*Y + d*Y**2
Z
# Create figure
= plt.subplots(figsize=(10, 8))
fig, ax
# Plot the conic section
=[c], colors=['blue'], linewidths=3)
ax.contour(X, Y, Z, levels
# Draw the eigenvectors (principal axes)
= ['red', 'green']
colors for i in range(2):
# Scale eigenvector for better visualization
= 1.0 / np.sqrt(abs(eigenvalues[i]) if abs(eigenvalues[i]) > 1e-10 else 1)
scale = eigenvectors[:, i] * scale
vector
= "+" if eigenvalues[i] > 0 else ("-" if eigenvalues[i] < 0 else "0")
eigenvalue_sign
0, 0, vector[0], vector[1],
ax.arrow(=0.1, head_length=0.1,
head_width=colors[i], ec=colors[i],
fc=True)
length_includes_head
0]*1.1, vector[1]*1.1,
ax.text(vector[f"λ = {eigenvalues[i]:.2f} {eigenvalue_sign}",
=colors[i])
color
# Add a marker at the origin
0, 0, 'ko', markersize=8)
ax.plot(
# Set labels and title
'X')
ax.set_xlabel('Y')
ax.set_ylabel(if not title:
= f"{conic_type.replace('_', ' ').title()}"
title
ax.set_title(title)
# Set equal aspect ratio
'equal')
ax.set_aspect(
# Set grid and limits
True, alpha=0.3)
ax.grid(-grid_size, grid_size)
ax.set_xlim(-grid_size, grid_size)
ax.set_ylim(
return fig, ax
Exercise 1: Create Examples
The signs and values of the eigenvalues determine the type of conic graph:
Eigenvalue Signs | Surface Type |
---|---|
All positive | Ellipse |
One positive and one negative | Hyperbola |
One positive and one zero | Two parallel lines |
Two negatives | Empty set |
Change the eigenvalues to obtain different shapes
# Create examples by changing the eigenvalues
= [4, 1]
eigenvalues = create_symmetric_matrix_2d(eigenvalues, random_seed=42)
A, Q, D, theta
print("Symmetric Matrix A:")
print(A)
print("\nEigenvectors Q (columns):")
print(Q)
print("\nEigenvalues D:")
print(D)
print(f"\nRotation angle: {np.degrees(theta):.2f} degrees")
# Verify spectral decomposition
print("\nVerify A = QDQ^T:")
print(np.round(Q @ D @ Q.T, 10))
# Plot
= plot_conic(A)
fig, ax
# Display the equation
= display_equation(A)
original, canonical #
f"""Original form: ${sp.latex(original)} = 1$"""))
display(Markdown(f"""Canonical form: ${sp.latex(canonical)} = 1$""")) display(Markdown(
Exercise 2:
- Use the functions to plot the quadratic function \(x^2+xy+y^2=1\). What is the rotation angle?
- Create a quadratic form in \(\mathbb{R}^2\) representing a hyperbola with axes aligned to the coordinate axes. How would you modify the matrix to rotate the hyperbola by 45 degrees counter clockwise?
Part 2: Quadratic Forms in \(\mathbb{R}^3\)
We can extend these concepts to 3D space, where quadratic forms create quadric surfaces.
A quadratic form in \(\mathbb{R}^3\) is: \(q(\mathbf{x}) = \mathbf{x}^T A \mathbf{x}\)
where \(\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix}\) and \(A\) is a \(3 \times 3\) symmetric matrix.
Let’s create similar helper functions for 3D.
- Notice how the random orthogonal matrix is created. First by creating a \(3\times 3\) random matrix with independent normally distributed values and then applying the QR decomposition.
- The second and third functions are similar to the ones in \(\mathbb{R}^2\).
- The fourth function simply applies the Spectral Theorem
# Helper functions
def create_symmetric_matrix_3d(eigenvalues, random_seed=42):
"""Create a 3×3 symmetric matrix with specified eigenvalues and random orientation"""
np.random.seed(random_seed)
# Create a random orthogonal matrix (for eigenvectors)
# Using QR decomposition of a random matrix
= np.random.randn(3, 3)
H = np.linalg.qr(H)
Q, R
# Make sure Q is orthogonal (det(Q) = 1)
if np.linalg.det(Q) < 0:
0] = -Q[:, 0]
Q[:,
# Create the diagonal matrix of eigenvalues
= np.diag(eigenvalues)
D
# Apply the spectral theorem formula: A = QDQ^T
= Q @ D @ Q.T
A
return A, Q, D
def get_surface_type(eigenvalues):
"""Determine the type of quadratic surface based on eigenvalue signs"""
= sum(1 for ev in eigenvalues if ev > 1e-10)
pos_count = sum(1 for ev in eigenvalues if ev < -1e-10)
neg_count = sum(1 for ev in eigenvalues if abs(ev) <= 1e-10)
zero_count
if pos_count == 3:
return "ellipsoid"
elif pos_count == 2 and neg_count == 1:
return "hyperboloid_one_sheet"
elif pos_count == 1 and neg_count == 2:
return "hyperboloid_two_sheets"
elif pos_count == 2 and zero_count == 1:
return "elliptic_cylinder"
elif pos_count == 1 and neg_count == 1 and zero_count == 1:
return "hyperbolic_cylinder"
elif pos_count == 1 and zero_count == 2:
return "parabolic_cylinder"
else:
return "unknown"
def display_3d_equation(A, c=1):
"""Display the equation of the quadric in both original and canonical form"""
# Define symbolic variables
= sp.symbols('x y z')
x, y, z
# Extract matrix elements
= A[0,0], A[0,1], A[0,2], A[1,1], A[1,2], A[2,2]
a, b, c_val, d, e, f
# Create the quadratic form expression
= a*x**2 + 2*b*x*y + 2*c_val*x*z + d*y**2 + 2*e*y*z + f*z**2
quadratic_form
# Get eigenvalues for canonical form
= np.linalg.eigvalsh(A)
eigenvalues
# Create canonical form expression
= sp.symbols("x' y' z'")
x_prime, y_prime, z_prime = eigenvalues[0] * x_prime**2 + eigenvalues[1] * y_prime**2 + eigenvalues[2] * z_prime**2
canonical_form
# Display the equations
return (quadratic_form,canonical_form)
def apply_spectral_theorem(A):
"""Apply the spectral theorem to decompose the matrix"""
= np.linalg.eigh(A)
eigenvalues, eigenvectors return eigenvalues, eigenvectors
Graph the surfaces in plotly
These are complicated functions that we are just using.
def generate_surface_points(A, c=1, num_points=50, range_scale=2.0):
"""Generate points for the quadratic surface x^T A x = c"""
= apply_spectral_theorem(A)
eigenvalues, eigenvectors = get_surface_type(eigenvalues)
surface_type
if surface_type == "ellipsoid":
# Parametrization in spherical coordinates
= np.linspace(0, 2 * np.pi, num_points)
u = np.linspace(0, np.pi, num_points)
v
# Generate points on unit sphere
= np.outer(np.cos(u), np.sin(v))
x_mesh = np.outer(np.sin(u), np.sin(v))
y_mesh = np.outer(np.ones_like(u), np.cos(v))
z_mesh
# Transform and scale
= np.zeros((num_points, num_points, 3))
points for i in range(num_points):
for j in range(num_points):
# Unit sphere point
= np.array([x_mesh[i, j], y_mesh[i, j], z_mesh[i, j]])
point # Scale by 1/sqrt(eigenvalues)
= point / np.sqrt(np.abs(eigenvalues))
scaled_point # Transform to original coordinates
= eigenvectors @ scaled_point * np.sqrt(c)
points[i, j, :]
elif surface_type == "hyperboloid_one_sheet":
# For hyperboloid of one sheet: x²/a² + y²/b² - z²/c² = 1
= np.linspace(0, 2 * np.pi, num_points)
u = np.linspace(-range_scale, range_scale, num_points)
v
= np.meshgrid(u, v)
U, V
# Find which eigenvalue is negative
= np.argmin(eigenvalues)
neg_index = [i for i in range(3) if i != neg_index]
pos_indices
# Create array mapping eigen-indices to parameter equations
= np.zeros((num_points, num_points, 3))
param_array
# Set the two positive eigenvalue dimensions
0]] = np.sqrt(c / eigenvalues[pos_indices[0]]) * np.cos(U) * np.cosh(V)
param_array[:, :, pos_indices[1]] = np.sqrt(c / eigenvalues[pos_indices[1]]) * np.sin(U) * np.cosh(V)
param_array[:, :, pos_indices[
# Set the negative eigenvalue dimension
= np.sqrt(c / -eigenvalues[neg_index]) * np.sinh(V)
param_array[:, :, neg_index]
# Transform back to original coordinates
= np.zeros((num_points, num_points, 3))
points for i in range(num_points):
for j in range(num_points):
= eigenvectors @ param_array[i, j, :]
points[i, j, :]
elif surface_type == "hyperboloid_two_sheets":
# For hyperboloid of two sheets: x²/a² - y²/b² - z²/c² = 1
= np.linspace(0, 2 * np.pi, num_points)
u = np.linspace(0.1, range_scale, num_points) # Start slightly above 0 to avoid singularity
v
= np.meshgrid(u, v)
U, V
# Find which eigenvalue is positive
= np.argmax(eigenvalues)
pos_index = [i for i in range(3) if i != pos_index]
neg_indices
# Create points for both sheets
= np.zeros((2, num_points, num_points, 3))
points
for sheet in range(2):
= 1 if sheet == 0 else -1
sign
# Create array mapping eigen-indices to parameter equations
= np.zeros((num_points, num_points, 3))
param_array
# Set the positive eigenvalue dimension (the axis of the hyperboloid)
= sign * np.sqrt(c / eigenvalues[pos_index]) * np.cosh(V)
param_array[:, :, pos_index]
# Set the negative eigenvalue dimensions
0]] = np.sqrt(c / -eigenvalues[neg_indices[0]]) * np.cos(U) * np.sinh(V)
param_array[:, :, neg_indices[1]] = np.sqrt(c / -eigenvalues[neg_indices[1]]) * np.sin(U) * np.sinh(V)
param_array[:, :, neg_indices[
# Transform back to original coordinates
for i in range(num_points):
for j in range(num_points):
= eigenvectors @ param_array[i, j, :]
points[sheet, i, j, :]
elif surface_type == "elliptic_cylinder":
# Find which eigenvalue is zero (or close to zero)
= np.argmin(np.abs(eigenvalues))
zero_index = [i for i in range(3) if i != zero_index]
nonzero_indices
# The cylinder extends along the eigenvector of the zero eigenvalue
# We'll parametrize as a cylinder with elliptical cross-section
= np.linspace(0, 2 * np.pi, num_points)
u = np.linspace(-range_scale, range_scale, num_points)
v
= np.meshgrid(u, v)
U, V
# Create array mapping eigen-indices to parameter equations
= np.zeros((num_points, num_points, 3))
param_array
# Set the nonzero eigenvalue dimensions (the elliptical cross-section)
0]] = np.sqrt(c / eigenvalues[nonzero_indices[0]]) * np.cos(U)
param_array[:, :, nonzero_indices[1]] = np.sqrt(c / eigenvalues[nonzero_indices[1]]) * np.sin(U)
param_array[:, :, nonzero_indices[
# Set the zero eigenvalue dimension (the axis of the cylinder)
= V
param_array[:, :, zero_index]
# Transform back to original coordinates
= np.zeros((num_points, num_points, 3))
points for i in range(num_points):
for j in range(num_points):
= eigenvectors @ param_array[i, j, :]
points[i, j, :]
elif surface_type == "hyperbolic_cylinder":
# Find indices of positive, negative, and zero eigenvalues
= np.argmax(eigenvalues)
pos_index = np.argmin(eigenvalues)
neg_index = 3 - pos_index - neg_index # The remaining index
zero_index
if abs(eigenvalues[zero_index]) > 1e-10: # Check it's actually zero
= neg_index if abs(eigenvalues[neg_index]) < abs(eigenvalues[pos_index]) else pos_index
zero_index = 0 if zero_index != 0 and pos_index != 0 else 1 if zero_index != 1 and pos_index != 1 else 2
neg_index = 3 - zero_index - neg_index
pos_index
# The cylinder extends along the eigenvector of the zero eigenvalue
= np.linspace(-range_scale, range_scale, num_points)
u = np.linspace(-range_scale, range_scale, num_points)
v
= np.meshgrid(u, v)
U, V
# Create points for both sheets of the hyperbola
= np.zeros((2, num_points, num_points, 3))
points
for sheet in range(2):
= 1 if sheet == 0 else -1
sign
# Create array mapping eigen-indices to parameter equations
= np.zeros((num_points, num_points, 3))
param_array
# Set the positive eigenvalue dimension
= sign * np.sqrt(c / eigenvalues[pos_index]) * np.cosh(U)
param_array[:, :, pos_index]
# Set the negative eigenvalue dimension
= np.sqrt(c / -eigenvalues[neg_index]) * np.sinh(U)
param_array[:, :, neg_index]
# Set the zero eigenvalue dimension (the axis of the cylinder)
= V
param_array[:, :, zero_index]
# Transform back to original coordinates
for i in range(num_points):
for j in range(num_points):
= eigenvectors @ param_array[i, j, :]
points[sheet, i, j, :]
else:
# For other surfaces or unknown types, return a placeholder
= None
points
return points, eigenvalues, eigenvectors, surface_type
def plot_quadratic_surface(A, c=1, title=None):
"""Plot the quadratic surface x^T A x = c"""
= generate_surface_points(A, c)
points, eigenvalues, eigenvectors, surface_type
if points is None:
print(f"Surface type '{surface_type}' visualization not implemented")
return None
= go.Figure()
fig
if surface_type == "ellipsoid":
= points[:, :, 0]
x = points[:, :, 1]
y = points[:, :, 2]
z
fig.add_trace(go.Surface(=x, y=y, z=z,
x='Viridis',
colorscale=0.8,
opacity=False
showscale
))
elif surface_type == "hyperboloid_one_sheet":
= points[:, :, 0]
x = points[:, :, 1]
y = points[:, :, 2]
z
fig.add_trace(go.Surface(=x, y=y, z=z,
x='Plasma',
colorscale=0.8,
opacity=False
showscale
))
elif surface_type == "hyperboloid_two_sheets":
# Add both sheets
for sheet in range(2):
= points[sheet, :, :, 0]
x = points[sheet, :, :, 1]
y = points[sheet, :, :, 2]
z
fig.add_trace(go.Surface(=x, y=y, z=z,
x='Cividis',
colorscale=0.8,
opacity=False
showscale
))
elif surface_type == "elliptic_cylinder":
= points[:, :, 0]
x = points[:, :, 1]
y = points[:, :, 2]
z
fig.add_trace(go.Surface(=x, y=y, z=z,
x='Blues',
colorscale=0.8,
opacity=False
showscale
))
elif surface_type == "hyperbolic_cylinder":
# Add both sheets
for sheet in range(2):
= points[sheet, :, :, 0]
x = points[sheet, :, :, 1]
y = points[sheet, :, :, 2]
z
fig.add_trace(go.Surface(=x, y=y, z=z,
x='Reds',
colorscale=0.8,
opacity=False
showscale
))
# Add principal axes (eigenvectors)
= ['red', 'green', 'blue']
colors for i in range(3):
# Scale eigenvector for visualization
= 2.5 / max(abs(e) for e in eigenvalues if abs(e) > 1e-10)
scale = eigenvectors[:, i] * scale
vector
= "+" if eigenvalues[i] > 1e-10 else ("-" if eigenvalues[i] < -1e-10 else "0")
eigenvalue_sign
fig.add_trace(go.Scatter3d(=[0, vector[0]], y=[0, vector[1]], z=[0, vector[2]],
x='lines+markers',
mode=dict(color=colors[i], width=6),
line=dict(size=5, color=colors[i]),
marker=f"Axis {i+1} (λ = {eigenvalues[i]:.2f} {eigenvalue_sign})"
name
))
# Add a marker at the origin
fig.add_trace(go.Scatter3d(=[0], y=[0], z=[0],
x='markers',
mode=dict(size=8, color='black'),
marker="Origin"
name
))
if not title:
= f"{surface_type.replace('_', ' ').title()}"
title
# Set plot layout
fig.update_layout(=title,
title=dict(
scene='X',
xaxis_title='Y',
yaxis_title='Z',
zaxis_title='data'
aspectmode
),=800,
width=800,
height=True
showlegend
)
return fig
Exercise 3: Create Examples
Types of Quadratic Surfaces
The signs and values of the eigenvalues determine the type of quadratic surface:
Eigenvalue Signs | Surface Type |
---|---|
All positive | Ellipsoid |
Two positive, one negative | Hyperboloid of one sheet |
One positive, two negative | Hyperboloid of two sheets |
Two positive, one zero | Elliptic cylinder |
One positive, one negative, one zero | Hyperbolic cylinder |
One positive, two zero | Two parallel planes |
Change the eigenvalues to generate the different objects
# Create
= [1 , 2, 3]
eigenvalues = create_symmetric_matrix_3d(eigenvalues)
A, Q, D
print("Symmetric Matrix A:")
print(A)
print("\nEigenvectors Q (columns):")
print(Q)
print("\nEigenvalues D:")
print(D)
# Verify spectral decomposition
print("\nVerify A = QDQ^T:")
print(np.round(Q @ D @ Q.T, 10))
# Display the equation
= display_3d_equation(A)
original, canonical #
f"""Original form: ${sp.latex(original)} = 1$"""))
display(Markdown(f"""Canonical form: ${sp.latex(canonical)} = 1$"""))
display(Markdown(
# Plot the ellipsoid using Plotly for interactive visualization
#fig = plot_quadric_surface(A)
= plot_quadratic_surface(A)
fig # In Jupyter, this displays an interactive 3D plot fig.show()
Exercise 4
- Use the functions to plot the quadratic surface \[2x^2+xy+xz+3y^2-yz-z^2=1\]