Geometry / Quadric module layering¶
The pytanga.geometry and pytanga.quadric packages are layered into a strict
import DAG so fundamental primitives sit at the bottom and nothing imports "up"
into a cyclic dependency.
The DAG¶
pytanga.entity ← leaf: Vec3, Point(Vec3), Direction(Vec3), Refinable,
MV-conversion registry (register_analyzer/_convert_mv)
pytanga.quadric ← Conic, Quadric3D (+ enums), refine_conic/refine_quadric,
refine_conic/refine_quadric, entity→MV creation
incl. the Q2/Q3 rotation rotor (_create.py),
MV→entity analysis incl. rotor analysis
(_analysis.py), point recovery (_pointset.py),
quadric intersection (_intersection.py) —
all lazy-import geometry.entities
pytanga.geometry.entities ← specific entities (Ellipse, Circle, …), re-exports
Point/Direction/Conic/Quadric3D, Entity Union alias
pytanga.geometry ← analysis (registers analyzers), create, duck-typed refine
pytanga.viz ← unchanged
Each layer imports only the layers below it. pytanga.entity imports nothing
from pytanga.geometry or pytanga.quadric.
Vec3 / Point / Direction¶
Vec3(pytanga.entity.vec3) is the fundamental 3D vector: component-wise+/-, scalar and element-wise*(elem_mul),dot,cross,mag,normalized, and the conversionsto_point()/to_direction()/from_point()/from_direction().Point(Vec3)andDirection(Vec3)subclassVec3and override the typed operations to return the semantically correct type (Point - Point → Direction,cross()→Direction,normalized()→Point/Direction). Their public behavior is unchanged from when they lived ingeometry.entities.
The Refinable protocol¶
pytanga.entity.base.Refinable is a @runtime_checkable Protocol with a
single refine() method. Conic and Quadric3D (in pytanga.quadric)
implement it; pytanga.geometry.refine is a duck-typed dispatcher:
def refine(entity):
fn = getattr(entity, "refine", None)
if not callable(fn):
raise TypeError(f"{type(entity).__name__} is not refinable")
return fn()
The actual quadric math stays in pytanga.quadric and imports
geometry.entities lazily (only when refine() / create_entity() /
analyze_entity() are actually called), so the quadric ↔ geometry.entities
cycle never fires at import time.
MV-conversion registry¶
pytanga.entity._util owns the analyzer registry (register_analyzer,
_convert_mv, _is_mv, _scalar). pytanga.geometry.analysis registers the
algebra-specific analyze_<name> callables at import time, so Point(mv) /
Direction(mv) can route an MV through the full analyzer without importing
analysis (which imports the entities) — no import-time cycle.
Backward-compatibility shims¶
The canonical definitions moved, but the old import paths keep working via thin re-export shims:
pytanga.geometry.entities.point/direction/conicre-export frompytanga.entity/pytanga.quadric.pytanga.geometry.Point,Direction,Conic,Quadric3D,EConicKind,EQuadricKindre-export unchanged.pytanga.geometry.refine/refine_entitykeep their public signatures.pytanga.geometry.create_q2/create_q3,analysis_q2/analysis_q3, and_pointsetre-export frompytanga.quadric._create,._analysis, and._pointsetrespectively, so the creation/analysis dispatchers keep working.analyze_operatoralso routesq2/q3MVs toquadric._analysis.analyze_operator, which returns apytanga.geometry.operators.Rotor(imported lazily inside the function to keep thequadric → geometryedge lazy).
Operator analysis expect= hint¶
analyze_operator / analyze (and the Geometry.which_operator /
Geometry.analyze facade) accept an optional expect= operator type. When the
natural classification is a half-turn reflection with a lossless reinterpretation
as the requested rotation, the rotation is returned instead — a 3D
ReflectionLine → GeneralRotor/Rotor, a 2D ReflectionPoint →
GeneralRotor/Rotor. This is an extension of the analysis layer only (no
layering change); the reinterpretation lives in analysis._coerce_operator,
gated on the algebra dimension.
Tolerance-aware refinement¶
Geometry carries an optional analysis tolerance (Geometry(algebra, tol=…) /
settable Geometry.tol). Geometry.refine(entity, tol=…) threads it through
the duck-typed pytanga.geometry.refine into Conic.refine(tol=…) /
Quadric3D.refine(tol=…) → pytanga.quadric.refine_conic / refine_quadric →
the tolerance-aware _classify_conic / _classify_quadric. This lets a noisy
degenerate quadric be classified "within a tolerance" (e.g. a near-cone as a
Cone) without changing the exact .kind / .rank / .signature defaults.
Hard-coded type masks (mask_for_<type>)¶
pytanga.geometry.mask.mask_for(basis, typ) dispatches a class to the
per-algebra create_* module's hard-coded mask_for_<key>(basis) -> BladeMask
function (the full blade set the type occupies, respecting basis.opns), and an
instance to BladeMask(create(basis, instance)) (its non-zero blades).
There is no instance template — masks are explicit, so they never depend on
sample values. A type may attach a reduced named basis inline
(create_n3.mask_for_twist_bivector returns the 9 raw twist blades with the
6-DOF directions e12, e13, e23, e1∧e∞, e2∧e∞, e3∧e∞ via with_basis); all
other types use the algebra's auto display basis. Full masks are pinned by
py/tests/geometry/test_geometry_mask.py.