Skip to content

Commit

Permalink
Solving d18 with Shapely
Browse files Browse the repository at this point in the history
  • Loading branch information
derailed-dash committed Jan 28, 2024
1 parent f586a35 commit cd6968a
Showing 1 changed file with 106 additions and 38 deletions.
144 changes: 106 additions & 38 deletions src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
"outputs": [],
"source": [
"%pip install jupyterlab-lsp colorama python-dotenv ipykernel ffmpeg graphviz mediapy sympy"
"%pip install jupyterlab-lsp colorama python-dotenv ipykernel ffmpeg graphviz mediapy sympy shapely"
]
},
{
Expand All @@ -76,48 +76,48 @@
"outputs": [],
"source": [
"from __future__ import annotations\n",
"import abc\n",
"import ast\n",
"from collections import Counter, deque, defaultdict\n",
"import copy\n",
"from dataclasses import asdict, dataclass, field\n",
"from typing import Optional, Callable, cast\n",
"from enum import Enum, auto\n",
"from functools import cache, reduce\n",
"from itertools import permutations, combinations, count, cycle\n",
"from collections import Counter, deque, defaultdict\n",
"from io import BytesIO\n",
"import abc\n",
"import ast\n",
"import heapq\n",
"import copy\n",
"import operator\n",
"import imageio.v3 as iio\n",
"from io import BytesIO\n",
"from itertools import permutations, combinations, count, cycle\n",
"import logging\n",
"import time\n",
"import math\n",
"import operator\n",
"import os\n",
"from pathlib import Path\n",
"import platform\n",
"import re\n",
"import sympy\n",
"from sympy.plotting import plot as sympyplot\n",
"import unittest\n",
"import requests\n",
"import imageio.v3 as iio\n",
"import math\n",
"from typing import Optional, Callable, cast\n",
"import sympy\n",
"from tqdm.notebook import tqdm\n",
"\n",
"from colorama import Fore, Back, Style\n",
"from dotenv import load_dotenv\n",
"from getpass import getpass\n",
"import graphviz\n",
"from IPython.display import display, YouTubeVideo\n",
"from IPython.core.display import Markdown\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib.colors as mcolors\n",
"import matplotlib.patches as mpatches\n",
"from matplotlib.animation import FuncAnimation\n",
"from matplotlib.patches import Rectangle\n",
"from matplotlib.markers import MarkerStyle\n",
"from matplotlib import path as pltpath\n",
"import graphviz\n",
"import mediapy as media\n",
"import numpy as np\n",
"import networkx as nx\n",
"import numpy as np\n",
"import pandas as pd\n",
"from tqdm.notebook import tqdm\n",
"from dotenv import load_dotenv\n",
"from pathlib import Path\n",
"from getpass import getpass\n",
"from colorama import Fore, Back, Style\n",
"from IPython.display import display, YouTubeVideo\n",
"from IPython.core.display import Markdown"
"from shapely import Polygon\n",
"from sympy.plotting import plot as sympyplot"
]
},
{
Expand Down Expand Up @@ -6938,10 +6938,12 @@
"\n",
"**If they follow their dig plan, how many cubic meters of lava could it hold?**\n",
"\n",
"**My solution:**\n",
"**My (first) solution:**\n",
"\n",
"My first solution is a flood fill. It works fine for Part 1.\n",
"\n",
"- First, parse the input data. For each instruction in this part, we only care about the direction and distance in that direction.\n",
"Then, let's plot the perimeter path with `process_plan()`:\n",
"- Then, let's plot the perimeter path with `process_plan()`:\n",
" - Create a list to represent the squares of the perimeter - i.e. what we'll dig with the instructions.\n",
" - Start at `(0,0)` and add it to the list.\n",
" - For each instruction, iterate over the required number of squares. For each iteration, add the current direction to the current square. This gives us the new current square. Add it to the `perimeter_path` list.\n",
Expand Down Expand Up @@ -7199,7 +7201,9 @@
"\n",
"Even with the sample data, we're told that our lagoon will hold `952408144115` cubic metres of lava. So clearly we have far too many points to go storing them in sets. Our Part 1 solution isn't going to scale! We need to do something smarter.\n",
"\n",
"We can use math! Here are some key formulae:\n",
"We can use math! I'm going to use the **Shoelace Formula** with **Pick's Theorem**, just as I did in Day 10. \n",
"\n",
"Here are the key formulae:\n",
"\n",
"- [Shoelace Formula](https://en.wikipedia.org/wiki/Shoelace_formula) - to calculate the area of any polygon, given the _consecutive coordinates_ of its vertices. One great thing about this formula is that it even works for self-overlapping / self-intersecting polygons!\n",
"\n",
Expand All @@ -7217,14 +7221,14 @@
"\n",
"```text\n",
"#######\n",
"#######\n",
"#######\n",
"..#####\n",
"..#####\n",
"#######\n",
"#####..\n",
"#######\n",
".######\n",
"#*****#\n",
"###***#\n",
"..#***#\n",
"..#***#\n",
"###*###\n",
"#***#..\n",
"##**###\n",
".#****#\n",
".######\n",
"```\n",
"\n",
Expand Down Expand Up @@ -7404,6 +7408,12 @@
" return total\n",
"\n",
"def interior_points(area: int, boundary_points: int):\n",
" \"\"\" Determine the number of interior points using Pick's Theorem.\n",
"\n",
" Args:\n",
" area (int): total polygon area\n",
" boundary_points (int): the number of boundary points\n",
" \"\"\"\n",
" return area - (boundary_points // 2) + 1\n",
"\n",
"def total_area(polygon: list[tuple[int,int]]) -> int:\n",
Expand Down Expand Up @@ -7453,13 +7463,13 @@
" plan = parse_plan(data)\n",
" perimeter_path = process_turns(plan) \n",
" area = total_area(perimeter_path)\n",
" logger.debug(f\"Part 1: {area=}\")\n",
" logger.info(f\"Part 1: {area=}\")\n",
" plot_path(perimeter_path)\n",
"\n",
" plan = parse_plan_hex(data)\n",
" perimeter_path = process_turns(plan) \n",
" area = total_area(perimeter_path)\n",
" logger.debug(f\"Part 2: {area=}\") \n",
" logger.info(f\"Part 2: {area=}\") \n",
" plot_path(perimeter_path) \n",
" \n",
" return area"
Expand All @@ -7473,7 +7483,8 @@
"source": [
"%%time\n",
"sample_inputs = []\n",
"sample_inputs.append(\"\"\"R 6 (#70c710)\n",
"sample_inputs.append(\"\"\"\\\n",
"R 6 (#70c710)\n",
"D 5 (#0dc571)\n",
"L 2 (#5713f0)\n",
"D 2 (#d2c081)\n",
Expand Down Expand Up @@ -7509,6 +7520,63 @@
"![Dig plan - Part 2](https://aoc.just2good.co.uk/assets/images/lava_lagoon_real_pt2.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Using Shapely Library \n",
"\n",
"I figured, let's try to do this with a standard Python library. So I'm going to use `Shapely`.\n",
"\n",
"The approach is similar to before, but with a few minor changes:\n",
"\n",
"1. We create a `Polygon` object, by passing in the list of tuples of vertices. We determine the vertices just as we did before.\n",
"1. Then, we can obtain the total polygon area using the `area` property of the `Polygon` object. So we don't need to use _Shoelace_ to calculate the area.\n",
"1. We still need to determine the number of interior points and add them to the perimeter channel. So we still need to use `Pick's Theorem`. But we can obtain the perimeter length by simply using the `length` property of our `Polygon`; rather than by adding up the lengths of all the edges.\n",
"1. Now we have our interior points, and we have have the volume of the perimeter. So just add them together to get the answer."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def solve_with_shapely(data) -> list[int]:\n",
" answers = []\n",
" \n",
" for part in (0, 1):\n",
" plan_processor = parse_plan if part == 0 else parse_plan_hex\n",
" perimeter_path = process_turns(plan_processor(data))\n",
" poly = Polygon(perimeter_path)\n",
" int_area = poly.area - (poly.length // 2) + 1 # Pick's Theorem\n",
" answers.append(int(int_area + poly.length))\n",
" \n",
" return answers\n",
"\n",
"sample_input = \"\"\"\\\n",
"R 6 (#70c710)\n",
"D 5 (#0dc571)\n",
"L 2 (#5713f0)\n",
"D 2 (#d2c081)\n",
"R 2 (#59c680)\n",
"D 2 (#411b91)\n",
"L 5 (#8ceee2)\n",
"U 2 (#caa173)\n",
"L 1 (#1b58a2)\n",
"U 2 (#caa171)\n",
"R 2 (#7807d2)\n",
"U 3 (#a77fa3)\n",
"L 2 (#015232)\n",
"U 2 (#7a21e3)\"\"\"\n",
"\n",
"answers = solve_with_shapely(sample_input.splitlines())\n",
"validate(answers[0], 62)\n",
"validate(answers[1], 952408144115)\n",
"logger.info(\"Tests passed!\")\n",
"logger.info(f\"Answers with Shapely={solve_with_shapely(input_data)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down

0 comments on commit cd6968a

Please sign in to comment.