Move tutte_embedding to lib, add bash completion, and fix NixOS setup
- Replace iterative tutte_embedding in lib with numpy direct-solve version from example.py - Import tutte_embedding into example.py from lib instead of defining it locally - Fix g._embedding -> g.get_embedding() in outer_face - Add bash completion to run.sh alongside existing zsh completion - Use nix-shell -p gcc for plantri build step on NixOS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,10 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast, TypedDict, Literal
|
from typing import Any, cast, TypedDict, Literal
|
||||||
|
import math
|
||||||
|
|
||||||
from sage.all import graphs, Graph, save, load # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
from sage.all import graphs, Graph, save, load # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||||
|
from lib.tutte_embedding import tutte_embedding
|
||||||
|
|
||||||
DIR = Path(__file__).parent.parent
|
DIR = Path(__file__).parent.parent
|
||||||
PALETTE = ['red', 'blue', 'green', 'yellow']
|
PALETTE = ['red', 'blue', 'green', 'yellow']
|
||||||
@@ -87,7 +89,9 @@ def save_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, Verte
|
|||||||
def outer_face(g: Graph) -> list[Any]:
|
def outer_face(g: Graph) -> list[Any]:
|
||||||
"""Return the vertices of the outer (unbounded) face of g using its planar embedding and positions."""
|
"""Return the vertices of the outer (unbounded) face of g using its planar embedding and positions."""
|
||||||
pos = g.get_pos()
|
pos = g.get_pos()
|
||||||
embedding = g._embedding
|
embedding = g.get_embedding()
|
||||||
|
if not pos or not embedding:
|
||||||
|
raise Exception("Position and embedding required to find outer face")
|
||||||
|
|
||||||
visited: set[tuple[Any, Any]] = set()
|
visited: set[tuple[Any, Any]] = set()
|
||||||
faces: list[list[Any]] = []
|
faces: list[list[Any]] = []
|
||||||
@@ -115,50 +119,12 @@ def outer_face(g: Graph) -> list[Any]:
|
|||||||
|
|
||||||
return min(faces, key=signed_area)
|
return min(faces, key=signed_area)
|
||||||
|
|
||||||
def tutte_embedding(g: Graph, outer: list[Any]) -> dict[Any, tuple[float, float]]:
|
|
||||||
"""Compute a Tutte embedding fixing outer on a convex polygon, solving for inner vertices."""
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
outer_set = set(outer)
|
|
||||||
inner = [v for v in g.vertices() if v not in outer_set]
|
|
||||||
|
|
||||||
pos: dict[Any, tuple[float, float]] = {}
|
|
||||||
for i, v in enumerate(outer):
|
|
||||||
angle = 2 * math.pi * i / len(outer)
|
|
||||||
pos[v] = (math.cos(angle), math.sin(angle))
|
|
||||||
|
|
||||||
if not inner:
|
|
||||||
return pos
|
|
||||||
|
|
||||||
inner_idx = {v: i for i, v in enumerate(inner)}
|
|
||||||
n = len(inner)
|
|
||||||
A = np.zeros((n, n))
|
|
||||||
bx = np.zeros(n)
|
|
||||||
by = np.zeros(n)
|
|
||||||
|
|
||||||
for i, v in enumerate(inner):
|
|
||||||
neighbors = g.neighbors(v)
|
|
||||||
deg = len(neighbors)
|
|
||||||
A[i, i] = 1.0
|
|
||||||
for w in neighbors:
|
|
||||||
if w in inner_idx:
|
|
||||||
A[i, inner_idx[w]] = -1.0 / deg
|
|
||||||
else:
|
|
||||||
bx[i] += pos[w][0] / deg
|
|
||||||
by[i] += pos[w][1] / deg
|
|
||||||
|
|
||||||
x = np.linalg.solve(A, bx)
|
|
||||||
y = np.linalg.solve(A, by)
|
|
||||||
for i, v in enumerate(inner):
|
|
||||||
pos[v] = (float(x[i]), float(y[i]))
|
|
||||||
|
|
||||||
return pos
|
|
||||||
|
|
||||||
def get_embedding_from_pos(g: Graph) -> dict[Any, list[Any]]:
|
def get_embedding_from_pos(g: Graph) -> dict[Any, list[Any]]:
|
||||||
"""Compute a combinatorial planar embedding by sorting each vertex's neighbors by angle."""
|
"""Compute a combinatorial planar embedding by sorting each vertex's neighbors by angle."""
|
||||||
import math
|
pos_or_none = g.get_pos()
|
||||||
pos = g.get_pos()
|
if pos_or_none is None:
|
||||||
|
raise Exception("Cannot get embedding without position")
|
||||||
|
pos: dict[Any, Any] = pos_or_none
|
||||||
return {
|
return {
|
||||||
v: sorted(g.neighbors(v), key=lambda w: math.atan2(pos[w][1] - pos[v][1], pos[w][0] - pos[v][0]))
|
v: sorted(g.neighbors(v), key=lambda w: math.atan2(pos[w][1] - pos[v][1], pos[w][0] - pos[v][0]))
|
||||||
for v in g.vertices()
|
for v in g.vertices()
|
||||||
|
|||||||
+35
-47
@@ -1,57 +1,45 @@
|
|||||||
"""Module for performing tutte embeddings"""
|
"""Module for performing tutte embeddings"""
|
||||||
from typing import Iterable, cast, Any
|
import math
|
||||||
from sage.all import vector, Graph, cos, pi, sin # type: ignore
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from sage.all import Graph # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def create_tutte_embedding(
|
def tutte_embedding(g: Graph, outer: list[Any]) -> dict[Any, tuple[float, float]]:
|
||||||
graph: Graph,
|
"""Compute a Tutte embedding fixing outer on a convex polygon, solving for inner vertices."""
|
||||||
outer_face: list[Any],
|
|
||||||
max_iter: int=50,
|
|
||||||
set_pos: bool = True) -> dict[Any, Any]:
|
|
||||||
"""
|
|
||||||
Performs a tutte force embedding with a prescribed outer face on
|
|
||||||
a given sage graph.
|
|
||||||
|
|
||||||
For a description on tutte embeddings, see the video [here](\
|
outer_set = set(outer)
|
||||||
https://www.youtube.com/watch?v=mEzPPMhR8XE).
|
inner = [v for v in g.vertices() if v not in outer_set]
|
||||||
"""
|
|
||||||
radius = 1000
|
|
||||||
pos: dict[Any, Any] = {}
|
|
||||||
num_outer_points = len(outer_face)
|
|
||||||
for i, v in enumerate(outer_face):
|
|
||||||
pos[v] = (
|
|
||||||
radius * cos((i / num_outer_points) * 2 * pi),
|
|
||||||
radius * sin((i / num_outer_points) * 2 * pi)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start off setting all other points to (0, 0)
|
pos: dict[Any, tuple[float, float]] = {}
|
||||||
num_inner_points = cast(int, graph.order()) - num_outer_points
|
for i, v in enumerate(outer):
|
||||||
|
angle = 2 * math.pi * i / len(outer)
|
||||||
|
pos[v] = (math.cos(angle), math.sin(angle))
|
||||||
|
|
||||||
for i, v in enumerate(cast(Iterable[Any], graph.vertices())): # type: ignore
|
if not inner:
|
||||||
if v in pos:
|
return pos
|
||||||
continue
|
|
||||||
pos[v] = (
|
|
||||||
radius/3 * cos((i / num_inner_points) * 2 * pi),
|
|
||||||
radius/3 * sin((i / num_inner_points) * 2 * pi)
|
|
||||||
)
|
|
||||||
|
|
||||||
i = 0
|
inner_idx = {v: i for i, v in enumerate(inner)}
|
||||||
while i < max_iter:
|
n = len(inner)
|
||||||
for v in cast(Iterable[Any], graph.vertices()): # type: ignore
|
A = np.zeros((n, n))
|
||||||
if v in outer_face:
|
bx = np.zeros(n)
|
||||||
continue
|
by = np.zeros(n)
|
||||||
neighbors = cast(list[Any], graph.neighbors(v)) # type: ignore
|
|
||||||
deg = len(neighbors)
|
|
||||||
x_desired = sum(map(lambda n: pos[n][0], neighbors)) / deg
|
|
||||||
y_desired = sum(map(lambda n: pos[n][1], neighbors)) / deg
|
|
||||||
pos[v] = tuple(
|
|
||||||
vector(pos[v]) - vector([ # type: ignore
|
|
||||||
pos[v][0] - x_desired, pos[v][1] - y_desired
|
|
||||||
])
|
|
||||||
)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if set_pos:
|
for i, v in enumerate(inner):
|
||||||
graph.set_pos(pos) # type: ignore
|
neighbors = g.neighbors(v)
|
||||||
|
deg = len(neighbors)
|
||||||
|
A[i, i] = 1.0
|
||||||
|
for w in neighbors:
|
||||||
|
if w in inner_idx:
|
||||||
|
A[i, inner_idx[w]] = -1.0 / deg
|
||||||
|
else:
|
||||||
|
bx[i] += pos[w][0] / deg
|
||||||
|
by[i] += pos[w][1] / deg
|
||||||
|
|
||||||
|
x = np.linalg.solve(A, bx)
|
||||||
|
y = np.linalg.solve(A, by)
|
||||||
|
for i, v in enumerate(inner):
|
||||||
|
pos[v] = (float(x[i]), float(y[i]))
|
||||||
|
|
||||||
return pos
|
return pos
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ with open("$SCRIPT_DIR/.vscode/settings.json", "w") as f:
|
|||||||
f.write("\n")
|
f.write("\n")
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
make -C "$SCRIPT_DIR/plantri"
|
if command -v nix-shell &>/dev/null; then
|
||||||
|
nix-shell -p gcc --run "make -C \"$SCRIPT_DIR/plantri\""
|
||||||
|
else
|
||||||
|
make -C "$SCRIPT_DIR/plantri"
|
||||||
|
fi
|
||||||
|
|
||||||
"$sage_python_path" -m venv "$SCRIPT_DIR/.venv"
|
"$sage_python_path" -m venv "$SCRIPT_DIR/.venv"
|
||||||
"$VENV_PYTHON" -m pip install -r "$SCRIPT_DIR/requirements.txt"
|
"$VENV_PYTHON" -m pip install -r "$SCRIPT_DIR/requirements.txt"
|
||||||
@@ -116,7 +120,7 @@ _run_sh() {
|
|||||||
_files
|
_files
|
||||||
;;
|
;;
|
||||||
completion)
|
completion)
|
||||||
_values 'shell' 'zsh'
|
_values 'shell' 'zsh' 'bash'
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@@ -124,10 +128,46 @@ _run_sh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compdef _run_sh run.sh
|
compdef _run_sh run.sh
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
cat <<'EOF'
|
||||||
|
_run_sh() {
|
||||||
|
local cur prev words cword
|
||||||
|
_init_completion || return
|
||||||
|
|
||||||
|
local subcmds='init_paper setup sage lint completion'
|
||||||
|
|
||||||
|
if [[ $cword -eq 1 ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case ${words[1]} in
|
||||||
|
init_paper)
|
||||||
|
# free-form paper title, no completion
|
||||||
|
;;
|
||||||
|
setup)
|
||||||
|
case $((cword - 1)) in
|
||||||
|
1) COMPREPLY=( $(compgen -f -- "$cur") ) ;;
|
||||||
|
2) COMPREPLY=( $(compgen -d -- "$cur") ) ;;
|
||||||
|
3) COMPREPLY=() ;; # system name, free-form
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
sage|lint)
|
||||||
|
COMPREPLY=( $(compgen -f -- "$cur") )
|
||||||
|
;;
|
||||||
|
completion)
|
||||||
|
COMPREPLY=( $(compgen -W 'zsh bash' -- "$cur") )
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _run_sh run.sh
|
||||||
EOF
|
EOF
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unsupported shell: $shell. Supported: zsh" >&2
|
echo "Unsupported shell: $shell. Supported: zsh bash" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user