{"id":16528475,"url":"https://github.com/danvk/computing-in-the-catskills","last_synced_at":"2026-01-15T22:17:03.221Z","repository":{"id":171470397,"uuid":"644940803","full_name":"danvk/computing-in-the-catskills","owner":"danvk","description":"What's wrong with a little math in the woods?","archived":false,"fork":false,"pushed_at":"2024-07-02T18:54:05.000Z","size":35892,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-03T05:41:20.623Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danvk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-24T15:10:49.000Z","updated_at":"2024-07-02T18:54:09.000Z","dependencies_parsed_at":null,"dependency_job_id":"64afb036-b095-41d4-a9d9-bfbb53328f0d","html_url":"https://github.com/danvk/computing-in-the-catskills","commit_stats":null,"previous_names":["danvk/computing-in-the-catskills"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/danvk/computing-in-the-catskills","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcomputing-in-the-catskills","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcomputing-in-the-catskills/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcomputing-in-the-catskills/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcomputing-in-the-catskills/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danvk","download_url":"https://codeload.github.com/danvk/computing-in-the-catskills/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcomputing-in-the-catskills/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28472624,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-15T22:13:38.078Z","status":"ssl_error","status_checked_at":"2026-01-15T22:12:11.737Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T17:40:30.907Z","updated_at":"2026-01-15T22:17:03.204Z","avatar_url":"https://github.com/danvk.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Computing in the Catskills\n\nWhat's wrong with a little math in the woods?\n\nQuestion: What's the least amount of hiking you have to do to hike all [33 of the Catskills High Peaks][peaks]? This turns out to be a surprisingly fun Math / CS problem that's equivalent to either the Traveling Salesman Problem or the Set Cover Problem, two famous problems in Computer Science.\n\nMy solution is available as an [online hike planner][planner], check it out!\n\n## Gallery of solutions\n\n### Shortest hiking distance\n\nThis [set of 11 hikes][geojson-95.2] weighs in at 95.2 miles of total hiking:\n\n[![A set of 11 through hikes at 95.2 miles](/gallery/11-through-hikes-95.2-miles.png)][geojson-95.2]\n\nOut-and-backs:\n\n1. Windham High Peak from Peck Rd (5.94 miles)\n1. Kaaterskill High Peak from Cortina Ln (6.51 miles)\n1. Halcott Mountain from SR 42 (3.13 miles)\n1. Panther Mountain from Giant Ledge Trailhead (6.04 miles)\n1. Balsam Lake Mountain via Dry Brook Ridge (3.27 miles)\n1. Bearpen \u0026 Vly from County Road 3 (6.46 miles)\n\nThrough hikes:\n\n7. Blackhead Range from Barnum Rd to Big Hollow Rd (3 peaks / 6.98 miles)\n7. Devil's Path East from Prediger Rd to Mink Hollow Rd (4 peaks / 8.32)\n7. FirBBiE Loop from Burnham Hollow to McKinley Hollow (4 peaks / 12.23 miles)\n7. The Nine from Oliverea Rd to Peekamoose Rd (9 peaks / 16.47 miles)\n7. Super Spruceton from Shaft Rd to Spruceton Rd (6 peaks / 19.85 miles)\n\n### Shortest hike using only loop / out-and-back hikes\n\nBest solution is 12 hikes / 104.89 miles.\n\n[![A set of 12 loop hikes at 104.89 miles](/gallery/12-loop-hikes-104.9-miles.png)][geojson-104.9]\n\nThe out-and-backs are the same as above, but the through hikes have all been replaced with loops:\n\n7. Blackhead Range from Big Hollow Rd (3 peaks / 7.29 miles)\n7. Devil's Path East from Mink Hollow Rd (4 peaks / 10.31 miles)\n7. Closed FirBBiE loop (4 peaks / 14.61 miles)\n7. The Nine from Slide Mountain PA (9 peaks / 18.7 miles)\n7. Spruceton Horseshoe (4 peaks / 15.45 miles)\n7. Sherrill / North Dome out \u0026 back (2 peaks / 7.17 miles)\n\n## Quickstart\n\n    poetry install\n    # Generate possible hikes\n    poetry run python loops.py\n    # Find the minimal set of hikes\n    poetry run python subset_cover.py\n\nTo regenerate data, see below.\n\n## Notes on the problem\n\nData comes mostly from OpenStreetMap with a few tweaks:\n\n- High peaks have been shifted to be on paths / herd paths.\n- I've added a few strategic bushwhacks using tracks from my [personal experience][blog].\n\nThe result of all this (including parking lots, high peaks, trailheads, trail junctions and hikes between all of them) is in `network+parking.geojson`.\n\nFinding the shortest set of hikes maps onto a few famous problems in Computer Science.\n\n### Traveling Salesman Problem\n\nThe [Traveling Salesman Problem][tsp] asks what the most efficient order is to visit a set of N cities, ending back where you started. To map the Catskills hiking problem onto the TSP:\n\n- Add an artificial node to the graph and connect all parking lots to it with a distance of zero. This simulates free driving between trailheads (since we only care about hiking distance).\n- Use all-pairs shortest paths to construct a subgraph consisting only of the 33 high peaks. The distances between them will reflect segments of longer hikes and they may go between two parking lots (if the fastest way from peak A to peak B is to hike to a parking lot and drive to another trailhead).\n- Run a TSP solver on this subgraph to get an optimal sequence of peaks.\n- Map this solution back onto the larger graph. Trips through the artificial node split the solution into multiple separate hikes.\n\nTo find the 12 hike / 95.4 mile solution I used Google's [OR-Tools] TSP solver.\n\n### Set Cover Problem\n\nThe [Set Cover Problem][scp] is another famous [NP-Complete] problem in Computer Science. It asks you to find the smallest collection of subsets whose union is the complete set.\n\nThe hiking problem maps onto the _Weighted_ Set Cover problem. In our case:\n\n- The \"universe\" is the 33 high peaks.\n- The subsets are possible hikes (the subset being the subset of high peaks that they visit).\n- The weight is the mileage for each hike.\n\nTo answer the hiking question using a Set Cover solver, we first need to generate the set of all possible hikes in the Catskills. With some clever filtering and optimization, this doesn't wind up being too bad (see below).\n\nThen we can run a set cover solver to get our set of hikes directly. This [old Python repo][SetCoverPy] works great. It's very fast compared to the TSP solver (\u003c1s vs. minutes to hours) and finds equally good solutions.\n\nThis approach is nice because it gives us more flexibility to answer variations of the problem. For example, we can only allow loop hikes or set a max distance on any one hike by filtering the set of hikes that we give the solver.\n\n### Generating all possible hikes\n\nThe code for this is in `loops.py`.\n\nIn order to frame shortest hike problems as set cover problems, we need to generate a list of all possible hikes in the Catskills. At first this seems unreasonable since there are so many peaks, so many trailheads and so many trails. For example, the Spruceton Road (Hunter/Westkill) area has 13 high peaks and 27 parking areas. In theory you could hike any subset of these peaks in any order between any two parking areas. That's trillions of possible hikes!\n\nFortunately we can do some aggressive pruning to pare this back:\n\n- Consider each pair of lots and each subset of peaks you can reach from them (this is potentially huge!)\n  - For each of these combinations, there is only one hike worth considering (the shortest one).\n  - If that hike crosses an extra peak, discard it (it might be a reasonable hike, but it will be tracked through a larger subset of peaks).\n\nThese two forms of filtering can be applied recursively. For example, if we're considering hikes that hit peaks 1, 2 and 3, 4 and 5 going from lot A to B (`A→{1,2,3,4,5}→B`), then we can solve the subproblem for each pair of peaks (`1→{2,3,4}→5`, `1→{3,4,5}→2`, etc.) and try adding each of those resulting possibilities to the larger problem. Combined with some memoization, this is extremely effective at efficiently paring back the total number of hikes. From the trillions of possibilities we started with, we only wind up with ~25,000 possible hikes to plug into the set cover problem.\n\n## Data ingestion flow\n\n### Catskills\n\nPull down data from OSM using the Overpass API:\n\n    for query in queries/catskills/*.txt; poetry run python run_overpass_query.py $query\n\nNext, augment the trails with some key bushwhacks that aren't in OSM:\n\n    poetry run python augment_trails.py\n    # produces data/catskills/additional-trails.json (just the extra bushwhacks) and\n    #          data/catskills/combined-trails.json (all the trails)\n\nThis will create fake OSM node and way IDs. These are all negative numbers to distinguish them from real IDs.\n\nNext we filter down to just the relevant peaks for the 3500 Club. This also attaches short codes to each peak (e.g. W for Wittenberg):\n\n    poetry run python filter_to_peak_list.py data/catskills/peaks.json data/catskills/peak-codes-gnis.txt \u003e data/catskills/peaks-3500.json\n\nThe nodes for peaks in OSM tend not to be on trails and in some cases (Vly) they are actually quite far off. The next step is to make a version of the peaks that are connected to the trail graph:\n\n    poetry run python shift_peaks.py data/catskills/peaks-3500.json data/catskills/combined-trails.json \u003e data/catskills/peaks-connected.json\n\nNext we produce the preliminary `network.geojson` file, which connects trailheads to peaks via trails:\n\n    poetry run python extract_network.py data/catskills/{spec.json5,peaks-connected.json,combined-trails.json,roads.json} \u003e data/catskills/network.geojson\n\nThis script also removes lots of trails that don't connect to a high peak. A \"trailhead\" is a node where the road network and the trail network meet. It might be on private land, or it might be in a residential area where you can't park. For that reason it's better to start and end hikes with parking lots:\n\n    poetry run python parking_lots.py data/catskills/{spec.json5,network.geojson,combined-trails.json,roads.json,parking.json,extra-lot-names.json,parking-connections.geojson,network+parking.geojson}\n\nThis is the key file that `loops.py`, `tsp.py` and `subset_cover.py` work off of.\n\nAt this point (optional), you can generate an optimal hike using Traveling Salesman (OR Tools):\n\n    poetry run python tsp.py data/catskills/network+parking.geojson \u003e hikes/tsp.geojson\n\nThis approach is slower and less flexible than Weighted Subset Cover, but it has the advantage of allowing hikes of any length using any number of peaks.\n\nPull in elevation data and add it to the network file:\n\n    poetry run eio clip -o data/catskills/ele.tif --bounds -74.9 41.6 -73.6 42.5\n    poetry run python elevation.py data/catskills/network+parking.geojson data/catskills/ele.tif \u003e data/catskills/network+parking+ele.geojson\n\nGenerate possible hikes:\n\n    poetry run python loops.py data/catskills/spec.json5 data/catskills/network+parking+ele.geojson \u003e data/catskills/hikes.json\n\nRelabel nodes:\n\n    poetry run python relabel_network.py data/catskills/{network+parking+ele.geojson,hikes.json} \u003e data/catskills/network-relabeled.geojson\n\nRegenerate possible hikes with the new labels:\n\n    poetry run python loops.py data/catskills/spec.json5 data/catskills/network-relabeled.geojson \u003e data/catskills/hikes.json\n\nAdd elevation data to these hikes:\n\n    poetry run python add_elevation_to_hikes.py data/catskills/{network-relabeled.geojson,hikes.json} \u003e data/catskills/hikes+ele.json\n\nGenerate optimal set of hikes using subset cover:\n\n    poetry run python all_hikes_subset_cover.py data/catskills/{network-relabeled.geojson,hikes+ele.json} 13 20\n\nThis produces data/hikes/*.geojson, which you can view using [geojson.io](https://geojson.io).\n\n\n### Adirondacks\n\nSimilar to Catskills!\n\n    poetry run python run_overpass_query.py queries/adk/*.txt\n    poetry run python filter_to_peak_list.py data/adk/peaks.json data/adk/peak-codes-gnis.txt \u003e data/adk/peaks-46ers.json\n    poetry run python shift_peaks.py data/adk/peaks-46ers.json data/adk/trails.json \u003e data/adk/peaks-connected.json\n    poetry run python extract_network.py data/adk/{spec.json5,peaks-connected.json,trails.json,roads.json} \u003e data/adk/network.geojson\n    poetry run python parking_lots.py data/adk/{spec.json5,network.geojson,trails.json,roads.json,parking.json,extra-lot-names.json,parking-connections.geojson,network+parking.geojson}\n    poetry run eio clip -o data/adk/ele.tif --bounds -74.3 43.978 -73.613 44.460\n    poetry run python elevation.py data/adk/{network+parking.geojson,ele.tif} \u003e data/adk/network+parking+ele.geojson\n    poetry run python loops.py data/adk/spec.json5 data/adk/network+parking+ele.geojson \u003e data/adk/hikes.json\n    poetry run python relabel_network.py data/adk/{network+parking+ele.geojson,hikes.json} \u003e data/adk/network-relabeled.geojson\n    poetry run python loops.py data/adk/spec.json5 data/adk/network-relabeled.geojson \u003e data/adk/hikes.json\n    poetry run python add_elevation_to_hikes.py data/adk/{network-relabeled.geojson,hikes.json} \u003e data/adk/hikes+ele.json\n\nThen generate some hikes:\n\n    poetry run python all_hikes_subset_cover.py data/adk/{network-relabeled.geojson,hikes+ele.json} 20 5\n\n## Update data for web UI\n\nApply 30mi hard cap on hikes and copy over network data:\n\n    poetry run python cap_hike_length.py data/catskills/hikes+ele.json 30 \u003e ~/github/catskills-hike-planner/data/catskills/hikes.json\n    poetry run python cap_hike_length.py data/adk/hikes+ele.json 30 \u003e ~/github/catskills-hike-planner/data/adk/hikes.json\n    cp data/catskills/network-relabeled.geojson ~/github/catskills-hike-planner/data/catskills/network.geojson\n    cp data/adk/network-relabeled.geojson ~/github/catskills-hike-planner/data/adk/network.geojson\n\nUpdate static assets:\n\n    poetry run python generate_web_data.py data/catskills/network-relabeled.geojson geojson \u003e ~/github/catskills/map-src/public/catskills-high-peaks.geojson\n    poetry run python generate_web_data.py data/adk/network-relabeled.geojson geojson \u003e ~/github/catskills/map-src/public/adk-high-peaks.geojson\n\n[peaks]: http://catskill-3500-club.org/peaks.php\n[geojson-95.2]: https://geojson.io/#id=github:danvk/computing-in-the-catskills/blob/main/gallery/11-through-hikes-95.2-miles.geojson\n[geojson-104.9]: https://geojson.io/#id=github:danvk/computing-in-the-catskills/blob/main/gallery/12-loop-hikes-104.9-miles.geojson\n[blog]: https://www.danvk.org/catskills/\n[tsp]: https://en.wikipedia.org/wiki/Travelling_salesman_problem\n[or-tools]: https://developers.google.com/optimization/routing/tsp\n[scp]: https://en.wikipedia.org/wiki/Set_cover_problem\n[np-complete]: https://en.wikipedia.org/wiki/NP-completeness\n[SetCoverPy]: https://github.com/guangtunbenzhu/SetCoverPy\n[planner]: https://www.danvk.org/catskills/map/planner/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcomputing-in-the-catskills","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanvk%2Fcomputing-in-the-catskills","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcomputing-in-the-catskills/lists"}