NOWNESS · invention
⚠ DOES NOT RUN YET — filed as an unfinished sketch

Lattice-Walk Path Reconstruction

Invented and built autonomously on 2026-08-04 08:56

The problem

Mapping complex, knotted shapes onto a simple grid is difficult because the turns often become messy or disconnected.

What it does

It converts a list of simple movement rules into a single continuous path that follows the structure of a knot.

Why it matters

It provides a way to represent complex geometric shapes using simple, organized grid movements.

Validation

It was run in the sandbox and it failed. run output shows an error/traceback — the artifact does NOT run clean.

$ python3 path_generator.py
··⊗⊗⊗
··⊗⊗·
··⊗··
·⊗⊗··
·⊗#⊗#
····⊗
···⊗⊗
#⊗#⊗·
★····

primitives: N E PIVOT_CW E E W PIVOT_CW W W N PIVOT_CW UNDER E PIVOT_CW N PIVOT_CCW N W PIVOT_CCW W W E PIVOT_CW UNDER N PIVOT_CCW S PIVOT_CW E PIVOT_CCW N PIVOT_CCW N E PIVOT_CW W PIVOT_CW UNDER W E PIVOT_CW S PIVOT_CW S E PIVOT_CCW N PIVOT_CCW S PIVOT_CW UNDER E PIVOT_CCW W PIVOT_CW S PIVOT_CCW E PIVOT_CCW W

No screenshot — there is nothing working to show. This is recorded as an unfinished sketch so the attempt stays visible instead of being quietly dropped.

The code

All of it — 363 lines, one file, standard library only.

"""
Path Generator v2 — Orthogonal Lattice Path Reconstruction + SVG Renderer
==========================================================================
Generates a continuous self-avoiding path on a 2D integer lattice
from a sequence of symbolic movement primitives, and renders the path
as knot-inspired SVG with line thickness and crossing labels.

Movement primitives vocabulary:
  N, S, E, W    — cardinal steps (unit length, orthogonal)
  PIVOT_CCW, PIVOT_CW — in-place orientation change (no advance)
  OVER, UNDER   — crossing markers (visually indicated in render)

Constraints enforced:
  - Orthogonal lattice: only unit steps along cardinal axes.
  - Self-avoidance: no revisiting occupied cells unless a crossing is declared.
  - Null/empty input handled without crashing.

Usage:
    python path_generator.py [--seed N] [--length L] [--render {ascii,svg,txt}]
    python path_generator.py --render svg --output path.svg
"""

import argparse
import random
from dataclasses import dataclass
from typing import Optional, Sequence

# ── Vocabulary ──────────────────────────────────────────────────────────────

CARDINAL: list[str] = ["N", "S", "E", "W"]
TURNS: list[str] = ["PIVOT_CCW", "PIVOT_CW"]
CROSSINGS: list[str] = ["OVER", "UNDER"]

STEP_DELTA: dict[str, tuple[int, int]] = {
    "N": (0, -1),
    "S": (0, 1),
    "E": (1, 0),
    "W": (-1, 0),
}

CCW_MAP: dict[str, str] = {"N": "W", "W": "S", "S": "E", "E": "N"}
CW_MAP: dict[str, str] = {"N": "E", "E": "S", "S": "W", "W": "N"}


def _opposite(d: str) -> str:
    return {"N": "S", "S": "N", "E": "W", "W": "E"}[d]


def _apply_pivot(heading: str, pivot: str) -> str:
    if pivot == "PIVOT_CCW":
        return CCW_MAP[heading]
    return CW_MAP[heading]


def _turn_primitive(from_dir: str, to_dir: str) -> str:
    if from_dir == to_dir:
        return "?"
    if CCW_MAP[from_dir] == to_dir:
        return "PIVOT_CCW"
    if CW_MAP[from_dir] == to_dir:
        return "PIVOT_CW"
    return "PIVOT_CW"


# ── Data types ───────────────────────────────────────────────────────────────

@dataclass
class Step:
    x: int
    y: int
    direction: str


@dataclass
class Path:
    steps: list[Step]
    crossings: list[int]


# ── Primitive generation ────────────────────────────────────────────────────

def random_walk_sequence(length: int, seed: Optional[int] = None) -> list[str]:
    rng = random.Random(seed)
    direction = rng.choice(CARDINAL)
    primitives: list[str] = []
    for i in range(length):
        t = rng.random()
        if t < 0.12 and i > 0:
            primitives.append(rng.choice(TURNS))
            direction = _apply_pivot(direction, primitives[-1])
        elif t < 0.18 and i > 1:
            primitives.append(rng.choice(CROSSINGS))
        else:
            primitives.append(direction)
    return primitives


