Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
*.lock
*.vsidx
*.sqlite
.venv/
288 changes: 288 additions & 0 deletions HANDOFF.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions PYTHONOCC_BACKEND_GOAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Goal: Complete the PythonOCC Core backend for topologicpy

Make `src/topologicpy/pythonocc_backend/` pass topologicpy's `Core` backend
contract well enough that `Core.SetBackend(PythonOCCBackend)` is a real
drop-in replacement for `topologic_core` — not a demo.

## Ground truth

- Spec: `TopologicPy_Replacement_Backend_Developer_Guide.docx`. Read it in
full — all tables via `python-docx`'s `d.tables`, not just paragraphs.
Treat it as authoritative over anything below.
- Appendix A ("Minimum backend checklist") and §13 (test plan) define
"done."
- Check `git log`/`git diff` in this repo before assuming anything is
broken or missing — prior work has already landed some fixes. Trust
what you observe over any stale summary, including this one.

## What matters most

Judge priority by how much of the checklist a fix unlocks, not by a fixed
list. As a compass:
- Non-manifold `CellComplex` (real shared faces across multiple cells) is
the single most important behavior in Topologic — if it's faked (e.g.
wrapping a single Cell), that's the highest-value fix.
- The guide's type IDs are bit flags, not sequential (Vertex=1, doubling
per type up to Topology=4096) — get this right early since much else
dispatches on it.
- Anything the guide defines as able to "fail" must return `None`, not an
empty/sentinel object.
- Prefer a smaller surface that's real and tested over broad stub
coverage.

## Freedom

Decide what to fix and in what order, as long as you move the checklist
forward without regressing what already works. Use your own judgment on
trade-offs — no external list here is mandatory.

## Constraints

- Work only in `pythonocc_backend/` — don't touch the abandoned
`PythonOCCBackend.py` single-file version.
- Don't touch the Mojo repos (`topokernel`/`geokernel`) — separate effort,
see `C:\Github\DigitalEngineer\topokernel\MOJO_BACKEND_GOAL.md`.
- Keep TopologicPy's analytical algorithms out of the backend — it's a
primitive kernel + persistence layer only (guide §1).
- Run tests when PythonOCC is actually installed; say plainly when you
can't verify rather than claiming untested code works.
- Don't commit; leave changes in the working tree for review.

## Acceptance

Appendix A's checklist and §13's test plan pass against
`Core.SetBackend(PythonOCCBackend)`, verified by actually running tests
with PythonOCC installed — not just code review.
639 changes: 639 additions & 0 deletions TopologicPy_Replacement_Backend_Developer_Guide.md

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions audit_pythonocc_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
Audit the PythonOCC backend against the topologicpy wrapper layer.

For every backend namespace (Vertex, Edge, Wire, Face, Shell, Cell,
CellComplex, Cluster, Graph, Topology, and their *Utility classes):

1. LIVE-GAP: wrapper static methods that the wrapper dispatches to the
backend via `Core.InstanceCall(<obj>, "MethodName", ...)` but which are
NOT present on the backend native class. These are real breakages
(they would raise AttributeError/TypeError at runtime).

2. STUB: backend methods that still call `not_implemented()` (print
"... - Not implemented.") or raise NotImplementedError.

3. (Optional) SMOKE: backend factory/constructor methods that return
None when handed valid inputs.

