Theory Notebook
Converted from
theory.ipynbfor web reading.
Geodesics
Geodesics define geometry-aware motion, interpolation, distance, and convexity on curved spaces used by latent models, embeddings, robotics, and optimization.
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: Straightest vs shortest paths
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 5
header("Demo 1 - Straightest vs shortest paths: 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: Great circles on spheres
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 7
header("Demo 2 - Great circles on spheres: 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: Why interpolation in latent space may be curved
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 9
header("Demo 3 - Why interpolation in latent space may be curved: 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: Energy minimization and path length
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 11
header("Demo 4 - Energy minimization and path length: 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: Geodesics as geometry-aware motion
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 13
header("Demo 5 - Geodesics as geometry-aware motion: 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: Curves on manifolds
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 15
header("Demo 6 - Curves $\\gamma(t)$ on manifolds: 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: Velocity field
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 17
header("Demo 7 - Velocity field $\\dot{\\gamma}(t)\\in T_{\\gamma(t)}M$: 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: Geodesic equation
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 19
header("Demo 8 - Geodesic equation $\\nabla_{\\dot{\\gamma}}\\dot{\\gamma}=0$: 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: Exponential map
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 21
header("Demo 9 - Exponential map $\\operatorname{Exp}_p(\\mathbf{v})$: 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: Logarithm map and injectivity-radius preview
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 23
header("Demo 10 - Logarithm map and injectivity-radius preview: 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: Christoffel symbols in coordinates
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 25
header("Demo 11 - Christoffel symbols in coordinates: 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: Geodesics on the sphere
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 27
header("Demo 12 - Geodesics on the sphere: 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: Geodesic distance and metric completeness
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 29
header("Demo 13 - Geodesic distance and metric completeness: 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: Parallel transport preview
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 31
header("Demo 14 - Parallel transport preview: 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: Geodesic convexity preview
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 33
header("Demo 15 - Geodesic convexity 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: Latent interpolation and representation paths
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 35
header("Demo 16 - Latent interpolation and representation paths: 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: Hyperbolic geodesics for tree-like data
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 37
header("Demo 17 - Hyperbolic geodesics for tree-like data: 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: Shortest paths on learned manifolds
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 39
header("Demo 18 - Shortest paths on learned manifolds: 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: Geodesic loss functions
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 41
header("Demo 19 - Geodesic loss functions: 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: Trajectory planning and robotics
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 43
header("Demo 20 - Trajectory planning and robotics: 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: Straightest vs shortest paths
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 45
header("Demo 21 - Straightest vs shortest paths: 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: Great circles on spheres
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 47
header("Demo 22 - Great circles on spheres: 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: Why interpolation in latent space may be curved
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 49
header("Demo 23 - Why interpolation in latent space may be curved: 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: Energy minimization and path length
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 51
header("Demo 24 - Energy minimization and path length: 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: Geodesics as geometry-aware motion
This demo turns the approved TOC concept into a small executable geometric calculation.
Code cell 53
header("Demo 25 - Geodesics as geometry-aware motion: 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")