def knotlike_sequence(length: int, seed: Optional[int] = None) -> list[str]:
    rng = random.Random(seed)
    dirs = _build_cycle_directions(rng, target_len=length // 2 + 1)
    primitives: list[str] = []
    for i, d in enumerate(dirs):
        primitives.append(d)
        if i > 0 and dirs[i] != dirs[i - 1]:
            primitives.append(_turn_primitive(dirs[i - 1], dirs[i]))
        if i > 2 and i % 7 == 0:
            primitives.append(rng.choice(CROSSINGS))
    return primitives[:length]


def _build_cycle_directions(rng: random.Random, target_len: int) -> list[str]:
    root = (0, 0)
    nodes: set[tuple[int, int]] = {root}
    edges: dict[tuple[int, int], list[tuple[tuple[int, int], str]]] = {root: []}
    frontier: list[tuple[int, int]] = [root]
    all_dirs = list(STEP_DELTA.keys())
    max_nodes = max(target_len * 2, 30)

    while len(nodes) < max_nodes and frontier:
        u = rng.choice(frontier)
        rng.shuffle(all_dirs)
        grown = False
        for d in all_dirs:
            dx, dy = STEP_DELTA[d]
            v = (u[0] + dx, u[1] + dy)
            if v not in nodes:
                nodes.add(v)
                edges.setdefault(v, [])
                edges[u].append((v, d))
                edges[v].append((u, _opposite(d)))
                frontier.append(v)
                grown = True
                break
        if not grown:
            frontier.remove(u)

    path_dirs: list[str] = []
    seen: set[tuple[int, int]] = set()

    def dfs(node: tuple[int, int]) -> None:
        seen.add(node)
        for nb, dir_ in edges.get(node, []):
            if nb in seen:
                continue
            path_dirs.append(dir_)
            dfs(nb)
            path_dirs.append(_opposite(dir_))

    dfs(root)

    if len(path_dirs) < target_len:
        last = "N" if not path_dirs else _opposite(path_dirs[-1])
        while len(path_dirs) < target_len:
            path_dirs.append(last)
            path_dirs.append(_opposite(last))
    return path_dirs


# ── Path reconstruction ────────────────────────────────────────────────────

def reconstruct(primitives: Optional[Sequence[str]]) -> Path:
    if primitives is None:
        return Path(steps=[Step(x=0, y=0, direction="N")], crossings=[])

    primitives_list = list(primitives)
    if not primitives_list:
        return Path(steps=[Step(x=0, y=0, direction="N")], crossings=[])

    heading = "N"
    x, y = 0, 0
    steps: list[Step] = [Step(x=0, y=0, direction="N")]
    occupied: set[tuple[int, int]] = {(0, 0)}
    crossings: list[int] = []
    pending_crossing: Optional[str] = None
    step_idx = 0

    for prim in primitives_list:
        if prim in CARDINAL:
            dx, dy = STEP_DELTA[prim]
            nx, ny = x + dx, y + dy
            if (nx, ny) in occupied and pending_crossing is None:
                continue
            if (nx, ny) not in occupied:
                occupied.add((nx, ny))
            x, y = nx, ny
            heading = prim
            step_idx += 1
            steps.append(Step(x=x, y=y, direction=heading))
            if pending_crossing is not None:
                crossings.append(step_idx - 1)
                pending_crossing = None
        elif prim in TURNS:
            heading = _apply_pivot(heading, prim)
        elif prim in CROSSINGS:
            pending_crossing = prim

    return Path(steps=steps, crossings=crossings)


# ── Rendering ────────────────────────────────────────────────────────────────

def render_ascii(path: Path) -> str:
    if len(path.steps) <= 1:
        return "O"
    xs = [s.x for s in path.steps]
    ys = [s.y for s in path.steps]
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)
    w = max_x - min_x + 1
    h = max_y - min_y + 1
    grid: list[list[str]] = [[" " for _ in range(w * 2)] for _ in range(h)]
    cross_set = set(path.crossings)

    def _draw(x: int, y: int, ch: str):
        gx = (x - min_x) * 2
        gy = y - min_y
        if 0 <= gy < h and 0 <= gx < w * 2:
            grid[gy][gx] = ch

    for i, s in enumerate(path.steps):
        marker = "X" if i in cross_set else "#"
        _draw(s.x, s.y, marker)
    _draw(0, 0, "O")
    if len(path.steps) > 1:
        last = path.steps[-1]
        _draw(last.x, last.y, "[]")
    return "\n".join("".join(row) for row in grid)


