Skip to content

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 conversions to_point() / to_direction() / from_point() / from_direction().
  • Point(Vec3) and Direction(Vec3) subclass Vec3 and 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 in geometry.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 / conic re-export from pytanga.entity / pytanga.quadric.
  • pytanga.geometry.Point, Direction, Conic, Quadric3D, EConicKind, EQuadricKind re-export unchanged.
  • pytanga.geometry.refine / refine_entity keep their public signatures.
  • pytanga.geometry.create_q2 / create_q3, analysis_q2 / analysis_q3, and _pointset re-export from pytanga.quadric._create, ._analysis, and ._pointset respectively, so the creation/analysis dispatchers keep working.
  • analyze_operator also routes q2/q3 MVs to quadric._analysis.analyze_operator, which returns a pytanga.geometry.operators.Rotor (imported lazily inside the function to keep the quadric → geometry edge 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.