diff --git a/README.md b/README.md new file mode 100644 index 0000000..12d6581 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# math-research + +Personal mathematics research repository by Eric Bauerfeld. Papers are written in AMS-LaTeX using the `amsart` document class. + +## Papers + +### `kempe_style_search_for_smaller_contradiction` +**Humans Suffice: A Novel Proof of the Four Color Theorem** + +An in-progress proof of the Four Color Theorem via a minimal counterexample argument. The paper builds on Kempe's 1879 strategy — establishing valid cases for vertices of degree ≤ 4, then extending the argument to the degree-5 case using properties of non-adjacent degree-5 vertices, merged subgraphs, and locked colorings. + +### `plane_depth_labelling` +**Plane Depth Labelling** + +Early-stage paper. Title and author information set; content in progress. + +## Creating a New Paper + +Use `run.sh` to scaffold a new paper from the AMS-LaTeX template: + +```sh +./run.sh init_paper "Your Paper Title" +``` + +This creates a new directory (name derived from the title) containing a `paper.tex` pre-filled with the title and author. + +## Setup + +The Python library code in `lib/` requires SageMath. To set up the linting environment, run: + +```sh +./run.sh setup +``` + +This creates a `.venv` using the SageMath Python interpreter and installs `pylint` into it. + +## Linting + +To lint the `lib/` directory: + +```sh +./run.sh lint +``` + +This runs `pyright` (via `npx`) and `pylint` using the SageMath Python interpreter. + +## Building + +Papers are compiled with LaTeX. From within a paper directory: + +```sh +latexmk -pdf paper.tex +``` diff --git a/lib/coloring.py b/lib/coloring.py new file mode 100644 index 0000000..fbbf1bb --- /dev/null +++ b/lib/coloring.py @@ -0,0 +1,19 @@ +"""Utilities for converting integer colorings to named colors.""" +from typing import Any + +COLORS = ['blue', 'red', 'green', 'yellow', 'purple'] + +def convert_coloring( + coloring: dict[Any, int], + vertices: list[Any] | None = None +) -> dict[str, list[Any]]: + """Convert an integer coloring dict to a dict mapping color names to vertex lists.""" + colors: dict[str, list[Any]] = {} + for k, v in coloring.items(): + if vertices and k not in vertices: + continue + color = COLORS[v] + if color not in colors: + colors[color] = [] + colors[color].append(k) + return colors diff --git a/lib/dual_graph.py b/lib/dual_graph.py new file mode 100644 index 0000000..6d2c97e --- /dev/null +++ b/lib/dual_graph.py @@ -0,0 +1,29 @@ +"""Utilities for working with planar dual graphs.""" +from typing import Any +from sage.all import Graph + + +def find_vertex_for_dual_face(dual_face: Any) -> Any | None: + """Return the primal vertex shared by all faces in the dual face, or None.""" + shared_vertices = None + for dual_edge in dual_face: + vertices = set(map(lambda e: e[0], dual_edge[0])) + if shared_vertices: + shared_vertices.intersection_update(vertices) + else: + shared_vertices = vertices + if len(shared_vertices) == 1: + return shared_vertices.pop() + return None + + +def _dual_edge_has_vertex(dual_edge: Any, vertex: Any) -> bool: + return any(vertex in edge for dual_vertex in dual_edge for edge in dual_vertex) + + +def find_dual_face_for_removed_vertex(planar_dual: Graph, vertex: Any) -> Any | None: + """Return the dual face of length 5 whose every dual edge contains the given vertex.""" + for dual_face in list[Any](planar_dual.faces()): # type: ignore[call-arg] + if len(dual_face) == 5 and all(_dual_edge_has_vertex(de, vertex) for de in dual_face): + return dual_face + return None diff --git a/lib/edge_for_dual_edge.py b/lib/edge_for_dual_edge.py new file mode 100644 index 0000000..6d1c27c --- /dev/null +++ b/lib/edge_for_dual_edge.py @@ -0,0 +1,12 @@ +"""Utilities for finding primal edges corresponding to dual edges.""" +from typing import Any, cast + +def get_edge_for_dual_edge(dual_edge: Any) -> tuple[int, int, None]: + """Return the primal edge shared by both faces of the given dual edge.""" + edges: list[set[Any]] = [] + for e in (dual_edge[0] + dual_edge[1]): + edge = set(e) + if edge in edges: + return cast(tuple[int, int, None], e) + edges.append(edge) + raise ValueError(f"Error finding edge for {dual_edge}") diff --git a/lib/edge_graph.py b/lib/edge_graph.py new file mode 100644 index 0000000..383f0ea --- /dev/null +++ b/lib/edge_graph.py @@ -0,0 +1,42 @@ +"""Utilities for constructing edge graphs from planar graphs.""" +from typing import Any, cast +from sage.all import Graph + + +def get_edge_graph_pos(edge_graph: Graph, pos_src: dict[Any, Any]) -> dict[Any, tuple[Any, Any]]: + """Return a position dict for edge graph vertices, using midpoints of the source positions.""" + pos: dict[Any, tuple[Any, Any]] = {} + for e in cast(list[Any], edge_graph.vertices()): # type: ignore + pos[e] = ( + (pos_src[e[0]][0] + pos_src[e[1]][0]) / 2, + (pos_src[e[0]][1] + pos_src[e[1]][1]) / 2 + ) + return pos + + +def get_edge_graph(graph: Graph, set_pos: bool = True) -> Graph: + """Return the edge graph of the given graph, optionally setting vertex positions.""" + pos_src: dict[Any, Any] | None = None + pos: dict[Any, tuple[Any, Any]] = {} + if set_pos: + pos_src = cast(dict[Any, Any], graph.get_pos()) # type: ignore + + g = Graph() + for e in cast(list[Any], graph.edges()): # type: ignore + g.add_vertex(e) # type: ignore + if pos_src: + pos[e] = ( + (pos_src[e[0]][0] + pos_src[e[1]][0]) / 2, + (pos_src[e[0]][1] + pos_src[e[1]][1]) / 2 + ) + + for v in cast(list[Any], graph.vertices()): # type: ignore + incident_edges = cast(list[Any], graph.edges_incident(v)) # type: ignore + if len(incident_edges) == 1: + continue + for i, e1 in enumerate(incident_edges): + e2 = incident_edges[(i + 1) % len(incident_edges)] + g.add_edge(e1, e2) # type: ignore + if set_pos: + g.set_pos(pos) # type: ignore + return g diff --git a/lib/tait_coloring.py b/lib/tait_coloring.py new file mode 100644 index 0000000..92251b7 --- /dev/null +++ b/lib/tait_coloring.py @@ -0,0 +1,18 @@ +"""Utilities for computing Tait colorings from vertex colorings of the dual graph.""" +from typing import Any, cast +from sage.all import Graph +from lib.edge_for_dual_edge import get_edge_for_dual_edge + +def get_tait_coloring(planar_dual: Graph, coloring: dict[Any, int]) -> dict[Any, int]: + """Return a Tait (edge 3-coloring) from a vertex 4-coloring of the dual graph.""" + tait_coloring: dict[Any, int] = {} + for dual_edge in cast(list[Any], planar_dual.edges()): # type: ignore + edge = get_edge_for_dual_edge(dual_edge) + colors = {coloring[edge[0]], coloring[edge[1]]} + if colors in ({0, 1}, {2, 3}): + tait_coloring[dual_edge] = 0 + elif colors in ({1, 2}, {0, 3}): + tait_coloring[dual_edge] = 1 + else: + tait_coloring[dual_edge] = 2 + return tait_coloring diff --git a/lib/tutte_embedding.py b/lib/tutte_embedding.py new file mode 100644 index 0000000..a3be232 --- /dev/null +++ b/lib/tutte_embedding.py @@ -0,0 +1,57 @@ +"""Module for performing tutte embeddings""" +from typing import Iterable, cast, Any +from sage.all import vector, Graph, cos, pi, sin # type: ignore + + +def create_tutte_embedding( + graph: Graph, + 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](\ + https://www.youtube.com/watch?v=mEzPPMhR8XE). + """ + 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) + num_inner_points = cast(int, graph.order()) - num_outer_points + + for i, v in enumerate(cast(Iterable[Any], graph.vertices())): # type: ignore + if v in pos: + continue + pos[v] = ( + radius/3 * cos((i / num_inner_points) * 2 * pi), + radius/3 * sin((i / num_inner_points) * 2 * pi) + ) + + i = 0 + while i < max_iter: + for v in cast(Iterable[Any], graph.vertices()): # type: ignore + if v in outer_face: + continue + 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: + graph.set_pos(pos) # type: ignore + + return pos diff --git a/lib/vertex_coloring.py b/lib/vertex_coloring.py new file mode 100644 index 0000000..586296d --- /dev/null +++ b/lib/vertex_coloring.py @@ -0,0 +1,11 @@ +"""Utilities for verifying vertex colorings.""" +from typing import Any, cast +from sage.all import Graph + +def check_vertex_coloring(graph: Graph, vertex_coloring: dict[Any, int]) -> bool: + """Raise ValueError if any two adjacent vertices share a color.""" + for v in cast(list[Any], graph.vertices()): # type: ignore + for nv in cast(list[Any], graph.neighbors(v)): # type: ignore + if vertex_coloring[nv] == vertex_coloring[v]: + raise ValueError(f"Color {nv} equal to color {v}") + return True diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..7bf1c1f --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,10 @@ +{ + "extraPaths": [ + "/Applications/SageMath-10-8.app/Contents/Frameworks/Sage.framework/Versions/10.8/local/lib/python3.13/site-packages" + ], + "reportUnknownMemberType": "warning", + "reportUnknownArgumentType": "warning", + "reportUnknownParameterType": "warning", + "reportMissingParameterType": "warning", + "reportUnknownVariableType": "warning" +} diff --git a/run.sh b/run.sh index b766d23..e98d57e 100755 --- a/run.sh +++ b/run.sh @@ -2,6 +2,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_PATH="/Applications/SageMath-10-8.app/Contents/Frameworks/Sage.framework/Versions/10.8/local/bin/python3" +SAGE_SITE_PACKAGES="/Applications/SageMath-10-8.app/Contents/Frameworks/Sage.framework/Versions/10.8/local/lib/python3.13/site-packages" +VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python3" init_paper() { local raw="${1:-.}" @@ -16,13 +19,31 @@ init_paper() { echo "Initialized paper.tex in $dest" } +setup() { + "$PYTHON_PATH" -m venv "$SCRIPT_DIR/.venv" + "$VENV_PYTHON" -m pip install pylint +} + +lint() { + npx pyright lib/ --pythonpath "$PYTHON_PATH" + "$VENV_PYTHON" -m pylint lib/ \ + --init-hook="import sys; sys.path.insert(0, '${SAGE_SITE_PACKAGES}'); sys.path.insert(0, '${SCRIPT_DIR}')" \ + --disable=fixme +} + case "${1:-}" in init_paper) shift init_paper "$@" ;; +setup) + setup + ;; +lint) + lint + ;; *) - echo "Usage: $0 {init_paper} [dest_dir]" >&2 + echo "Usage: $0 {init_paper|setup|lint} [args]" >&2 exit 1 ;; esac