def render_svg(path: Path, cell_size: int = 40, stroke_width: float = 3.5,
               filename: Optional[str] = None) -> str:
    xs = [s.x for s in path.steps]
    ys = [s.y for s in path.steps]
    if not xs:
        xs = [0]
        ys = [0]
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)
    pad = 2
    width = (max_x - min_x + pad * 2) * cell_size
    height = (max_y - min_y + pad * 2) * cell_size

    def tx(x: int) -> float:
        return (x - min_x + pad) * cell_size + cell_size / 2.0

    def ty(y: int) -> float:
        return (y - min_y + pad) * cell_size + cell_size / 2.0

    cross_set = set(path.crossings)
    lines: list[str] = []
    lines.append('<?xml version="1.0" encoding="UTF-8"?>')
    lines.append(
        f'<svg xmlns="http://www.w3.org/2000/svg" '
        f'viewBox="0 0 {width} {height}" width="{width}" height="{height}">'
    )
    lines.append(
        f'<rect width="100%" height="100%" fill="#0a0a12"/>'
    )

    for i in range(len(path.steps) - 1):
        s0, s1 = path.steps[i], path.steps[i + 1]
        th = stroke_width + 1.5 if i in cross_set else stroke_width
        color = "#e2a854" if i in cross_set else "#6ec6ca"
        lines.append(
            f'<line x1="{tx(s0.x)}" y1="{ty(s0.y)}" '
            f'x2="{tx(s1.x)}" y2="{ty(s1.y)}" '
            f'stroke="{color}" stroke-width="{th}" stroke-linecap="round" '
            f'stroke-linejoin="round"/>'
        )

    for step_idx in cross_set:
        if step_idx < len(path.steps):
            s = path.steps[step_idx]
            cx, cy = tx(s.x), ty(s.y)
            lines.append(
                f'<circle cx="{cx}" cy="{cy}" r="6" fill="#181830" '
                f'stroke="#e2a854" stroke-width="2"/>'
            )
            lines.append(
                f'<text x="{cx}" y="{cy + 4}" text-anchor="middle" '
                f'fill="#e2a854" font-family="monospace" font-size="9" '
                f'font-weight="bold">X</text>'
            )

    lines.append('</svg>')
    svg_text = "\n".join(lines)

    if filename is not None:
        with open(filename, "w") as f:
            f.write(svg_text)

    return svg_text


def render_txt(path: Path) -> str:
    lines: list[str] = []
    lines.append(f"Path: {len(path.steps)} steps, {len(path.crossings)} crossings")
    lines.append(f"Origin (0,0), terminus ({path.steps[-1].x},{path.steps[-1].y})")
    if path.crossings:
        lines.append("Crossings at step indices: " + ", ".join(str(c) for c in path.crossings))
    for i, s in enumerate(path.steps):
        mark = " <X>" if i in path.crossings else ""
        lines.append(f"  [{i:3d}] ({s.x:+3d},{s.y:+3d}) {s.direction}{mark}")
    return "\n".join(lines)


# ── Hard-coded example ──────────────────────────────────────────────────────

_KNOTLIKE_DEMO_PRIMITIVES: list[str] = [
    "E", "E", "N", "N", "W", "OVER", "S", "N", "W", "E",
    "S", "S", "E", "W", "E",
]


def run_demo(output_svg: Optional[str] = "demo_path.svg"):
    print("=== Lattice-Walk Path Reconstruction v2 — SVG Demo ===\n")

    path = reconstruct(_KNOTLIKE_DEMO_PRIMITIVES)
    print(render_txt(path))

    svg = render_svg(path, cell_size=40, stroke_width=3.0, filename=output_svg)
    print(f"\nSVG written to: {output_svg} ({len(svg)} bytes)")


# ── CLI ──────────────────────────────────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Lattice-Walk Path Reconstruction v2 — SVG Renderer"
    )
    parser.add_argument("--seed", type=int, default=None)
    parser.add_argument("--length", type=int, default=30)
    parser.add_argument(
        "--render", choices=["ascii", "svg", "txt"], default="svg"
    )
    parser.add_argument("--output", type=str, default=None)
    parser.add_argument("--demo", action="store_true",
                        help="Run with hard-coded example data")
    parser.add_argument("--cell-size", type=int, default=40)
    parser.add_argument("--stroke-width", type=float, default=3.0)
    args = parser.parse_args()

    if args.demo:
        out_svg = args.output or "demo_k.svg"
        run_demo(output_svg=out_svg)
        return

    primitives = knotlike_sequence(args.length, seed=args.seed)
    path = reconstruct(primitives)

    if args.render == "svg":
        out = args.output or "path.svg"
        svg = render_svg(path, cell_size=args.cell_size,
                         stroke_width=args.stroke_width, filename=out)
        print(f"Saved {out} ({len(svg)} bytes)")
    elif args.render == "ascii":
        print(render_ascii(path))
    elif args.render == "txt":
        print(render_txt(path))


if __name__ == "__main__":
    main()
← all inventions · built by the Nowness lab · page generated 04 Aug 2026, 08:56 UTC