Run:
TOPOLOGICPY_CORE_BACKEND=pythonocc \
/path/to/python.exe audit_pythonocc_backend.py
"""
from __future__ import annotations
import os, re, sys, types

# Make sure we import the *wrapper* layer, with the pythonocc backend active.
os.environ.setdefault("TOPOLOGICPY_CORE_BACKEND", "pythonocc")

REPO = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(REPO, "src")
if SRC not in sys.path:
sys.path.insert(0, SRC)

from topologicpy import Core # noqa: E402 (triggers backend selection)

# The active backend instance (Core._backend is set lazily on first use).
def _active_backend_name():
try:
b = getattr(Core, "_backend", None)
if b is None:
import topologicpy.Vertex as V
_ = V.Vertex.ByCoordinates(0, 0, 0)
b = getattr(Core, "_backend", None)
return type(b).__name__ if b is not None else "unknown"
except Exception:
return "unknown"

# --------------------------------------------------------------------------
# Namespace map: (wrapper_module, wrapper_class, backend_module, backend_class)
# --------------------------------------------------------------------------
NAMESPACES = [
("Vertex", "Vertex", "vertex", "Vertex"),
("Edge", "Edge", "edge", "Edge"),
("Wire", "Wire", "wire", "Wire"),
("Face", "Face", "face", "Face"),
("Shell", "Shell", "shell", "Shell"),
("Cell", "Cell", "cell", "Cell"),
("CellComplex","CellComplex", "cell_complex","CellComplex"),
("Cluster", "Cluster", "cluster", "Cluster"),
("Graph", "Graph", "graph", "Graph"),
("Topology", "Topology", "topology", "Topology"),
# NOTE: VertexUtility/EdgeUtility/... are BACKEND-ONLY classes
# (the wrapper topologicpy.<Ns> has no <Ns>Utility class). There is
# no wrapper counterpart to diff, so they are skipped here.
]

# Method names that the backend exposes as generic (already-known) helpers
KNOWN_BACKEND_ONLY = set()

def find_dispatched_methods(wrapper_src: str):
"""Return the set of method names the wrapper dispatches via Core.InstanceCall."""
return set(re.findall(
r'Core\.InstanceCall\(\s*\w+\s*,\s*[\'"](\w+)[\'"]', wrapper_src))

def find_not_implemented(backend_src: str):
"""Return method names in the backend source that are still stubbed."""
out = set()
# Matches: Name = staticmethod(_x_not_implemented("Name")) or lambda calling not_implemented
for m in re.finditer(r'(\w+)\s*=\s*(?:staticmethod\()?_(?:\w+_)?not_implemented\([\'"](\w+)[\'"]', backend_src):
out.add(m.group(2))
return out

def main():
print(f"Active backend: {_active_backend_name()}\n")
grand_total_gap = 0
grand_total_stub = 0

for wmod, wcls, bmod, bcls in NAMESPACES:
wpath = os.path.join(SRC, "topologicpy", f"{wmod}.py")
bpath = os.path.join(SRC, "topologicpy", "pythonocc_backend", f"{bmod}.py")
if not (os.path.exists(wpath) and os.path.exists(bpath)):
continue

w_src = open(wpath, encoding="utf-8", errors="ignore").read()
b_src = open(bpath, encoding="utf-8", errors="ignore").read()

# Wrapper class dir
w_mod = __import__(f"topologicpy.{wmod}", fromlist=[wcls])
w_class = getattr(w_mod, wcls)
wrap_methods = {m for m in dir(w_class) if not m.startswith("_")}

# Backend class dir
b_mod = __import__(f"topologicpy.pythonocc_backend.{bmod}", fromlist=[bcls])
b_class = getattr(b_mod, bcls)
back_methods = {m for m in dir(b_class) if not m.startswith("_")}

dispatched = find_dispatched_methods(w_src)
stubs = find_not_implemented(b_src)

# Live gaps: dispatched to backend but missing on backend class
live_gap = sorted(m for m in dispatched if m not in back_methods)
# Stub methods actually present as stub assignments
stub_present = sorted(m for m in stubs if m in back_methods or m in wrap_methods)

# Also: wrapper methods NOT on backend AND not pure-python helpers
# (heuristic: skip the long known pure-python algorithm lists)
missing_nonlive = sorted(
m for m in wrap_methods
if m not in back_methods and m not in dispatched
)

flag = "GAP" if live_gap else "ok"
print(f"[{flag}] {wcls:14} (backend {bcls})")
print(f" wrapper_methods={len(wrap_methods)} backend_methods={len(back_methods)} "
f"dispatched={len(dispatched)}")
if live_gap:
print(f" LIVE-GAP (dispatched but backend missing): {live_gap}")
grand_total_gap += len(live_gap)
if stub_present:
print(f" STUB (not_implemented still present): {stub_present}")
grand_total_stub += len(stub_present)
# Report the non-live missing list size only (too long to enumerate)
if missing_nonlive:
print(f" non-live-wrapper-only methods (pure-python algo/IO, not backend-dispatched): {len(missing_nonlive)}")
print()

print("=" * 60)
print(f"TOTAL LIVE-GAPS (real breakages): {grand_total_gap}")
print(f"TOTAL STUB placeholders: {grand_total_stub}")
if grand_total_gap == 0 and grand_total_stub == 0:
print("=> Backend is fully operational for all live-dispatched methods. No stubs.")
else:
print("=> See above for items to implement.")

if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ addopts = "-nauto --strict-markers --strict-config -v"
# to be defined and raise on invalid config values
# treat xpasses as test failures so they get converted to regular tests as soon as possible
xfail_strict = true

[dependency-groups]
dev = [
"pytest>=8.3.5",
]
13 changes: 12 additions & 1 deletion src/topologicpy/Cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -2196,7 +2196,18 @@ def Dodecahedron(origin= None,
vertices = [Vertex.ByCoordinates(coords) for coords in geo['vertices']]
vertices = Vertex.Fuse(vertices)
coords = [Vertex.Coordinates(v) for v in vertices]
dodecahedron = Topology.RemoveCoplanarFaces(Topology.SelfMerge(Topology.ByGeometry(vertices=coords, faces=geo['faces'])))
rebuilt = Topology.RemoveCoplanarFaces(Topology.SelfMerge(Topology.ByGeometry(vertices=coords, faces=geo['faces'])))
# Under the pythonOCC backend the rebuilt geometry comes back as a Shell;
# re-solidify it into a Cell so the constructor honours its contract.
if not Topology.IsInstance(rebuilt, "cell"):
try:
rebuilt = Cell.ByFaces(Topology.Faces(rebuilt, silent=True), tolerance=tolerance, silent=True)
except Exception:
try:
rebuilt = Cell.ByShell(rebuilt, tolerance=tolerance, silent=True)
except Exception:
rebuilt = None
dodecahedron = rebuilt
dodecahedron = Topology.Orient(dodecahedron, origin=Vertex.Origin(), dirA=[0, 0, 1], dirB=direction, tolerance=tolerance)
dodecahedron = Topology.Place(dodecahedron, originA=Vertex.Origin(), originB=origin)
return dodecahedron
Expand Down
14 changes: 13 additions & 1 deletion src/topologicpy/Core.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,16 @@
larger architectural migration.
"""

