Theory Notebook
Converted from
theory.ipynbfor web reading.
Manifolds
Manifolds give the language for curved state spaces, latent spaces, symmetry spaces, and locally linear representations in AI.
This notebook is the executable companion to notes.md. It uses spheres, tangent projections, geodesic interpolation, SPD matrices, and orthogonality constraints as concrete geometry laboratories.
Code cell 2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
try:
import seaborn as sns
sns.set_theme(style="whitegrid", palette="colorblind")
HAS_SNS = True
except ImportError:
plt.style.use("seaborn-v0_8-whitegrid")
HAS_SNS = False
mpl.rcParams.update({
"figure.figsize": (10, 6),
"figure.dpi": 120,
"font.size": 13,
"axes.titlesize": 15,
"axes.labelsize": 13,
"xtick.labelsize": 11,
"ytick.labelsize": 11,
"legend.fontsize": 11,
"legend.framealpha": 0.85,
"lines.linewidth": 2.0,
"axes.spines.top": False,
"axes.spines.right": False,
"savefig.bbox": "tight",
"savefig.dpi": 150,
})
np.random.seed(42)
print("Plot setup complete.")
Code cell 3
COLORS = {
"primary": "#0077BB",
"secondary": "#EE7733",
"tertiary": "#009988",
"error": "#CC3311",
"neutral": "#555555",
"highlight": "#EE3377",
}
def header(title):
print("\n" + "=" * 72)
print(title)
print("=" * 72)
def check_true(condition, name):
ok = bool(condition)
print(f"{'PASS' if ok else 'FAIL'} - {name}")
assert ok, name
def check_close(value, target, tol=1e-8, name="value"):
ok = abs(float(value) - float(target)) <= tol
print(f"{'PASS' if ok else 'FAIL'} - {name}: got {float(value):.6f}, expected {float(target):.6f}")
assert ok, name
def normalize(x):
x = np.asarray(x, dtype=float)
n = np.linalg.norm(x)
if n == 0:
raise ValueError("cannot normalize zero vector")
return x / n
def tangent_projection_sphere(x, v):
x = normalize(x)
v = np.asarray(v, dtype=float)
return v - np.dot(x, v) * x
def exp_sphere(x, v):
x = normalize(x)
v = tangent_projection_sphere(x, v)
n = np.linalg.norm(v)
if n < 1e-12:
return x.copy()
return np.cos(n) * x + np.sin(n) * v / n
def retract_sphere(x, v):
return normalize(np.asarray(x, dtype=float) + np.asarray(v, dtype=float))
def slerp(x, y, t):
x = normalize(x)
y = normalize(y)
dot = np.clip(np.dot(x, y), -1.0, 1.0)
theta = np.arccos(dot)
if theta < 1e-12:
return x.copy()
return (np.sin((1 - t) * theta) * x + np.sin(t * theta) * y) / np.sin(theta)
def riemannian_gradient_sphere(x, euclidean_grad):
return tangent_projection_sphere(x, euclidean_grad)
def sphere_descent(target, steps=20, eta=0.3):
target = normalize(target)
x = normalize(np.array([1.0, 0.2, 0.1]))
values = []
for _ in range(steps):
values.append(float(-np.dot(target, x)))
egrad = -target
rgrad = riemannian_gradient_sphere(x, egrad)
x = retract_sphere(x, -eta * rgrad)
values.append(float(-np.dot(target, x)))
return x, np.array(values)
def stiefel_tangent_projection(Q, Z):
Q = np.asarray(Q, dtype=float)
Z = np.asarray(Z, dtype=float)
sym = 0.5 * (Q.T @ Z + Z.T @ Q)
return Z - Q @ sym
def qr_retraction(Y):
Q, R = np.linalg.qr(Y)
signs = np.sign(np.diag(R))
signs[signs == 0] = 1.0
return Q @ np.diag(signs)
def spd_from_eigs(eigs):
Q = np.array([[np.cos(0.4), -np.sin(0.4)], [np.sin(0.4), np.cos(0.4)]])
return Q @ np.diag(eigs) @ Q.T
def mat_log_spd(A):
vals, vecs = np.linalg.eigh(A)
return vecs @ np.diag(np.log(vals)) @ vecs.T
def mat_invsqrt_spd(A):
vals, vecs = np.linalg.eigh(A)
return vecs @ np.diag(1.0 / np.sqrt(vals)) @ vecs.T
def spd_distance(A, B):
C = mat_invsqrt_spd(A) @ B @ mat_invsqrt_spd(A)
return float(np.linalg.norm(mat_log_spd(C), ord="fro"))
print("Differential-geometry helpers ready.")
Demo 1: Why curved spaces need local coordinates
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 5
header("Demo 1 - Why curved spaces need local coordinates: sphere tangent projection")
x = normalize(np.array([1.0, 1.0, 1.0]))
v = np.array([2.0, -1.0, 0.5])
tangent = tangent_projection_sphere(x, v)
print("Point on sphere:", np.round(x, 3).tolist())
print("Projected tangent:", np.round(tangent, 3).tolist())
check_close(np.dot(x, tangent), 0.0, tol=1e-10, name="orthogonal to base point")
Demo 2: The manifold hypothesis in ML
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 7
header("Demo 2 - The manifold hypothesis in ML: local chart for circle")
theta = np.linspace(-1.2, 1.2, 200)
circle = np.column_stack([np.cos(theta), np.sin(theta)])
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(circle[:, 0], circle[:, 1], color=COLORS["primary"], label="chart image")
ax.scatter([1], [0], color=COLORS["highlight"], label="base point")
ax.set_title("Local coordinate patch on the circle")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
print("Chart parameter range:", (float(theta.min()), float(theta.max())))
check_true(circle.shape == (200, 2), "chart maps one coordinate to ambient R^2")
Demo 3: Local Euclidean behavior vs global curvature
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 9
header("Demo 3 - Local Euclidean behavior vs global curvature: spherical geodesic interpolation")
x = normalize(np.array([1.0, 0.0, 0.0]))
y = normalize(np.array([0.0, 1.0, 0.0]))
pts = np.array([slerp(x, y, t) for t in np.linspace(0, 1, 25)])
norms = np.linalg.norm(pts, axis=1)
print("First point:", pts[0].round(3).tolist())
print("Middle point:", pts[len(pts)//2].round(3).tolist())
check_true(np.all(np.abs(norms - 1.0) < 1e-10), "slerp stays on sphere")
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(pts[:, 0], pts[:, 1], color=COLORS["secondary"], marker="o", label="geodesic arc")
ax.set_title("Great-circle interpolation")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 4: Charts atlases and coordinate patches
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 11
header("Demo 4 - Charts atlases and coordinate patches: Riemannian gradient on sphere")
x = normalize(np.array([0.7, 0.2, 0.6]))
target = normalize(np.array([0.0, 1.0, 0.0]))
egrad = -target
rgrad = riemannian_gradient_sphere(x, egrad)
print("Euclidean gradient:", np.round(egrad, 3).tolist())
print("Riemannian gradient:", np.round(rgrad, 3).tolist())
check_close(np.dot(x, rgrad), 0.0, tol=1e-10, name="Riemannian gradient is tangent")
print("Interpretation: steepest descent must live in the tangent space.")
Demo 5: Examples: sphere torus Stiefel Grassmann SPD matrices
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 13
header("Demo 5 - Examples: sphere torus Stiefel Grassmann SPD matrices: sphere gradient descent")
target = np.array([0.0, 1.0, 0.0])
x_final, values = sphere_descent(target, steps=25, eta=0.25)
print("Final point:", np.round(x_final, 3).tolist())
print("Initial objective:", round(values[0], 4))
print("Final objective:", round(values[-1], 4))
check_true(values[-1] < values[0], "objective decreases on sphere")
fig, ax = plt.subplots()
ax.plot(values, color=COLORS["primary"], label="objective")
ax.set_title("Riemannian gradient descent on the sphere")
ax.set_xlabel("Step")
ax.set_ylabel("$-a^T x$")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 6: Topological manifold
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 15
header("Demo 6 - Topological manifold: Stiefel tangent projection")
Q = np.eye(3, 2)
Z = np.array([[0.2, 1.0], [1.5, -0.3], [0.7, 0.4]])
Xi = stiefel_tangent_projection(Q, Z)
constraint = Q.T @ Xi + Xi.T @ Q
print("Tangent constraint matrix:", np.round(constraint, 10))
check_true(np.linalg.norm(constraint) < 1e-10, "Stiefel tangent condition holds")
Y = qr_retraction(Q + 0.2 * Xi)
check_true(np.linalg.norm(Y.T @ Y - np.eye(2)) < 1e-10, "QR retraction returns orthonormal columns")
print("Retracted columns are orthonormal.")
Demo 7: Smooth atlas and smooth compatibility
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 17
header("Demo 7 - Smooth atlas and smooth compatibility: SPD manifold distance")
A = spd_from_eigs([1.0, 3.0])
B = spd_from_eigs([2.0, 5.0])
d_ab = spd_distance(A, B)
d_ba = spd_distance(B, A)
print("Distance A to B:", round(d_ab, 6))
print("Distance B to A:", round(d_ba, 6))
check_close(d_ab, d_ba, tol=1e-10, name="SPD distance symmetry")
check_true(d_ab > 0, "distinct SPD matrices have positive distance")
Demo 8: Smooth manifold
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 19
header("Demo 8 - Smooth manifold $M$: exponential vs retraction")
x = normalize(np.array([1.0, 0.0, 0.0]))
v = tangent_projection_sphere(x, np.array([0.0, 0.2, 0.1]))
exp_point = exp_sphere(x, v)
ret_point = retract_sphere(x, v)
print("Exponential point:", np.round(exp_point, 5).tolist())
print("Retraction point:", np.round(ret_point, 5).tolist())
check_true(abs(np.linalg.norm(exp_point) - 1.0) < 1e-10, "exponential stays on sphere")
check_true(abs(np.linalg.norm(ret_point) - 1.0) < 1e-10, "retraction stays on sphere")
print("Distance between exp and retraction:", round(float(np.linalg.norm(exp_point - ret_point)), 6))
Demo 9: Smooth maps between manifolds
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 21
header("Demo 9 - Smooth maps between manifolds: sphere tangent projection")
x = normalize(np.array([1.0, 1.0, 1.0]))
v = np.array([2.0, -1.0, 0.5])
tangent = tangent_projection_sphere(x, v)
print("Point on sphere:", np.round(x, 3).tolist())
print("Projected tangent:", np.round(tangent, 3).tolist())
check_close(np.dot(x, tangent), 0.0, tol=1e-10, name="orthogonal to base point")
Demo 10: Embedded and immersed submanifolds
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 23
header("Demo 10 - Embedded and immersed submanifolds: local chart for circle")
theta = np.linspace(-1.2, 1.2, 200)
circle = np.column_stack([np.cos(theta), np.sin(theta)])
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(circle[:, 0], circle[:, 1], color=COLORS["primary"], label="chart image")
ax.scatter([1], [0], color=COLORS["highlight"], label="base point")
ax.set_title("Local coordinate patch on the circle")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
print("Chart parameter range:", (float(theta.min()), float(theta.max())))
check_true(circle.shape == (200, 2), "chart maps one coordinate to ambient R^2")
Demo 11: Tangent vectors as velocities of curves
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 25
header("Demo 11 - Tangent vectors as velocities of curves: spherical geodesic interpolation")
x = normalize(np.array([1.0, 0.0, 0.0]))
y = normalize(np.array([0.0, 1.0, 0.0]))
pts = np.array([slerp(x, y, t) for t in np.linspace(0, 1, 25)])
norms = np.linalg.norm(pts, axis=1)
print("First point:", pts[0].round(3).tolist())
print("Middle point:", pts[len(pts)//2].round(3).tolist())
check_true(np.all(np.abs(norms - 1.0) < 1e-10), "slerp stays on sphere")
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(pts[:, 0], pts[:, 1], color=COLORS["secondary"], marker="o", label="geodesic arc")
ax.set_title("Great-circle interpolation")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 12: Tangent spaces
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 27
header("Demo 12 - Tangent spaces $T_pM$: Riemannian gradient on sphere")
x = normalize(np.array([0.7, 0.2, 0.6]))
target = normalize(np.array([0.0, 1.0, 0.0]))
egrad = -target
rgrad = riemannian_gradient_sphere(x, egrad)
print("Euclidean gradient:", np.round(egrad, 3).tolist())
print("Riemannian gradient:", np.round(rgrad, 3).tolist())
check_close(np.dot(x, rgrad), 0.0, tol=1e-10, name="Riemannian gradient is tangent")
print("Interpretation: steepest descent must live in the tangent space.")
Demo 13: Differentials and pushforwards
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 29
header("Demo 13 - Differentials and pushforwards $dF_p:T_pM\\to T_{F(p)}N$: sphere gradient descent")
target = np.array([0.0, 1.0, 0.0])
x_final, values = sphere_descent(target, steps=25, eta=0.25)
print("Final point:", np.round(x_final, 3).tolist())
print("Initial objective:", round(values[0], 4))
print("Final objective:", round(values[-1], 4))
check_true(values[-1] < values[0], "objective decreases on sphere")
fig, ax = plt.subplots()
ax.plot(values, color=COLORS["primary"], label="objective")
ax.set_title("Riemannian gradient descent on the sphere")
ax.set_xlabel("Step")
ax.set_ylabel("$-a^T x$")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 14: Tangent bundle
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 31
header("Demo 14 - Tangent bundle $TM$: Stiefel tangent projection")
Q = np.eye(3, 2)
Z = np.array([[0.2, 1.0], [1.5, -0.3], [0.7, 0.4]])
Xi = stiefel_tangent_projection(Q, Z)
constraint = Q.T @ Xi + Xi.T @ Q
print("Tangent constraint matrix:", np.round(constraint, 10))
check_true(np.linalg.norm(constraint) < 1e-10, "Stiefel tangent condition holds")
Y = qr_retraction(Q + 0.2 * Xi)
check_true(np.linalg.norm(Y.T @ Y - np.eye(2)) < 1e-10, "QR retraction returns orthonormal columns")
print("Retracted columns are orthonormal.")
Demo 15: Vector fields and flows preview
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 33
header("Demo 15 - Vector fields and flows preview: SPD manifold distance")
A = spd_from_eigs([1.0, 3.0])
B = spd_from_eigs([2.0, 5.0])
d_ab = spd_distance(A, B)
d_ba = spd_distance(B, A)
print("Distance A to B:", round(d_ab, 6))
print("Distance B to A:", round(d_ba, 6))
check_close(d_ab, d_ba, tol=1e-10, name="SPD distance symmetry")
check_true(d_ab > 0, "distinct SPD matrices have positive distance")
Demo 16: Data manifolds and representation learning
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 35
header("Demo 16 - Data manifolds and representation learning: exponential vs retraction")
x = normalize(np.array([1.0, 0.0, 0.0]))
v = tangent_projection_sphere(x, np.array([0.0, 0.2, 0.1]))
exp_point = exp_sphere(x, v)
ret_point = retract_sphere(x, v)
print("Exponential point:", np.round(exp_point, 5).tolist())
print("Retraction point:", np.round(ret_point, 5).tolist())
check_true(abs(np.linalg.norm(exp_point) - 1.0) < 1e-10, "exponential stays on sphere")
check_true(abs(np.linalg.norm(ret_point) - 1.0) < 1e-10, "retraction stays on sphere")
print("Distance between exp and retraction:", round(float(np.linalg.norm(exp_point - ret_point)), 6))
Demo 17: Latent spaces in VAEs and diffusion models
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 37
header("Demo 17 - Latent spaces in VAEs and diffusion models: sphere tangent projection")
x = normalize(np.array([1.0, 1.0, 1.0]))
v = np.array([2.0, -1.0, 0.5])
tangent = tangent_projection_sphere(x, v)
print("Point on sphere:", np.round(x, 3).tolist())
print("Projected tangent:", np.round(tangent, 3).tolist())
check_close(np.dot(x, tangent), 0.0, tol=1e-10, name="orthogonal to base point")
Demo 18: Embedding manifolds and local linearization
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 39
header("Demo 18 - Embedding manifolds and local linearization: local chart for circle")
theta = np.linspace(-1.2, 1.2, 200)
circle = np.column_stack([np.cos(theta), np.sin(theta)])
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(circle[:, 0], circle[:, 1], color=COLORS["primary"], label="chart image")
ax.scatter([1], [0], color=COLORS["highlight"], label="base point")
ax.set_title("Local coordinate patch on the circle")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
print("Chart parameter range:", (float(theta.min()), float(theta.max())))
check_true(circle.shape == (200, 2), "chart maps one coordinate to ambient R^2")
Demo 19: Symmetry and quotient spaces preview
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 41
header("Demo 19 - Symmetry and quotient spaces preview: spherical geodesic interpolation")
x = normalize(np.array([1.0, 0.0, 0.0]))
y = normalize(np.array([0.0, 1.0, 0.0]))
pts = np.array([slerp(x, y, t) for t in np.linspace(0, 1, 25)])
norms = np.linalg.norm(pts, axis=1)
print("First point:", pts[0].round(3).tolist())
print("Middle point:", pts[len(pts)//2].round(3).tolist())
check_true(np.all(np.abs(norms - 1.0) < 1e-10), "slerp stays on sphere")
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(pts[:, 0], pts[:, 1], color=COLORS["secondary"], marker="o", label="geodesic arc")
ax.set_title("Great-circle interpolation")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.axis("equal")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 20: Manifold learning diagnostics
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 43
header("Demo 20 - Manifold learning diagnostics: Riemannian gradient on sphere")
x = normalize(np.array([0.7, 0.2, 0.6]))
target = normalize(np.array([0.0, 1.0, 0.0]))
egrad = -target
rgrad = riemannian_gradient_sphere(x, egrad)
print("Euclidean gradient:", np.round(egrad, 3).tolist())
print("Riemannian gradient:", np.round(rgrad, 3).tolist())
check_close(np.dot(x, rgrad), 0.0, tol=1e-10, name="Riemannian gradient is tangent")
print("Interpretation: steepest descent must live in the tangent space.")
Demo 21: Why curved spaces need local coordinates
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 45
header("Demo 21 - Why curved spaces need local coordinates: sphere gradient descent")
target = np.array([0.0, 1.0, 0.0])
x_final, values = sphere_descent(target, steps=25, eta=0.25)
print("Final point:", np.round(x_final, 3).tolist())
print("Initial objective:", round(values[0], 4))
print("Final objective:", round(values[-1], 4))
check_true(values[-1] < values[0], "objective decreases on sphere")
fig, ax = plt.subplots()
ax.plot(values, color=COLORS["primary"], label="objective")
ax.set_title("Riemannian gradient descent on the sphere")
ax.set_xlabel("Step")
ax.set_ylabel("$-a^T x$")
ax.legend()
fig.tight_layout()
plt.show()
plt.close(fig)
Demo 22: The manifold hypothesis in ML
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 47
header("Demo 22 - The manifold hypothesis in ML: Stiefel tangent projection")
Q = np.eye(3, 2)
Z = np.array([[0.2, 1.0], [1.5, -0.3], [0.7, 0.4]])
Xi = stiefel_tangent_projection(Q, Z)
constraint = Q.T @ Xi + Xi.T @ Q
print("Tangent constraint matrix:", np.round(constraint, 10))
check_true(np.linalg.norm(constraint) < 1e-10, "Stiefel tangent condition holds")
Y = qr_retraction(Q + 0.2 * Xi)
check_true(np.linalg.norm(Y.T @ Y - np.eye(2)) < 1e-10, "QR retraction returns orthonormal columns")
print("Retracted columns are orthonormal.")
Demo 23: Local Euclidean behavior vs global curvature
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 49
header("Demo 23 - Local Euclidean behavior vs global curvature: SPD manifold distance")
A = spd_from_eigs([1.0, 3.0])
B = spd_from_eigs([2.0, 5.0])
d_ab = spd_distance(A, B)
d_ba = spd_distance(B, A)
print("Distance A to B:", round(d_ab, 6))
print("Distance B to A:", round(d_ba, 6))
check_close(d_ab, d_ba, tol=1e-10, name="SPD distance symmetry")
check_true(d_ab > 0, "distinct SPD matrices have positive distance")
Demo 24: Charts atlases and coordinate patches
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 51
header("Demo 24 - Charts atlases and coordinate patches: exponential vs retraction")
x = normalize(np.array([1.0, 0.0, 0.0]))
v = tangent_projection_sphere(x, np.array([0.0, 0.2, 0.1]))
exp_point = exp_sphere(x, v)
ret_point = retract_sphere(x, v)
print("Exponential point:", np.round(exp_point, 5).tolist())
print("Retraction point:", np.round(ret_point, 5).tolist())
check_true(abs(np.linalg.norm(exp_point) - 1.0) < 1e-10, "exponential stays on sphere")
check_true(abs(np.linalg.norm(ret_point) - 1.0) < 1e-10, "retraction stays on sphere")
print("Distance between exp and retraction:", round(float(np.linalg.norm(exp_point - ret_point)), 6))
Demo 25: Examples: sphere torus Stiefel Grassmann SPD matrices
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 53
header("Demo 25 - Examples: sphere torus Stiefel Grassmann SPD matrices: sphere tangent projection")
x = normalize(np.array([1.0, 1.0, 1.0]))
v = np.array([2.0, -1.0, 0.5])
tangent = tangent_projection_sphere(x, v)
print("Point on sphere:", np.round(x, 3).tolist())
print("Projected tangent:", np.round(tangent, 3).tolist())
check_close(np.dot(x, tangent), 0.0, tol=1e-10, name="orthogonal to base point")