Quadric module¶
pytanga.quadric is the projective quadric-space layer of the Python library.
It represents 2D conics (symmetric 3×3 matrices) and 3D quadrics
(symmetric 4×4 matrices) as grade-1 blades of a Euclidean-rescaled geometric
algebra, and owns all the quadric math: construction from points, MV ↔ entity
analysis, classification / refinement, entity → MV creation, point recovery, the
quadric-space rotation rotor, and two-quadric intersection.
It is deliberately a leaf-ish package: most modules are numpy-only. Its
public surface is GA-centric — conics/quadrics are built with geo(...), and
fitting/intersection are expressed with the GA operations (op/join, dual,
sp) — while the expression system (pytanga.expression / pytanga.blade_mask)
backs the point-tuple recovery. It imports
pytanga.entity (Vec3 / Point / Direction) directly, and it imports
pytanga.geometry.entities lazily so the quadric ↔ geometry.entities cycle
never fires at import time. The cross-package layering is described in
Geometry & quadric layering; this page describes
the module's internal structure.
Module map¶
| File | Responsibility |
|---|---|
_basis.py |
BasisQ2 / BasisQ3 (incl. BasisQ3(c) cone lift), CONE_BLADE_MAP |
_embedding.py |
_embed_point — the x·xᵀ point embedding |
_mapping.py |
_to_coeffs / _from_coeffs — symmetric-matrix ↔ coefficient-vector |
conic.py |
Conic / Quadric3D dataclasses (matrix- or coeff-vector constructors), EConicKind / EQuadricKind, classification (tolerance-aware) |
refine.py |
refine_conic / refine_quadric — matrix → specific geometry entity (optional tol) |
_create.py |
create_* — entity → MV, plus create_rotor / create_translator |
_analysis.py |
analyze_entity / analyze_operator / analyze_rotor — MV → entity |
_pointset.py |
point_from_embedding, pointset_from_blade, _two_conic_intersection |
_intersection.py |
_intersect_quadrics / _intersect_three_quadrics — the pencil backends of analyze |
Representation and data flow¶
A conic/quadric is stored as a coefficient vector (6 entries for a conic, 10
for a quadric) that is the exact inverse of the symmetric-matrix form — see
_mapping.py for the fixed ordering. The Euclidean rescaling puts the √2/2
factors only in _embedding.py and _mapping.py, so the plain Algebra(6,0)
/ Algebra(10,0) metric is used everywhere else.
The main pipeline is:
points ──geo(Point)──▶ MVs ──op/join──▶ blade ──analyze──▶ Conic/Quadric3D
│
┌───────────────────────┘
▼
refine() ──▶ specific entity (Ellipse, Cone, …)
│
▼
viz serializer ──▶ renderer (curve.js, plane_pair.js, …)
- Construction (GA): embed the points with
geo(Point(...)), take theirop/join(the smallest containing blade), andanalyzeit — the dual of the blade is the grade-1 coefficient MV. The SVD fit is the same incidence expressionc.sp(p) = 0over the embedded points, solved with the expression system'slstsq(the singular spectrum flags an underdetermined point set). - Analysis (
_analysis.py):analyze_entityprunes numerical noise, then dispatches on algebra dimension (Q2 vs Q3) and blade grade. IPNS input is dualized to OPNS first, so a single OPNS dispatch path handles both. - Refine (
refine.py, reached viaConic.refine(tol=…)/Quadric3D.refine(tol=…), and fromGeometry.refine(..., tol=…)): classifies the matrix — optionally within a tolerance so a noisy degenerate quadric can still be recognized as, e.g., a cone — and builds the concrete entity via eigen-decomposition. - Creation (
_create.py): inverts the round-trip —create_circle,create_ellipse,create_cone,create_rotor, … build the coefficient vector / MV from an entity. Every entity type here has a matchingrefine_*branch. - Cone lift (
_basis.py::BasisQ3.__call__,_basis.py::CONE_BLADE_MAP): a base conic (in the planez = 1) is relabeled onto the cone quadric slots via the generalAlgebra.embed/MV.to_algebrablade-map primitive (also exposed asBasisQ3(c)), then translated to a chosen apex withgeo(Translator(...))— a rank-3 cone.
Classification and refine¶
Conic / Quadric3D (in conic.py) expose cached properties computed from the
matrix's affine block form [[Q, b], [bᵀ, c]]:
rank(full matrix) andsignature(inertia(n⁺, n⁻, n⁰)),kind— the coarse projective classification, from the rank of the full matrix against the rank of the quadratic partQ(parabola,ellipse,hyperbola,line_pair,cone,hyperboloid_2s,plane_pair, …),center,eigenvalues,principal_directions, andrho(circle/sphere radius).
refine.py turns those into pytanga.geometry.entities objects. The degenerate
cases mirror Perwass's conic-space analysis: a rank-2 quadric is a plane pair
whose two homogeneous plane vectors are √λ₊·v₊ ± √(−λ₋)·v₋ built from the
eigen-decomposition of the full matrix; a rank-1 quadratic part is a parallel
plane pair; a rank-3 quadric with a null vector is a cone whose apex is that null
vector (dehomogenised). _line_pair_from_conic is the 2D counterpart, and the two
share the same "factor a degenerate matrix" idea.
Two-quadric intersection¶
_intersect_quadrics(Q1, Q2) (_intersection.py, the private backend of
analyze) is numpy-only (no scipy). It
finds the real degenerate members of the pencil span{Q1, Q2} and returns
either:
- a
PlaneConicPair— a rank-2 member factors into two planes; each plane is intersected with the other quadric to give a conic; or - a sampled
Curve— a rank-3 cone member is ruled: each generator (a line through the apex) meets the companion quadric in a quadratic, and the roots trace the quartic. That quartic is generally unbounded, so its asymptotic directions are solved analytically (a quartic intan(θ/2)on the base conic) and sampled out to the±extentbox, while the bounded body is sampled at a low resolution.
Degenerate-member detection combines the generalised-eigenvalue solve path with
a homogeneous-cubic fallback for pencils whose generators are both singular and
through the origin (e.g. a rotated exact cube). The elliptic hard case — no real
degenerate member — raises NotImplementedError.
Point tuples and three-quadric intersection¶
A k-point join (OPNS blade, grades 2–7 in Q3, 2–4 in Q2) is analyzed as a
PointSet by dualizing to the space of quadrics through the points and
intersecting a generic triple of them (_pointset.py):
k = 2uses the rank-1 pencil (a binary quadratic); Q2k = 3, 4uses the conic complement +_two_conic_intersection; Q3k = 3..7uses the quadric complement +_intersect_three_quadrics._intersect_three_quadrics(Q1, Q2, Q3)reduces to_intersect_quadrics(Q1, Q2)and findsQ3 = 0along the quartic: exact through a plane-pair member's plane-conics, numeric by Newton-refining the cone-sample polyline. Several random triplets are unioned and filtered to the points on all complement quadrics._analyze_q3dualizes IPNS → OPNS first, so every IPNS grade routes through the same OPNS path.
A k = 7 join returns eight points — the seven originals plus the
Cayley–Bacharach partner of the 3-dim net (see
dev/theory/quadric-point-tuples.md).
Where to look for a change¶
| I want to… | File(s) |
|---|---|
| add a new conic/quadric kind | conic.py (enum + classifier), refine.py, _create.py |
| change how a point is embedded or a matrix maps to coeffs | _embedding.py, _mapping.py |
| change how an MV is analyzed (grade dispatch) | _analysis.py |
| change two-quadric intersection | _intersection.py |
| change the rotation rotor | _create.py (create_rotor), _analysis.py (analyze_rotor) |
| recover points from a blade / intersect two conics | _pointset.py |