import os
from typing import Any, List, Optional

# Opt-in backend selection. Set the TOPOLOGICPY_CORE_BACKEND environment
# variable to "pythonocc" before the first Core.Backend() access to use the
# PythonOCC replacement backend instead of the default topologic_core
# backend. topologic_core remains the default for existing users; nothing
# changes unless this variable is explicitly set.
_BACKEND_ENV_VAR = "TOPOLOGICPY_CORE_BACKEND"


class _MissingNamespace:
"""
Expand Down Expand Up @@ -242,7 +250,11 @@ def Backend() -> Any:
``TopologicCoreBackend``.
"""
if Core._backend is None:
Core._backend = TopologicCoreBackend()
if os.environ.get(_BACKEND_ENV_VAR, "").strip().lower() == "pythonocc":
from topologicpy.pythonocc_backend import PythonOCCBackend
Core._backend = PythonOCCBackend()
else:
Core._backend = TopologicCoreBackend()
return Core._backend

@staticmethod
Expand Down
6 changes: 3 additions & 3 deletions src/topologicpy/Face.py
Original file line number Diff line number Diff line change
Expand Up @@ -5463,7 +5463,7 @@ def Square(origin= None, size: float = 1.0, direction: list = [0, 0, 1], placeme


@staticmethod
def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001):
def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001, silent: bool = False):
"""
Creates a Squircle which is a hybrid between a circle and a square. See https://en.wikipedia.org/wiki/Squircle

Expand Down Expand Up @@ -5500,7 +5500,7 @@ def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.
return Face.ByWire(wire)

@staticmethod
def Star(origin= None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
def Star(origin= None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001, silent: bool = False):
"""
Creates a star.

Expand Down Expand Up @@ -5592,7 +5592,7 @@ def ThirdVertex(face, tolerance: float = 0.0001, silent: bool = False):
return None

@staticmethod
def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001, silent: bool = False):
"""
Creates a trapezoid.

Expand Down
Loading