Conic & quadric space — analysis and visualization¶
This page describes the mathematics behind pytanga.quadric: how a 2D conic or a
3D quadric is represented in a projective geometric algebra, how an MV is analyzed
into a concrete entity, and how that entity is turned into geometry the viewer can
draw.
The projective quadric space¶
A conic is the zero set of a symmetric quadratic form in the affine plane, and a quadric the zero set of a symmetric quadratic form in affine 3-space:
Perwass's conic space linearises this: the symmetric matrix becomes a vector,
so conics (and quadrics) live in a linear space that a geometric algebra can act
on. pytanga.quadric uses
BasisQ2— conic spaceCA{6}, a 6-dimensional algebra (Algebra(6, 0)),BasisQ3— quadric spaceCA{10}, a 10-dimensional algebra (Algebra(10, 0)),
(the 6 and 10 are exactly the number of independent symmetric-matrix entries).
Perwass's original bases are non-Euclidean; here they are rescaled to a
Euclidean metric so the plain Algebra(6,0) / Algebra(10,0) product can be
used unchanged. The √2/2 factors that the rescaling introduces appear only in
_embedding.py (point embedding) and _mapping.py (matrix ↔ coefficients); every
other module works with the Euclidean basis b₁…b₆ / b₁…b₁₀.
Representation: symmetric matrix ↔ coefficients¶
_to_coeffs / _from_coeffs are exact inverses and fix the coefficient order:
- conic (3×3):
(a₁₃, a₂₃, (√2/2)a₃₃, (√2/2)a₁₁, (√2/2)a₂₂, a₁₂) - quadric (4×4):
(q₁₄, q₂₄, q₃₄, (√2/2)q₄₄, (√2/2)q₁₁, (√2/2)q₂₂, (√2/2)q₃₃, q₁₂, q₁₃, q₂₃)
Conic / Quadric3D are thin dataclasses over that vector: matrix rebuilds the
symmetric matrix, and the classification properties read the affine block form
[[Q, b], [bᵀ, c]].
Point embedding¶
A point is embedded as the rank-1 outer product \(x\,x^{\mathsf T}\), expressed in the coefficient basis:
- 2D:
x b₁ + y b₂ + (√2/2) b₃ + (√2/2)x² b₄ + (√2/2)y² b₅ + xy b₆ - 3D:
x b₁ + y b₂ + z b₃ + (√2/2) b₄ + (√2/2)x² b₅ + (√2/2)y² b₆ + (√2/2)z² b₇ + xy b₈ + xz b₉ + yz b₁₀
The rescaling makes the basis pairing exact:
so "the point lies on the conic" is the inner product being zero. This is the identity that makes the whole analysis work: a conic's coefficients and a point's embedding are dual, and the incidence relation is a scalar product.
OPNS, IPNS and grade dispatch¶
The same conic/quadric can be represented in two dual ways:
- OPNS — the outer product null space: blades that pass through a point
(the point embedding
embed(x)is a grade-1 OPNS blade), and a point set is the join (wedge) of its points. - IPNS — the inner product null space: the conic/quadric itself is a grade-1 blade (its coefficient vector), and a point is incident when the inner product with it vanishes.
The two are related by duality. analyze_entity normalises IPNS input to OPNS by
dualising first, then dispatches on the blade grade:
| space | OPNS grade | entity |
|---|---|---|
| Q2 (conic) | 1 | Point |
| Q2 | 2, 3, 4 | PointSet |
| Q2 | 5 | Conic |
| Q3 (quadric) | 1 | Point |
| Q3 | 2…7 | PointSet |
| Q3 | 8 | degenerate quadric intersection → PlaneConicPair / Curve |
| Q3 | 9 | Quadric3D |
For Q3 IPNS the dispatch dualises to OPNS first (so grade \(k\) IPNS maps to
grade \(10-k\) OPNS and every grade routes through the same OPNS table above);
e.g. IPNS grade 1 → Quadric3D, grade 2 → the quadric intersection, grade 9 →
Point. Coefficients below the algebra precision are pruned first, so numerical
residue from a versor product (e.g. a rotated quadric carrying ~1e-12 in lower
grades) does not misclassify as a mixed-grade MV.
A PointSet is recovered through the dual/net correspondence: the join of
k points is dual to the \((N-k)\)-dim space of conics/quadrics through them, and
three generic members of that space meet in eight (Q3) / four (Q2) base points,
of which the k join points are the subset lying on all members. For k = 7
in Q3 this yields eight points — the seven originals plus the Cayley–Bacharach
partner of the 3-dim net (see dev/theory/quadric-point-tuples.md).
Classification and refinement¶
Given the matrix \(A = \begin{bmatrix} Q & b \\ b^{\mathsf T} & c\end{bmatrix}\), the entity is classified by rank and inertia, not by solving anything:
- \(\operatorname{rank} A\) gives the coarse family; \(\operatorname{rank} Q\) splits it further (e.g. a rank-3 conic is a parabola when \(\operatorname{rank} Q = 1\) and an ellipse/hyperbola when \(\operatorname{rank} Q = 2\));
- the inertia
(n⁺, n⁻, n⁰)of \(A\) distinguishes real from imaginary and splits the hyperboloid families; centersolves \(Q\,c = -b\);eigenvalues/principal_directionsare the eigen-decomposition of \(Q\); the semi-axis along an eigenvector \(v_i\) is \(\sqrt{-f'/\lambda_i}\) with \(f' = c + b^{\mathsf T}c\) (Perwass's conic analysis).
refine() maps a classified matrix to a specific entity — Circle, Ellipse,
Hyperbola, Parabola, LinePair, Ellipsoid, Cone, PlanePair, … The
degenerate cases are factored rather than fitted: a rank-2 matrix is a plane
pair whose two homogeneous plane vectors are
\(\sqrt{\lambda_+}\,v_+ \pm \sqrt{-\lambda_-}\,v_-\) from the eigen-decomposition
(the 2D analogue gives a LinePair), and a rank-1 quadratic part gives a
parallel plane/line pair.
Two-quadric intersection¶
Two quadrics span a pencil \(\{Q_1 - \lambda Q_2\}\) of quadrics through their common curve. Every member is a quadric of the same projective type, and the curve is easiest to read off the degenerate members — those of rank ≤ 3:
- a rank-2 member factors into two planes; intersecting each plane with the
other quadric gives two conics (a
PlaneConicPair); - a rank-3 cone member is a ruled surface: writing a generator as \(x(\tau) = v + \tau\,d\) (through the apex \(v\)), the companion quadric becomes a quadratic \(a\tau^2 + b\tau + c = 0\) whose roots are the curve points. Sweeping the generator over the cone's base conic traces the quartic intersection curve, split into arcs where the discriminant changes sign.
That quartic is generally unbounded: it tends to infinity where the generator
is asymptotic to the companion quadric. Those directions are solved analytically —
substituting the base-conic parametrisation into \(d^{\mathsf T}q_Q d = 0\) gives a
quadratic in \((\cos\theta, \sin\theta)\), i.e. a quartic in \(\tan(\theta/2)\) —
and each real direction is then sampled out to a ±extent clip box with a
geometric ladder of small \(\theta\)-offsets, while the bounded body is sampled at a
low resolution. The result is an on-curve, finite polyline (a Curve) that still
reaches the view box.
Where the two generators of the pencil are both singular and through the origin (a rotated exact cube, for instance), the generalised-eigenvalue path degenerates; a homogeneous cubic \(\det(A - \lambda B) = 0\) on the 3×3 quadratic parts recovers the rank-2 plane-pair members instead. If the pencil has no real degenerate member, the intersection is an elliptic hard case and is not resolved.
Rotation rotor¶
Rotations act on the quadric space as versors \(A \mapsto R A \tilde R\).
create_rotor builds \(R\) from an angle and axis, and analyze_rotor inverts it
back to Rotor(angle, axis):
- Q2: \(R = R_2 R_1\) with \(R_1 = \cos\theta - \tfrac{\sqrt2}{2}\sin\theta\,(b_4\wedge b_6 - b_5\wedge b_6)\) and \(R_2 = \cos(\theta/2) - \sin(\theta/2)\,b_1\wedge b_2\) (the axis is implicit — rotation in the \(b_1b_2\) plane);
- Q3: three commuting factors \(R_{\text{lin}} \cdot R_{\text{mixed}} \cdot R_{\text{quad}}\) acting on the linear, mixed-quadratic and traceless-quadratic monomials at rates \(\theta, \theta, 2\theta\).
The rotor is an even versor (grades 0, 2, 4 in Q2; 0, 2, 4, 6 in Q3) and is
independent of the OPNS/IPNS flag. Analysis sandwiches the linear basis blades
only (the other factors commute with them), so the rotation matrix — and hence the
angle and axis — is read off directly. The full derivations live in
dev/notes/quadric-rotor-derivation.md and dev/notes/quadric-plane-pair-derivation.md.
Translation (linear map)¶
Rotations act as versors (A ↦ R A R̃), but the quadric space has no translator
versor — translation of a conic/quadric is an affine map on the coefficient
vector, not an isometry. geo(Translator(t)) therefore returns a linear-map
Expression for BasisQ2/BasisQ3 (built with
pytanga.expression.linear_map) instead of an MV, applied by contraction:
geo = Geometry(BasisQ3())
trans = geo(Translator(Direction(1, -2, 3))) # a linear-map Expression
cone = trans.evaluate(cone_at_origin) # translate the cone
The translation matrix is coeffs(Hᵀ Q H) = M_t · coeffs(Q) with H = [[I, −t],[0,1]],
computed by applying Q ↦ Hᵀ Q H to each coefficient basis vector. This is the
"algebraic way to specify the apex" of a cone: q3(base) gives the cone with apex
at the origin, and geo(Translator(apex)) moves it.
Visualization¶
Conics and quadrics reach the viewer as concrete entities:
- A
Quadric3Drenders through the analytic ray path (RayStyle): the frontend intersects the view ray with the quadric in the fragment shader, with a bounding-box proxy that writesgl_FragDepth. - Conic/curve entities (
Ellipse,Hyperbola,Parabola,LinePair,PlaneConic,PlaneConicPair,Curve, …) are sampled on the Python side into ordered polylines (one polyline per connected component, so the frontend never draws a spurious chord between branches) and streamed to per-kind renderers:curve.js,plane_pair.js, and the 2D conic renderers. Unbounded 2D conics (hyperbolas, parabolas, lines) are clipped to a spatialextentrather than an unbounded parameter range.
Because the sampled geometry is just polylines, adding a new quadric entity only
requires a refine/create branch, a style, a serializer, and a renderer — see
Viz architecture for the frontend extension recipe.