From 83b11a307c7005e689a9251b111a33e9b8651064 Mon Sep 17 00:00:00 2001 From: Isaac Karth Date: Fri, 14 Aug 2020 12:42:57 -0700 Subject: [PATCH 1/4] removed submodule --- wfc | 1 - 1 file changed, 1 deletion(-) delete mode 160000 wfc diff --git a/wfc b/wfc deleted file mode 160000 index 524d284..0000000 --- a/wfc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 524d28483b20a92c99836f717cd5578b0a678612 From 5dec0e559b348ca7fd5978c173db97d14552e345 Mon Sep 17 00:00:00 2001 From: Isaac Karth Date: Fri, 14 Aug 2020 12:43:58 -0700 Subject: [PATCH 2/4] removed submodule --- wfc/.gitignore | 3 + wfc/LICENSE | 21 + wfc/README.md | 50 + wfc/doc/conf.py | 56 + wfc/doc/dot/chain.dot | 16 + wfc/doc/dot/dependency.dot | 37 + wfc/doc/dot/design.dot | 89 + wfc/doc/index.rst | 17 + wfc/images/samples/3Bricks.png | Bin 0 -> 351 bytes wfc/images/samples/Angular.png | Bin 0 -> 179 bytes wfc/images/samples/Castle/bridge.png | Bin 0 -> 129 bytes wfc/images/samples/Castle/data.xml | 150 ++ wfc/images/samples/Castle/ground.png | Bin 0 -> 98 bytes wfc/images/samples/Castle/river.png | Bin 0 -> 105 bytes wfc/images/samples/Castle/riverturn.png | Bin 0 -> 125 bytes wfc/images/samples/Castle/road.png | Bin 0 -> 114 bytes wfc/images/samples/Castle/roadturn.png | Bin 0 -> 129 bytes wfc/images/samples/Castle/t.png | Bin 0 -> 124 bytes wfc/images/samples/Castle/tower.png | Bin 0 -> 117 bytes wfc/images/samples/Castle/wall.png | Bin 0 -> 131 bytes wfc/images/samples/Castle/wallriver.png | Bin 0 -> 148 bytes wfc/images/samples/Castle/wallroad.png | Bin 0 -> 149 bytes wfc/images/samples/Cat.png | Bin 0 -> 262 bytes wfc/images/samples/Cats.png | Bin 0 -> 260 bytes wfc/images/samples/Cave.png | Bin 0 -> 168 bytes wfc/images/samples/Chess.png | Bin 0 -> 106 bytes wfc/images/samples/Circles/b.png | Bin 0 -> 103 bytes wfc/images/samples/Circles/b_half.png | Bin 0 -> 474 bytes wfc/images/samples/Circles/b_i.png | Bin 0 -> 559 bytes wfc/images/samples/Circles/b_quarter.png | Bin 0 -> 509 bytes wfc/images/samples/Circles/data.xml | 238 +++ wfc/images/samples/Circles/w.png | Bin 0 -> 119 bytes wfc/images/samples/Circles/w_half.png | Bin 0 -> 464 bytes wfc/images/samples/Circles/w_i.png | Bin 0 -> 566 bytes wfc/images/samples/Circles/w_quarter.png | Bin 0 -> 554 bytes wfc/images/samples/Circuit/bridge.png | Bin 0 -> 134 bytes wfc/images/samples/Circuit/component.png | Bin 0 -> 103 bytes wfc/images/samples/Circuit/connection.png | Bin 0 -> 168 bytes wfc/images/samples/Circuit/corner.png | Bin 0 -> 116 bytes wfc/images/samples/Circuit/data.xml | 366 ++++ wfc/images/samples/Circuit/dskew.png | Bin 0 -> 190 bytes wfc/images/samples/Circuit/skew.png | Bin 0 -> 180 bytes wfc/images/samples/Circuit/substrate.png | Bin 0 -> 103 bytes wfc/images/samples/Circuit/t.png | Bin 0 -> 133 bytes wfc/images/samples/Circuit/track.png | Bin 0 -> 118 bytes wfc/images/samples/Circuit/transition.png | Bin 0 -> 185 bytes wfc/images/samples/Circuit/turn.png | Bin 0 -> 148 bytes wfc/images/samples/Circuit/viad.png | Bin 0 -> 159 bytes wfc/images/samples/Circuit/vias.png | Bin 0 -> 185 bytes wfc/images/samples/Circuit/wire.png | Bin 0 -> 116 bytes wfc/images/samples/City.png | Bin 0 -> 160 bytes wfc/images/samples/Colored City.png | Bin 0 -> 211 bytes wfc/images/samples/Dungeon.png | Bin 0 -> 205 bytes wfc/images/samples/Fabric.png | Bin 0 -> 120 bytes wfc/images/samples/Flowers.png | Bin 0 -> 271 bytes wfc/images/samples/Forest.png | Bin 0 -> 169 bytes wfc/images/samples/Hogs.png | Bin 0 -> 186 bytes wfc/images/samples/Knot.png | Bin 0 -> 177 bytes wfc/images/samples/Knots/corner.png | Bin 0 -> 155 bytes wfc/images/samples/Knots/cross.png | Bin 0 -> 181 bytes wfc/images/samples/Knots/data.xml | 180 ++ wfc/images/samples/Knots/empty.png | Bin 0 -> 99 bytes wfc/images/samples/Knots/line.png | Bin 0 -> 117 bytes wfc/images/samples/Knots/t.png | Bin 0 -> 146 bytes wfc/images/samples/Lake.png | Bin 0 -> 326 bytes wfc/images/samples/Less Rooms.png | Bin 0 -> 172 bytes wfc/images/samples/Link 2.png | Bin 0 -> 198 bytes wfc/images/samples/Link.png | Bin 0 -> 213 bytes wfc/images/samples/Magic Office.png | Bin 0 -> 216 bytes wfc/images/samples/Maze.png | Bin 0 -> 200 bytes wfc/images/samples/Mazelike.png | Bin 0 -> 250 bytes wfc/images/samples/More Flowers.png | Bin 0 -> 318 bytes wfc/images/samples/Mountains.png | Bin 0 -> 160 bytes wfc/images/samples/Nested.png | Bin 0 -> 195 bytes wfc/images/samples/Office 2.png | Bin 0 -> 332 bytes wfc/images/samples/Office.png | Bin 0 -> 171 bytes wfc/images/samples/Paths.png | Bin 0 -> 173 bytes wfc/images/samples/Platformer.png | Bin 0 -> 451 bytes wfc/images/samples/Qud.png | Bin 0 -> 203 bytes wfc/images/samples/Red Dot.png | Bin 0 -> 167 bytes wfc/images/samples/Red Maze.png | Bin 0 -> 105 bytes wfc/images/samples/Rooms.png | Bin 0 -> 181 bytes wfc/images/samples/Rooms/bend.png | Bin 0 -> 102 bytes wfc/images/samples/Rooms/corner.png | Bin 0 -> 99 bytes wfc/images/samples/Rooms/corridor.png | Bin 0 -> 101 bytes wfc/images/samples/Rooms/data.xml | 112 + wfc/images/samples/Rooms/door.png | Bin 0 -> 97 bytes wfc/images/samples/Rooms/empty.png | Bin 0 -> 101 bytes wfc/images/samples/Rooms/side.png | Bin 0 -> 95 bytes wfc/images/samples/Rooms/t.png | Bin 0 -> 98 bytes wfc/images/samples/Rooms/turn.png | Bin 0 -> 100 bytes wfc/images/samples/Rooms/wall.png | Bin 0 -> 89 bytes wfc/images/samples/Rule 126.png | Bin 0 -> 516 bytes wfc/images/samples/Scaled Maze.png | Bin 0 -> 135 bytes wfc/images/samples/Sewers.png | Bin 0 -> 287 bytes wfc/images/samples/Simple Knot.png | Bin 0 -> 148 bytes wfc/images/samples/Simple Maze.png | Bin 0 -> 99 bytes wfc/images/samples/Simple Wall.png | Bin 0 -> 138 bytes wfc/images/samples/Skew 1.png | Bin 0 -> 253 bytes wfc/images/samples/Skew 2.png | Bin 0 -> 259 bytes wfc/images/samples/Skyline 2.png | Bin 0 -> 6424 bytes wfc/images/samples/Skyline.png | Bin 0 -> 330 bytes wfc/images/samples/Smile City.png | Bin 0 -> 163 bytes wfc/images/samples/Spirals.png | Bin 0 -> 207 bytes wfc/images/samples/Summer/cliff 0.png | Bin 0 -> 4906 bytes wfc/images/samples/Summer/cliff 1.png | Bin 0 -> 5113 bytes wfc/images/samples/Summer/cliff 2.png | Bin 0 -> 4806 bytes wfc/images/samples/Summer/cliff 3.png | Bin 0 -> 5475 bytes wfc/images/samples/Summer/cliffcorner 0.png | Bin 0 -> 4621 bytes wfc/images/samples/Summer/cliffcorner 1.png | Bin 0 -> 4608 bytes wfc/images/samples/Summer/cliffcorner 2.png | Bin 0 -> 4865 bytes wfc/images/samples/Summer/cliffcorner 3.png | Bin 0 -> 4975 bytes wfc/images/samples/Summer/cliffturn 0.png | Bin 0 -> 5251 bytes wfc/images/samples/Summer/cliffturn 1.png | Bin 0 -> 5167 bytes wfc/images/samples/Summer/cliffturn 2.png | Bin 0 -> 5266 bytes wfc/images/samples/Summer/cliffturn 3.png | Bin 0 -> 5198 bytes wfc/images/samples/Summer/data.xml | 138 ++ wfc/images/samples/Summer/grass 0.png | Bin 0 -> 4094 bytes wfc/images/samples/Summer/grasscorner 0.png | Bin 0 -> 5891 bytes wfc/images/samples/Summer/grasscorner 1.png | Bin 0 -> 5942 bytes wfc/images/samples/Summer/grasscorner 2.png | Bin 0 -> 5932 bytes wfc/images/samples/Summer/grasscorner 3.png | Bin 0 -> 5923 bytes wfc/images/samples/Summer/road 0.png | Bin 0 -> 5853 bytes wfc/images/samples/Summer/road 1.png | Bin 0 -> 5848 bytes wfc/images/samples/Summer/road 2.png | Bin 0 -> 5870 bytes wfc/images/samples/Summer/road 3.png | Bin 0 -> 5834 bytes wfc/images/samples/Summer/roadturn 0.png | Bin 0 -> 5763 bytes wfc/images/samples/Summer/roadturn 1.png | Bin 0 -> 5681 bytes wfc/images/samples/Summer/roadturn 2.png | Bin 0 -> 5654 bytes wfc/images/samples/Summer/roadturn 3.png | Bin 0 -> 5645 bytes wfc/images/samples/Summer/water_a 0.png | Bin 0 -> 3962 bytes wfc/images/samples/Summer/water_b 0.png | Bin 0 -> 4339 bytes wfc/images/samples/Summer/water_c 0.png | Bin 0 -> 4337 bytes wfc/images/samples/Summer/watercorner 0.png | Bin 0 -> 4635 bytes wfc/images/samples/Summer/watercorner 1.png | Bin 0 -> 4688 bytes wfc/images/samples/Summer/watercorner 2.png | Bin 0 -> 4918 bytes wfc/images/samples/Summer/watercorner 3.png | Bin 0 -> 5016 bytes wfc/images/samples/Summer/waterside 0.png | Bin 0 -> 4700 bytes wfc/images/samples/Summer/waterside 1.png | Bin 0 -> 5370 bytes wfc/images/samples/Summer/waterside 2.png | Bin 0 -> 4705 bytes wfc/images/samples/Summer/waterside 3.png | Bin 0 -> 4802 bytes wfc/images/samples/Summer/waterturn 0.png | Bin 0 -> 5212 bytes wfc/images/samples/Summer/waterturn 1.png | Bin 0 -> 5173 bytes wfc/images/samples/Summer/waterturn 2.png | Bin 0 -> 5133 bytes wfc/images/samples/Summer/waterturn 3.png | Bin 0 -> 5011 bytes wfc/images/samples/Town.png | Bin 0 -> 214 bytes wfc/images/samples/Trick Knot.png | Bin 0 -> 193 bytes wfc/images/samples/Village.png | Bin 0 -> 173 bytes wfc/images/samples/Water.png | Bin 0 -> 149 bytes wfc/images/samples/blackdots.png | Bin 0 -> 2804 bytes wfc/images/samples/blackdotsladder.png | Bin 0 -> 2831 bytes wfc/images/samples/blackdotsred.png | Bin 0 -> 2808 bytes wfc/images/samples/blackdotsstripe.png | Bin 0 -> 2804 bytes wfc/images/samples/dash.png | Bin 0 -> 2807 bytes wfc/images/samples/extrapolate_1a.png | Bin 0 -> 3147 bytes wfc/images/samples/forest1.png | Bin 0 -> 2912 bytes wfc/images/samples/forest3b.png | Bin 0 -> 3902 bytes wfc/images/samples/forest4c.png | Bin 0 -> 3958 bytes wfc/images/samples/redpath1.png | Bin 0 -> 2903 bytes wfc/images/samples/redpath2.png | Bin 0 -> 3049 bytes wfc/images/samples/trees1.png | Bin 0 -> 3147 bytes wfc/images/samples/village2.png | Bin 0 -> 3267 bytes wfc/images/samples/village3.png | Bin 0 -> 3106 bytes wfc/logs/logs.txt | 1 + wfc/pyproject.toml | 33 + wfc/samples.xml | 3 + wfc/samples_cats.xml | 33 + wfc/samples_original.xml | 75 + wfc/samples_reference.xml | 77 + wfc/samples_reference_continue.xml | 71 + wfc/samples_reference_nohogs.xml | 76 + wfc/samples_test.xml | 3 + wfc/samples_test_ground.xml | 18 + wfc/samples_test_vis.xml | 14 + wfc/setup.py | 6 + wfc/tests/__init__.py | 0 wfc/tests/conftest.py | 13 + wfc/tests/test_wfc_adjacency.py | 35 + wfc/tests/test_wfc_patterns.py | 47 + wfc/tests/test_wfc_solver.py | 203 ++ wfc/tests/test_wfc_tiles.py | 28 + wfc/wfc/__init__.py | 0 wfc/wfc/wfc_adjacency.py | 48 + wfc/wfc/wfc_control.py | 451 ++++ wfc/wfc/wfc_patterns.py | 179 ++ wfc/wfc/wfc_solver.py | 439 ++++ wfc/wfc/wfc_tiles.py | 55 + wfc/wfc/wfc_utilities.py | 58 + wfc/wfc/wfc_visualize.py | 733 +++++++ wfc/wfc1/wfc_adjacency.py | 503 +++++ wfc/wfc1/wfc_control.py | 408 ++++ wfc/wfc1/wfc_example.py | 242 +++ wfc/wfc1/wfc_extra.py | 159 ++ wfc/wfc1/wfc_minizinc.py | 114 + wfc/wfc1/wfc_patterns.py | 412 ++++ wfc/wfc1/wfc_solver.py | 2131 +++++++++++++++++++ wfc/wfc1/wfc_solver_two.py | 535 +++++ wfc/wfc1/wfc_tiles.py | 453 ++++ wfc/wfc1/wfc_utilities.py | 64 + wfc/wfc_run.py | 324 +++ 200 files changed, 9517 insertions(+) create mode 100644 wfc/.gitignore create mode 100644 wfc/LICENSE create mode 100644 wfc/README.md create mode 100644 wfc/doc/conf.py create mode 100644 wfc/doc/dot/chain.dot create mode 100644 wfc/doc/dot/dependency.dot create mode 100644 wfc/doc/dot/design.dot create mode 100644 wfc/doc/index.rst create mode 100644 wfc/images/samples/3Bricks.png create mode 100644 wfc/images/samples/Angular.png create mode 100644 wfc/images/samples/Castle/bridge.png create mode 100644 wfc/images/samples/Castle/data.xml create mode 100644 wfc/images/samples/Castle/ground.png create mode 100644 wfc/images/samples/Castle/river.png create mode 100644 wfc/images/samples/Castle/riverturn.png create mode 100644 wfc/images/samples/Castle/road.png create mode 100644 wfc/images/samples/Castle/roadturn.png create mode 100644 wfc/images/samples/Castle/t.png create mode 100644 wfc/images/samples/Castle/tower.png create mode 100644 wfc/images/samples/Castle/wall.png create mode 100644 wfc/images/samples/Castle/wallriver.png create mode 100644 wfc/images/samples/Castle/wallroad.png create mode 100644 wfc/images/samples/Cat.png create mode 100644 wfc/images/samples/Cats.png create mode 100644 wfc/images/samples/Cave.png create mode 100644 wfc/images/samples/Chess.png create mode 100644 wfc/images/samples/Circles/b.png create mode 100644 wfc/images/samples/Circles/b_half.png create mode 100644 wfc/images/samples/Circles/b_i.png create mode 100644 wfc/images/samples/Circles/b_quarter.png create mode 100644 wfc/images/samples/Circles/data.xml create mode 100644 wfc/images/samples/Circles/w.png create mode 100644 wfc/images/samples/Circles/w_half.png create mode 100644 wfc/images/samples/Circles/w_i.png create mode 100644 wfc/images/samples/Circles/w_quarter.png create mode 100644 wfc/images/samples/Circuit/bridge.png create mode 100644 wfc/images/samples/Circuit/component.png create mode 100644 wfc/images/samples/Circuit/connection.png create mode 100644 wfc/images/samples/Circuit/corner.png create mode 100644 wfc/images/samples/Circuit/data.xml create mode 100644 wfc/images/samples/Circuit/dskew.png create mode 100644 wfc/images/samples/Circuit/skew.png create mode 100644 wfc/images/samples/Circuit/substrate.png create mode 100644 wfc/images/samples/Circuit/t.png create mode 100644 wfc/images/samples/Circuit/track.png create mode 100644 wfc/images/samples/Circuit/transition.png create mode 100644 wfc/images/samples/Circuit/turn.png create mode 100644 wfc/images/samples/Circuit/viad.png create mode 100644 wfc/images/samples/Circuit/vias.png create mode 100644 wfc/images/samples/Circuit/wire.png create mode 100644 wfc/images/samples/City.png create mode 100644 wfc/images/samples/Colored City.png create mode 100644 wfc/images/samples/Dungeon.png create mode 100644 wfc/images/samples/Fabric.png create mode 100644 wfc/images/samples/Flowers.png create mode 100644 wfc/images/samples/Forest.png create mode 100644 wfc/images/samples/Hogs.png create mode 100644 wfc/images/samples/Knot.png create mode 100644 wfc/images/samples/Knots/corner.png create mode 100644 wfc/images/samples/Knots/cross.png create mode 100644 wfc/images/samples/Knots/data.xml create mode 100644 wfc/images/samples/Knots/empty.png create mode 100644 wfc/images/samples/Knots/line.png create mode 100644 wfc/images/samples/Knots/t.png create mode 100644 wfc/images/samples/Lake.png create mode 100644 wfc/images/samples/Less Rooms.png create mode 100644 wfc/images/samples/Link 2.png create mode 100644 wfc/images/samples/Link.png create mode 100644 wfc/images/samples/Magic Office.png create mode 100644 wfc/images/samples/Maze.png create mode 100644 wfc/images/samples/Mazelike.png create mode 100644 wfc/images/samples/More Flowers.png create mode 100644 wfc/images/samples/Mountains.png create mode 100644 wfc/images/samples/Nested.png create mode 100644 wfc/images/samples/Office 2.png create mode 100644 wfc/images/samples/Office.png create mode 100644 wfc/images/samples/Paths.png create mode 100644 wfc/images/samples/Platformer.png create mode 100644 wfc/images/samples/Qud.png create mode 100644 wfc/images/samples/Red Dot.png create mode 100644 wfc/images/samples/Red Maze.png create mode 100644 wfc/images/samples/Rooms.png create mode 100644 wfc/images/samples/Rooms/bend.png create mode 100644 wfc/images/samples/Rooms/corner.png create mode 100644 wfc/images/samples/Rooms/corridor.png create mode 100644 wfc/images/samples/Rooms/data.xml create mode 100644 wfc/images/samples/Rooms/door.png create mode 100644 wfc/images/samples/Rooms/empty.png create mode 100644 wfc/images/samples/Rooms/side.png create mode 100644 wfc/images/samples/Rooms/t.png create mode 100644 wfc/images/samples/Rooms/turn.png create mode 100644 wfc/images/samples/Rooms/wall.png create mode 100644 wfc/images/samples/Rule 126.png create mode 100644 wfc/images/samples/Scaled Maze.png create mode 100644 wfc/images/samples/Sewers.png create mode 100644 wfc/images/samples/Simple Knot.png create mode 100644 wfc/images/samples/Simple Maze.png create mode 100644 wfc/images/samples/Simple Wall.png create mode 100644 wfc/images/samples/Skew 1.png create mode 100644 wfc/images/samples/Skew 2.png create mode 100644 wfc/images/samples/Skyline 2.png create mode 100644 wfc/images/samples/Skyline.png create mode 100644 wfc/images/samples/Smile City.png create mode 100644 wfc/images/samples/Spirals.png create mode 100644 wfc/images/samples/Summer/cliff 0.png create mode 100644 wfc/images/samples/Summer/cliff 1.png create mode 100644 wfc/images/samples/Summer/cliff 2.png create mode 100644 wfc/images/samples/Summer/cliff 3.png create mode 100644 wfc/images/samples/Summer/cliffcorner 0.png create mode 100644 wfc/images/samples/Summer/cliffcorner 1.png create mode 100644 wfc/images/samples/Summer/cliffcorner 2.png create mode 100644 wfc/images/samples/Summer/cliffcorner 3.png create mode 100644 wfc/images/samples/Summer/cliffturn 0.png create mode 100644 wfc/images/samples/Summer/cliffturn 1.png create mode 100644 wfc/images/samples/Summer/cliffturn 2.png create mode 100644 wfc/images/samples/Summer/cliffturn 3.png create mode 100644 wfc/images/samples/Summer/data.xml create mode 100644 wfc/images/samples/Summer/grass 0.png create mode 100644 wfc/images/samples/Summer/grasscorner 0.png create mode 100644 wfc/images/samples/Summer/grasscorner 1.png create mode 100644 wfc/images/samples/Summer/grasscorner 2.png create mode 100644 wfc/images/samples/Summer/grasscorner 3.png create mode 100644 wfc/images/samples/Summer/road 0.png create mode 100644 wfc/images/samples/Summer/road 1.png create mode 100644 wfc/images/samples/Summer/road 2.png create mode 100644 wfc/images/samples/Summer/road 3.png create mode 100644 wfc/images/samples/Summer/roadturn 0.png create mode 100644 wfc/images/samples/Summer/roadturn 1.png create mode 100644 wfc/images/samples/Summer/roadturn 2.png create mode 100644 wfc/images/samples/Summer/roadturn 3.png create mode 100644 wfc/images/samples/Summer/water_a 0.png create mode 100644 wfc/images/samples/Summer/water_b 0.png create mode 100644 wfc/images/samples/Summer/water_c 0.png create mode 100644 wfc/images/samples/Summer/watercorner 0.png create mode 100644 wfc/images/samples/Summer/watercorner 1.png create mode 100644 wfc/images/samples/Summer/watercorner 2.png create mode 100644 wfc/images/samples/Summer/watercorner 3.png create mode 100644 wfc/images/samples/Summer/waterside 0.png create mode 100644 wfc/images/samples/Summer/waterside 1.png create mode 100644 wfc/images/samples/Summer/waterside 2.png create mode 100644 wfc/images/samples/Summer/waterside 3.png create mode 100644 wfc/images/samples/Summer/waterturn 0.png create mode 100644 wfc/images/samples/Summer/waterturn 1.png create mode 100644 wfc/images/samples/Summer/waterturn 2.png create mode 100644 wfc/images/samples/Summer/waterturn 3.png create mode 100644 wfc/images/samples/Town.png create mode 100644 wfc/images/samples/Trick Knot.png create mode 100644 wfc/images/samples/Village.png create mode 100644 wfc/images/samples/Water.png create mode 100644 wfc/images/samples/blackdots.png create mode 100644 wfc/images/samples/blackdotsladder.png create mode 100644 wfc/images/samples/blackdotsred.png create mode 100644 wfc/images/samples/blackdotsstripe.png create mode 100644 wfc/images/samples/dash.png create mode 100644 wfc/images/samples/extrapolate_1a.png create mode 100644 wfc/images/samples/forest1.png create mode 100644 wfc/images/samples/forest3b.png create mode 100644 wfc/images/samples/forest4c.png create mode 100644 wfc/images/samples/redpath1.png create mode 100644 wfc/images/samples/redpath2.png create mode 100644 wfc/images/samples/trees1.png create mode 100644 wfc/images/samples/village2.png create mode 100644 wfc/images/samples/village3.png create mode 100644 wfc/logs/logs.txt create mode 100644 wfc/pyproject.toml create mode 100644 wfc/samples.xml create mode 100644 wfc/samples_cats.xml create mode 100644 wfc/samples_original.xml create mode 100644 wfc/samples_reference.xml create mode 100644 wfc/samples_reference_continue.xml create mode 100644 wfc/samples_reference_nohogs.xml create mode 100644 wfc/samples_test.xml create mode 100644 wfc/samples_test_ground.xml create mode 100644 wfc/samples_test_vis.xml create mode 100644 wfc/setup.py create mode 100644 wfc/tests/__init__.py create mode 100644 wfc/tests/conftest.py create mode 100644 wfc/tests/test_wfc_adjacency.py create mode 100644 wfc/tests/test_wfc_patterns.py create mode 100644 wfc/tests/test_wfc_solver.py create mode 100644 wfc/tests/test_wfc_tiles.py create mode 100644 wfc/wfc/__init__.py create mode 100644 wfc/wfc/wfc_adjacency.py create mode 100644 wfc/wfc/wfc_control.py create mode 100644 wfc/wfc/wfc_patterns.py create mode 100644 wfc/wfc/wfc_solver.py create mode 100644 wfc/wfc/wfc_tiles.py create mode 100644 wfc/wfc/wfc_utilities.py create mode 100644 wfc/wfc/wfc_visualize.py create mode 100644 wfc/wfc1/wfc_adjacency.py create mode 100644 wfc/wfc1/wfc_control.py create mode 100644 wfc/wfc1/wfc_example.py create mode 100644 wfc/wfc1/wfc_extra.py create mode 100644 wfc/wfc1/wfc_minizinc.py create mode 100644 wfc/wfc1/wfc_patterns.py create mode 100644 wfc/wfc1/wfc_solver.py create mode 100644 wfc/wfc1/wfc_solver_two.py create mode 100644 wfc/wfc1/wfc_tiles.py create mode 100644 wfc/wfc1/wfc_utilities.py create mode 100644 wfc/wfc_run.py diff --git a/wfc/.gitignore b/wfc/.gitignore new file mode 100644 index 0000000..97594ab --- /dev/null +++ b/wfc/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +/output/* +/build diff --git a/wfc/LICENSE b/wfc/LICENSE new file mode 100644 index 0000000..5842edf --- /dev/null +++ b/wfc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Isaac Karth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wfc/README.md b/wfc/README.md new file mode 100644 index 0000000..7987d27 --- /dev/null +++ b/wfc/README.md @@ -0,0 +1,50 @@ +# wfc_2019f + +This is my research implementation of WaveFunctionCollapse in Python. It has two goals: + +* Make it easier to understand how the algorithm operates +* Provide a testbed for experimenting with alternate heuristics and features + +For more general-purpose WFC information, the original reference repository remains the best resource: https://github.com/mxgmn/WaveFunctionCollapse + +## Running WFC + +If you want direct control over running WFC, call `wfc_control.execute_wfc()`. + +The arguments it accepts are: + +- `filename`: path to the input image file +- `tile_size=1`: size of the tiles it uses (1 is fine for pixel images, larger is for things like a Super Metroid map) +- `pattern_width=2`: size of the patterns; usually 2 or 3 because bigger gets slower and +- `rotations=8`: how many reflections and/or rotations to use with the patterns +- `output_size=[48,48]`: how big the output image is +- `ground=None`: which patterns should be placed along the bottom-most line +- `attempt_limit=10`: stop after this many tries +- `output_periodic=True`: the output wraps at the edges +- `input_periodic=True`: the input wraps at the edges +- `loc_heuristic="lexical"`: what location heuristic to use; `entropy` is the original WFC behavior. The heuristics that are implemented are `lexical`, `hilbert`, `spiral`, `entropy`, `anti-entropy`, `simple`, `random`, but when in doubt stick with `entropy`. +- `choice_heuristic="lexical"`: what choice heuristic to use; `weighted` is the original WFC behavior. +- `visualize=True`: write intermediate images to disk? +- `global_constraint=False`: what global constraint to use. Currently the only one implemented is `allpatterns` +- `backtracking=False`: do we use backtracking if we run into a contradiction? +- `log_filename="log"`: what should the log file be named? +- `logging=True`: should we write to a log file? +- `log_stats_to_output=None` + +## Test + +``` +pytest +``` + +## Documentation + +``` +python setup.py build_sphinx +``` + +With linux the doculentation can be displayed with: + +``` +xdg-open build/sphinx/index.html +``` diff --git a/wfc/doc/conf.py b/wfc/doc/conf.py new file mode 100644 index 0000000..8a44009 --- /dev/null +++ b/wfc/doc/conf.py @@ -0,0 +1,56 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'wfc_python' +copyright = '2020, Isaac Karth' +author = 'Isaac Karth' + +# The full version, including alpha/beta/rc tags +release = '0.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.graphviz', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['static'] \ No newline at end of file diff --git a/wfc/doc/dot/chain.dot b/wfc/doc/dot/chain.dot new file mode 100644 index 0000000..025a6b9 --- /dev/null +++ b/wfc/doc/dot/chain.dot @@ -0,0 +1,16 @@ +digraph { + read_xml_command -> import_image -> make_tile_catalog -> make_pattern_catalog -> make_adjacency_matrix -> solve_constraint_problem -> output_solution_image + make_tile_catalog -> output_solution_image + make_tile_catalog -> instrumentation [color=gray] + make_pattern_catalog -> instrumentation [color=gray] + make_adjacency_matrix -> instrumentation [color=gray] + solve_constraint_problem -> instrumentation [color=gray] + output_solution_image -> visualization [color=gray] + make_tile_catalog -> visualization [color=gray] + make_pattern_catalog -> visualization [color=gray] + make_adjacency_matrix -> visualization [color=gray] + solve_constraint_problem -> visualization [color=gray] + visualization [color=gray, fontcolor=gray] + instrumentation [color=gray, fontcolor=gray] + visualization -> make_tile_catalog [color=magenta] +} \ No newline at end of file diff --git a/wfc/doc/dot/dependency.dot b/wfc/doc/dot/dependency.dot new file mode 100644 index 0000000..642eb85 --- /dev/null +++ b/wfc/doc/dot/dependency.dot @@ -0,0 +1,37 @@ +digraph { + wfc_run -> wfc_control + wfc_control -> wfc_utilities + wfc_control -> wfc_solver + wfc_solver -> numpy + wfc_tiles -> numpy + wfc_patterns -> numpy + wfc_tiles -> wfc_utilities + wfc_control -> wfc_tiles + wfc_control -> wfc_patterns + wfc_patterns -> wfc_utilities + wfc_tiles -> imageio + wfc_control -> wfc_adjacency + wfc_control -> wfc_visualize + wfc_visualize -> matplotlib + wfc_visualize -> wfc_utilities + wfc_adjacency -> wfc_utilities + wfc_adjacency -> numpy + wfc_control -> wfc_instrumentation + + implemented [style=filled, fillcolor=gray] + partial [style=filled, fillcolor=cyan] + unimplemented [style=filled, fillcolor=firebrick] + wfc_run + wfc_control [] + wfc_solver + numpy [color=gray, fontcolor=gray] + wfc_tiles + wfc_patterns [style=filled, fillcolor=cyan] + wfc_utilities + imageio [color=gray, fontcolor=gray] + wfc_adjacency + wfc_visualize [style=filled, fillcolor=cyan] + matplotlib [color=gray, fontcolor=gray] + wfc_instrumentation [style=filled, fillcolor=firebrick] + label="Modules in WFC 19f" +} \ No newline at end of file diff --git a/wfc/doc/dot/design.dot b/wfc/doc/dot/design.dot new file mode 100644 index 0000000..a5c11a3 --- /dev/null +++ b/wfc/doc/dot/design.dot @@ -0,0 +1,89 @@ +digraph { + things_to_implement [label="{Things that aren't implemented yet|Intermediate visualization|timing and profiling|performance statistics|outputting images|most heuristics|removing ground patterns|rotated patterns}", shape=record, fillcolor="cyan", style=filled] + read_data [label="Read data from XML", fillcolor="cyan", shape=box, style=filled] + read_data -> input_data + input_data [shape=record, label="XML"] + input_data -> execute_wfc + solver [label="Solver", shape=house] + solver -> make_wave + make_wave -> remove_patterns + remove_patterns [label="Remove ground patterns", fillcolor="cyan", style=filled] + input_data -> remove_patterns + input_data -> solver + remove_patterns -> solver_run [headport=n] + subgraph cluster_solver_run { + label="wfc_solver.py" + + solver_run [label="solver.run()"] + solver_observe [label="solver.observe()"] + solver_propagate [label="solver.propagate()"] + solver_on_backtrack [label="solver.onBacktrack()", shape=invhouse] + solver_on_choice [label="solver.onChoice()", shape=invhouse] + on_choice [label="onChoice()", shape=note] + on_backtrack [label="onBacktrack()", shape=note, fillcolor="cyan", style=filled] + solver_if_backtracking [label="if backtracking", shape=diamond] + pattern_heuristic [label="pattern heuristic", shape=note] + location_heuristic [label="location heuristic", shape=note] + + + {rank=same pattern_heuristic location_heuristic} + solver_run -> solver_check_feasible + solver_check_feasible -> solver_propagate + solver_propagate -> solver_observe + solver_observe -> pattern_heuristic + solver_observe -> location_heuristic + solver_observe -> solver_on_choice + solver_on_choice -> on_choice + solver_recurse -> except_contradictions [color=red] + solver_on_choice -> solver_if_finished + solver_recurse -> solver_run [headport=n, tailport=w] + solver_if_finished [shape=diamond] + solver_if_finished -> solver_recurse [splines=polyline, dir=both, arrowhead=dotvee, arrowtail=dot, tailport=s, headport=n, color="black:green:black"] + except_contradictions -> solver_if_backtracking + solver_if_backtracking -> solver_on_backtrack [label="Yes"] + solver_on_backtrack -> on_backtrack + on_backtrack -> solver_run [headport=n] + solver_if_backtracking -> cant_solve [splines=curved, label="No", dir=both, arrowhead=dotvee, arrowtail=dot, tailport=e, headport=ne, color="grey"] + } + solver_if_finished -> solver_solution [tailport=w, color="black:blue:black"] + + execute_wfc [shape=invhouse, fillcolor="cyan", style=filled] + execute_wfc -> import_image + import_image [shape=box] + import_image -> make_tile_catalog + subgraph cluster_tile_py { + label="wfc_tiles.py" + make_tile_catalog -> image_to_tiles + } + image_to_tiles -> tile_catalog + tile_catalog [label="Tile Catalog|{dictionary of tiles|image in tile IDs|set of tiles|frequency of tile occurance}", shape=record] + subgraph cluster_patterns { + label="wfc_patterns.py" + tile_catalog -> make_pattern_catalog + {rank=same unique_patterns_2d rotate_or_reflect} + make_pattern_catalog -> unique_patterns_2d -> rotate_or_reflect -> unique_patterns_2d + make_pattern_catalog [fillcolor="cyan", style=filled] + rotate_or_reflect [fillcolor="cyan", style=filled] + } + unique_patterns_2d -> pattern_catalog + pattern_catalog [label="Pattern Catalog|{dictionary of patterns|ordered list of pattern weights|ordered list of pattern contents}", shape=record] + pattern_catalog -> extract_adjacency + subgraph cluster_adjacency { + extract_adjacency -> is_valid_overlap + } + extract_adjacency -> adjacency_relations + adjacency_relations [label="{Adjacency Relations|tuples of (edge,pattern,pattern)}", shape=record] + adjacency_relations -> combine_inputs + combine_inputs -> adjacency_matrix + adjacency_matrix [label="{Adjacency Matrix|boolean matrix of pattern x pattern x direction}", shape=record] + adjacency_matrix -> solver + pattern_catalog -> solver + cant_solve [label="Can't Solve", shape=box] + solver_solution [shape=record, label="Solution|grid of pattern IDs"] + solver_solution -> visualizer + visualizer -> output_image + output_image [shape=box, label="Output Image", style=filled, fillcolor=cyan] + pattern_catalog -> visualizer + tile_catalog -> visualizer + visualizer [fillcolor=cyan, style=filled] +} diff --git a/wfc/doc/index.rst b/wfc/doc/index.rst new file mode 100644 index 0000000..f2413fd --- /dev/null +++ b/wfc/doc/index.rst @@ -0,0 +1,17 @@ +Documentation +============= + +Module dependencies +------------------- + +.. graphviz:: dot/dependency.dot + +Design +------ + +.. graphviz:: dot/design.dot + +Chain +----- + +.. graphviz:: dot/chain.dot diff --git a/wfc/images/samples/3Bricks.png b/wfc/images/samples/3Bricks.png new file mode 100644 index 0000000000000000000000000000000000000000..33205e56fa70e421a3c97c1ad832c6fcdb3e984a GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvg8-ipS8YLta4m+ZUO+xkphW4` zT%bbE0*}aI1_o{+5N5n|x9$&6@T#YaV~EDY+=&-?k0|gs3#)Z(cHH!TLi+5wIqTZ5 zu<@MGS9?@G=dNts&hm&GaXa!Jb7?15rLDgp!5NdJQNf&EB4}fH=g|{}?nwc0%DaD_ zu1ajtIk(6&Ih)hDJvW@|!J`{L54lQrG2P{7Z1PEwNHSYn?PSt&SxU3S_yONl^W&2b z*E}`c&EG7=AAQdztoL|ktiF0E?#Q{qzExIzGjrX86j1PZy85}S Ib4q9e0GqFOGynhq literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Angular.png b/wfc/images/samples/Angular.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8efd64a042f088ca8e64fd1998bb18dcb19361 GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sDEfH31!Z9ZwBpogc6V~9p@@*jQ{9v7xHU6PWL3w2l+G!L)* zv9^b6%JBmVf1dC~7z$7E7g2~cJQ0=B!`*PV=bc-LL$&$gxsDtSa~z#`j0_kU-u=~L U-ROVr9MDV#Pgg&ebxsLQ0R2KT`2YX_ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/bridge.png b/wfc/images/samples/Castle/bridge.png new file mode 100644 index 0000000000000000000000000000000000000000..737c692ca6f759e70cccc120eb483463db4eb023 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP|(=Z z#W93qX7Zdu7n8^LQrT1toIR(1{O@VU#&h7uf8k{c%)(-ZF^hC~u5X-i+UT>n^)hLO Y{R??dWOT%=0cvOPboFyt=akR{0H@9+H2?qr literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/data.xml b/wfc/images/samples/Castle/data.xml new file mode 100644 index 0000000..0fdaf8d --- /dev/null +++ b/wfc/images/samples/Castle/data.xml @@ -0,0 +1,150 @@ +<<<<<<< HEAD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Castle/ground.png b/wfc/images/samples/Castle/ground.png new file mode 100644 index 0000000000000000000000000000000000000000..8eff3f28ccd8df9c2e3bc14aff61efb559ff1786 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*B9v r#W93qX7Zdumnn?Q28mo3V?`JqWHZ)3j^vaA$}@Pn`njxgN@xNAe2x{u literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/river.png b/wfc/images/samples/Castle/river.png new file mode 100644 index 0000000000000000000000000000000000000000..5c5f5f6d5327b116e736e756c96d9d6290022db6 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*B>_ y#W93qX7Zdu7n8^LQrT1toIR&^GD>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*C5~ z#W93qX7Zdu7n8^LQrT1toIR&^GBT&!+1=whrLmE5`wJe`*f&`hRU#&=ZQIOvzm(Ho T>gdsapjHM?S3j3^P6>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*BCw z#W93qX7Zdumnn^nim~ranL*(28Sh0rJUl);HyCPN89O;y>U|_xZUWUac)I$ztaD0e F0ss^v8e{+f literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/roadturn.png b/wfc/images/samples/Castle/roadturn.png new file mode 100644 index 0000000000000000000000000000000000000000..77a0f33daf5936315090aa1ba74599feace9d20d GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP|(=Z z#W93qX7Zdumnn^nim~ranL*(28Sh0rJUl);HyF63=+3A}0U< literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/t.png b/wfc/images/samples/Castle/t.png new file mode 100644 index 0000000000000000000000000000000000000000..77aa8e7ca3eb355536b20cd5edf0b52a31f585e3 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*Bg) z#W93qX7Zdumnn?Q28mpT1_lQI{>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP*B~| z#W93qW^w`;tXRLkeKHRZPtOS%j*Og~6c^z|)1uhc>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP|(!V z#W93qX7Zdu7n8%!N^5IvnL)rgIr(vq@}eH&z7D(lbB{K#sKhQebbRvANx*FF6()u# X71@X7W>U&P0~kDA{an^LB{Ts5wFx7^ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/wallriver.png b/wfc/images/samples/Castle/wallriver.png new file mode 100644 index 0000000000000000000000000000000000000000..a17adce1825ada4512861b98ce9186b601486881 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP|(fO z#W93qX7ZdumoGox>9DCNytL|_n0S%9&7$+yud0rKJD@KaozeTVkKe4vR8p00i_>zopr01~|{BLDyZ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Castle/wallroad.png b/wfc/images/samples/Castle/wallroad.png new file mode 100644 index 0000000000000000000000000000000000000000..a306816a0b168d390c36b90b59cf05013f9d942e GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1SGu;4zvI%&H|6fVg?3oArNM~bhqvgP|)4e z#W93qX7Zdu7n8%!N^5IvnL)rgIr(vq@}eH&z7D(lbB{K#sKhQe{P+LJffL6Y)_nV~ q&Zfqe!?0VUtazqE+QC+Z9ENl1QlUHkFFOV_lEKr}&t;ucLK6T>Sud6V literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Cat.png b/wfc/images/samples/Cat.png new file mode 100644 index 0000000000000000000000000000000000000000..fc3f22088556e3e88b841ab6b7f3e4f6b8607d79 GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9Q!3HFy+4N(86k~CayA#8@b22Z19F}xPUq=Rp zjs4tz5?O(K&H|6fVg?4jBOuH;Rhv&5C^*~G#WBRff9>Q(z7_)x=k|~L&G-M=!X4Uo z@ZF+)DT#u&3?Dry4v0&*7~!&HeSJq(c<*7hP1j= zwFUgY!von&US56o=|qPz!@|I|rhBudarahqwLa2d-`5uV_3^Sow(O@D5+A(m$@zWr zUeP|&|J*4|2V@qV(pP?PHZ4NfV9&pt8FK2!lvI6;>1s;*b3=DinK$vl=HlH+5aHgk=V~B|CbWZfwgH_7A1TP$t6;9}3;3@MxkS)TQ+H>}z?c^1A)~2BR0pkS1z zi(`n!#HAAt3Na|KxJ3W|f8J__l#}6G-H&0nCG3-)oepHF=Y8;ET9+h8BHNy{6}xpS zFYjK(H#_6NP46xBCl9K8J0fov^5)&zB?~jxFdSO*cuL7?k=OEH*K*!WR~M|%|Jehy Og~8L+&t;ucLK6T|nLNY* literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Chess.png b/wfc/images/samples/Chess.png new file mode 100644 index 0000000000000000000000000000000000000000..090ce477b28e05dfdead9e967e6274c408062e91 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oArNM~bhqvgP*BFx z#W93qX7Z2!|LvKXnbl4flP2^nwWajZGFfg82)bS6KltHedKoEwjx?6XckkBzvuOK0wL0xzb^aAbKTzCZ* zE;O+d*2K6r;s*I=Q3>NPnM`ILZ%3_2L46-p1x?IM-xkcQMx&8jb&e*YAPCfSI_(zy z8ME0;B}qa=N~vzq0*vEWEtg9o0?@5MfUDJtilVUAQm1PWl(n`f3Ied(?TE-2)0IF_ z+wB$r0k~W)!{LxRYC<4`!Qgy81E8v^0POdB$$niR5a%3#s;a22YXD_gE*1;=@}Q(V zNs{OD381cP0`Pvn0eJ6a+yCv5LTl~ydTp=-Kzad?WmyzO(&j(ygPCPAQ50oa)&PGB znpB?WaU2to*4h~JR}C{8W3<*pG@sA&JpZKsB4}>BUN7fdGbp8$*1Bb4X03Je{~KeR zb7fh!=xu=vPM+}Ir)e67A^qX=m1G!(X_|WP1ub*`4F3I)knJm_nE8Ic9}b7(@pw9& zZnxXx@gSlg2qu$Bk|gW(db8P#$K%hE_Iq@G3bmr+L$nw4f?m)IdcpU^4_jL$?daPE Q_y7O^07*qoM6N<$f_C)PPXGV_ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circles/b_i.png b/wfc/images/samples/Circles/b_i.png new file mode 100644 index 0000000000000000000000000000000000000000..cc5e2057f6c9abd19982e2bf26fdb65c5faee7ca GIT binary patch literal 559 zcmV+~0?_@5P)!*LAZjBLJJthKQt; zx&#zzyBMmifU2qju-$Hz_xl105{4mws;a1I8URI6%w{wC zcA-?b@B8=r9YE7G1fZ^K07;UlZhu)r1-h<#KA&4G0Z_RBh@!}G9F^uj^@A~{D&sg# z6h$rYr$FmU)70}kA`(JKDgP?N7?V;8A&6)?ou+B}O8-TmU3femVHmcB;+zX1-jo<) zLWuVKODV%JEQ;cd{w|;jr~XKiBnSfAw&@SsS2=Cl4uT*_5`|WEe>L`bNT}{}&KYCZ z>vgx=9S(=%@p!phZnql|S(Y^#jeOr1=@R|NCY#-+W)GU# zQDJ=886Vd$ec{0Lm1+9Y=N}&*d7g95KM$oZ8vxFEp6C1hUiAC@IF9uS5Rn4*dOcCs xb)M%M(XZAtjd(tvWmy_}{FY^@|6Tni^8<4;MFy)lo$UYs002ovPDHLkV1iu`{j~r9 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circles/b_quarter.png b/wfc/images/samples/Circles/b_quarter.png new file mode 100644 index 0000000000000000000000000000000000000000..5932768d9ddcbbf3da96e2d0259a57f9ded67844 GIT binary patch literal 509 zcmV!!GkEDb+olccX1&N0s`3NzcPLX&B zew5^pZ$fLGJP<_@$?bNV%jJ;ZL-A7?V@S^Db0ipoUrUVfcnlzwN+l0G20j=Jk}eskdI=ccfroLw-~0VO0N?lDpZSk~7_wI~53Ti^WB&~hL*}tqEV9|GM0xoj z{sD*~d$rr`I-L#xrPOOL_BS9UIie_<&1U6t8Gz?`&!%|>knX}+g)o3(oHk;{m`o{ns-}=iX zkEXSjUrKVn->=u}&1MsZ;o)$&-EP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Circles/w.png b/wfc/images/samples/Circles/w.png new file mode 100644 index 0000000000000000000000000000000000000000..4804c6cfb4b98def78e8f17c1b7a7a4e65769369 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WI14-?iy0WWg+Q3`(%rg0KtWAU z7sn8f&bMa`85tNj4jUZU|2&atUx>qH&sUo(_c8-jfI-7yvEv_^ZM`&S>Vo*5u6{1- HoD!M0WbcEP)KlrfItKoCX0n*<{fBOF^Y5+q1I0|8f~Z{R~fL;zd? zVSEN?iMHebN6_8X6V%9JMzgcp9dERLkyUcN>aK4&Afg}$@Ne+mi~Ie~Ppzu~)O9Vc z*K51#S1y-}$nzWk5w)vkVNn#~a5w;HJq8x;_j^Q9q_y_m6OnHTR%xxHC=yDk^?D7! zIoH-7MvRx7Uld;m+Nl<#s*BMigi@#wub z#{9(2?WfaeFc>sZLkIyNNfJ)`uV-+h)_O9TG}?D~QE8f%Wy#n4qdbU+H&d2nnx>8L z16+#Z_Ck*~~<&FlBBndyC z&-tRCbvm86>$(7>l=-3w=y@LB?REfiF9QL$+bs;k2!cQ2=p-(N15 z0J1F0Yx{&+tyaG813(l9UG#d5${YIlvtyTdfN#gsy>$;oGX1!itE|=$!_Z|OsoFbIFMK3D0$i;r9#r|l; zwu-`=UE%E-MPE1o=JUDw*qmn(YOz=VDE`tPkH@;M=Q}_$#-xlMt<`EuseFe!0>*L7iXPwp0NMPg7(;u*fdBvi07*qoM6N<$ EfoXG0|1!w zkdEV+1#C7OL#_V;7K;TKHLz_P0E7^x0JGWbd_GGl4N1uLdc9mO!Pt}}08Azm0MJ^S z0*po@0FY9e00013mNlQxiAX6$L6?PyN~v%-T&vZTQo(!hf~M2y{eDkGTI+xfctoeu zDH@Gdt5t&%sZ?sa-3BgUF~hPf*LAPgYrS4KBJtm2JRTE~@B2X+d;s{X3x&e*c%w6pNMX^+b<0s0E7@i2t*tX zhree$mS9FG6dDW$MC5ti+nIkkC?N#zRkzzEBH#Dli~YMWgb#3oCO|{#S9GGLLkg|>2BR0prD1P zi(`n!#N?E-Pm&Tq;D_yB>BFsBY$gd8*uoFA=(S5vSTKd9Hdc^((8m%R%#gTd3)&t;ucLK6V3QzqpA literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/component.png b/wfc/images/samples/Circuit/component.png new file mode 100644 index 0000000000000000000000000000000000000000..fbb5fa1110d65a86139a45daf90b3d00e0049aeb GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0prE9u wi(`n!#AFjw(^LgM1HqMfAum@1v9Yl+O#jQUds>(E2cRMbPgg&ebxsLQ01UPmqyPW_ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/connection.png b/wfc/images/samples/Circuit/connection.png new file mode 100644 index 0000000000000000000000000000000000000000..3061705319b63c503c73f1845a46a99c8922ff46 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0pkS1z zi(`n!#N?E-Pm&Tq;D_yB>BFsBY$gd8*uoFA=(S64STNIZ&8458pD*@QS5rH5Q^g|T zz*Hlrrnld}#3oCO|{#S9GGLLkg|>2BR0prD$k zi(`n!#N?E-Pm&b)3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Circuit/dskew.png b/wfc/images/samples/Circuit/dskew.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a07f492cc909577d3e9b4a94e8bbcbe7eafc8c GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0pkRTg zi(`n!#NLU!1se=_oafiNA2wNYluJ@Udqdc=g$oxleA;_s$Cte)CMliX$8?}{4ufXE zd9lX!i+gJB=WKKgKA|-!G&WbWN>2A|>Evs-df!G{zi0bv`k`4bb>|j|x#gOvK30pD l?#sLU>R-C!@g>H;*s}VZ1Kw)x+Xl3o!PC{xWt~$(699YvNG<>X literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/skew.png b/wfc/images/samples/Circuit/skew.png new file mode 100644 index 0000000000000000000000000000000000000000..260df54c83bf5fb81bdb6a4d7b0eb94a90b64627 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0pkSJ( zi(`n!#Hr^u@-`?4xIJX=?7h5UnzjLh_71VjH*Q2Y=oLI_toM|QFUqqnW7u(?TfzNS zE6bE)r<4B*2Tgyb(Is4(RsBnHTNrQj>NZ=y_OI!`)~yY>`grq|Q&WYvZk>K*lj_&< aUksZn<$ft8TkZqe$l&Sf=d#Wzp$Py6{6We9 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/substrate.png b/wfc/images/samples/Circuit/substrate.png new file mode 100644 index 0000000000000000000000000000000000000000..2c3aec02bfe32acfbb1a625a1c1aaa587ed2b3d0 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0prE9u wi(`n!#N?E-Pm&b)3zopr04f9+y8r+H literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/t.png b/wfc/images/samples/Circuit/t.png new file mode 100644 index 0000000000000000000000000000000000000000..084c3602bb8e0d2a19da7d468d09983ecc628fd1 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0prE;@ zi(`n!#N?E-Pm&b)3-nFUtWJWOCp9b c_YN0>d>ea+#HDFhfkrTRy85}Sb4q9e05}{azyJUM literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/track.png b/wfc/images/samples/Circuit/track.png new file mode 100644 index 0000000000000000000000000000000000000000..01158cbf91a0d33c534541cb10412183d5f84bfc GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0prD4Q zi(`n!#N?E-Pm&Tq;D_yB>BFsBY$gd8*uoFA=(S7tXvv0hF|5>Nof5mCSQV&;!PC{x JWt~$(696erA8G&q literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/transition.png b/wfc/images/samples/Circuit/transition.png new file mode 100644 index 0000000000000000000000000000000000000000..42ea71f4ffe39aa05383e64882e3aa89637dec15 GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0pkTJA zi(`n!#N?E-Pm&Tq;D_zs+kbw3mJD!AN>Ykp6lT8Iz?r9_Ct9GX_UF}vP-owxC8{z9 zmU@)GP6+6_ntq}wU}#3oCO|{#S9GGLLkg|>2BR0prD(l zi(`n!#N?E-Pm&Tq;D_yB>BFsBY$gd8*uoFA=(S5vSTLn^LD&7u`vsSaocy|_@O6W> pD=%ZSnT?m^!;fyh>=PD#3oCO|{#S9GGLLkg|>2BR0pkSb< zi(`n!#N?E-Pm&b)HXMlDzr6nNDyBR!gTe~DWM4f Dl^HW% literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/vias.png b/wfc/images/samples/Circuit/vias.png new file mode 100644 index 0000000000000000000000000000000000000000..eb7a35eb26f11d861a780b4fdaad177863fbc7fd GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0pkTJA zi(`n!#H*7w@-`Uou;>e#_#F1qjBI3#P)rR=nq`uyq2se*O+fL||2mP|4)4)rxKQ-; zj^UbiuF0HhZV4_~`IKu)kk6bTrX!uq{@qGP_B~k?D8rNfW1-gX;)@zZ`8T%y7H+xD fU$NrDiuo~Y50y4rwl9?dTFc<+>gTe~DWM4fGS@&T literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Circuit/wire.png b/wfc/images/samples/Circuit/wire.png new file mode 100644 index 0000000000000000000000000000000000000000..958d10c0b9ab92abee6b5cdde7794dc97d64b9c8 GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#S9GGLLkg|>2BR0prD$k zi(`n!#N?E-Pm&b)31|%O$WD@{VjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCf`#@Q5sCVBk9f!i-b3`J{n@rk*a2AsWHSfB0E=SQsJ%m6esJbaOhS9T43+ zHL#IOx#7cP>jYPy0J}Jcbk`%l`A!{Yl#pPEdM5vioo`zf&;SNcS3j3^P6!lvI6;>1s;*b3=DinK$vl=HlH+5FvHWuF+?LcIVB>~z*g7(_LqmYWb|3Hc2M;R6bjlKT8%FeSo>69;u%7V=kHj(d15yX(IUe4@ z5W#$+_=SXcgSBVnWPO>(%qrEh_H;BCbTabrFkHwqc`EaScRJ8c22WQ%mvv4FO#npC BKDqz^ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Dungeon.png b/wfc/images/samples/Dungeon.png new file mode 100644 index 0000000000000000000000000000000000000000..64ce90dfa9bfdc59ebdc327dfb5e5bf0f04a89fd GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^f=GV8wJWt2k!5Fbg{BE{?ML!fr%%qg`?e!eowM6RV%vYDc_X2ce3QgJ@1!x{=eSo xcf9DBhIX6Zwj;MZZ>4;D*mLknOH)##`1Gg7f96@#SpjWh@O1TaS?83{1OWCUNJRhu literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Fabric.png b/wfc/images/samples/Fabric.png new file mode 100644 index 0000000000000000000000000000000000000000..538259dd60891f70a7017afff883f80e6fda7030 GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61SFYwH*Nw_oCO|{#S9GGLLkg|>2BR0prDqg zi(?4K%-mCod<+IWhaGnRuP-**;H7MAm2jKs)lmiqp?z^^cwAFuL%Yr#Vu^Wiqk%J`BhzWBjIaUQ)@~7#fERuz;_VqjzkQ$n zWyuns^IG-ohiYF`y)uq<)Kc~=*voM8!q?AEH+2BR0pkTD8 zi(`n!#JQ6K`3@*>ILDuPms?f5f0_@s)f@Is5iU(81p(0)!Y5~M4QVfR3=-RXQutwv z(ies==C=>u{jVIga_yI;Hx54P4QwlWa5-?Rb)Gn>~}-L0x$6>fB%o^n-n!CdRlr?xNMet6;(%kAryX_b6D`u`{6 Y=S5m7=gVfC16s%6>FVdQ&MBb@0EhHKzyJUM literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Knots/corner.png b/wfc/images/samples/Knots/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc6ef7d65562ffc3386651e5b509c580cfa33e2 GIT binary patch literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V6Od#IhtfB(JbKN%AZ1UMz8 zn*Dk(`Q9%dv3?U_*EV5>o`U+*euX;M_Z&KK;6i2W^TKm=CH0$66=`P`85kIBsC(GP b&BQS8qVke2egSiVRx)_H`njxgN@xNARO>+B literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Knots/data.xml b/wfc/images/samples/Knots/data.xml new file mode 100644 index 0000000..f4ece3f --- /dev/null +++ b/wfc/images/samples/Knots/data.xml @@ -0,0 +1,180 @@ +<<<<<<< HEAD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Knots/empty.png b/wfc/images/samples/Knots/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..ccae147a479f665c44ab2237c7a0c61eb0368f44 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V6Od#IhU%~7Pz!^n LtDnm{r-UW|8#f{H literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Knots/t.png b/wfc/images/samples/Knots/t.png new file mode 100644 index 0000000000000000000000000000000000000000..cdaba5a8392b6d6637703c3df746a95154c6f676 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V6Od#Ihe!cIIa3$w1Q4b pX@R3y$Nt>h+>1S}9l{%=8T=E3YtqtO+JWXVc)I$ztaD0e0ssuiE-L^4 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Lake.png b/wfc/images/samples/Lake.png new file mode 100644 index 0000000000000000000000000000000000000000..56c9ceba610a2ddb7e5362ab68a9414594f69bf2 GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz6!3HEJ^yqE{QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XBj({-ZRBb+Kpx|v!7sn6_|Fu&bg_;$3TrUQf+;;6h7+Phq z{*d9W7uTXA_W2qdVM#n@?%X-6rk=sNRbbncpm}^rJtA8tmc|&r-*0a8bbF47_&S+a zeJZiK+gWBjoECWCG1nHJ>A!2FPFL}~-YJ#ae%gz1;dzopr09fLHw*UYD literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Less Rooms.png b/wfc/images/samples/Less Rooms.png new file mode 100644 index 0000000000000000000000000000000000000000..8f36e626245fb254c5fc47224548b06bb20817dd GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9GGLLkg|>2BR0pkSP* zi(`n!#I=(G`5Fv3oGVYitAF|{QAOo&Wba>P?O08hmu^5JbJ7~yD-z3{SN)L6zHz;f!LLSF{D|%T Q0H93_p00i_>zopr0QgimUjP6A literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Link 2.png b/wfc/images/samples/Link 2.png new file mode 100644 index 0000000000000000000000000000000000000000..8a8b337395c3ac42443b97cd93414a5e73b07142 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DinK$vl=HlH+5Fxu0_F+?LcIVBdUjF}9KJpGKSg!lvI6;>1s;*b3=DinK$vl=HlH+5Fw4`$F+?Lc`42w}j|#&Ewx_=P0`$~&yxuN( zs)=a=+tOY;1uo_U;q<0g%2EgBvTXh}ca7GJV{9#Q_tOq!F?%qz_%6>naMQqH-{boX0?#v%n*= zn1O*?5QG`)Q{pEA1uHyV978nDCzllQ{y6WTmXNf7VZ#B5>T>OataD@EFZ^)v=$zR1 ziMQUosp8jbJmzbd)wr9_+?qKF?49dr5TI3!}W3F29 znAaeJW3jf=GhrTsnno9c10Q}$F5jHEOzHBC8wLj~5+s=!R(y26_%Xdy3+M(0Pgg&e IbxsLQ0F|*!>i_@% literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Maze.png b/wfc/images/samples/Maze.png new file mode 100644 index 0000000000000000000000000000000000000000..6cfd1b7d834bfecdfeccc03ed6444c0ac1e3d764 GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9GGLLkg|>2BR0pkS4! zi(`n!#M%kod<=>l&TAk33r`c9wai&ijLm+_dL`rOA`IX5FcnlWc(eP>pP4f)RK)PB zW%fr-^+Hb9rsgW4yyz3N7pgqGT=933)T%IxzuKPfFYJ1GQe<7g-L>tHJ)>{0y?T7> uwzw$|!!nxwEiA4(5Ssq&mc++WhKkn sOrHpjC}H2i_eU9j>C9tazqwdH^ZeQ$Y)9q!flg!aboFyt=akR{0M7zhRR910 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/More Flowers.png b/wfc/images/samples/More Flowers.png new file mode 100644 index 0000000000000000000000000000000000000000..bc781e48137c8b79fda372289e3ebaa19e24b955 GIT binary patch literal 318 zcmV-E0m1%>P)LlQ9m$FbqYVh^a%*!U<5>xB(~N5*&lmv{Nfi!`z_* z3k;D`_y2Ds)J*Aj^5Sp1X?)GLdu5iorj`IWu#n!Vcmz>c^*OaJVksjvq*E+l(Hyt^e4kanMpkR=v zi(`n!#NJ7sd<+I0On?6`f4OpH;mk(|O!lvI6;>1s;*bKn+Jgm~pB$pEOV~!qdeu#KQmSgg`C^MGlwa@9NXbkC&-BJMFJl zIM%G`%kUyvH$+tI$-=IVBML`Po!r=eUQ%Su)H4$W-Uu|wB+Kg;A1Pn+>HUj?f3HnW l-v6w4*{bA&*NXW*Fx+Sj`Z~${eIC#b22WQ%mvv4FO#taHLc#z5 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Office 2.png b/wfc/images/samples/Office 2.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e23cda4b6dfb345f11645ba928bcdf9eaf919a GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv>!3HFi<}R-XQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XBj({-ZRBb+Kpx{GK7sn6_|Fx4Ed7BM*SZ2Sw_*eaflR{~) z$^07!ZT>L@`FCxQeQqc(*W2~=-=1H=x?AO6FExw$AU^L~&6?L)7v0vR{!R!#o-#2t zQfHU{vg@6TUK~7Cv-pyzQ&nn{obK7!1x7zFixxHU*)UtiC?>iiqvXSSi@llzp z9>Ob3o8BxF$y$H)sayTA&N;z`_9aJmU*=BOac*9!kD+O#Na2<4WfF^DIqZG0DZ*yP c{FPt1d%IsPU8wfY8t7RDPgg&ebxsLQ08{0Nm;e9( literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Office.png b/wfc/images/samples/Office.png new file mode 100644 index 0000000000000000000000000000000000000000..a4912f28fd57271aaa5fdfdbd647204a992fb6d8 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^{2oCO|{#S9GGLLkg|>2BR0pkS=0 zi(`n!#HkYl`3@-XIGx_~JAP*E3=1_`w|`5P*ll^`vO;T;CgYg{rF(9>3N^@83+=r= z*LHi>(=di|sYzw8Gm5q|tIPfKnq>WEr+x5Iu8{m+{8#SV{oPaUk(pl25IMnyt$X?d Q6`(~7p00i_>zopr0E_rK!TGqMI@*IbwU;K1SHrGA+L zzMP16v`wcO2adZi5;$Y)ym(w~Z$Dpp>2e0DXftieOppe=(|gheV3T~*Pavi#%zPgy zcbTD}nF*r3Yh$mxQ^H_r{xC7)u>R#zMR<2Tp|{0nESTrFb3=S9Zq$=jRlPmpbdNL@ zL5!5DPQ)tK$^D3i3v-hDEsX^Wqle^zg#lKD^1_Hj zdR;_4S-`kQFsFY@m|#W&E+5%?vRZM>7LszBP7*YL3ai*37M=K=u@SOx*!lvI6;>1s;*bKn+Jgm~pB$pEOV~!PCVt#KJ%M4?hb}3d0&HNlD3tI&T=3ur5Eg z-AF4yj=A~6Wa|RE4&Fw}1mB_-MrLM)z=zhm(*Lae477{E)78&qol`;+0HTLGP5=M^ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Red Dot.png b/wfc/images/samples/Red Dot.png new file mode 100644 index 0000000000000000000000000000000000000000..44e5afba106eb84255b8a8446f5d79e44fbae049 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCf`#@Q5sCVBk9h!i=ICUJXD&8&4O<5RKsE8tuiO<~Is3XdYsB_~a9?HQ|F_ zm}uWb#u&j}EJpH2Rbm=UT%G#!og)Pr4yhk2cWDu3W@cF0@0a_b;)Es890pHUKbLh* G2~7ZhCM_-i literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Red Maze.png b/wfc/images/samples/Red Maze.png new file mode 100644 index 0000000000000000000000000000000000000000..f716db2532191a60ce7a32e1c9c1011c8d90b5c8 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1SHiab7}%9&H|6fVg?3oArNM~bhqvgP*B>_ w#W93qX7Zo^|LvJ&6pCjoa5^F5#s&fm;+xqS+~k)o1S(_jboFyt=akR{0RFcZC;$Ke literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms.png b/wfc/images/samples/Rooms.png new file mode 100644 index 0000000000000000000000000000000000000000..4210deb1f6e15dc462fdd7e7c874737e38552040 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9GGLLkg|>2BR0pkTVE zi(`n!#JPcvdzDHJ@IZsD(?&( z#Y_X{2J1PKREp1beUBAu-(8dSe`Jepr6HBW!pWVI9R aC*vHxTZ;KfD=!6F$>8bg=d#Wzp$P!w;yrNy literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/bend.png b/wfc/images/samples/Rooms/bend.png new file mode 100644 index 0000000000000000000000000000000000000000..fd27d078adaaf6c31ffc3294ea43f32af232ef13 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prC}O si(?4K%w&t29|p`|Ak55TaOfyQ&=i(whrVw$1*&22boFyt=akR{0NJw_CIA2c literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/corner.png b/wfc/images/samples/Rooms/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..5d9bf27ca1cc98a3b6f2295599d8497ac24e5718 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prELy oi(?4K%;W?xu&DWAz%0hVV3fk-Wo8h#0;qt&)78&qol`;+0HF63uK)l5 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/corridor.png b/wfc/images/samples/Rooms/corridor.png new file mode 100644 index 0000000000000000000000000000000000000000..2fd942f90e7565cfd9fbca20e2aee35f3e9ad562 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prE*? ui(?4K%;bcG1dEy<2F%RN!puAh5e$~qOnxjYzDEF+FnGH9xvX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Rooms/door.png b/wfc/images/samples/Rooms/door.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd050017c1db98662be7697f02483a5840ea12e GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prEj) qi(?4K%w(II9|p`a4i-KIb__|AS*Cr-*;xXVX7F_Nb6Mw<&;$UBh!(^E literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/empty.png b/wfc/images/samples/Rooms/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..c2eea5ad3ae5bbc92be3f96f542f4280e605a99e GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prE*? qi(?4K%w(II9|p`|Ak5685W(P3!sP$!mcT!t5(ZCKKbLh*2~7a0suivP literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/side.png b/wfc/images/samples/Rooms/side.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe2883e7b3bf64cadbcb55e521d4670ebcd819d GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prD|q oi(?4K%;bavEIsY~@@*R!gbJ9v_pSdu2Pnzl>FVdQ&MBb@0BM#MqW}N^ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/t.png b/wfc/images/samples/Rooms/t.png new file mode 100644 index 0000000000000000000000000000000000000000..47b93c775ec185d703f03a1b729f49f8702faba9 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prDAS ri(?4K%;bay#$$)u`43z22=FnO9Auf&G4)V7P@cil)z4*}Q$iB}tDqM# literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/turn.png b/wfc/images/samples/Rooms/turn.png new file mode 100644 index 0000000000000000000000000000000000000000..9db18d7086fbcce6b0c4e8c3b48badadefcb5032 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0prDwi ti(?4K%;bcG1dEy<2F%RN%5z#77@}vfOgS*gcO6g#gQu&X%Q~loCIHKz7hM1V literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Rooms/wall.png b/wfc/images/samples/Rooms/wall.png new file mode 100644 index 0000000000000000000000000000000000000000..90aefbe165bda984cfbf0252714fed7357cccd83 GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9GGLLkg|>2BR0pdh!W ii(?4K%;bav>pGwn;=mR9JZ)qqkZ3dA?-_cl+h zJ9FxL=qX6CpO850)$E?ZL8Kb3J`SQWi_m{WCB;$Y#J4y?byZG0gTv}|aJE)PmAFLXjpRG>5- zJe$XQ9_vHX!@bNK{RJsz5Tb)yo4!z;XvTIWAIxLQ6GfmKGdt*=AA%le4xRP=&{z*N zcVmNtd!vKqZd|*1F;@R#I1jm7F%F&9IB3+6rU~`xw5FI8U z>{I?g&#JpDG~r8+bP+Dxb~y#V!EaoRq1q4}}-Q^7$10000!lvI6;>1s;*b3=DjSL74G){)!Z!pp2)BV~9j}@{fLIfk_DmSX?|6Hg`^TV7#F! aA;ECDGk#&xGfr#R24i5WYYCN`|u zRvRFe$8hCz!)1mThMNf+7#*aZlyq8}BYDhb*AFsy=mzTj z2wT?Qn8=VWp}?HNYjEIdso)!?9gH_b9x@v+Yw)@7FgvI%POf2C&F$dVoaoe`q5q&* z^jd_%u{4Q>-3+#Mova_uuw3qWW0S;?e7@bOX{TU{#;(^1Ng~UcC!J=_VMt++aOPEJ dH2%ZRaN%|h^Ws}>wm|nYc)I$ztaD0e0s!U0TIB!$ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Simple Knot.png b/wfc/images/samples/Simple Knot.png new file mode 100644 index 0000000000000000000000000000000000000000..0e805cda16286b310fa1a8be8ba7dfff87094baa GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1SHkYJtzcHoCO|{#XvD35N5n|x9$&6(9P4u zF+^f&>jXzG1_ch5=(qpvue}v;>0G&e>QPCSlY!@WBoD}X9y@mLUhYak&%88Li&pg>pB}Vg!QYomyhlZ479A*i1T>Ms)78&qol`;+0NQdhE&u=k literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Simple Maze.png b/wfc/images/samples/Simple Maze.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c9bd4ebc5b311b88ef7ac561b68ffbf04d5481 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1SHiab7}%9&H|6fVg?3oArNM~bhqvgP*Bv< s#W93qX7Zo^|LvJ&6pCjAv`k24i1z3H(Ow@g3#fp>)78&qol`;+0G@Ui;s5{u literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Simple Wall.png b/wfc/images/samples/Simple Wall.png new file mode 100644 index 0000000000000000000000000000000000000000..9369ff6ca81e0eaa30c1a0b21a53d309613d3a19 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^f*{Pn1SGfcUswyII14-?iy0WWg+Q3`(%rg0KtUT% z7sn8diOGNd|F>t>N=QjLBH|*JerU>KP6rF->5~nXo+!MXaLt)7AtfO}!Dz>W1cP?Q hzn7(*I>ag&8Il(&^4b-eWCG1$@O1TaS?83{1ORBZC@ug1 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Skew 1.png b/wfc/images/samples/Skew 1.png new file mode 100644 index 0000000000000000000000000000000000000000..65f27eafd92eb8c6f9d5ab616d9b3825663227e0 GIT binary patch literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3HQ#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DinK$vl=HlH+5aFVBsV~BOM-;eG&+QnrQm{KOp z=u@UFRhT)ei$f-L6^Bly)`m$wkxPV(1s7z_(`$b7Cc=1@n-Wu|d2)d&(@LH@9&Af} u80YKWXL+r+HdFo<^J|B5DfY`UzcF1*w-u9k#S3&Q1B0ilpUXO@geCxS2342< literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Skew 2.png b/wfc/images/samples/Skew 2.png new file mode 100644 index 0000000000000000000000000000000000000000..8c60b39956770543efe5c9e542f5bb6c98dbe784 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv_!3HFka9+I#q!^2X+?^QKos)S9a~60+7BevL9RXp+soH$fK*1TFE{-7<{%a>j^ED{&uyB9hZ@fvX?fAEa z7MvfO-`rU0>EdOp=%qPHNNG+@eZ8X2;Sf2+Nc~Mq*7dZSF4Ww-ETR0BnXVvn^s>pW zE*#r0N2CM_CQo?V;jpUd!{dxa%T5%(Uw`1^l^%}0LWgI+<(i)Rj$`5ybNz2cO^P9s zA6kmLm!|cH&b&L*=z!BL{}{;^hp%l>l-!^quEai%)lRqf#=HqYH!^s-`njxgN@xNA DPm^7i literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Skyline 2.png b/wfc/images/samples/Skyline 2.png new file mode 100644 index 0000000000000000000000000000000000000000..a7cd1e5601b52297ed51fd58109e5e28372f0824 GIT binary patch literal 6424 zcmV+z8RzDSP)004&$004{<0091~004v4004#&008y@0028f000$`8$yyL000** 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Smile City.png b/wfc/images/samples/Smile City.png new file mode 100644 index 0000000000000000000000000000000000000000..8681f9f1889c0b02192aac31b36b989fdc02c7d7 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sDEfH31!Z9ZwBpoOQ4V~9p@a!NwN5B?(-j1i0i4iy;<6DLk= zXI!IZQrIFFxiP@ESEP5M|kO@&E&a`)1}SqwcQw~}U6HxA yJGAm#U(^}9^`DgMf9~W^*(~m+FmtO(Q|*TzhU}IB0_;Gm89ZJ6T-G@yGywqHpGwdG literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliff 0.png b/wfc/images/samples/Summer/cliff 0.png new file mode 100644 index 0000000000000000000000000000000000000000..a921d79b78a9c1009bedc49619b311a481b92210 GIT binary patch literal 4906 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliff 1.png b/wfc/images/samples/Summer/cliff 1.png new file mode 100644 index 0000000000000000000000000000000000000000..6f367315df6e942e8fd4bcfb45e753a54eea5041 GIT binary patch literal 5113 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 b&$9U6V1ro7U~eAh00000NkvXXu0mjfb>I}~ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliff 2.png b/wfc/images/samples/Summer/cliff 2.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b96f220bb787eba7ca40a998c1d541f464b79e GIT binary patch literal 4806 zcmV;%5;^UOP);g5Y|zXccTe}6?$gtKE_JFZtMXELi;Vm2 z;~_)lEi+PzFTcI_T6?Xf4E`<8KG5Q*c>NmBLshXX77Ly}bAJ6<)M}w9Bnf57!$;cJ zFN;MH*)yI$r`x67CeOLO#dVoZ`QZ=s@i>3|nxFin^YY~?Z1j8=>3 zlqXO4`fHv&uU^y>njw6qF_1SJxABsY;aw&j4UIL>2%m^*ly`|EfL26mc{uw zx{eSuo8&n`z`c87yJa?GI>j(ZQ?^?s6Y{(|IUx)I6otV6*Tr#=va&5hN(_U^gx!aW z3#7zx&@GY#T_+4t6w(w;!?tNOP*qfw&4w^cM594fktDQQ42Kj2NkUOD7!XC(YQ<#2 zY=)+BcgJ9WVW4TWI{=#vmPMn1reRrRS(atl@i9U$nGi+wsg8rLW12J?m?rPuQI-Is z5qo?1KCVkyQdKlPM`RiA-qG*VXz0FAS(2uxDytPyM6ZXc((lvlve{6Ub${C}PuDRF zTo=Q@ad4a3n>U0Z^Er+~yUo!NMM0;-{ysv`=`f$yuZm-qOQb}Ix}mmBvx!g?^~t}` zuHROgAPK5YSIpRj+ya!MeK(79?fB0zr)4S`> zIK1Qs4?eoP^%XD6N;I7~=J*)Ps*!Ko0PPmOk67V6=YvVx_$s~oYdM zp|3mpS^Vq&i@%3F#j_<>?x1!?)Uyf?HC4>~{p%bT^&y+1KN|HU-EAg~hG z(W(s(K0f$-b*t*RArwETv^b9Q96-}3%R&=W1*ZyJu)L$;@WJmmAMHQr4MyP?|M?eh zF$#?fBdrbL1A6vHF^ET z>)AAxl9f-Vr9C+J7I%x~nr=(l7Da)WO_#6Up}~XmmXuY$XCVQy3{f#`aOicKg>%$C z`_bQhcu(-QK$jd@?Deo6yWR1={pMZMbHDuREpUB3{pRbpo~!kH_RBB7@i(h4fAh^E z=D+?T|J{$;|LWiT;dh=snk@h<1IIB9L-#jZKf-l1Rj3Ucp$HMDoC}8g16?z0+f*Qo zS*>v_o;>OeN5eeN0)MqP8afuhu+2aIgQ3^7p8x9AH{VXa_*&jv-<}>1Uw-T7d9m6g zz^jY+{QP)&Gx_3WZo_+B{_vAO8Xg{JS#BBtUXxb4xecN$&4+D{Pll$YA02i@RhDr@ zDY+R>qi8Fo)S;AECXepxPu_nr80`bg+0G(HSXL#oMe%k_C1tDA_FQ@%-8Qb*`49i@Pygp%{EcmK2)3oCNtUH~o);gyZ`+RT zZ(~I$+Siv+3X$Z0f3`in_j7;CvB}f>hG}TS(IC&#?PhH@TZ(Qh7TZ#Xn3Llnn(=y4 z#6CYhKiRAUKPZ4okbez;jKdL0}aL*uW0_CNmnH}v7K$w`-@D2t*f zib6_pa&|De^_L+}T3yu;+$Ch7C^%(4UncXUoQI`aF`b2mT9su{NGWvl)mIl$oMzj~ z)r*JkJtAJcxY#~EZ{19`Ss_Pz{V0q55?$l|fRjVxCx7xUzkTu9^S>GQyGoSs-+sP+ zzfGK=K&y);^row&NhYO?B4)GX#cgI4g*J~+6ckA$ncRd$!Fzq{|J)?AAUAYOb(y}r z(@{S9{$XAw8(#*JrC+UmA2t2(v^kq?!%(>mN>ju=ul1|(nv7nH{@$n**4x*&Z|_h{ zX~Mt*n~{ZMQk7Z|Da%z6B|=JqyD;*rn}AO*w%RJC3k8%Z+c5JyP98L0TqKu3!Ot(_ zvLvW5tk>&w?c zm?dzrK(tX=+j^WmV1BMebJ_R9mS6fB%qt%PovE-fYqDGS^_7 z0i+d~1cmjA=~Z&yvya>#^HO)+`z1eQ{ILAy@$@n|KNB2>VI7=w+y;uF=qAP$Lu2QxID17|5k#~f3s;Z`i`H1o<$4^lb)RMvb zqVs#^!IAdy{k;R1w?#JmF~w75-C)n8T2tkeNn*+3FWv^jmN*&NFIV}4zM(g?cQQ|M zwiVkLPcS%Oe}F&ZYML)$At+T;lSdEr$KP$PuF_6HN8z7;(wV!}tH-hWq0k23XYhN< zo0aU}V{{DHG(T3KFXA8lq}y5?M}PT7l-tfzrb|YD2Wz1HaM1kao0a@P&aKEiATg_} zGEPknpEP*i`1N@DPlx-z`RVjFuF`$6Ix#$3J1p|QewiM&v5%GfP|A$;cdD_Ks$XgU z`jhXey4L>i(U;%+G$WiSlcPl5rv5=1x<)NbP>BhQPlh;H<<$Cta(A3h;^Lv` zt>D#^NXPP|F!6m7rP?&Yg9DaJHk&GJFuKpoBO2syj7!TQdBs#tl>5$JM=eg3)wMc0 zE)rX|$L6LVug!`rTZOzJuFyY_|7rTORX3UVWq)7Z+%R6FitiyMdwaF49*;>96op2k z*lbv@sVW?s)6>r7WwH~fUL+s)yO4rD}7UBsa`c(bh|8Wnr4QuCZQc>ot}|o^yS@ zDocc@Jycn;Tw)lUpCcvHDQ9Q+KEP^K8%xKL8>LhrU12r}5;~b&uE;XFUGf}7plNKk z_&%D(`8j!xYX3fA$k8!jK%V0`j7IoAtrnY&Gz^3w3`r7Pm&v5Ih&%J;IILE9UhNXE zu5exQymped&HejERh6bmyG;~ver{h~6?smU)$Xjx*mot4X)O$-AmDGH>N zWr=0c>yc$No3%KWlCorfpUnobvvWmJUwySA3|TJ8vOJ17I-;tO63^rGlz4@vp=qqw zgdzL;G@C>bLBP#TsH*gOcpkRR!2wb-o6&4~nucYGW)t5}fH1@~u`JeW0K?$$&<5VT z!Lkspi>~8&NQvuW+ibVATKGO;NL8^rrqN)vLO2YE0IOTVkZ!l@dEDM|bOdm4z+!=} zvsjR26a|-;_`V;Zk8chEusjoCG*{=OS+2}1@0)w1yY$}re$xVc&NdR$$SBuuB} zZeTkd77JXL-E^i&vE5QtY_}k(3WQ*_ssWT`G#aYsp&3TKHwuv$25r|wp(16G{yIs&uKL9eK3I< z_MWHq`(+Ri$7P-)?(UdO@H~L4E5>77m!tbgNf6YPD+)|g9gOfd930SYi|v;6nmERF z>2@&;vJBV7G|e=n-DNbYiOn#uZPK)+^*kqvFih^<=lIxIt(ect-8O8s?5aXk6;Nk6 zWyyAn5EKP*Tvu`zQe|1WOB6MY)v5}^I>s9e8fl7Y8nfAM`Nun`8wPPqqd^?wI1~lj zZSBx^*=WDda+xIw(d|i3;jr)5%WJXV;1JJaJZ3UM z*9ij7&Qu{(DcNqbGzAC(E-tu#pH@q+Dom5XfJTE>t4`t^hyDHf>uz?Vh^iuvu}pKZ zK&UFiA*N}z+W@zLrUTp-;c|xIyyqvX|*Vdx&{XagZZqeDzdEHWv-nLXJ?Ha z_f{3Q&G@<|-qA=M4r`^=Y9WS0lDIHU_Vz@lUkjAoke;8rq=aD(T2mpRinVc6R9dIF8Y5q7FVmQE0U_f0OML zldA4Omn+H+`SS9TP6x~4_LfEi!(ckC_u_cW(UI_dxm)J5S+QP|=RAC92vO!a?M^*6 zSz5K*?DE&~n1>I!yF*b40yJ$K1UOE)SRghVG>wb5#ZD6E>_y3;@$ z*Yb2nCqck?%>F)UTIxED2Az(GBKG%D6z216FfbMigi?1i&xvEgkRYJj#daLSz_QS^ zdi!P>{XSx+6*Z07tPYfmJfBW63}YAR2LYGE^wLGu0 z`c8*dt5zmEf!yhkWgMTPDCl}Ebk=K*j&NPO-KHpN#h2%}Zp}v1DWg&CI;PXiwh;o; gL{Vx+==HV#2kG-=m17Qkl>h($07*qoM6N<$f|*)%(*OVf literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliff 3.png b/wfc/images/samples/Summer/cliff 3.png new file mode 100644 index 0000000000000000000000000000000000000000..4c45cfeb94ef011710c088de15111d3754cb5ca7 GIT binary patch literal 5475 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 DFY8Dk literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffcorner 1.png b/wfc/images/samples/Summer/cliffcorner 1.png new file mode 100644 index 0000000000000000000000000000000000000000..d94efeddce56490fa618300ce7c579c4635aef3a GIT binary patch literal 4608 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffcorner 3.png b/wfc/images/samples/Summer/cliffcorner 3.png new file mode 100644 index 0000000000000000000000000000000000000000..14e3035e90ed8e53dda6efc5243aa0a4249aba03 GIT binary patch literal 4975 zcmV-#6OinQP)0ND|(B(-;oRJg2?Ie8$tKeD}MR!5|zC z$ueGj)sCXfw&iT51_KNO&!b*vu|Re@M61X!Fbp;}02&QOBZ2_KV7XZ4IgUdRxKTu& z10)Hn71b*3HjO6BC5;BH7U$ zq9DsC3WSh5yUZ@hGPbvw&8XE7LQW>}aLC0)(r$BcLBCJ0hh;IDpcGlQt^k-!Xf#+X zh$4DDvW&AcS}pP%(`2=x)sp=lu8VDBS+rV6$zriCKrWZqHuXBK7T4E=p(=7}I}`=W zCEXobt(sD5IAnX9Jf|#)W0YdCU_2&Ca9!e4UL9X z6r7$?tI_FT+eoc-S+cPqH#U9Wr(UPmqbzAOFik9rEX&(%Rx3K4O1n)QBLo|pR4T+V z+uLZRZ=3IVG`9)56a}@KAqCwoVMwQA1G5>!A*~j1%Ep6ilbF z5EuqhG8)bEoHQvci+UYZq7 zwuV8oNf;u9I6F)793%$^vfslq*|!isVfTOF{ul`ef#XQW!F5@!C<+b^?0TInTi-s4 z*xPf@&lwCb4CC;SJkLFkN`;#n<+}JjMZxtomPH&Vo=2V|1oJtS3YLYG>mjdHDz_My z3UVC9c5Imcm&T_o&e10r2R~4Qf!b_)rjdssam;MS!GW<@lv1vlquIo^g=s1&aa~=O z7zVCKrHT~Xa@fTMj)UhBhJ_Go0#u3z57wmMSPXAyl!@23iYGdIfuZH(RKKzi{u7YoETS;-RPpnofYX+Jj*c+mIxz#1JgvQ zvfn3;F^u(y8U{+K$(XAvEQ{d~+s1LoGDagDhrs|L65r?MhI*YB&oE3H4Q_6*EjBmZ zBFpoHVn+5e#(#$W4$Buz2eg)KZW(E)UtDr0;pJK|pJpc+tsP$7MFPH$Wvx-!Y@#){ zgSJ=z5|vdH#4)`d)hcn!);7Q`{6$LoWm>#X;{)6uv3!^1W5yYF ziAs=jzWU%TIiX0n8nc*Y{VDN;Nl$z^HqBe+oz2!eRS3e6I;2YaUaIW|J(V z-KN)LHp?9cDc97Z6hKj6+iW(7-ot!R)CWvwlo4)*Hn1f7O`@E7S9-gh+@uPdzVY5= zZZD}gggsWGFss$|8d8cdWIRSGM#EKEqBX4+Wr+~$YT_8vq|v~(*W~us(m+ZY4ZjpvOeO?D z&}c9oQ?IXIn9Z42J(vSIDhnNXo+X>@5(M*PX|E;|@zV+3`Vl){tu8{gn zMw1}$ANU;a8?kbw%u$)pVyC+u%oc|GEz%UARwE4YJb*C7vdHr_s_xt&P1o|kH1U0w zOI~`(I6Ku{Wk3IMIZqIpB;;#*vf@~?Fn%#4iqHihx9ajQ|M*XTbVix-^?SVj+TBN= zJZsdD$c=`Xmqoj#@7?J*t~Izxq-EWTCW?Y60vM0!^{{Ok4Ge=crBacT38u-`mbF|m zpVRG<#3d=-7xH^;e*K7FXi)PxS=gUn!LNLiQ}NPs#H)4 z)3UNG?{*0SY>Ubb>Zi%WBexKoO_PHg(&za!(iNX>N(G$2c=+o6|N4_tSM%^;JMf18 z^S@l0_T~OApZ924;wwpJm@IHJX2iAQ0oR&N7l@Jwdy>)a#zsYBVYx zhsgvWj3j&#^I*e!-qT4R^59`;X`23PQ6?x9nCrjhy-z5xn5YX?ly^Zf<+enFN zDj|en=tjf+_=BR>Ad0BfPz%EUhyR8A+HPm7tc8nc9!=AdfAUX%=i9&adyYBZs4VO} zKIqbDR5vxH}&7G~Q)AQ3OCvTsA zwrSa$hr0>^hU?m$&gRD-eDwFf=jBCNCYdVLa!fB{M{3(*G*goiDO3eYv09Zo+tS$n zwY77&CBu51I3`JO9Om;|P0e7y@i9d~7_pdgM^nW7=|?mtC*f?en9LrXTz>HW^-rJQ zy!*c|PapSVy&A@mrL*a1V%wJId*e}P8WPkhk?pR7C@dk%TxSKjiLJ3Ls!emTC`~`$ zR`Blb5(Hdbt-a~(b`yd$<=}uU;}-S4A@;WPvA17&CHlkvHtD)&am?ouX=8-Q5HBPM zSR{;!WM_x{?cmc-#!1T2zV7wZBImGGy_hD6WeSn^$GTEONYac(RrYT%n!keUlBVqJ zFrBV<`)WmZ$Go{#ySt1=>*ADRJV1TI-sjX?tGhdUZ`?IZ8JPwzH#pwp<$#x)xF-E2 zpRXuwVvFT#*l03_d{SBvG(|7mDQ=OH>QXXxWixBg6D>)8DK~XRq z^7XGoS;j^P=V|Nfu9n9f-#gr;JHATAhdLXA_S zvrViS47rGLwK%W|8v5u^o}DXE@|AAzjc*?O$)Alg5zWJ8mg&u^9u4)##*8BF+#$6;X`w?CXKNf^fD*n@BRJ{TFqCkuKx8XoGzvIOMH(-$%aoG zF6Jy3R5XU1bWPdVkDRqOcgbonNUm95UtJ2q4>A|?!EUP?k>&M)rg%fq69R@J2hkXA5fOu zMklUIwaRd)vkWOIODv1YweU>2{qt4uB2Px)9nx~mbBk` zYu6l)+1{oo*x2wEi+E#$M~~LtTkonG~S{9pggcmi5(c!-i}m5b}_=K99W zbCkmOQK~cys#RCYq}j9|J<3`wrW1CWTwm(>R0M_IHP{yzCfMXQ>5NceO3W11!g1(N z87(Mt62a4JMl0PP={Uy_YST_%>u@>aa-sCkdb7T;W$9Pt>#x{lVdO66?ZCc=mfffoGv89A7OI}Ur>0BOq83FnH_5KDo^0Y?(}lyd~2 zK$B`RkOCRAbn$$qF4Iq!li}muTt6wrSIpXQWf=B1krJhF-Rw4{i({3g;`_v;gQZ@x`K_I{kV|sNA%hbM#Pc{mr(O}?C>q5d&6X2w*h~jJn{uy1 zyTx$GDq^F-)eVlrFy(%mJ9iL@C}tkw7$`}Saecvy5(gX?owF)2A%NHRu`B%XmqhTg zE1I4JsidW?N(|=K7J0rF?K?Z}$w@LA@s(GtlXs>s2GWjo*CARW4Wfwg45fJUWxoC0 zuNFn|{?9(WJm>hnY_%Fso=rY^%n)h?-4?Y9?G5ZDS&rdQz@bmQhVSvpE3MoVADvA- zVVHqPx3L-&TBT*4)V1M%hX)U=XU~*U?C$!PmuZ@EeC#Qs*nZueTY0}n79nz`8EL`R zf%Voe9@ZT{oQI3$ROS5cKl`0u{O#YfoyAslSt;Xt`;4c&-A6|3cPSK$oSMb`Bl3br z&)ICm0Ltf{L&6BKffQzS6k9=X>SBUPs88pU?=fEeeGTw#DN! z@|@k5kw^7VQURmFtmMi!WlE!*f zgw9rT`MzxKI=1WfZI$g5)_#~?=`6!>WWS%gF6M3QRj*^3WEtJA_u@s|Zj($&1O`;= zOrG-gUnIkr`xqpzd%ixt zE1x`5Tc+6Vc$IqV(lkeQQEsNDqS@r;Cdu=4s$t%Gu?G)o7Z(fQiX~gqu<(WzjAT?uZGL%Qfr^TQbHbW!NrVa#Fj^Fa;-5-YJraWJdHWK zQhmYwKm@+AD%%q+i*~4tB1_f*#;pTeEHKZ{xpxoCGH;J=Eu!Z!nP3=v?K1tf?|f4n zak|wRJiqKce|G-GlcNJA3LPbKXQ!HG^TCj+M_Ga-RhR-jWkWL(%yOnV5==oZMB&$_ tsxm9oxRO}`X-cEPWU~HaF`3|b{C`0>c(tP+6#oDK002ovPDHLkV1n&c(-!~$ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffturn 0.png b/wfc/images/samples/Summer/cliffturn 0.png new file mode 100644 index 0000000000000000000000000000000000000000..1dadb94f89445f55f5b8772c326d5e72e8b94f22 GIT binary patch literal 5251 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 JPDHLkV1hPrVT%9& literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffturn 1.png b/wfc/images/samples/Summer/cliffturn 1.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e9ad2645aea8eeae1eff36f8d5650af1245d06 GIT binary patch literal 5167 zcmV+~6wvF5P)cxqpM9_|hT)@*h5-X64;CZ{CS}v&HZ#rco|*3H?y9b? zJ@<&n*yAleR7v!I2q5ssi}$bJ_x?xj{wJKD!}qYaOFnt5{e5)*4|fakVvbHgzx(SO zd$(@nNgRc-QqpVnA3i*8xkb0*9iLD357lHPs}gNT54!MOEFzdDdL2#(7-a^h8bnF4 z%4E0WURtVT@ghZc7123bz@}2EA_!dznGDE zgwG-z&ehRe&0|H0W>kSP&L~mR^{l`7+u48o!K1J2*hOCa^$#xy$-`cL-_D~j^j%akeog)eT3s(OBq7)H6hwZ3tE z-8Okq)EASgD9fGe1FdzMRV|ODmF_gLv2HDv;?)F23XAGYXr*C0)UizAMgC$ zpZ=L?l35j=@6N}i4gvruOR$d8?^-+A2jStN^rslQSV=T&@{MkFMi_>ssb4=kmqyTSa+B+q`^Rg&hHKNZ5UWJ)hk9J= zoen$~7gM+lWrc$bs|b)N3M|9whd)03

lIS~s~{9vo``g8?l|NlzB~uYVFB0s+7R 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffturn 2.png b/wfc/images/samples/Summer/cliffturn 2.png new file mode 100644 index 0000000000000000000000000000000000000000..d37cefbbd6d16d94b2d49d1106e344597dd12d92 GIT binary patch literal 5266 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 Y0rsKQR9RBhC;$Ke07*qoM6N<$f;f{@Pyhe` literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/cliffturn 3.png b/wfc/images/samples/Summer/cliffturn 3.png new file mode 100644 index 0000000000000000000000000000000000000000..ba08391314620e49869afd05a33c2c3c9e486def GIT binary patch literal 5198 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 IM6N<$f(@(!C;$Ke literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/data.xml b/wfc/images/samples/Summer/data.xml new file mode 100644 index 0000000..f9a9b73 --- /dev/null +++ b/wfc/images/samples/Summer/data.xml @@ -0,0 +1,138 @@ +<<<<<<< HEAD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1fe4f1f60ebd57b99ca7148fb003edefa7979d94 + \ No newline at end of file diff --git a/wfc/images/samples/Summer/grass 0.png b/wfc/images/samples/Summer/grass 0.png new file mode 100644 index 0000000000000000000000000000000000000000..d8cbedbe83e76cf53eebec97ea04af77fac6592b GIT binary patch literal 4094 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 wV>~8`RLFqiN1Bo4HQ>bp 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/grasscorner 1.png b/wfc/images/samples/Summer/grasscorner 1.png new file mode 100644 index 0000000000000000000000000000000000000000..15e7bd01bedcef9dba2dde6e2104a3408f34efd3 GIT binary patch literal 5942 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 Y2gJuss0KRpxBvhE07*qoM6N<$g4}$Qwg3PC literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/grasscorner 2.png b/wfc/images/samples/Summer/grasscorner 2.png new file mode 100644 index 0000000000000000000000000000000000000000..f8438d84b986c571d5db0759231ea6d8fd705577 GIT binary patch literal 5932 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 Ef?&>a{Qv*} literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/road 3.png b/wfc/images/samples/Summer/road 3.png new file mode 100644 index 0000000000000000000000000000000000000000..f889273b311471c796642b3681be4013b3aeaeea GIT binary patch literal 5834 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 z zGVvsVECZT`Fa(l>YK>hkz0C%CJ%}PO3}6_bC}3Hb&!K;x{2vE{detwLFeia#w^LTt z{K!~!oOkY!y?!@vR@2c4C&|mXL6TtGkft6ZgRaA91UwJ*Iy8Hb=ioR%)0Z5l zC`_rQmI?JbkR+;U*&y-%x%;QT+v)!euk6}Osfg~svxnvp$?~)yZH(~bjUg1~8sR)X zAJzgCffTZ^tg5@$??u5HC&)DNukCfZ+Z_Z!KYQ?V0E}@GFc!Ii9W6CCL2Y*>pM_HX1^nI(QsI zl7Jx4nik~wWj#a@Xc}}KI1cJ{@I26Us5ij2U^2PfaUBP`T?|FBJO|s3!Vpr(eukgE zbp79n!z_(k9Z}1N2k5IR2gM>-WPtIdD2iV^`SjxSb+uZ?NeV%+G>Ov4Xm_M$yOyO1 z#(xh~6;zee_0aPGK|q#aIt7XX+Xi<7{XGZ*Fbrrmh$zY?lS?|NDliO#m;JL~T?-oJh0y$9HrgIM?d%N`vK##M)kyp<#sEf%UPm-X`08rT51BwDg0g8g<6mH$+(=@UykfaPJU^=}NAc8=eCM1O; zNSvlX5P;*r^AJU_b3^*y+T)+T`aCM@>ot%hFbuTY+#Ai6Sb1BPi!7U_X_OK#4vsz7 z5B#vx>sRW{DDdz~1p+^zC=dj6R?Y|lf&fSoq6oIO_`|Op*Y!LPSQcCtTo*VFWSMZ? z7{>uYz%so;5k@1BByb#PwZJrCp)I2bj5Tl^on?V-14)AK2NN&-_x>mUHiD{x?0f1S zTZa<8-LECFbcikvSHso9!vCsgK@ccK@nsp31R71zGA+k(PzewOu`G~fa9zN0AW4`` z*h=+;$vb*;Lm7HBG>AxHwOI|B{4dh23ld6h#*cP}R#33jn?kaSR*>G!1bKjsuek z)M^lhDM0|ofh3`x2eld?2rQS8qL8`{C~_&ux5mbG!Er#AfnnLV7X%!KJO_rklm(h3 zkfwm+plJ}tplOiA@nQiaNd^HV2{;a57_8S|n!vJPnt&ki)~Ey?*f!`oL=lX~Fc@S> z0;LiV1ehimMuehZ7!U-oT*78^`L^SL>q43&mIVj`g#t__APC_55Qczq9OOAr6f~Pa zQEwxMm?VK9@HhsN1fB;;f@1N~nY;}ZB?+PkS{*O8L=NVCiB!MUb41+iZ zo(Dy_%)zVG%PGECL<|Ec3KRth0-gts1En(l{{aydAO&XJlTZKv002ovPDHLkV1oA` B=Jfyo literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/roadturn 1.png b/wfc/images/samples/Summer/roadturn 1.png new file mode 100644 index 0000000000000000000000000000000000000000..5191e19bc09837b7fad63df2322c6093f8afc059 GIT binary patch literal 5681 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/roadturn 3.png b/wfc/images/samples/Summer/roadturn 3.png new file mode 100644 index 0000000000000000000000000000000000000000..b80c01bba17077fdc22705a8d9ac54766341c953 GIT binary patch literal 5645 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/watercorner 0.png b/wfc/images/samples/Summer/watercorner 0.png new file mode 100644 index 0000000000000000000000000000000000000000..c629e2db48d75cd3e89404edcb36e2d5d3b9be34 GIT binary patch literal 4635 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 R#*hF2002ovPDHLkV1ho{ literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/watercorner 1.png b/wfc/images/samples/Summer/watercorner 1.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d57665805c9467d5173ba2c4bc6a06c5ecb25b GIT binary patch literal 4688 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 S^<(h}nQW#sf&U+V=F&^0;r$y?tMG>sD1( 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 Y{{?zo*&iNAKmY&$07*qoM6N<$g7Y>Rt^fc4 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/waterside 2.png b/wfc/images/samples/Summer/waterside 2.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a1faf0ce7dbe199efdc55fcd677eb28da5cf2d GIT binary patch literal 4705 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/waterturn 0.png b/wfc/images/samples/Summer/waterturn 0.png new file mode 100644 index 0000000000000000000000000000000000000000..3d3444919f06452641364a6f19d84a7f6855b461 GIT binary patch literal 5212 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 W$8paeo%`+p0000P~L} literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/waterturn 1.png b/wfc/images/samples/Summer/waterturn 1.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9319f4023fb63a1931b5a18f37c49b4ecdf650 GIT binary patch literal 5173 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/waterturn 2.png b/wfc/images/samples/Summer/waterturn 2.png new file mode 100644 index 0000000000000000000000000000000000000000..baf8eae3a64281be4d8584e0beb8253bfe3b1b9f GIT binary patch literal 5133 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Summer/waterturn 3.png b/wfc/images/samples/Summer/waterturn 3.png new file mode 100644 index 0000000000000000000000000000000000000000..bf26d7c92e7d7a8102c4eee89956b9772f2fa23d GIT binary patch literal 5011 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Town.png b/wfc/images/samples/Town.png new file mode 100644 index 0000000000000000000000000000000000000000..925de00c01cb18a34746b4e51a8fc302828b5137 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DinK$vl=HlH+5Fx%6`F+?Lc`O2Dv|K}Un*0FAKj^yEqdR^}n zDR6*gsUd@h;;tlvhFc8B75@F7T$3ObDbr?{<50$D*07LqwebW7KmWzqEuDrmdKI;Vst E0HMx6CjbBd literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Trick Knot.png b/wfc/images/samples/Trick Knot.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9c6e6e137106a214c2026065ae77e44a4324aa GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^f*{Pn1SGfcUswyII14-?iy0WWg+Q3`(%rg0K*3^9 z7sn8diF+q`^Bqv&aQXS=pZT%E#GtKxN6IcnNezQzpiR^CeV5YPgg&ebxsLQ0KEf9anMpkTbG zi(`n!#H|w@g$^iiFe}$jaet(4E0P{{WQ)wK$vkgV6h3U1%D-LpLj1tIr9}sp|6bK@ zTr_{fgoVrveCqn_H}5)6THtmkn6uEHNp){*=ILq7FZpE{>zY&8>zY4ZO7)#JRV4e$ TkuSnPs~9|8{an^LB{Ts5)m%Es literal 0 HcmV?d00001 diff --git a/wfc/images/samples/Water.png b/wfc/images/samples/Water.png new file mode 100644 index 0000000000000000000000000000000000000000..9af51fceef5334cadd2459ec8dd07c038069054b GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9GGLLkg|>2BR0prE^_ zi(`n!#N?C%?Eel+^zh8kc~oL#pj6y(v42_9hBOzopr0ADpOlmGw# literal 0 HcmV?d00001 diff --git a/wfc/images/samples/blackdots.png b/wfc/images/samples/blackdots.png new file mode 100644 index 0000000000000000000000000000000000000000..6feedb68803fa14116b958ec4281401f5974fc51 GIT binary patch literal 2804 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 literal 0 HcmV?d00001 diff --git a/wfc/images/samples/blackdotsstripe.png b/wfc/images/samples/blackdotsstripe.png new file mode 100644 index 0000000000000000000000000000000000000000..663bd44e307a155c76b8a418c57a608e88c2e674 GIT binary patch literal 2804 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 JPDHLkV1l!oHjV%Q literal 0 HcmV?d00001 diff --git a/wfc/images/samples/extrapolate_1a.png b/wfc/images/samples/extrapolate_1a.png new file mode 100644 index 0000000000000000000000000000000000000000..bfef84e2852f558e8fd0a227ff8f0fa5c9df2aaf GIT binary patch literal 3147 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 B9Jc@f literal 0 HcmV?d00001 diff --git a/wfc/images/samples/village3.png b/wfc/images/samples/village3.png new file mode 100644 index 0000000000000000000000000000000000000000..0ea201305f952a8bf381583024b0b0a55dc14709 GIT binary patch literal 3106 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 literal 0 HcmV?d00001 diff --git a/wfc/logs/logs.txt b/wfc/logs/logs.txt new file mode 100644 index 0000000..66f6b63 --- /dev/null +++ b/wfc/logs/logs.txt @@ -0,0 +1 @@ +Log files will be placed in this directory. diff --git a/wfc/pyproject.toml b/wfc/pyproject.toml new file mode 100644 index 0000000..3d4ad49 --- /dev/null +++ b/wfc/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "wfc_python" +version = "2019f" +description = "Implementation of wave function collapse in Python." +authors = [ + "Isaac Karth " +] +license = "MIT" +readme = "README.md" +python = "^3.5" +repository = "https://github.com/ikarth/wfc_python" +keywords = ["sample", "wfc", "wave function collapse"] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Topic :: Utilities", + "License :: OSI Approved :: MIT License", +] + +[build-system] + +requires = [ + "setuptools", +] +build-backend = "setuptools.build_meta" + +[dependencies] +numpy +imageio + +[dev-dependencies] +pytest +sphinx diff --git a/wfc/samples.xml b/wfc/samples.xml new file mode 100644 index 0000000..6e9c8e0 --- /dev/null +++ b/wfc/samples.xml @@ -0,0 +1,3 @@ + + + diff --git a/wfc/samples_cats.xml b/wfc/samples_cats.xml new file mode 100644 index 0000000..1a41e62 --- /dev/null +++ b/wfc/samples_cats.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_original.xml b/wfc/samples_original.xml new file mode 100644 index 0000000..04be934 --- /dev/null +++ b/wfc/samples_original.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_reference.xml b/wfc/samples_reference.xml new file mode 100644 index 0000000..a5e566a --- /dev/null +++ b/wfc/samples_reference.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_reference_continue.xml b/wfc/samples_reference_continue.xml new file mode 100644 index 0000000..e126c90 --- /dev/null +++ b/wfc/samples_reference_continue.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_reference_nohogs.xml b/wfc/samples_reference_nohogs.xml new file mode 100644 index 0000000..f354a4b --- /dev/null +++ b/wfc/samples_reference_nohogs.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_test.xml b/wfc/samples_test.xml new file mode 100644 index 0000000..5c3d16b --- /dev/null +++ b/wfc/samples_test.xml @@ -0,0 +1,3 @@ + + + diff --git a/wfc/samples_test_ground.xml b/wfc/samples_test_ground.xml new file mode 100644 index 0000000..2132117 --- /dev/null +++ b/wfc/samples_test_ground.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/wfc/samples_test_vis.xml b/wfc/samples_test_vis.xml new file mode 100644 index 0000000..91945d8 --- /dev/null +++ b/wfc/samples_test_vis.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/wfc/setup.py b/wfc/setup.py new file mode 100644 index 0000000..bac24a4 --- /dev/null +++ b/wfc/setup.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import setuptools + +if __name__ == "__main__": + setuptools.setup() diff --git a/wfc/tests/__init__.py b/wfc/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wfc/tests/conftest.py b/wfc/tests/conftest.py new file mode 100644 index 0000000..e0cf714 --- /dev/null +++ b/wfc/tests/conftest.py @@ -0,0 +1,13 @@ +import os.path +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) + +class Resources: + def get_image(self, image): + return os.path.join(PROJECT_ROOT, "images", image) + + +@pytest.fixture("session") +def resources(): + return Resources() \ No newline at end of file diff --git a/wfc/tests/test_wfc_adjacency.py b/wfc/tests/test_wfc_adjacency.py new file mode 100644 index 0000000..e8a743d --- /dev/null +++ b/wfc/tests/test_wfc_adjacency.py @@ -0,0 +1,35 @@ +"""Convert input data to adjacency information""" + +import imageio +from wfc import wfc_tiles +from wfc import wfc_patterns +from wfc import wfc_adjacency + + +def test_adjacency_extraction(resources): + # TODO: generalize this to more than the four cardinal directions + direction_offsets = list(enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)])) + + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tile_size = 1 + pattern_width = 2 + rotations = 0 + _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) + pattern_catalog, _pattern_weights, _pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( + tile_grid, pattern_width, rotations + ) + adjacency_relations = wfc_adjacency.adjacency_extraction( + pattern_grid, pattern_catalog, direction_offsets + ) + assert ((0, -1), -6150964001204120324, -4042134092912931260) in adjacency_relations + assert ((-1, 0), -4042134092912931260, 3069048847358774683) in adjacency_relations + assert ((1, 0), -3950451988873469076, -3950451988873469076) in adjacency_relations + assert ((-1, 0), -3950451988873469076, -3950451988873469076) in adjacency_relations + assert ((0, 1), -3950451988873469076, 3336256675067683735) in adjacency_relations + assert ( + not ((0, -1), -3950451988873469076, -3950451988873469076) in adjacency_relations + ) + assert ( + not ((0, 1), -3950451988873469076, -3950451988873469076) in adjacency_relations + ) diff --git a/wfc/tests/test_wfc_patterns.py b/wfc/tests/test_wfc_patterns.py new file mode 100644 index 0000000..4c19a73 --- /dev/null +++ b/wfc/tests/test_wfc_patterns.py @@ -0,0 +1,47 @@ +import imageio +import numpy as np +from wfc import wfc_patterns +from wfc import wfc_tiles + + +def test_unique_patterns_2d(resources): + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tile_size = 1 + pattern_width = 2 + _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) + + _patterns_in_grid, pattern_contents_list, patch_codes = wfc_patterns.unique_patterns_2d( + tile_grid, pattern_width, True + ) + assert patch_codes[1][2] == 4867810695119132864 + assert pattern_contents_list[7][1][1] == 8253868773529191888 + + +def test_make_pattern_catalog(resources): + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tile_size = 1 + pattern_width = 2 + _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) + + pattern_catalog, pattern_weights, pattern_list, _pattern_grid = wfc_patterns.make_pattern_catalog( + tile_grid, pattern_width + ) + assert pattern_weights[-6150964001204120324] == 1 + assert pattern_list[3] == 2800765426490226432 + assert pattern_catalog[5177878755649963747][0][1] == -8754995591521426669 + + +def test_pattern_to_tile(resources): + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tile_size = 1 + pattern_width = 2 + _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) + + pattern_catalog, _pattern_weights, _pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( + tile_grid, pattern_width + ) + new_tile_grid = wfc_patterns.pattern_grid_to_tiles(pattern_grid, pattern_catalog) + assert np.array_equal(tile_grid, new_tile_grid) diff --git a/wfc/tests/test_wfc_solver.py b/wfc/tests/test_wfc_solver.py new file mode 100644 index 0000000..9cdeddc --- /dev/null +++ b/wfc/tests/test_wfc_solver.py @@ -0,0 +1,203 @@ +import imageio +import numpy +from wfc import wfc_solver +from wfc import wfc_tiles +from wfc import wfc_patterns +from wfc import wfc_adjacency + + +def test_makeWave(): + wave = wfc_solver.makeWave(3, 10, 20, ground=[-1]) + # print(wave) + # print(wave.sum()) + # print((2*10*19) + (1*10*1)) + assert wave.sum() == (2 * 10 * 19) + (1 * 10 * 1) + assert wave[2, 5, 19] == True + assert wave[1, 5, 19] == False + + +def test_entropyLocationHeuristic(): + wave = numpy.ones((5, 3, 4), dtype=bool) # everthing is possible + wave[1:, 0, 0] = False # first cell is fully observed + wave[4, :, 2] = False + preferences = numpy.ones((3, 4), dtype=float) * 0.5 + preferences[1, 2] = 0.3 + preferences[1, 1] = 0.1 + heu = wfc_solver.makeEntropyLocationHeuristic(preferences) + result = heu(wave) + assert [1, 2] == result + + +def test_observe(): + + my_wave = numpy.ones((5, 3, 4), dtype=bool) + my_wave[0, 1, 2] = False + + def locHeu(wave): + assert numpy.array_equal(wave, my_wave) + return 1, 2 + + def patHeu(weights, wave): + assert numpy.array_equal(weights, my_wave[:, 1, 2]) + return 3 + + assert wfc_solver.observe(my_wave, locationHeuristic=locHeu, patternHeuristic=patHeu) == ( + 3, + 1, + 2, + ) + + +def test_propagate(): + wave = numpy.ones((3, 3, 4), dtype=bool) + adjLists = {} + # checkerboard #0/#1 or solid fill #2 + adjLists[(+1, 0)] = adjLists[(-1, 0)] = adjLists[(0, +1)] = adjLists[(0, -1)] = [ + [1], + [0], + [2], + ] + wave[:, 0, 0] = False + wave[0, 0, 0] = True + adj = wfc_solver.makeAdj(adjLists) + wfc_solver.propagate(wave, adj, periodic=False) + expected_result = numpy.array( + [ + [ + [True, False, True, False], + [False, True, False, True], + [True, False, True, False], + ], + [ + [False, True, False, True], + [True, False, True, False], + [False, True, False, True], + ], + [ + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ], + ] + ) + assert numpy.array_equal(wave, expected_result) + + +def test_run(): + wave = wfc_solver.makeWave(3, 3, 4) + adjLists = {} + adjLists[(+1, 0)] = adjLists[(-1, 0)] = adjLists[(0, +1)] = adjLists[(0, -1)] = [ + [1], + [0], + [2], + ] + adj = wfc_solver.makeAdj(adjLists) + + first_result = wfc_solver.run( + wave.copy(), + adj, + locationHeuristic=wfc_solver.lexicalLocationHeuristic, + patternHeuristic=wfc_solver.lexicalPatternHeuristic, + periodic=False, + ) + + expected_first_result = numpy.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1]]) + + assert numpy.array_equal(first_result, expected_first_result) + + event_log = [] + + def onChoice(pattern, i, j): + event_log.append((pattern, i, j)) + + def onBacktrack(): + event_log.append("backtrack") + + second_result = wfc_solver.run( + wave.copy(), + adj, + locationHeuristic=wfc_solver.lexicalLocationHeuristic, + patternHeuristic=wfc_solver.lexicalPatternHeuristic, + periodic=True, + backtracking=True, + onChoice=onChoice, + onBacktrack=onBacktrack, + ) + + expected_second_result = numpy.array([[2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]) + + assert numpy.array_equal(second_result, expected_second_result) + print(event_log) + assert event_log == [(0, 0, 0), "backtrack", (2, 0, 0)] + + class Infeasible(Exception): + pass + + def explode(wave): + if wave.sum() < 20: + raise Infeasible + + try: + result = wfc_solver.run( + wave.copy(), + adj, + locationHeuristic=wfc_solver.lexicalLocationHeuristic, + patternHeuristic=wfc_solver.lexicalPatternHeuristic, + periodic=True, + backtracking=True, + checkFeasible=explode, + ) + print(result) + happy = False + except wfc_solver.Contradiction: + happy = True + + assert happy + + +def _test_recurse_vs_loop(resources): + # FIXME: run_recurse or run_loop do not exist anymore + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tile_size = 1 + pattern_width = 2 + rotations = 0 + output_size = [84, 84] + direction_offsets = list(enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)])) + _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) + pattern_catalog, pattern_weights, pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( + tile_grid, pattern_width, rotations + ) + adjacency_relations = wfc_adjacency.adjacency_extraction( + pattern_grid, pattern_catalog, direction_offsets + ) + number_of_patterns = len(pattern_weights) + decode_patterns = {x: i for i, x in enumerate(pattern_list)} + adjacency_list = {} + for i, d in direction_offsets: + adjacency_list[d] = [set() for i in pattern_weights] + for i in adjacency_relations: + adjacency_list[i[0]][decode_patterns[i[1]]].add(decode_patterns[i[2]]) + wave = wfc_solver.makeWave(number_of_patterns, output_size[0], output_size[1]) + adjacency_matrix = wfc_solver.makeAdj(adjacency_list) + solution_loop = wfc_solver.run( + wave.copy(), + adjacency_matrix, + locationHeuristic=wfc_solver.lexicalLocationHeuristic, + patternHeuristic=wfc_solver.lexicalPatternHeuristic, + periodic=True, + backtracking=False, + onChoice=None, + onBacktrack=None, + ) + solution_recurse = wfc_solver.run_recurse( + wave.copy(), + adjacency_matrix, + locationHeuristic=wfc_solver.lexicalLocationHeuristic, + patternHeuristic=wfc_solver.lexicalPatternHeuristic, + periodic=True, + backtracking=False, + onChoice=None, + onBacktrack=None, + ) + assert numpy.array_equiv(solution_loop, solution_recurse) diff --git a/wfc/tests/test_wfc_tiles.py b/wfc/tests/test_wfc_tiles.py new file mode 100644 index 0000000..608d1fc --- /dev/null +++ b/wfc/tests/test_wfc_tiles.py @@ -0,0 +1,28 @@ +"""Breaks an image into consituant tiles.""" + +import imageio +from wfc import wfc_tiles + + +def test_image_to_tiles(resources): + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + tiles = wfc_tiles.image_to_tiles(img, 1) + assert tiles[2][2][0][0][0] == 255 + assert tiles[2][2][0][0][1] == 0 + + +def test_make_tile_catalog(resources): + filename = resources.get_image("samples/Red Maze.png") + img = imageio.imread(filename) + print(img) + tc, tg, cl, ut = wfc_tiles.make_tile_catalog(img, 1) + print("tile catalog") + print(tc) + print("tile grid") + print(tg) + print("code list") + print(cl) + print("unique tiles") + print(ut) + assert ut[1][0] == 7 diff --git a/wfc/wfc/__init__.py b/wfc/wfc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wfc/wfc/wfc_adjacency.py b/wfc/wfc/wfc_adjacency.py new file mode 100644 index 0000000..caed14a --- /dev/null +++ b/wfc/wfc/wfc_adjacency.py @@ -0,0 +1,48 @@ +"""Convert input data to adjacency information""" +import numpy as np + + +def adjacency_extraction( + pattern_grid, pattern_catalog, direction_offsets, pattern_size=[2, 2] +): + """Takes a pattern grid and returns a list of all of the legal adjacencies found in it.""" + + def is_valid_overlap_xy(adjacency_direction, pattern_1, pattern_2): + """Given a direction and two patterns, find the overlap of the two patterns + and return True if the intersection matches.""" + dimensions = (1, 0) + not_a_number = -1 + + # TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... + shifted = np.roll( + np.pad( + pattern_catalog[pattern_2], + max(pattern_size), + mode="constant", + constant_values=not_a_number, + ), + adjacency_direction, + dimensions, + ) + compare = shifted[ + pattern_size[0] : pattern_size[0] + pattern_size[0], + pattern_size[1] : pattern_size[1] + pattern_size[1], + ] + + left = max(0, 0, +adjacency_direction[0]) + right = min(pattern_size[0], pattern_size[0] + adjacency_direction[0]) + top = max(0, 0 + adjacency_direction[1]) + bottom = min(pattern_size[1], pattern_size[1] + adjacency_direction[1]) + a = pattern_catalog[pattern_1][top:bottom, left:right] + b = compare[top:bottom, left:right] + res = np.array_equal(a, b) + return res + + pattern_list = list(pattern_catalog.keys()) + legal = [] + for pattern_1 in pattern_list: + for pattern_2 in pattern_list: + for _direction_index, direction in direction_offsets: + if is_valid_overlap_xy(direction, pattern_1, pattern_2): + legal.append((direction, pattern_1, pattern_2)) + return legal diff --git a/wfc/wfc/wfc_control.py b/wfc/wfc/wfc_control.py new file mode 100644 index 0000000..864612a --- /dev/null +++ b/wfc/wfc/wfc_control.py @@ -0,0 +1,451 @@ +from .wfc_tiles import make_tile_catalog +from .wfc_patterns import ( + pattern_grid_to_tiles, + make_pattern_catalog_with_rotations, +) +from .wfc_adjacency import adjacency_extraction +from .wfc_solver import ( + run, + makeWave, + makeAdj, + lexicalLocationHeuristic, + lexicalPatternHeuristic, + makeWeightedPatternHeuristic, + Contradiction, + StopEarly, + makeEntropyLocationHeuristic, + make_global_use_all_patterns, + makeRandomLocationHeuristic, + makeRandomPatternHeuristic, + TimedOut, + simpleLocationHeuristic, + makeSpiralLocationHeuristic, + makeHilbertLocationHeuristic, + makeAntiEntropyLocationHeuristic, + makeRarestPatternHeuristic, +) +from .wfc_visualize import ( + figure_list_of_tiles, + figure_false_color_tile_grid, + figure_pattern_catalog, + render_tiles_to_output, + figure_adjacencies, + make_solver_visualizers, + make_solver_loggers, +) +import imageio +import numpy as np +import time + + +def visualize_tiles(unique_tiles, tile_catalog, tile_grid): + if False: + figure_list_of_tiles(unique_tiles, tile_catalog) + figure_false_color_tile_grid(tile_grid) + + +def visualize_patterns(pattern_catalog, tile_catalog, pattern_weights, pattern_width): + if False: + figure_pattern_catalog( + pattern_catalog, tile_catalog, pattern_weights, pattern_width + ) + + +def make_log_stats(): + log_line = 0 + + def log_stats(stats, filename): + nonlocal log_line + if stats: + log_line += 1 + with open(filename, "a", encoding="utf_8") as logf: + if log_line < 2: + for s in stats.keys(): + print(str(s), end="\t", file=logf) + print("", file=logf) + for s in stats.keys(): + print(str(stats[s]), end="\t", file=logf) + print("", file=logf) + + return log_stats + + +def execute_wfc( + filename, + tile_size=0, + pattern_width=2, + rotations=8, + output_size=[48, 48], + ground=None, + attempt_limit=10, + output_periodic=True, + input_periodic=True, + loc_heuristic="lexical", + choice_heuristic="lexical", + visualize=True, + global_constraint=False, + backtracking=False, + log_filename="log", + logging=True, + global_constraints=None, + log_stats_to_output=None, +): + timecode = f"{time.time()}" + time_begin = time.time() + output_destination = r"./output/" + input_folder = r"./images/samples/" + + rotations -= 1 # change to zero-based + + input_stats = { + "filename": filename, + "tile_size": tile_size, + "pattern_width": pattern_width, + "rotations": rotations, + "output_size": output_size, + "ground": ground, + "attempt_limit": attempt_limit, + "output_periodic": output_periodic, + "input_periodic": input_periodic, + "location heuristic": loc_heuristic, + "choice heuristic": choice_heuristic, + "global constraint": global_constraint, + "backtracking": backtracking, + } + + # Load the image + img = imageio.imread(input_folder + filename + ".png") + img = img[:, :, :3] # TODO: handle alpha channels + + # TODO: generalize this to more than the four cardinal directions + direction_offsets = list(enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)])) + + tile_catalog, tile_grid, _code_list, _unique_tiles = make_tile_catalog(img, tile_size) + ( + pattern_catalog, + pattern_weights, + pattern_list, + pattern_grid, + ) = make_pattern_catalog_with_rotations( + tile_grid, pattern_width, input_is_periodic=input_periodic, rotations=rotations + ) + + print("pattern catalog") + + # visualize_tiles(unique_tiles, tile_catalog, tile_grid) + # visualize_patterns(pattern_catalog, tile_catalog, pattern_weights, pattern_width) + # figure_list_of_tiles(unique_tiles, tile_catalog, output_filename=f"visualization/tilelist_{filename}_{timecode}") + # figure_false_color_tile_grid(tile_grid, output_filename=f"visualization/tile_falsecolor_{filename}_{timecode}") + if visualize: + figure_pattern_catalog( + pattern_catalog, + tile_catalog, + pattern_weights, + pattern_width, + output_filename=f"visualization/pattern_catalog_{filename}_{timecode}", + ) + + print("profiling adjacency relations") + adjacency_relations = None + + if False: + import pprofile + profiler = pprofile.Profile() + with profiler: + adjacency_relations = adjacency_extraction( + pattern_grid, + pattern_catalog, + direction_offsets, + [pattern_width, pattern_width], + ) + profiler.dump_stats(f"logs/profile_adj_{filename}_{timecode}.txt") + else: + adjacency_relations = adjacency_extraction( + pattern_grid, + pattern_catalog, + direction_offsets, + [pattern_width, pattern_width], + ) + + print("adjacency_relations") + + if visualize: + figure_adjacencies( + adjacency_relations, + direction_offsets, + tile_catalog, + pattern_catalog, + pattern_width, + [tile_size, tile_size], + output_filename=f"visualization/adjacency_{filename}_{timecode}_A", + ) + # figure_adjacencies(adjacency_relations, direction_offsets, tile_catalog, pattern_catalog, pattern_width, [tile_size, tile_size], output_filename=f"visualization/adjacency_{filename}_{timecode}_B", render_b_first=True) + + print(f"output size: {output_size}\noutput periodic: {output_periodic}") + number_of_patterns = len(pattern_weights) + print(f"# patterns: {number_of_patterns}") + decode_patterns = dict(enumerate(pattern_list)) + encode_patterns = {x: i for i, x in enumerate(pattern_list)} + _encode_directions = {j: i for i, j in direction_offsets} + + adjacency_list = {} + for i, d in direction_offsets: + adjacency_list[d] = [set() for i in pattern_weights] + # print(adjacency_list) + for i in adjacency_relations: + # print(i) + # print(decode_patterns[i[1]]) + adjacency_list[i[0]][encode_patterns[i[1]]].add(encode_patterns[i[2]]) + + print(f"adjacency: {len(adjacency_list)}") + + time_adjacency = time.time() + + ### Ground ### + + ground_list = [] + if ground != 0: + ground_list = np.vectorize(lambda x: encode_patterns[x])( + pattern_grid.flat[(ground - 1) :] + ) + if len(ground_list) < 1: + ground_list = None + + if not (ground_list is None): + ground_catalog = { + encode_patterns[k]: v + for k, v in pattern_catalog.items() + if encode_patterns[k] in ground_list + } + if visualize: + figure_pattern_catalog( + ground_catalog, + tile_catalog, + pattern_weights, + pattern_width, + output_filename=f"visualization/patterns_ground_{filename}_{timecode}", + ) + + wave = makeWave( + number_of_patterns, output_size[0], output_size[1], ground=ground_list + ) + adjacency_matrix = makeAdj(adjacency_list) + + ### Heuristics ### + + encoded_weights = np.zeros((number_of_patterns), dtype=np.float64) + for w_id, w_val in pattern_weights.items(): + encoded_weights[encode_patterns[w_id]] = w_val + choice_random_weighting = np.random.random(wave.shape[1:]) * 0.1 + + pattern_heuristic = lexicalPatternHeuristic + if choice_heuristic == "rarest": + pattern_heuristic = makeRarestPatternHeuristic(encoded_weights) + if choice_heuristic == "weighted": + pattern_heuristic = makeWeightedPatternHeuristic(encoded_weights) + if choice_heuristic == "random": + pattern_heuristic = makeRandomPatternHeuristic(encoded_weights) + + print(loc_heuristic) + location_heuristic = lexicalLocationHeuristic + if loc_heuristic == "anti-entropy": + location_heuristic = makeAntiEntropyLocationHeuristic(choice_random_weighting) + if loc_heuristic == "entropy": + location_heuristic = makeEntropyLocationHeuristic(choice_random_weighting) + if loc_heuristic == "random": + location_heuristic = makeRandomLocationHeuristic(choice_random_weighting) + if loc_heuristic == "simple": + location_heuristic = simpleLocationHeuristic + if loc_heuristic == "spiral": + location_heuristic = makeSpiralLocationHeuristic(choice_random_weighting) + if loc_heuristic == "hilbert": + location_heuristic = makeHilbertLocationHeuristic(choice_random_weighting) + + ### Visualization ### + + ( + visualize_choice, + visualize_wave, + visualize_backtracking, + visualize_propagate, + visualize_final, + visualize_after, + ) = (None, None, None, None, None, None) + if visualize: + ( + visualize_choice, + visualize_wave, + visualize_backtracking, + visualize_propagate, + visualize_final, + visualize_after, + ) = make_solver_visualizers( + f"{filename}_{timecode}", + wave, + decode_patterns=decode_patterns, + pattern_catalog=pattern_catalog, + tile_catalog=tile_catalog, + tile_size=[tile_size, tile_size], + ) + if logging: + ( + visualize_choice, + visualize_wave, + visualize_backtracking, + visualize_propagate, + visualize_final, + visualize_after, + ) = make_solver_loggers(f"{filename}_{timecode}", input_stats.copy()) + if logging and visualize: + vis = make_solver_visualizers( + f"{filename}_{timecode}", + wave, + decode_patterns=decode_patterns, + pattern_catalog=pattern_catalog, + tile_catalog=tile_catalog, + tile_size=[tile_size, tile_size], + ) + log = make_solver_loggers(f"{filename}_{timecode}", input_stats.copy()) + + def visfunc(idx): + def vf(*args, **kwargs): + if vis[idx]: + vis[idx](*args, **kwargs) + if log[idx]: + return log[idx](*args, **kwargs) + + return vf + + ( + visualize_choice, + visualize_wave, + visualize_backtracking, + visualize_propagate, + visualize_final, + visualize_after, + ) = [visfunc(x) for x in range(len(vis))] + + ### Global Constraints ### + active_global_constraint = lambda wave: True + if global_constraint == "allpatterns": + active_global_constraint = make_global_use_all_patterns() + print(active_global_constraint) + + ### Search Depth Limit + def makeSearchLengthLimit(max_limit): + search_length_counter = 0 + + def searchLengthLimit(wave): + nonlocal search_length_counter + search_length_counter += 1 + return search_length_counter <= max_limit + + return searchLengthLimit + + combined_constraints = [active_global_constraint, makeSearchLengthLimit(1200)] + + def combinedConstraints(wave): + print + return all([fn(wave) for fn in combined_constraints]) + + ### Solving ### + + time_solve_start = None + time_solve_end = None + + solution_tile_grid = None + print("solving...") + attempts = 0 + while attempts < attempt_limit: + attempts += 1 + end_early = False + time_solve_start = time.time() + stats = {} + # profiler = pprofile.Profile() + if True: + # with profiler: + # with PyCallGraph(output=GraphvizOutput(output_file=f"visualization/pycallgraph_{filename}_{timecode}.png")): + try: + solution = run( + wave.copy(), + adjacency_matrix, + locationHeuristic=location_heuristic, + patternHeuristic=pattern_heuristic, + periodic=output_periodic, + backtracking=backtracking, + onChoice=visualize_choice, + onBacktrack=visualize_backtracking, + onObserve=visualize_wave, + onPropagate=visualize_propagate, + onFinal=visualize_final, + checkFeasible=combinedConstraints, + ) + if visualize_after: + stats = visualize_after() + # print(solution) + # print(stats) + solution_as_ids = np.vectorize(lambda x: decode_patterns[x])(solution) + solution_tile_grid = pattern_grid_to_tiles( + solution_as_ids, pattern_catalog + ) + + print("Solution:") + # print(solution_tile_grid) + render_tiles_to_output( + solution_tile_grid, + tile_catalog, + [tile_size, tile_size], + output_destination + filename + "_" + timecode + ".png", + ) + + time_solve_end = time.time() + stats.update({"outcome": "success"}) + except StopEarly: + print("Skipping...") + end_early = True + stats.update({"outcome": "skipped"}) + except TimedOut: + print("Timed Out") + if visualize_after: + stats = visualize_after() + stats.update({"outcome": "timed_out"}) + except Contradiction: + print("Contradiction") + if visualize_after: + stats = visualize_after() + stats.update({"outcome": "contradiction"}) + # profiler.dump_stats(f"logs/profile_{filename}_{timecode}.txt") + + outstats = {} + outstats.update(input_stats) + solve_duration = time.time() - time_solve_start + try: + solve_duration = time_solve_end - time_solve_start + except TypeError: + pass + adjacency_duration = 0 + try: + adjacency_duration = time_solve_start - time_adjacency + except TypeError: + pass + outstats.update( + { + "attempts": attempts, + "time_start": time_begin, + "time_adjacency": time_adjacency, + "adjacency_duration": adjacency_duration, + "time solve start": time_solve_start, + "time solve end": time_solve_end, + "solve duration": solve_duration, + "pattern count": number_of_patterns, + } + ) + outstats.update(stats) + if not log_stats_to_output is None: + log_stats_to_output(outstats, output_destination + log_filename + ".tsv") + if not solution_tile_grid is None: + return solution_tile_grid + if end_early: + return None + + return None diff --git a/wfc/wfc/wfc_patterns.py b/wfc/wfc/wfc_patterns.py new file mode 100644 index 0000000..90dd3ed --- /dev/null +++ b/wfc/wfc/wfc_patterns.py @@ -0,0 +1,179 @@ +"Extract patterns from grids of tiles." +from .wfc_utilities import hash_downto +from collections import Counter +import numpy as np + + +def unique_patterns_2d(agrid, ksize, periodic_input): + assert ksize >= 1 + if periodic_input: + agrid = np.pad( + agrid, + ((0, ksize - 1), (0, ksize - 1), *(((0, 0),) * (len(agrid.shape) - 2))), + mode="wrap", + ) + else: + # TODO: implement non-wrapped image handling + # a = np.pad(a, ((0,k-1),(0,k-1),*(((0,0),)*(len(a.shape)-2))), mode='constant', constant_values=None) + agrid = np.pad( + agrid, + ((0, ksize - 1), (0, ksize - 1), *(((0, 0),) * (len(agrid.shape) - 2))), + mode="wrap", + ) + + patches = np.lib.stride_tricks.as_strided( + agrid, + ( + agrid.shape[0] - ksize + 1, + agrid.shape[1] - ksize + 1, + ksize, + ksize, + *agrid.shape[2:], + ), + agrid.strides[:2] + agrid.strides[:2] + agrid.strides[2:], + writeable=False, + ) + patch_codes = hash_downto(patches, 2) + uc, ui = np.unique(patch_codes, return_index=True) + locs = np.unravel_index(ui, patch_codes.shape) + up = patches[locs[0], locs[1]] + ids = np.vectorize({code: ind for ind, code in enumerate(uc)}.get)(patch_codes) + return ids, up, patch_codes + + +def unique_patterns_brute_force(grid, size, periodic_input): + padded_grid = np.pad( + grid, + ((0, size - 1), (0, size - 1), *(((0, 0),) * (len(grid.shape) - 2))), + mode="wrap", + ) + patches = [] + for x in range(grid.shape[0]): + row_patches = [] + for y in range(grid.shape[1]): + row_patches.append( + np.ndarray.tolist(padded_grid[x : x + size, y : y + size]) + ) + patches.append(row_patches) + patches = np.array(patches) + patch_codes = hash_downto(patches, 2) + uc, ui = np.unique(patch_codes, return_index=True) + locs = np.unravel_index(ui, patch_codes.shape) + up = patches[locs[0], locs[1]] + ids = np.vectorize({c: i for i, c in enumerate(uc)}.get)(patch_codes) + return ids, up + + +def make_pattern_catalog(tile_grid, pattern_width, input_is_periodic=True): + """Returns a pattern catalog (dictionary of pattern hashes to consituent tiles), +an ordered list of pattern weights, and an ordered list of pattern contents.""" + _patterns_in_grid, pattern_contents_list, patch_codes = unique_patterns_2d( + tile_grid, pattern_width, input_is_periodic + ) + dict_of_pattern_contents = {} + for pat_idx in range(pattern_contents_list.shape[0]): + p_hash = hash_downto(pattern_contents_list[pat_idx], 0) + dict_of_pattern_contents.update( + {p_hash.item(): pattern_contents_list[pat_idx]} + ) + pattern_frequency = Counter(hash_downto(pattern_contents_list, 1)) + return ( + dict_of_pattern_contents, + pattern_frequency, + hash_downto(pattern_contents_list, 1), + patch_codes, + ) + + +def identity_grid(grid): + """Do nothing to the grid""" + # return np.array([[7,5,5,5],[5,0,0,0],[5,0,1,0],[5,0,0,0]]) + return grid + + +def reflect_grid(grid): + """Reflect the grid left/right""" + return np.fliplr(grid) + + +def rotate_grid(grid): + """Rotate the grid""" + return np.rot90(grid, axes=(1, 0)) + + +def make_pattern_catalog_with_rotations( + tile_grid, pattern_width, rotations=7, input_is_periodic=True +): + rotated_tile_grid = tile_grid.copy() + merged_dict_of_pattern_contents = {} + merged_pattern_frequency = Counter() + merged_pattern_contents_list = None + merged_patch_codes = None + + def _make_catalog(): + nonlocal rotated_tile_grid, merged_dict_of_pattern_contents, merged_pattern_contents_list, merged_pattern_frequency, merged_patch_codes + ( + dict_of_pattern_contents, + pattern_frequency, + pattern_contents_list, + patch_codes, + ) = make_pattern_catalog(rotated_tile_grid, pattern_width, input_is_periodic) + merged_dict_of_pattern_contents.update(dict_of_pattern_contents) + merged_pattern_frequency.update(pattern_frequency) + if merged_pattern_contents_list is None: + merged_pattern_contents_list = pattern_contents_list.copy() + else: + merged_pattern_contents_list = np.unique( + np.concatenate((merged_pattern_contents_list, pattern_contents_list)) + ) + if merged_patch_codes is None: + merged_patch_codes = patch_codes.copy() + + counter = 0 + grid_ops = [ + identity_grid, + reflect_grid, + rotate_grid, + reflect_grid, + rotate_grid, + reflect_grid, + rotate_grid, + reflect_grid, + ] + while counter <= (rotations): + # print(rotated_tile_grid.shape) + # print(np.array_equiv(reflect_grid(rotated_tile_grid.copy()), rotate_grid(rotated_tile_grid.copy()))) + + # print(counter) + # print(grid_ops[counter].__name__) + rotated_tile_grid = grid_ops[counter](rotated_tile_grid.copy()) + # print(rotated_tile_grid) + # print("---") + _make_catalog() + counter += 1 + + # assert False + return ( + merged_dict_of_pattern_contents, + merged_pattern_frequency, + merged_pattern_contents_list, + merged_patch_codes, + ) + + +def pattern_grid_to_tiles(pattern_grid, pattern_catalog): + anchor_x = 0 + anchor_y = 0 + + def pattern_to_tile(pattern): + # if isinstance(pattern, list): + # ptrns = [] + # for p in pattern: + # print(p) + # ptrns.push(pattern_to_tile(p)) + # print(ptrns) + # assert False + # return ptrns + return pattern_catalog[pattern][anchor_x][anchor_y] + + return np.vectorize(pattern_to_tile)(pattern_grid) diff --git a/wfc/wfc/wfc_solver.py b/wfc/wfc/wfc_solver.py new file mode 100644 index 0000000..2eea139 --- /dev/null +++ b/wfc/wfc/wfc_solver.py @@ -0,0 +1,439 @@ +from scipy import sparse +import numpy +import sys +import math +import itertools + +# By default Python has a very low recursion limit. +# Might still be better to rewrite te recursion as a loop, of course +sys.setrecursionlimit(5500) + + +class Contradiction(Exception): + """Solving could not proceed without backtracking/restarting.""" + + pass + + +class TimedOut(Exception): + """Solve timed out.""" + + pass + + +class StopEarly(Exception): + """Aborting solve early.""" + + pass + + +def makeWave(n, w, h, ground=None): + wave = numpy.ones((n, w, h), dtype=bool) + if ground is not None: + wave[:, :, h - 1] = 0 + for g in ground: + wave[g, :,] = 0 + wave[g, :, h - 1] = 1 + # print(wave) + # for i in range(wave.shape[0]): + # print(wave[i]) + return wave + + +def makeAdj(adjLists): + adjMatrices = {} + # print(adjLists) + num_patterns = len(list(adjLists.values())[0]) + for d in adjLists: + m = numpy.zeros((num_patterns, num_patterns), dtype=bool) + for i, js in enumerate(adjLists[d]): + # print(js) + for j in js: + m[i, j] = 1 + adjMatrices[d] = sparse.csr_matrix(m) + return adjMatrices + + +###################################### +# Location Heuristics + + +def makeRandomLocationHeuristic(preferences): + def randomLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where(unresolved_cell_mask, preferences, numpy.inf) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + return randomLocationHeuristic + + +def makeEntropyLocationHeuristic(preferences): + def entropyLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where( + unresolved_cell_mask, + preferences + numpy.count_nonzero(wave, axis=0), + numpy.inf, + ) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + return entropyLocationHeuristic + + +def makeAntiEntropyLocationHeuristic(preferences): + def antiEntropyLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where( + unresolved_cell_mask, + preferences + numpy.count_nonzero(wave, axis=0), + -numpy.inf, + ) + row, col = numpy.unravel_index(numpy.argmax(cell_weights), cell_weights.shape) + return [row, col] + + return antiEntropyLocationHeuristic + + +def spiral_transforms(): + for N in itertools.count(start=1): + if N % 2 == 0: + yield (0, 1) # right + for _ in range(N): + yield (1, 0) # down + for _ in range(N): + yield (0, -1) # left + else: + yield (0, -1) # left + for _ in range(N): + yield (-1, 0) # up + for _ in range(N): + yield (0, 1) # right + + +def spiral_coords(x, y): + yield x, y + for transform in spiral_transforms(): + x += transform[0] + y += transform[1] + yield x, y + + +def fill_with_curve(arr, curve_gen): + arr_len = numpy.prod(arr.shape) + fill = 0 + for _, coord in enumerate(curve_gen): + # print(fill, idx, coord) + if fill < arr_len: + try: + arr[coord[0], coord[1]] = fill / arr_len + fill += 1 + except IndexError: + pass + else: + break + # print(arr) + return arr + + +def makeSpiralLocationHeuristic(preferences): + # https://stackoverflow.com/a/23707273/5562922 + + spiral_gen = ( + sc for sc in spiral_coords(preferences.shape[0] // 2, preferences.shape[1] // 2) + ) + + cell_order = fill_with_curve(preferences, spiral_gen) + + def spiralLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where(unresolved_cell_mask, cell_order, numpy.inf) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + return spiralLocationHeuristic + + +from hilbertcurve.hilbertcurve import HilbertCurve + + +def makeHilbertLocationHeuristic(preferences): + curve_size = math.ceil(math.sqrt(max(preferences.shape[0], preferences.shape[1]))) + print(curve_size) + curve_size = 4 + h_curve = HilbertCurve(curve_size, 2) + + def h_coords(): + for i in range(100000): + # print(i) + try: + coords = h_curve.coordinates_from_distance(i) + except ValueError: + coords = [0, 0] + # print(coords) + yield coords + + cell_order = fill_with_curve(preferences, h_coords()) + # print(cell_order) + + def hilbertLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where(unresolved_cell_mask, cell_order, numpy.inf) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + return hilbertLocationHeuristic + + +def simpleLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where( + unresolved_cell_mask, numpy.count_nonzero(wave, axis=0), numpy.inf + ) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + +def lexicalLocationHeuristic(wave): + unresolved_cell_mask = numpy.count_nonzero(wave, axis=0) > 1 + cell_weights = numpy.where(unresolved_cell_mask, 1.0, numpy.inf) + row, col = numpy.unravel_index(numpy.argmin(cell_weights), cell_weights.shape) + return [row, col] + + +##################################### +# Pattern Heuristics + + +def lexicalPatternHeuristic(weights, wave): + return numpy.nonzero(weights)[0][0] + + +def makeWeightedPatternHeuristic(weights): + num_of_patterns = len(weights) + + def weightedPatternHeuristic(wave, _): + # TODO: there's maybe a faster, more controlled way to do this sampling... + weighted_wave = weights * wave + weighted_wave /= weighted_wave.sum() + result = numpy.random.choice(num_of_patterns, p=weighted_wave) + return result + + return weightedPatternHeuristic + + +def makeRarestPatternHeuristic(weights): + """Return a function that chooses the rarest (currently least-used) pattern.""" + def weightedPatternHeuristic(wave, total_wave): + print(total_wave.shape) + # [print(e) for e in wave] + wave_sums = numpy.sum(total_wave, (1, 2)) + # print(wave_sums) + selected_pattern = numpy.random.choice( + numpy.where(wave_sums == wave_sums.max())[0] + ) + return selected_pattern + + return weightedPatternHeuristic + + +def makeMostCommonPatternHeuristic(weights): + """Return a function that chooses the most common (currently most-used) pattern.""" + def weightedPatternHeuristic(wave, total_wave): + print(total_wave.shape) + # [print(e) for e in wave] + wave_sums = numpy.sum(total_wave, (1, 2)) + selected_pattern = numpy.random.choice( + numpy.where(wave_sums == wave_sums.min())[0] + ) + return selected_pattern + + return weightedPatternHeuristic + + +def makeRandomPatternHeuristic(weights): + num_of_patterns = len(weights) + + def randomPatternHeuristic(wave, _): + # TODO: there's maybe a faster, more controlled way to do this sampling... + weighted_wave = 1.0 * wave + weighted_wave /= weighted_wave.sum() + result = numpy.random.choice(num_of_patterns, p=weighted_wave) + return result + + return randomPatternHeuristic + + +###################################### +# Global Constraints + + +def make_global_use_all_patterns(): + def global_use_all_patterns(wave): + """Returns true if at least one instance of each pattern is still possible.""" + return numpy.all(numpy.any(wave, axis=(1, 2))) + + return global_use_all_patterns + + +##################################### +# Solver + + +def propagate(wave, adj, periodic=False, onPropagate=None): + last_count = wave.sum() + + while True: + supports = {} + if periodic: + padded = numpy.pad(wave, ((0, 0), (1, 1), (1, 1)), mode="wrap") + else: + padded = numpy.pad( + wave, ((0, 0), (1, 1), (1, 1)), mode="constant", constant_values=True + ) + + for d in adj: + dx, dy = d + shifted = padded[ + :, 1 + dx : 1 + wave.shape[1] + dx, 1 + dy : 1 + wave.shape[2] + dy + ] + # print(f"shifted: {shifted.shape} | adj[d]: {adj[d].shape} | d: {d}") + # raise StopEarly + # supports[d] = numpy.einsum('pwh,pq->qwh', shifted, adj[d]) > 0 + supports[d] = (adj[d] @ shifted.reshape(shifted.shape[0], -1)).reshape( + shifted.shape + ) > 0 + + for d in adj: + wave *= supports[d] + + if wave.sum() == last_count: + break + else: + last_count = wave.sum() + + if onPropagate: + onPropagate(wave) + + if wave.sum() == 0: + raise Contradiction + + +def observe(wave, locationHeuristic, patternHeuristic): + i, j = locationHeuristic(wave) + pattern = patternHeuristic(wave[:, i, j], wave) + return pattern, i, j + + +# def run_loop(wave, adj, locationHeuristic, patternHeuristic, periodic=False, backtracking=False, onBacktrack=None, onChoice=None, checkFeasible=None): +# stack = [] +# while True: +# if checkFeasible: +# if not checkFeasible(wave): +# raise Contradiction +# stack.append(wave.copy()) +# propagate(wave, adj, periodic=periodic) +# try: +# pattern, i, j = observe(wave, locationHeuristic, patternHeuristic) +# if onChoice: +# onChoice(pattern, i, j) +# wave[:, i, j] = False +# wave[pattern, i, j] = True +# propagate(wave, adj, periodic=periodic) +# if wave.sum() > wave.shape[1] * wave.shape[2]: +# pass +# else: +# return numpy.argmax(wave, 0) +# except Contradiction: +# if backtracking: +# if onBacktrack: +# onBacktrack() +# wave = stack.pop() +# wave[pattern, i, j] = False +# else: +# raise + + +def run( + wave, + adj, + locationHeuristic, + patternHeuristic, + periodic=False, + backtracking=False, + onBacktrack=None, + onChoice=None, + onObserve=None, + onPropagate=None, + checkFeasible=None, + onFinal=None, + depth=0, + depth_limit=None, +): + # print("run.") + if checkFeasible: + if not checkFeasible(wave): + raise Contradiction + if depth_limit: + if depth > depth_limit: + raise TimedOut + if depth % 50 == 0: + print(depth) + original = wave.copy() + propagate(wave, adj, periodic=periodic, onPropagate=onPropagate) + try: + pattern, i, j = observe(wave, locationHeuristic, patternHeuristic) + if onChoice: + onChoice(pattern, i, j) + wave[:, i, j] = False + wave[pattern, i, j] = True + if onObserve: + onObserve(wave) + propagate(wave, adj, periodic=periodic, onPropagate=onPropagate) + if wave.sum() > wave.shape[1] * wave.shape[2]: + # return run(wave, adj, locationHeuristic, patternHeuristic, periodic, backtracking, onBacktrack) + return run( + wave, + adj, + locationHeuristic, + patternHeuristic, + periodic=periodic, + backtracking=backtracking, + onBacktrack=onBacktrack, + onChoice=onChoice, + onObserve=onObserve, + onPropagate=onPropagate, + checkFeasible=checkFeasible, + depth=depth + 1, + depth_limit=depth_limit, + ) + else: + if onFinal: + onFinal(wave) + return numpy.argmax(wave, 0) + except Contradiction: + if backtracking: + if onBacktrack: + onBacktrack() + wave = original + wave[pattern, i, j] = False + return run( + wave, + adj, + locationHeuristic, + patternHeuristic, + periodic=periodic, + backtracking=backtracking, + onBacktrack=onBacktrack, + onChoice=onChoice, + onObserve=onObserve, + onPropagate=onPropagate, + checkFeasible=checkFeasible, + depth=depth + 1, + depth_limit=depth_limit, + ) + else: + if onFinal: + onFinal(wave) + raise diff --git a/wfc/wfc/wfc_tiles.py b/wfc/wfc/wfc_tiles.py new file mode 100644 index 0000000..df51a83 --- /dev/null +++ b/wfc/wfc/wfc_tiles.py @@ -0,0 +1,55 @@ +"""Breaks an image into consituant tiles.""" +import numpy as np +from .wfc_utilities import hash_downto + + +def image_to_tiles(img, tile_size): + """ + Takes an images, divides it into tiles, return an array of tiles. + """ + padding_argument = [(0, 0), (0, 0), (0, 0)] + for input_dim in [0, 1]: + padding_argument[input_dim] = ( + 0, + (tile_size - img.shape[input_dim]) % tile_size, + ) + img = np.pad(img, padding_argument, mode="constant") + tiles = img.reshape( + ( + img.shape[0] // tile_size, + tile_size, + img.shape[1] // tile_size, + tile_size, + img.shape[2], + ) + ).swapaxes(1, 2) + return tiles + + +def make_tile_catalog(image_data, tile_size): + """ + Takes an image and tile size and returns the following: + tile_catalog is a dictionary tiles, with the hashed ID as the key + tile_grid is the original image, expressed in terms of hashed tile IDs + code_list is the original image, expressed in terms of hashed tile IDs and reduced to one dimension + unique_tiles is the set of tiles, plus the frequency of occurance + """ + channels = image_data.shape[2] # Number of color channels in the image + tiles = image_to_tiles(image_data, tile_size) + tile_list = np.array(tiles, dtype=np.int64).reshape( + (tiles.shape[0] * tiles.shape[1], tile_size, tile_size, channels) + ) + code_list = np.array(hash_downto(tiles, 2), dtype=np.int64).reshape( + (tiles.shape[0] * tiles.shape[1]) + ) + tile_grid = np.array(hash_downto(tiles, 2), dtype=np.int64) + unique_tiles = np.unique(tile_grid, return_counts=True) + + tile_catalog = {} + for i, j in enumerate(code_list): + tile_catalog[j] = tile_list[i] + return tile_catalog, tile_grid, code_list, unique_tiles + + +def tiles_to_images(tile_grid, tile_catalog): + return diff --git a/wfc/wfc/wfc_utilities.py b/wfc/wfc/wfc_utilities.py new file mode 100644 index 0000000..e2f63b6 --- /dev/null +++ b/wfc/wfc/wfc_utilities.py @@ -0,0 +1,58 @@ +"""Utility data and functions for WFC""" + +import collections +import numpy as np + + +CoordXY = collections.namedtuple("coords_xy", ["x", "y"]) +CoordRC = collections.namedtuple("coords_rc", ["row", "column"]) + + +def hash_downto(a, rank, seed=0): + state = np.random.RandomState(seed) + assert rank < len(a.shape) + # print((np.prod(a.shape[:rank]),-1)) + # print(np.array([np.prod(a.shape[:rank]),-1], dtype=np.int64).dtype) + u = a.reshape( + np.array([np.prod(a.shape[:rank]), -1], dtype=np.int64) + ) # change because lists are by default float64? + # u = a.reshape((np.prod(a.shape[:rank]),-1)) + v = state.randint(1 - (1 << 63), 1 << 63, np.prod(a.shape[rank:]), dtype="int64") + return np.inner(u, v).reshape(a.shape[:rank]).astype("int64") + + +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False + + +def load_visualizer(wfc_ns): + if IN_COLAB: + from google.colab import files + + uploaded = files.upload() + for fn in uploaded.keys(): + print( + 'User uploaded file "{name}" with length {length} bytes'.format( + name=fn, length=len(uploaded[fn]) + ) + ) + else: + import matplotlib + import matplotlib.pylab + from matplotlib.pyplot import figure + from matplotlib.pyplot import subplot + from matplotlib.pyplot import title + from matplotlib.pyplot import matshow + + wfc_ns.img_filename = f"images/{wfc_ns.img_filename}" + return wfc_ns + + +def find_pattern_center(wfc_ns): + # wfc_ns.pattern_center = (math.floor((wfc_ns.pattern_width - 1) / 2), math.floor((wfc_ns.pattern_width - 1) / 2)) + wfc_ns.pattern_center = (0, 0) + return wfc_ns diff --git a/wfc/wfc/wfc_visualize.py b/wfc/wfc/wfc_visualize.py new file mode 100644 index 0000000..ab86e5b --- /dev/null +++ b/wfc/wfc/wfc_visualize.py @@ -0,0 +1,733 @@ +"Visualize the patterns into tiles and so on." + +import math +import pathlib +import itertools +import imageio +import matplotlib +import struct +import matplotlib.pyplot as plt +import numpy as np +from .wfc_patterns import pattern_grid_to_tiles + +## Helper functions +RGB_CHANNELS = 3 + + +def rgb_to_int(rgb_in): + """"Takes RGB triple, returns integer representation.""" + return struct.unpack( + "I", struct.pack("<" + "B" * 4, *(rgb_in + [0] * (4 - len(rgb_in)))) + )[0] + + +def int_to_rgb(val): + """Convert hashed int to RGB values""" + return [x for x in val.to_bytes(RGB_CHANNELS, "little")] + + +WFC_PARTIAL_BLANK = np.nan + + +def tile_to_image(tile, tile_catalog, tile_size, visualize=False): + """ + Takes a single tile and returns the pixel image representation. + """ + new_img = np.zeros((tile_size[0], tile_size[1], 3), dtype=np.int64) + for u in range(tile_size[0]): + for v in range(tile_size[1]): + ## If we want to display a partial pattern, it is helpful to + ## be able to show empty cells. Therefore, in visualize mode, + ## we use -1 as a magic number for a non-existant tile. + pixel = [200, 0, 200] + if (visualize) and ((-1 == tile) or (WFC_PARTIAL_BLANK == tile)): + if 0 == (u + v) % 2: + pixel = [255, 0, 255] + else: + if (visualize) and -2 == tile: + pixel = [0, 255, 255] + else: + pixel = tile_catalog[tile][u, v] + new_img[u, v] = pixel + return new_img + + +def argmax_unique(arr, axis): + """Return a mask so that we can exclude the nonunique maximums, i.e. the nodes that aren't completely resolved""" + arrm = np.argmax(arr, axis) + arrs = np.sum(arr, axis) + nonunique_mask = np.ma.make_mask((arrs == 1) is False) + uni_argmax = np.ma.masked_array(arrm, mask=nonunique_mask, fill_value=-1) + return uni_argmax, nonunique_mask + + +def make_solver_loggers(filename, stats={}): + counter_choices = 0 + counter_wave = 0 + counter_backtracks = 0 + counter_propagate = 0 + + def choice_count(pattern, i, j, wave=None): + nonlocal counter_choices + counter_choices += 1 + + def wave_count(wave): + nonlocal counter_wave + counter_wave += 1 + + def backtrack_count(): + nonlocal counter_backtracks + counter_backtracks += 1 + + def propagate_count(wave): + nonlocal counter_propagate + counter_propagate += 1 + + def final_count(wave): + print( + f"{filename}: choices: {counter_choices}, wave:{counter_wave}, backtracks: {counter_backtracks}, propagations: {counter_propagate}" + ) + stats.update( + { + "choices": counter_choices, + "wave": counter_wave, + "backtracks": counter_backtracks, + "propagations": counter_propagate, + } + ) + return stats + + def report_count(): + stats.update( + { + "choices": counter_choices, + "wave": counter_wave, + "backtracks": counter_backtracks, + "propagations": counter_propagate, + } + ) + return stats + + return ( + choice_count, + wave_count, + backtrack_count, + propagate_count, + final_count, + report_count, + ) + + +def make_solver_visualizers( + filename, + wave, + decode_patterns=None, + pattern_catalog=None, + tile_catalog=None, + tile_size=[1, 1], +): + """Construct visualizers for displaying the intermediate solver status""" + print(wave.shape) + pattern_total_count = wave.shape[0] + resolution_order = np.full( + wave.shape[1:], np.nan + ) # pattern_wave = when was this resolved? + backtracking_order = np.full( + wave.shape[1:], np.nan + ) # on which iternation was this resolved? + pattern_solution = np.full(wave.shape[1:], np.nan) # what is the resolved result? + resolution_method = np.zeros( + wave.shape[1:] + ) # did we set this via observation or propagation? + choice_count = 0 + vis_count = 0 + backtracking_count = 0 + max_choices = math.floor((wave.shape[1] * wave.shape[2]) / 3) + output_individual_visualizations = False + + tile_wave = np.zeros(wave.shape, dtype=np.int64) + for i in range(wave.shape[0]): + local_solution_as_ids = np.full(wave.shape[1:], decode_patterns[i]) + local_solution_tile_grid = pattern_grid_to_tiles( + local_solution_as_ids, pattern_catalog + ) + tile_wave[i] = local_solution_tile_grid + + def choice_vis(pattern, i, j, wave=None): + nonlocal choice_count + nonlocal resolution_order + nonlocal resolution_method + choice_count += 1 + resolution_order[i][j] = choice_count + pattern_solution[i][j] = pattern + resolution_method[i][j] = 2 + if output_individual_visualizations: + figure_solver_data( + f"visualization/{filename}_choice_{choice_count}.png", + "order of resolution", + resolution_order, + 0, + max_choices, + "gist_ncar", + ) + figure_solver_data( + f"visualization/{filename}_solution_{choice_count}.png", + "chosen pattern", + pattern_solution, + 0, + pattern_total_count, + "viridis", + ) + figure_solver_data( + f"visualization/{filename}_resolution_{choice_count}.png", + "resolution method", + resolution_method, + 0, + 2, + "inferno", + ) + if wave: + _assigned_patterns, nonunique_mask = argmax_unique(wave, 0) + resolved_by_propagation = ( + np.ma.mask_or(nonunique_mask, resolution_method != 0) == 0 + ) + resolution_method[resolved_by_propagation] = 1 + resolution_order[resolved_by_propagation] = choice_count + if output_individual_visualizations: + figure_solver_data( + f"visualization/{filename}_wave_{choice_count}.png", + "patterns remaining", + np.count_nonzero(wave > 0, axis=0), + 0, + wave.shape[0], + "plasma", + ) + + def wave_vis(wave): + nonlocal vis_count + nonlocal resolution_method + nonlocal resolution_order + vis_count += 1 + pattern_left_count = np.count_nonzero(wave > 0, axis=0) + # assigned_patterns, nonunique_mask = argmax_unique(wave, 0) + resolved_by_propagation = ( + np.ma.mask_or(pattern_left_count > 1, resolution_method != 0) != 1 + ) + # print(resolved_by_propagation) + resolution_method[resolved_by_propagation] = 1 + resolution_order[resolved_by_propagation] = choice_count + backtracking_order[resolved_by_propagation] = backtracking_count + if output_individual_visualizations: + figure_wave_patterns(filename, pattern_left_count, pattern_total_count) + figure_solver_data( + f"visualization/{filename}_wave_patterns_{choice_count}.png", + "patterns remaining", + pattern_left_count, + 0, + pattern_total_count, + "magma", + ) + if decode_patterns and pattern_catalog and tile_catalog: + solution_as_ids = np.vectorize(lambda x: decode_patterns[x])( + np.argmax(wave, 0) + ) + solution_tile_grid = pattern_grid_to_tiles(solution_as_ids, pattern_catalog) + if output_individual_visualizations: + figure_solver_data( + f"visualization/{filename}_tiles_assigned_{choice_count}.png", + "tiles assigned", + solution_tile_grid, + 0, + pattern_total_count, + "plasma", + ) + img = tile_grid_to_image(solution_tile_grid.T, tile_catalog, tile_size) + + masked_tile_wave = np.ma.MaskedArray( + data=tile_wave, mask=(wave == False), dtype=np.int64 + ) + masked_img = tile_grid_to_average( + np.transpose(masked_tile_wave, (0, 2, 1)), tile_catalog, tile_size + ) + + if output_individual_visualizations: + figure_solver_image( + f"visualization/{filename}_solution_partial_{choice_count}.png", + "solved_tiles", + img.astype(np.uint8), + ) + imageio.imwrite( + f"visualization/{filename}_solution_partial_img_{choice_count}.png", + img.astype(np.uint8), + ) + fig_list = [ + # {"title": "resolved by propagation", "data": resolved_by_propagation.T, "vmin": 0, "vmax": 2, "cmap": "inferno", "datatype":"figure"}, + { + "title": "order of resolution", + "data": resolution_order.T, + "vmin": 0, + "vmax": max_choices / 4, + "cmap": "hsv", + "datatype": "figure", + }, + { + "title": "chosen pattern", + "data": pattern_solution.T, + "vmin": 0, + "vmax": pattern_total_count, + "cmap": "viridis", + "datatype": "figure", + }, + { + "title": "resolution method", + "data": resolution_method.T, + "vmin": 0, + "vmax": 2, + "cmap": "magma", + "datatype": "figure", + }, + { + "title": "patterns remaining", + "data": pattern_left_count.T, + "vmin": 0, + "vmax": pattern_total_count, + "cmap": "viridis", + "datatype": "figure", + }, + { + "title": "tiles assigned", + "data": solution_tile_grid.T, + "vmin": None, + "vmax": None, + "cmap": "prism", + "datatype": "figure", + }, + { + "title": "solved tiles", + "data": masked_img.astype(np.uint8), + "datatype": "image", + }, + ] + figure_unified( + "Solver Readout", + f"visualization/{filename}_readout_{choice_count:03}_{vis_count:03}.png", + fig_list, + ) + + def backtrack_vis(): + nonlocal vis_count + nonlocal pattern_solution + nonlocal backtracking_count + backtracking_count += 1 + vis_count += 1 + pattern_solution = np.full(wave.shape[1:], -1) + + return choice_vis, wave_vis, backtrack_vis, None, wave_vis, None + + +def figure_unified(figure_name_overall, filename, data): + matfig, axs = plt.subplots( + 1, len(data), sharey="row", gridspec_kw={"hspace": 0, "wspace": 0} + ) + + for idx, _data_obj in enumerate(data): + if "image" == data[idx]["datatype"]: + axs[idx].imshow(data[idx]["data"], interpolation="nearest") + else: + axs[idx].matshow( + data[idx]["data"], + vmin=data[idx]["vmin"], + vmax=data[idx]["vmax"], + cmap=data[idx]["cmap"], + ) + axs[idx].get_xaxis().set_visible(False) + axs[idx].get_yaxis().set_visible(False) + axs[idx].label_outer() + + plt.savefig(filename, bbox_inches="tight", pad_inches=0, dpi=600) + plt.close(fig=matfig) + plt.close("all") + + +vis_count = 0 + + +def visualize_solver(wave): + pattern_left_count = np.count_nonzero(wave > 0, axis=0) + pattern_total_count = wave.shape[0] + figure_wave_patterns(pattern_left_count, pattern_total_count) + + +def make_figure_solver_image(plot_title, img): + visfig = plt.figure(figsize=(4, 4), edgecolor="k", frameon=True) + plt.imshow(img, interpolation="nearest") + plt.title(plot_title) + plt.grid(None) + plt.grid(None) + an_ax = plt.gca() + an_ax.get_xaxis().set_visible(False) + an_ax.get_yaxis().set_visible(False) + return visfig + + +def figure_solver_image(filename, plot_title, img): + visfig = make_figure_solver_image(plot_title, img) + plt.savefig(filename, bbox_inches="tight", pad_inches=0) + plt.close(fig=visfig) + plt.close("all") + + +def make_figure_solver_data(plot_title, data, min_count, max_count, cmap_name): + visfig = plt.figure(figsize=(4, 4), edgecolor="k", frameon=True) + plt.title(plot_title) + plt.matshow(data, vmin=min_count, vmax=max_count, cmap=cmap_name) + plt.grid(None) + plt.grid(None) + ax = plt.gca() + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + return visfig + + +def figure_solver_data(filename, plot_title, data, min_count, max_count, cmap_name): + visfig = make_figure_solver_data(plot_title, data, min_count, max_count, cmap_name) + plt.savefig(filename, bbox_inches="tight", pad_inches=0) + plt.close(fig=visfig) + plt.close("all") + + +def figure_wave_patterns(filename, pattern_left_count, max_count): + global vis_count + vis_count += 1 + visfig = plt.figure(figsize=(4, 4), edgecolor="k", frameon=True) + + plt.title("wave") + plt.matshow(pattern_left_count, vmin=0, vmax=max_count, cmap="plasma") + plt.grid(None) + + plt.grid(None) + plt.savefig(f"{filename}_wave_patterns_{vis_count}.png") + plt.close(fig=visfig) + + +def tile_grid_to_average(tile_grid, tile_catalog, tile_size, color_channels=3): + """ + Takes a masked array of tile grid stacks and transforms it into an image, taking + the average colors of the tiles in tile_catalog. + """ + new_img = np.zeros( + ( + tile_grid.shape[1] * tile_size[0], + tile_grid.shape[2] * tile_size[1], + color_channels, + ), + dtype=np.int64, + ) + for i in range(tile_grid.shape[1]): + for j in range(tile_grid.shape[2]): + tile_stack = tile_grid[:, i, j] + for u in range(tile_size[0]): + for v in range(tile_size[1]): + pixel = [200, 0, 200] + pixel_list = np.array( + [ + tile_catalog[t][u, v] + for t in tile_stack[tile_stack.mask == False] + ], + dtype=np.int64, + ) + pixel = np.mean(pixel_list, axis=0) + # TODO: will need to change if using an image with more than 3 channels + new_img[(i * tile_size[0]) + u, (j * tile_size[1]) + v] = np.resize( + pixel, + new_img[(i * tile_size[0]) + u, (j * tile_size[1]) + v].shape, + ) + return new_img + + +def tile_grid_to_image( + tile_grid, tile_catalog, tile_size, visualize=False, partial=False, color_channels=3 +): + """ + Takes a tile_grid and transforms it into an image, using the information + in tile_catalog. We use tile_size to figure out the size the new image + should be, and visualize for displaying partial tile patterns. + """ + new_img = np.zeros( + ( + tile_grid.shape[0] * tile_size[0], + tile_grid.shape[1] * tile_size[1], + color_channels, + ), + dtype=np.int64, + ) + if partial and (len(tile_grid.shape)) > 2: + # TODO: implement rendering partially completed solution + # Call tile_grid_to_average() instead. + assert False + else: + for i in range(tile_grid.shape[0]): + for j in range(tile_grid.shape[1]): + tile = tile_grid[i, j] + for u in range(tile_size[0]): + for v in range(tile_size[1]): + pixel = [200, 0, 200] + ## If we want to display a partial pattern, it is helpful to + ## be able to show empty cells. Therefore, in visualize mode, + ## we use -1 as a magic number for a non-existant tile. + if visualize and ((-1 == tile) or (-2 == tile)): + if -1 == tile: + if 0 == (i + j) % 2: + pixel = [255, 0, 255] + if -2 == tile: + pixel = [0, 255, 255] + else: + pixel = tile_catalog[tile][u, v] + # TODO: will need to change if using an image with more than 3 channels + new_img[ + (i * tile_size[0]) + u, (j * tile_size[1]) + v + ] = np.resize( + pixel, + new_img[ + (i * tile_size[0]) + u, (j * tile_size[1]) + v + ].shape, + ) + return new_img + + +def figure_list_of_tiles(unique_tiles, tile_catalog, output_filename="list_of_tiles"): + plt.figure(figsize=(4, 4), edgecolor="k", frameon=True) + plt.title("Extracted Tiles") + s = math.ceil(math.sqrt(len(unique_tiles))) + 1 + for i, tcode in enumerate(unique_tiles[0]): + sp = plt.subplot(s, s, i + 1).imshow(tile_catalog[tcode]) + sp.axes.tick_params(labelleft=False, labelbottom=False, length=0) + plt.title(f"{i}\n{tcode}", fontsize=10) + sp.axes.grid(False) + fp = pathlib.Path(output_filename + ".pdf") + plt.savefig(fp, bbox_inches="tight") + plt.close() + + +def figure_false_color_tile_grid(tile_grid, output_filename="./false_color_tiles"): + figure_plot = plt.matshow( + tile_grid, + cmap="gist_ncar", + extent=(0, tile_grid.shape[1], tile_grid.shape[0], 0), + ) + plt.title("False Color Map of Tiles in Input Image") + figure_plot.axes.grid(None) + plt.savefig(output_filename + ".png", bbox_inches="tight") + plt.close() + + +def figure_tile_grid(tile_grid, tile_catalog, tile_size): + tile_grid_to_image(tile_grid, tile_catalog, tile_size) + + +def render_pattern(render_pattern, tile_catalog): + """Turn a pattern into an image""" + rp_iter = np.nditer(render_pattern, flags=["multi_index"]) + output = np.zeros(render_pattern.shape + (3,), dtype=np.uint32) + while not rp_iter.finished: + # Note that this truncates images with more than 3 channels down to just the channels in the output. + # If we want to have alpha channels, we'll need a different way to handle this. + output[rp_iter.multi_index] = np.resize( + tile_catalog[render_pattern[rp_iter.multi_index]], + output[rp_iter.multi_index].shape, + ) + rp_iter.iternext() + return output + + +def figure_pattern_catalog( + pattern_catalog, + tile_catalog, + pattern_weights, + pattern_width, + output_filename="pattern_catalog", +): + s_columns = 24 // min(24, pattern_width) + s_rows = 1 + (int(len(pattern_catalog)) // s_columns) + _fig = plt.figure(figsize=(s_columns, s_rows * 1.5)) + plt.title("Extracted Patterns") + counter = 0 + for i, _tcode in pattern_catalog.items(): + pat_cat = pattern_catalog[i] + ptr = render_pattern(pat_cat, tile_catalog).astype(np.uint8) + sp = plt.subplot(s_rows, s_columns, counter + 1) + spi = sp.imshow(ptr) + spi.axes.xaxis.set_label_text(f"({pattern_weights[i]})") + sp.set_title(f"{counter}\n{i}", fontsize=3) + spi.axes.tick_params( + labelleft=False, labelbottom=False, left=False, bottom=False + ) + spi.axes.grid(False) + counter += 1 + plt.savefig(output_filename + "_patterns.pdf", bbox_inches="tight") + plt.close() + + +def render_tiles_to_output(tile_grid, tile_catalog, tile_size, output_filename): + img = tile_grid_to_image(tile_grid.T, tile_catalog, tile_size) + imageio.imwrite(output_filename, img.astype(np.uint8)) + + +def blit(destination, sprite, upper_left, layer=False, check=False): + """ + Blits one multidimensional array into another numpy array. + """ + lower_right = [ + ((a + b) if ((a + b) < c) else c) + for a, b, c in zip(upper_left, sprite.shape, destination.shape) + ] + if min(lower_right) < 0: + return + + for i_index, i in enumerate(range(upper_left[0], lower_right[0])): + for j_index, j in enumerate(range(upper_left[1], lower_right[1])): + if (i >= 0) and (j >= 0): + if len(destination.shape) > 2: + destination[i, j, layer] = sprite[i_index, j_index] + else: + if check: + if ( + (destination[i, j] == sprite[i_index, j_index]) + or (destination[i, j] == -1) + or {sprite[i_index, j_index] == -1} + ): + destination[i, j] = sprite[i_index, j_index] + else: + print( + "ERROR, mismatch: destination[{i},{j}] = {destination[i, j]}, sprite[{i_index}, {j_index}] = {sprite[i_index, j_index]}" + ) + else: + destination[i, j] = sprite[i_index, j_index] + return destination + + +class InvalidAdjacency(Exception): + """The combination of patterns and offsets results in pattern combinations that don't match.""" + + pass + + +def validate_adjacency( + pattern_a, pattern_b, preview_size, upper_left_of_center, adj_rel +): + preview_adj_a_first = np.full((preview_size, preview_size), -1, dtype=np.int64) + preview_adj_b_first = np.full((preview_size, preview_size), -1, dtype=np.int64) + blit( + preview_adj_b_first, + pattern_b, + ( + upper_left_of_center[1] + adj_rel[0][1], + upper_left_of_center[0] + adj_rel[0][0], + ), + check=True, + ) + blit(preview_adj_b_first, pattern_a, upper_left_of_center, check=True) + + blit(preview_adj_a_first, pattern_a, upper_left_of_center, check=True) + blit( + preview_adj_a_first, + pattern_b, + ( + upper_left_of_center[1] + adj_rel[0][1], + upper_left_of_center[0] + adj_rel[0][0], + ), + check=True, + ) + if not np.array_equiv(preview_adj_a_first, preview_adj_b_first): + print(adj_rel) + print(pattern_a) + print(pattern_b) + print(preview_adj_a_first) + print(preview_adj_b_first) + raise InvalidAdjacency + + +def figure_adjacencies( + adjacency_relations_list, + adjacency_directions, + tile_catalog, + patterns, + pattern_width, + tile_size, + output_filename="adjacency", + render_b_first=False, +): + # try: + adjacency_directions_list = list(dict(adjacency_directions).values()) + _figadj = plt.figure( + figsize=(12, 1 + len(adjacency_relations_list[:64])), edgecolor="b" + ) + plt.title("Adjacencies") + max_offset = max( + [abs(x) for x in list(itertools.chain.from_iterable(adjacency_directions_list))] + ) + + for i, adj_rel in enumerate(adjacency_relations_list[:64]): + preview_size = pattern_width + max_offset * 2 + preview_adj = np.full((preview_size, preview_size), -1, dtype=np.int64) + upper_left_of_center = [max_offset, max_offset] + + pattern_a = patterns[adj_rel[1]] + pattern_b = patterns[adj_rel[2]] + validate_adjacency( + pattern_a, pattern_b, preview_size, upper_left_of_center, adj_rel + ) + if render_b_first: + blit( + preview_adj, + pattern_b, + ( + upper_left_of_center[1] + adj_rel[0][1], + upper_left_of_center[0] + adj_rel[0][0], + ), + check=True, + ) + blit(preview_adj, pattern_a, upper_left_of_center, check=True) + else: + blit(preview_adj, pattern_a, upper_left_of_center, check=True) + blit( + preview_adj, + pattern_b, + ( + upper_left_of_center[1] + adj_rel[0][1], + upper_left_of_center[0] + adj_rel[0][0], + ), + check=True, + ) + + ptr = tile_grid_to_image( + preview_adj, tile_catalog, tile_size, visualize=True + ).astype(np.uint8) + + subp = plt.subplot(math.ceil(len(adjacency_relations_list[:64]) / 4), 4, i + 1) + spi = subp.imshow(ptr) + spi.axes.tick_params( + left=False, bottom=False, labelleft=False, labelbottom=False + ) + plt.title( + f"{i}:\n({adj_rel[1]} +\n{adj_rel[2]})\n by {adj_rel[0]}", fontsize=10 + ) + + indicator_rect = matplotlib.patches.Rectangle( + (upper_left_of_center[1] - 0.51, upper_left_of_center[0] - 0.51), + pattern_width, + pattern_width, + Fill=False, + edgecolor="b", + linewidth=3.0, + linestyle=":", + ) + + spi.axes.add_artist(indicator_rect) + spi.axes.grid(False) + plt.savefig(output_filename + "_adjacency.pdf", bbox_inches="tight") + plt.close() + + +# except ValueError as e: +# print(e) diff --git a/wfc/wfc1/wfc_adjacency.py b/wfc/wfc1/wfc_adjacency.py new file mode 100644 index 0000000..b5144d5 --- /dev/null +++ b/wfc/wfc1/wfc_adjacency.py @@ -0,0 +1,503 @@ +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +from wfc.wfc_tiles import tiles_to_images +import matplotlib.pyplot as plt +from matplotlib.pyplot import figure, subplot, subplots, title, matshow +import numpy as np +import itertools +import math +import matplotlib + +# In[15]: + + +# def is_valid_overlap_xy(d, p1, p2, pattern_catalog, pattern_width, adjacency_directions): +# """Given a direction, two patterns, and a pattern catalog, return True +# if we overlap pattern two on top of pattern one and the intersection +# of the two patterns is an exact match.""" +# dimensions = (1,0) +# not_a_number = -1 +# adjacency_directions_inverted = CoordXY(x = 0 - adjacency_directions[d].x, y = 0 - adjacency_directions[d].y) + +# ##TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... +# shifted = np.roll(np.pad(pattern_catalog[p2], pattern_width, mode='constant', constant_values = not_a_number), adjacency_directions[d], dimensions) +# #print("*") +# #print(shifted) +# compare = shifted[pattern_width:pattern_width+pattern_width, pattern_width:pattern_width+pattern_width] +# left = max(0,0 + adjacency_directions_inverted.x) +# right = min(pattern_width, pattern_width + adjacency_directions_inverted.x) +# top = max(0,0 + adjacency_directions_inverted.y) +# bottom = min(pattern_width, pattern_width + adjacency_directions_inverted.y) +# a = pattern_catalog[p1][top:bottom,left:right] +# b = compare[top:bottom,left:right] + +# #a = pattern_catalog[p1] +# #b = pattern_catalog[p2] +# #b_shift = np.roll(b, (adjacency_directions[d].y,adjacency_directions[d].x), (0,1)) +# #a_slice = a[0+adjacency_directions[d].y:pattern_width+1+adjacency_directions[d].y, 0+adjacency_directions[d].x:pattern_width+1+adjacency_directions[d].x] +# #b_slice = b_shift[0+adjacency_directions[d].y:pattern_width+1+adjacency_directions[d].y, 0+adjacency_directions[d].x:pattern_width+1+adjacency_directions[d].x] + +# print(a) +# print(b) +# print(compare) +# print(shifted) +# res = np.array_equal(a,b) + +# print(f"is_valid_overlap: {p1}+{p2} at {d}{adjacency_directions[d]} = {res}") +# #print(f"\n{a}\n = \n{b}\n{b_shift}") +# #print(a_slice) +# #print('<-') +# #print(b_slice) + +# #shifted = np.roll(np.pad(pattern_catalog[p2], +# # pattern_width, +# # mode='constant', +# # constant_values = not_a_number), +# # adjacency_directions[d], dimensions) +# #print(a,b) + +# return res + + +def is_valid_overlap_xy( + dir_id, p1, p2, pattern_catalog, pattern_width, adjacency_directions +): + """Given a direction, two patterns, and a pattern catalog, return True + if we overlap pattern two on top of pattern one and the intersection + of the two patterns is an exact match.""" + # dir_corrected = (0 - adjacency_directions[dir_id].x, 0 - adjacency_directions[dir_id].y) + dir_corrected = ( + 0 + adjacency_directions[dir_id].x, + 0 + adjacency_directions[dir_id].y, + ) + dimensions = (1, 0) + not_a_number = -1 + # TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... + # print(d, p2, p1) + shifted = np.roll( + np.pad( + pattern_catalog[p2], + pattern_width, + mode="constant", + constant_values=not_a_number, + ), + dir_corrected, + dimensions, + ) + compare = shifted[ + pattern_width : pattern_width + pattern_width, + pattern_width : pattern_width + pattern_width, + ] + left = max(0, 0 + dir_corrected[0]) + right = min(pattern_width, pattern_width + dir_corrected[0]) + top = max(0, 0 + dir_corrected[1]) + bottom = min(pattern_width, pattern_width + dir_corrected[1]) + a = pattern_catalog[p1][top:bottom, left:right] + b = compare[top:bottom, left:right] + res = np.array_equal(a, b) + # print(f"res: {res}") + return res + + +def valid_overlap(d, p1, p2): + dimensions = (1, 0) + not_a_number = 0 + # TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... + shifted = numpy.roll( + numpy.pad( + pattern_catalog[p2], + max(patternsize), + mode="constant", + constant_values=not_a_number, + ), + d, + dimensions, + ) + compare = shifted[ + patternsize[0] : patternsize[0] + patternsize[0], + patternsize[1] : patternsize[1] + patternsize[1], + ] + left = max(0, 0 + d[0]) + right = min(patternsize[0], patternsize[0] + d[0]) + top = max(0, 0 + d[1]) + bottom = min(patternsize[1], patternsize[1] + d[1]) + + a = pattern_catalog[p1][top:bottom, left:right] + b = compare[top:bottom, left:right] + res = numpy.array_equal(a, b) + return res + + +def adjacency_extraction_consistent(wfc_ns, pattern_cat): + """Takes a pattern catalog, returns a list of all legal adjacencies.""" + # This is a brute force implementation. We should really use the adjacency list we've already calculated... + legal = [] + # print(f"pattern_cat\n{pattern_cat}") + for p1, pattern1 in enumerate(pattern_cat): + for d_index, d in enumerate(wfc_ns.adjacency_directions): + for p2, pattern2 in enumerate(pattern_cat): + if is_valid_overlap_xy( + d, + p1, + p2, + pattern_cat, + wfc_ns.pattern_width, + wfc_ns.adjacency_directions, + ): + legal.append((d_index, p1, p2)) + return legal + + +# .. .. *. .* .. +# .* *. .. .. .. + +# 1+0 +# ... +# .*. + +# 1+1 +# ... +# *X. + +# 1+2 +# .X. +# .X. + +# 1+3 +# .X. +# .X. + +# 1+4 +# ... +# .X. + + +# In[16]: + + +# def adjacency_efficent_extraction_observed(codes): +# adjacency_relations = list() +# +# #if mode == 'observed': # just pairing seen in the input image +# for i,adj_dir in ns.adjacency_directions.items(): +# #print(codes) +# u = adj_dir.x +# v = adj_dir.y +# a = codes[max(0,0+u):codes.shape[0]+u,max(0,0+v):codes.shape[1]+v] +# b = codes[max(0,0-u):codes.shape[0]-u,max(0,0-v):codes.shape[1]-v] +# triples = [(i,ns.tile_ids[j],ns.tile_ids[k]) for j,k in zip(a.ravel(),b.ravel())] +# adjacency_relations.extend(set(triples)) +# return adjacency_relations + +import collections +import itertools + + +def adjacency_efficent_extraction_consistent(patterns): + assert ns.pattern_width > 0 + adjacency_relations = list() + for i, adj_dir in ns.adjacency_directions.items(): + a = hash_downto( + patterns[ + :, + max(0, 0 + adj_dir.x) : ns.pattern_width + adj_dir.x, + max(0, 0 + adj_dir.y) : ns.pattern_width + adj_dir.y, + ], + 1, + ) + b = hash_downto( + patterns[ + :, + max(0, 0 - adj_dir.x) : ns.pattern_width - adj_dir.x, + max(0, 0 - adj_dir.y) : ns.pattern_width - adj_dir.y, + ], + 1, + ) + rel = collections.defaultdict(lambda: ([], [])) + for ia, key in enumerate(a): + rel[key][0].append(ia) + for ib, key in enumerate(b): + rel[key][1].append(ib) + + triples = [] + for (ias, ibs) in rel.values(): + adjacency_relations.extend( + [(i, ia, ib) for ia, ib in itertools.product(ias, ibs)] + ) + + return adjacency_relations + + +# +# """%%time +# adjacency_relations2 = adjacency_efficent_extraction_observed(ns.patterns) +# adjacency_relations3 = adjacency_efficent_extraction_consistent(ns.patterns) +# print(adjacency_relations2) +# """ + + +# In[17]: + + +# test = np.array([[0,1,2,3],[4,5,6,7]]) +# print(test) +# print() +# print(np.repeat(test[:,:,np.newaxis], 4, axis=2)) + + +# In[18]: + + +def blit(destination, sprite, upper_left, layer=False, check=False): + """ + Blits one multidimensional array into another numpy array. + """ + lower_right = [ + ((a + b) if ((a + b) < c) else c) + for a, b, c in zip(upper_left, sprite.shape, destination.shape) + ] + if min(lower_right) < 0: + return + + for i_index, i in enumerate(range(upper_left[0], lower_right[0])): + for j_index, j in enumerate(range(upper_left[1], lower_right[1])): + if (i >= 0) and (j >= 0): + if len(destination.shape) > 2: + destination[i, j, layer] = sprite[i_index, j_index] + else: + if check: + if ( + (destination[i, j] == sprite[i_index, j_index]) + or (destination[i, j] == -1) + or {sprite[i_index, j_index] == -1} + ): + destination[i, j] = sprite[i_index, j_index] + else: + print( + "ERROR, mismatch: destination[{i},{j}] = {destination[i, j]}, sprite[{i_index}, {j_index}] = {sprite[i_index, j_index]}" + ) + else: + destination[i, j] = sprite[i_index, j_index] + return destination + + +# In[19]: + + +import pprint + + +def show_adjacencies(wfc_ns, adjacency_relations_list): + try: + figadj = figure(figsize=(12, 1 + len(adjacency_relations_list)), edgecolor="b") + title("Adjacencies") + max_offset = max( + [ + abs(x) + for x in list( + itertools.chain.from_iterable(wfc_ns.adjacency_directions.values()) + ) + ] + ) + + for i, adj_rel in enumerate(adjacency_relations_list): + preview_size = wfc_ns.pattern_width + max_offset * 2 + preview_adj = np.full((preview_size, preview_size), -1, dtype=np.int64) + upper_left_of_center = CoordXY( + x=max_offset, y=max_offset + ) # (ns.pattern_width, ns.pattern_width) + # print(f"adj_rel: {adj_rel}") + blit( + preview_adj, + wfc_ns.patterns[adj_rel[1]], + upper_left_of_center, + check=True, + ) + blit( + preview_adj, + wfc_ns.patterns[adj_rel[2]], + ( + upper_left_of_center.y + wfc_ns.adjacency_directions[adj_rel[0]].y, + upper_left_of_center.x + wfc_ns.adjacency_directions[adj_rel[0]].x, + ), + check=True, + ) + + ptr = tiles_to_images( + wfc_ns, + preview_adj, + wfc_ns.tile_catalog, + wfc_ns.tile_size, + visualize=True, + ).astype(np.uint8) + + subp = subplot(math.ceil(len(adjacency_relations_list) / 4), 4, i + 1) + spi = subp.imshow(ptr) + spi.axes.tick_params( + left=False, bottom=False, labelleft=False, labelbottom=False + ) + title( + f"{i}: ({adj_rel[1]} + {adj_rel[2]}) by\n{wfc_ns.adjacency_directions[adj_rel[0]]}", + fontsize=10, + ) + + indicator_rect = matplotlib.patches.Rectangle( + (upper_left_of_center.y - 0.51, upper_left_of_center.x - 0.51), + wfc_ns.pattern_width, + wfc_ns.pattern_width, + Fill=False, + edgecolor="b", + linewidth=3.0, + linestyle=":", + ) + + spi.axes.add_artist(indicator_rect) + spi.axes.grid(False) + plt.savefig(wfc_ns.output_filename + "_adjacency.pdf", bbox_inches="tight") + plt.close() + except ValueError as e: + print(e) + + +# # Solvers + +# ## Adjacency Grid + +# In[20]: + + +def make_adjacency_grid(shape: tuple, directions): + adj_grid_shape = ((len(directions)), *shape) + + def within_bounds(x, limit): + while x < 0: + x += limit + while x > limit - 1: + x -= limit + return x + + def add_offset(a, b): + offset = [sum(x) for x in zip(a, b)] + return (within_bounds(offset[0], shape[0]), within_bounds(offset[1], shape[1])) + + adj_grid = np.zeros(adj_grid_shape, dtype=np.uint32) + + # TODO: grids bigger than max(uint32 - 1) can't index the entire array + # Therefore, output shapes should not exceed approximately 65535 x 65535 + for d_idx, d in directions.items(): + for y in range(shape[0]): + for x in range(shape[1]): + cell = (y, x) + offset_cell = add_offset(d, cell) + adj_grid[d_idx, y, x] = offset_cell[0] + (offset_cell[1] * shape[0]) + # Adds one to the index so we can use zero in MiniZinc to + # indicate non-edges for non-wrapping output images and similar + # adj_grid[d_idx,x,y] = 1 + (offset_cell[0] + (offset_cell[1] * shape[0])) + return adj_grid + + +def make_reverse_adjacency_directions(adjacency_directions): + reverse_adjacency_directions_val = {} + for k, v in adjacency_directions.items(): + reverse_adjacency_directions_val[k] = tuple([(i * -1) for i in v]) + # print(f"reverse_adjacency direction {v} to {reverse_adjacency_directions_val[k]}") + return reverse_adjacency_directions_val # , reverse_directions_by_index + + +def make_reverse_adjacency_grid(shape, directions): + reverse_directions = make_reverse_adjacency_directions(directions) + return make_adjacency_grid(shape, reverse_directions) + + +def adjacency_index(shape, index_num): + x = math.floor((index_num) % shape[0]) + y = math.floor((index_num) / shape[0]) + return (x, y) + + +def reverse_direction_index(directions): + rev_directions = make_reverse_adjacency_directions( + directions + ) # [tuple([(i * -1) for i in o]) for o in directions.values()] + inverted_offset = {} + # Match with first offset value that matches + for rev_key, rev_val in rev_directions.items(): + for key, val in directions.items(): + if val == rev_val: + inverted_offset[rev_key] = key + break + return inverted_offset + + +def get_direction_from_offset(rdirections, offset): + # TODO: Currently assumes a one-to-one mapping, which dictionaries do not enforce. + for rev_key, rev_val in rdirections.items(): + if rev_val == offset: + return rev_key + raise ValueError("Offset not found in directions.") + + +# In[21]: + + +# adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) +# print(wfc_ns_chess.adjacency_directions) +# reverse_adjacency_directions = make_reverse_adjacency_directions(wfc_ns_chess.adjacency_directions) +# print(reverse_adjacency_directions) +# reverse_adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, reverse_adjacency_directions) +# adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) +# reverse_adjacency_grid = make_reverse_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) + +# print(f"reverse: {reverse_direction_index(wfc_ns_chess.adjacency_directions)}") + +# print(wfc_ns_chess.adjacency_directions) +##print(adjacency_grid) +##print(reverse_adjacency_grid) +# print(adjacency_index(wfc_ns_chess.generated_size, 2)) +# print(reverse_direction_index(wfc_ns_chess.adjacency_directions)) +# print(reverse_direction_index(wfc_ns_chess.adjacency_directions)[get_direction_from_offset(wfc_ns_chess.adjacency_directions, (0,-1))]) + + +if __name__ == "__main__": + import types + + test_ns = types.SimpleNamespace( + img_filename="red_maze.png", + seed=87386, + tile_size=1, + pattern_width=2, + channels=3, + adjacency_directions=dict( + enumerate( + [ + CoordXY(x=0, y=-1), + CoordXY(x=1, y=0), + CoordXY(x=0, y=1), + CoordXY(x=-1, y=0), + ] + ) + ), + periodic_input=True, + periodic_output=True, + generated_size=(3, 3), + screenshots=1, + iteration_limit=0, + allowed_attempts=1, + ) + test_ns = wfc_utilities.find_pattern_center(test_ns) + test_ns = wfc_utilities.load_visualizer(test_ns) + test_ns.img = load_source_image(test_ns.img_filename) + ( + test_ns.tile_catalog, + test_ns.tile_grid, + test_ns.code_list, + test_ns.unique_tiles, + ) = make_tile_catalog(test_ns) + test_ns.tile_ids = { + v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() + } + test_ns.tile_weights = { + a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) + } + import doctest + + doctest.testmod() diff --git a/wfc/wfc1/wfc_control.py b/wfc/wfc1/wfc_control.py new file mode 100644 index 0000000..96d2f46 --- /dev/null +++ b/wfc/wfc1/wfc_control.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- + + +import types +import wfc.wfc_utilities +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE +from wfc.wfc_tiles import ( + load_source_image, + image_to_tiles, + make_tile_catalog, + show_input_to_output, + show_extracted_tiles, + show_false_color_tile_grid, +) +import wfc.wfc_patterns +from wfc.wfc_patterns import ( + make_pattern_catalog_no_rotations, + show_pattern_catalog, + make_pattern_catalog_with_symmetry, +) +from wfc.wfc_adjacency import adjacency_extraction_consistent, show_adjacencies +import wfc.wfc_solver +from wfc.wfc_solver import wfc_run +from wfc.wfc_solver import ( + wfc_init, + show_wfc_patterns, + show_pattern_adjacency, + visualize_propagator_matrix, + visualize_entropy, + wfc_clear, + visualize_compatible_count, + show_rendered_patterns, + render_patterns_to_output, + wfc_observe, + wfc_partial_output, + wrap_coords, + show_crystal_time, +) + +# from wfc.wfc_minizinc import mz_run + +import logging + +logging.basicConfig(level=logging.INFO) +wfc_logger = logging.getLogger() + +import numpy as np + +wfc_logger.info(f"Using numpy version {np.__version__}") + +np.set_printoptions(threshold=np.inf) + +import xml.etree.ElementTree as ET + +import cProfile, pstats +import time + +import moviepy.editor as mpy +from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter + + +import copy + + +def string2bool(strn): + if isinstance(strn, bool): + return strn + return strn.lower() in ["true"] + + +def wfc_execute(WFC_VISUALIZE=False, WFC_PROFILE=False, WFC_LOGGING=False): + + solver_to_use = "default" # "minizinc" + + wfc_stats_tracking = { + "observations": 0, + "propagations": 0, + "time_start": None, + "time_end": None, + "choices_before_success": 0, + "choices_per_run": [], + "success": False, + } + wfc_stats_data = [] + stats_file_name = f"output/stats_{time.time()}.tsv" + + with open(stats_file_name, "a+") as stats_file: + stats_file.write( + "id\tname\tsuccess?\tattempts\tobservations\tpropagations\tchoices_to_solution\ttotal_observations_before_solution_in_last_restart\ttotal_choices_before_success_across_restarts\tbacktracking_total\ttime_passed\ttime_start\ttime_end\tfinal_time_end\tgenerated_size\tpattern_count\tseed\tbacktracking?\tallowed_restarts\tforce_the_use_of_all_patterns?\toutput_filename\n" + ) + + default_backtracking = False + default_allowed_attempts = 10 + default_force_use_all_patterns = False + + xdoc = ET.ElementTree(file="samples_original.xml") + counter = 0 + choices_before_success = 0 + for xnode in xdoc.getroot(): + counter += 1 + choices_before_success = 0 + if "#comment" == xnode.tag: + continue + + name = xnode.get("name", "NAME") + global hackstring + hackstring = name + print("< {0} ".format(name), end="") + if "backtracking_on" == xnode.tag: + default_backtracking = True + if "backtracking_off" == xnode.tag: + default_backtracking = False + if "one_allowed_attempts" == xnode.tag: + default_allowed_attempts = 1 + if "ten_allowed_attempts" == xnode.tag: + default_allowed_attempts = 10 + if "force_use_all_patterns" == xnode.tag: + default_force_use_all_patterns = True + if "overlapping" == xnode.tag: + choices_before_success = 0 + print("beginning...") + print(xnode.attrib) + current_output_file_number = 97000 + (counter * 10) + wfc_ns = types.SimpleNamespace( + output_path="output/", + img_filename="samples/" + + xnode.get("name", "NAME") + + ".png", # name of the input file + output_file_number=current_output_file_number, + operation_name=xnode.get("name", "NAME"), + output_filename="output/" + + xnode.get("name", "NAME") + + "_" + + str(current_output_file_number) + + "_" + + str(time.time()) + + ".png", # name of the output file + debug_log_filename="output/" + + xnode.get("name", "NAME") + + "_" + + str(current_output_file_number) + + "_" + + str(time.time()) + + ".log", + seed=11975, # seed for random generation, can be any number + tile_size=int(xnode.get("tile_size", 1)), # size of tile, in pixels + pattern_width=int( + xnode.get("N", 2) + ), # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + symmetry=int(xnode.get("symmetry", 8)), + ground=int(xnode.get("ground", 0)), + adjacency_directions=dict( + enumerate( + [ + CoordXY(x=0, y=-1), + CoordXY(x=1, y=0), + CoordXY(x=0, y=1), + CoordXY(x=-1, y=0), + ] + ) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=string2bool( + xnode.get("periodicInput", True) + ), # Does the input wrap? + periodic_output=string2bool( + xnode.get("periodicOutput", False) + ), # Do we want the output to wrap? + generated_size=( + int(xnode.get("width", 48)), + int(xnode.get("height", 48)), + ), # Size of the final image + screenshots=int( + xnode.get("screenshots", 3) + ), # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=int( + xnode.get("iteration_limit", 0) + ), # After this many iterations, time out. 0 = never time out. + allowed_attempts=int( + xnode.get("allowed_attempts", default_allowed_attempts) + ), # Give up after this many contradictions + stats_tracking=wfc_stats_tracking.copy(), + backtracking=string2bool( + xnode.get("backtracking", default_backtracking) + ), + force_use_all_patterns=default_force_use_all_patterns, + force_fail_first_solution=False, + ) + wfc_ns.stats_tracking["choices_before_success"] += choices_before_success + wfc_ns.stats_tracking["time_start"] = time.time() + pr = cProfile.Profile() + pr.enable() + wfc_ns = find_pattern_center(wfc_ns) + wfc_ns = wfc.wfc_utilities.load_visualizer(wfc_ns) + ## + ## Load image and make tile data structures + ## + wfc_ns.img = load_source_image(wfc_ns.img_filename) + wfc_ns.channels = wfc_ns.img.shape[ + -1 + ] # detect if it uses channels other than RGB... + wfc_ns.tiles = image_to_tiles(wfc_ns.img, wfc_ns.tile_size) + ( + wfc_ns.tile_catalog, + wfc_ns.tile_grid, + wfc_ns.code_list, + wfc_ns.unique_tiles, + ) = make_tile_catalog(wfc_ns) + wfc_ns.tile_ids = { + v: k for k, v in dict(enumerate(wfc_ns.unique_tiles[0])).items() + } + wfc_ns.tile_weights = { + a: b for a, b in zip(wfc_ns.unique_tiles[0], wfc_ns.unique_tiles[1]) + } + + if WFC_VISUALIZE: + show_input_to_output(wfc_ns) + show_extracted_tiles(wfc_ns) + show_false_color_tile_grid(wfc_ns) + + ( + wfc_ns.pattern_catalog, + wfc_ns.pattern_weights, + wfc_ns.patterns, + wfc_ns.pattern_grid, + ) = make_pattern_catalog_with_symmetry( + wfc_ns.tile_grid, + wfc_ns.pattern_width, + wfc_ns.symmetry, + wfc_ns.periodic_input, + ) + if WFC_VISUALIZE: + show_pattern_catalog(wfc_ns) + adjacency_relations = adjacency_extraction_consistent( + wfc_ns, wfc_ns.patterns + ) + if WFC_VISUALIZE: + show_adjacencies(wfc_ns, adjacency_relations[:256]) + wfc_ns = wfc.wfc_patterns.detect_ground(wfc_ns) + pr.disable() + + screenshots_collected = 0 + while screenshots_collected < wfc_ns.screenshots: + wfc_logger.info(f"Starting solver #{screenshots_collected}") + screenshots_collected += 1 + wfc_ns.seed += 100 + + choice_before_success = 0 + # wfc_ns.stats_tracking["choices_before_success"] = 0# += choices_before_success + wfc_ns.stats_tracking["time_start"] = time.time() + wfc_ns.stats_tracking["final_time_end"] = None + + # update output name so each iteration has a unique filename + output_filename = ( + "output/" + + xnode.get("name", "NAME") + + "_" + + str(current_output_file_number) + + "_" + + str(time.time()) + + "_" + + str(wfc_ns.seed) + + ".png", + ) # name of the output file + + profile_filename = ( + "" + + str(wfc_ns.output_path) + + "setup_" + + str(wfc_ns.output_file_number) + + "_" + + str(wfc_ns.seed) + + "_" + + str(time.time()) + + "_" + + str(wfc_ns.seed) + + ".profile" + ) + if WFC_PROFILE: + with open(profile_filename, "w") as profile_file: + ps = pstats.Stats(pr, stream=profile_file) + ps.sort_stats("cumtime", "ncalls") + ps.print_stats(20) + solution = None + + if "minizinc" == solver_to_use: + attempt_count = 0 + # while attempt_count < wfc_ns.allowed_attempts: + # attempt_count += 1 + # solution = mz_run(wfc_ns) + # solution.wfc_ns.stats_tracking["attempt_count"] = attempt_count + # solution.wfc_ns.stats_tracking["choices_before_success"] += solution.wfc_ns.stats_tracking["observations"] + + else: + if True: + attempt_count = 0 + # print("allowed attempts: " + str(wfc_ns.allowed_attempts)) + attempt_wfc_ns = copy.deepcopy(wfc_ns) + attempt_wfc_ns.stats_tracking["time_start"] = time.time() + attempt_wfc_ns.stats_tracking["choices_before_success"] = 0 + attempt_wfc_ns.stats_tracking[ + "total_observations_before_success" + ] = 0 + wfc.wfc_solver.reset_backtracking_count() # reset the count of how many times we've backtracked, because multiple attempts are handled here instead of there + while attempt_count < wfc_ns.allowed_attempts: + attempt_count += 1 + print(attempt_count, end=" ") + attempt_wfc_ns.seed += 7 # change seed for each attempt... + solution = wfc_run( + attempt_wfc_ns, + visualize=WFC_VISUALIZE, + logging=WFC_LOGGING, + ) + solution.wfc_ns.stats_tracking[ + "attempt_count" + ] = attempt_count + solution.wfc_ns.stats_tracking[ + "choices_before_success" + ] += solution.wfc_ns.stats_tracking["observations"] + attempt_wfc_ns.stats_tracking[ + "total_observations_before_success" + ] += solution.wfc_ns.stats_tracking["total_observations"] + wfc_logger.info( + "result: {} is {}".format( + attempt_count, solution.result + ) + ) + if solution.result == -2: + attempt_count = wfc_ns.allowed_attempts + solution.wfc_ns.stats_tracking["time_end"] = time.time() + wfc_stats_data.append(solution.wfc_ns.stats_tracking.copy()) + solution.wfc_ns.stats_tracking["final_time_end"] = time.time() + print("tracking choices before success...") + choices_before_success = solution.wfc_ns.stats_tracking[ + "choices_before_success" + ] + time_passed = None + if None != solution.wfc_ns.stats_tracking["time_end"]: + time_passed = ( + solution.wfc_ns.stats_tracking["time_end"] + - solution.wfc_ns.stats_tracking["time_start"] + ) + else: + if None != solution.wfc_ns.stats_tracking["final_time_end"]: + time_passed = ( + solution.wfc_ns.stats_tracking["final_time_end"] + - solution.wfc_ns.stats_tracking["time_start"] + ) + + print("...finished calculating time passed") + # print(wfc_stats_data) + print("writing stats...", end="") + + with open(stats_file_name, "a+") as stats_file: + stats_file.write( + f"{solution.wfc_ns.output_file_number}\t{solution.wfc_ns.operation_name}\t{solution.wfc_ns.stats_tracking['success']}\t{solution.wfc_ns.stats_tracking['attempt_count']}\t{solution.wfc_ns.stats_tracking['observations']}\t{solution.wfc_ns.stats_tracking['propagations']}\t{solution.wfc_ns.stats_tracking['choices_before_success']}\t{solution.wfc_ns.stats_tracking['total_observations']}\t{attempt_wfc_ns.stats_tracking['total_observations_before_success']}\t{solution.backtracking_total}\t{time_passed}\t{solution.wfc_ns.stats_tracking['time_start']}\t{solution.wfc_ns.stats_tracking['time_end']}\t{solution.wfc_ns.stats_tracking['final_time_end']}\t{solution.wfc_ns.generated_size}\t{len(solution.wfc_ns.pattern_weights.keys())}\t{solution.wfc_ns.seed}\t{solution.wfc_ns.backtracking}\t{solution.wfc_ns.allowed_attempts}\t{solution.wfc_ns.force_use_all_patterns}\t{solution.wfc_ns.output_filename}\n" + ) + print("done") + + if WFC_VISUALIZE: + print("visualize") + if None == solution: + print("n u l l") + # print(solution) + print(1) + solution_vis = wfc.wfc_solver.render_recorded_visualization( + solution.recorded_vis + ) + # print(solution) + print(2) + + video_fn = f"{solution.wfc_ns.output_path}/crystal_example_{solution.wfc_ns.output_file_number}_{time.time()}.mp4" + wfc_logger.info("*****************************") + wfc_logger.warning(video_fn) + print( + f"solver recording stack len - {len(solution_vis.solver_recording_stack)}" + ) + print(solution_vis.solver_recording_stack[0].shape) + if len(solution_vis.solver_recording_stack) > 0: + wfc_logger.info(solution_vis.solver_recording_stack[0].shape) + writer = FFMPEG_VideoWriter( + video_fn, + [ + solution_vis.solver_recording_stack[0].shape[0], + solution_vis.solver_recording_stack[0].shape[1], + ], + 12.0, + ) + for img_data in solution_vis.solver_recording_stack: + writer.write_frame(img_data) + print("!", end="") + writer.close() + mpy.ipython_display(video_fn, height=700) + print("recording done") + if WFC_VISUALIZE: + solution = wfc_partial_output(solution) + show_rendered_patterns(solution, True) + print("render to output") + render_patterns_to_output(solution, True, False) + print("completed") + print("\n{0} >".format(name)) + + elif "simpletiled" == xnode.tag: + print("> ", end="\n") + continue + else: + continue diff --git a/wfc/wfc1/wfc_example.py b/wfc/wfc1/wfc_example.py new file mode 100644 index 0000000..adc6a33 --- /dev/null +++ b/wfc/wfc1/wfc_example.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Mar 11 12:08:10 2019 + +@author: Isaac +""" + +import matplotlib + +# matplotlib.use('agg') + +import types +import math +from IPython.core.debugger import set_trace +import collections + +import matplotlib.pyplot as plt + +# from matplotlib.pyplot import figure, subplot, subplots, title, matshow + + +import wfc_utilities +from wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +from wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE + +import wfc_tiles +from wfc_tiles import ( + load_source_image, + image_to_tiles, + tiles_to_images, + make_tile_catalog, + show_input_to_output, + show_extracted_tiles, + show_false_color_tile_grid, +) + +import wfc_patterns +from wfc_patterns import ( + make_pattern_catalog_no_rotations, + render_pattern, + show_pattern_catalog, +) + +from wfc_adjacency import ( + adjacency_extraction_consistent, + is_valid_overlap_xy, + blit, + show_adjacencies, +) + +import wfc_solver +from wfc_solver import ( + wfc_init, + wfc_run, + show_wfc_patterns, + show_pattern_adjacency, + visualize_propagator_matrix, + visualize_entropy, + wfc_clear, + visualize_compatible_count, + show_rendered_patterns, + render_patterns_to_output, + wfc_observe, + wfc_partial_output, + wrap_coords, + show_crystal_time, +) +import logging +import numpy as np + +print(f"Using numpy version {np.__version__}") + +np.set_printoptions(threshold=np.inf) + +## +## Set up the namespace +## + + +wfc_ns_chess = types.SimpleNamespace( + output_path="output/", + img_filename="samples/Red Maze.png", # name of the input file + output_file_number=40, + seed=587386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate( + [ + CoordXY(x=0, y=-1), + CoordXY(x=1, y=0), + CoordXY(x=0, y=1), + CoordXY(x=-1, y=0), + ] + ) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(16, 8), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=1, +) # Give up after this many contradictions + +wfc_ns_chess = find_pattern_center(wfc_ns_chess) + +wfc_ns_chess = wfc_utilities.load_visualizer(wfc_ns_chess) + +## +## Load image and make tile data structures +## + +wfc_ns_chess.img = load_source_image(wfc_ns_chess.img_filename) +wfc_ns_chess.tiles = image_to_tiles(wfc_ns_chess.img, wfc_ns_chess.tile_size) +( + wfc_ns_chess.tile_catalog, + wfc_ns_chess.tile_grid, + wfc_ns_chess.code_list, + wfc_ns_chess.unique_tiles, +) = make_tile_catalog(wfc_ns_chess) +wfc_ns_chess.tile_ids = { + v: k for k, v in dict(enumerate(wfc_ns_chess.unique_tiles[0])).items() +} +print(wfc_ns_chess.unique_tiles) +wfc_ns_chess.tile_weights = { + a: b for a, b in zip(wfc_ns_chess.unique_tiles[0], wfc_ns_chess.unique_tiles[1]) +} +print(wfc_ns_chess.tile_weights) + +# print("wfc_ns_chess.tile_catalog") +# print(wfc_ns_chess.tile_catalog) +# print("wfc_ns_chess.tile_grid") +# print(wfc_ns_chess.tile_grid) +# print("wfc_ns_chess.code_list") +# print(wfc_ns_chess.code_list) +# print("wfc_ns_chess.unique_tiles") +# print(wfc_ns_chess.unique_tiles) + +# assert False + +show_input_to_output(wfc_ns_chess) +show_extracted_tiles(wfc_ns_chess) +show_false_color_tile_grid(wfc_ns_chess) + +# im = np.array([[[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], +# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]]]) + +# plt.imshow(im) +# plt.show() + + +# def adjacency_extraction_consistent(wfc_ns, pattern_cat): +# """Takes a pattern catalog, returns a list of all legal adjacencies.""" +# # This is a brute force implementation. We should really use the adjacency list we've already calculated... +# legal = [] +# #print(f"pattern_cat\n{pattern_cat}") +# for p1, pattern1 in enumerate(pattern_cat): +# for d_index, d in enumerate(wfc_ns.adjacency_directions): +# for p2, pattern2 in enumerate(pattern_cat): +# if is_valid_overlap_xy(d, p1, p2, pattern_cat, wfc_ns.pattern_width, wfc_ns.adjacency_directions): +# legal.append((d_index, p1, p2)) +# return legal + +# from pycallgraph import PyCallGraph +# from pycallgraph.output import GraphvizOutput +# with PyCallGraph(output=GraphvizOutput()): + + +## +## Patterns +## + +( + wfc_ns_chess.pattern_catalog, + wfc_ns_chess.pattern_weights, + wfc_ns_chess.patterns, +) = make_pattern_catalog_no_rotations( + wfc_ns_chess.tile_grid, wfc_ns_chess.pattern_width +) +show_pattern_catalog(wfc_ns_chess) +adjacency_relations = adjacency_extraction_consistent( + wfc_ns_chess, wfc_ns_chess.patterns +) +# print(adjacency_relations) +show_adjacencies(wfc_ns_chess, adjacency_relations[:256]) +# print(wfc_ns_chess.patterns) + + +## +## Run the solver +## + +solution = wfc_run(wfc_ns_chess, visualize=False) + +## +## Output the results +## + +import moviepy.editor as mpy +from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter + +# from lucid_serialize_array import _normalize_array + +solution = wfc_solver.render_recorded_visualization(solution) + +video_fn = f"{solution.wfc_ns.output_path}/crystal_example_{solution.wfc_ns.output_file_number}.mp4" +print("*****************************") +print(solution.solver_recording_stack[0].shape) +writer = FFMPEG_VideoWriter(video_fn, solution.solver_recording_stack[0].shape, 12.0) +# for i in range(24): +# writer.write_frame(solution.solver_recording_stack[0]) +for img_data in solution.solver_recording_stack: + writer.write_frame(img_data) + # print(_normalize_array(img_data)) + print("!", end="") +# for i in range(24): +# writer.write_frame(solution.solver_recording_stack[-1]) +# for i in range(24): +# writer.write_frame(solution.solver_recording_stack[0]) +# for img_data in solution.solver_recording_stack: +# writer.write_frame(img_data) +# #print(_normalize_array(img_data)) +# print('!',end='') +# for i in range(24): +# writer.write_frame(solution.solver_recording_stack[-1]) + +writer.close() + +mpy.ipython_display(video_fn, height=700) + +solution = wfc_partial_output(solution) +show_rendered_patterns(solution, True) +render_patterns_to_output(solution, True) +# show_crystal_time(solution, True) +# show_rendered_patterns(solution, False) +# render_patterns_to_output(solution, False) diff --git a/wfc/wfc1/wfc_extra.py b/wfc/wfc1/wfc_extra.py new file mode 100644 index 0000000..db73ad1 --- /dev/null +++ b/wfc/wfc1/wfc_extra.py @@ -0,0 +1,159 @@ +# In[ ]: + + +wfc_ns_chess48 = types.SimpleNamespace( + img_filename="Chess.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_chess48 = prepare_wfc_namespace(wfc_ns_chess48, visualize=True) +wfc_main(wfc_ns_chess48) + + +# In[ ]: + + +wfc_ns_chess47 = types.SimpleNamespace( + img_filename="Chess.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(47, 47), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_chess47 = prepare_wfc_namespace(wfc_ns_chess47, visualize=True) +wfc_main(wfc_ns_chess47) + + +# In[ ]: + + +wfc_ns_blackdots = types.SimpleNamespace( + img_filename="blackdots.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_blackdots = prepare_wfc_namespace(wfc_ns_blackdots, visualize=True) +wfc_main(wfc_ns_blackdots) + + +# In[ ]: + + +wfc_ns_blackdotsred = types.SimpleNamespace( + img_filename="blackdotsred.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_blackdotsred = prepare_wfc_namespace(wfc_ns_blackdotsred, visualize=True) +wfc_main(wfc_ns_blackdotsred) + + +# In[ ]: + + +wfc_ns_blackdotsstripe = types.SimpleNamespace( + img_filename="blackdotsstripe.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_blackdotsstripe = prepare_wfc_namespace(wfc_ns_blackdotsstripe, visualize=True) +wfc_main(wfc_ns_blackdotsstripe) + + +# In[ ]: + + +wfc_ns_Skyline = types.SimpleNamespace( + img_filename="Skyline.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_Skyline = prepare_wfc_namespace(wfc_ns_Skyline, visualize=True) +wfc_main(wfc_ns_Skyline) + + +# In[ ]: + + +wfc_ns_flowers = types.SimpleNamespace( + img_filename="Flowers.png", # name of the input file + seed=87386, # seed for random generation, can be any number + tile_size=1, # size of tile, in pixels + pattern_width=3, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. + channels=3, # Color channels in the image (usually 3 for RGB) + adjacency_directions=dict( + enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) + ), # The list of adjacencies that we care about - these will be turned into the edges of the graph + periodic_input=True, # Does the input wrap? + periodic_output=True, # Do we want the output to wrap? + generated_size=(48, 48), # Size of the final image + screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit=0, # After this many iterations, time out. 0 = never time out. + allowed_attempts=2, +) # Give up after this many contradictions +wfc_ns_flowers = prepare_wfc_namespace(wfc_ns_flowers, visualize=True) +wfc_main(wfc_ns_flowers) diff --git a/wfc/wfc1/wfc_minizinc.py b/wfc/wfc1/wfc_minizinc.py new file mode 100644 index 0000000..bf09873 --- /dev/null +++ b/wfc/wfc1/wfc_minizinc.py @@ -0,0 +1,114 @@ +# An interface to other solvers via MiniZinc +import pymzn +import time +import wfc.wfc_solver +from wfc.wfc_adjacency import adjacency_extraction_consistent +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center + + +import logging + +logging.basicConfig(level=logging.INFO) +wfc_logger = logging.getLogger() + +import pdb + + +def calculate_adjacency_grid(shape: tuple, directions): + adj_grid_shape = ((len(directions)), *shape) + + def within_bounds(x, limit): + while x < 0: + x += limit + while x > limit - 1: + x -= limit + return x + + def add_offset(a, b): + offset = [sum(x) for x in zip(a, b)] + return (within_bounds(offset[0], shape[0]), within_bounds(offset[1], shape[1])) + + adj_grid = numpy.zeros(adj_grid_shape, dtype=numpy.uint32) + # TODO: grids bigger than max(uint32 - 1) can't index the entire array + # Therefore, output shapes should not exceed approximately 65535 x 65535 + for d_idx, d in enumerate(directions): + for x in range(shape[0]): + for y in range(shape[1]): + cell = (x, y) + offset_cell = add_offset(d, cell) + # Adds one to the index so we can use zero in MiniZinc to + # indicate non-edges for non-wrapping output images and similar + adj_grid[d_idx, y, x] = 1 + ( + offset_cell[0] + (offset_cell[1] * shape[0]) + ) + return adj_grid + + +solns = pymzn.minizinc("knapsack.mzn", "knapsack.dzn", data={"capacity": 20}) +print(solns) + +pymzn.dict2dzn( + { + "w": 8, + "h": 8, + "pattern_count": 95, + "direction_count": 4, + "adjacency_count": 992, + "pattern_names": [], + "relation_matrix": [], + "adjaceny_table": [], + "adjacency_matrix": [], + }, + fout="test.dzn", +) + + +def mz_init(prestate): + prestate.adjacency_directions_rc = { + i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() + } + prestate = wfc.wfc_utilities.find_pattern_center(prestate) + wfc_state = types.SimpleNamespace(wfc_ns=prestate) + + wfc_state.result = None + wfc_state.adjacency_relations = adjacency_extraction_consistent( + wfc_state.wfc_ns, wfc_state.wfc_ns.patterns + ) + wfc_state.patterns = np.array(list(wfc_state.wfc_ns.pattern_catalog.keys())) + wfc_state.pattern_translations = list(wfc_state.wfc_ns.pattern_catalog.values()) + wfc_state.number_of_patterns = wfc_state.patterns.size + wfc_state.number_of_directions = len(wfc_state.wfc_ns.adjacency_directions) + + wfc_state.propagator_matrix = np.zeros( + ( + wfc_state.number_of_directions, + wfc_state.number_of_patterns, + wfc_state.number_of_patterns, + ), + dtype=np.bool_, + ) + for d, p1, p2 in wfc_state.adjacency_relations: + wfc_state.propagator_matrix[(d, p1, p2)] = True + + wfc_state.mz_dzn = { + "w": wfc_state.wfc_ns.generated_size[0], + "h": wfc_state.wfc_ns.generated_size[1], + "pattern_count": wfc_state.number_of_patterns, + "direction_count": wfc_state.number_of_directions, + "adjacency_count": len(wfc_state.adjacency_relations), + "pattern_names": list( + wfc_state.patterns, zip(range(wfc_state.number_of_patterns)) + ), + "relation_matrix": calculate_adjacency_grid( + wfc_state.wfc_ns.generated_size, wfc_state.wfc_ns.adjacency_directions + ), + "adjaceny_table": [], + "adjacency_matrix": [], + } + + +def mz_run(wfc_seed_state): + wfc_logger.info("Invoking MiniZinc solver") + wfc_state = mz_init(wfc_seed_state) + + return wfc_state diff --git a/wfc/wfc1/wfc_patterns.py b/wfc/wfc1/wfc_patterns.py new file mode 100644 index 0000000..06ac170 --- /dev/null +++ b/wfc/wfc1/wfc_patterns.py @@ -0,0 +1,412 @@ +import logging + +# from matplotlib.pyplot import figure, subplot, subplots, title, matshow +import wfc.wfc_utilities +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.pyplot import figure, subplot, subplots, title, matshow + +import logging + +logging.basicConfig(level=logging.INFO) +wfc_logger = logging.getLogger() + +# # Patterns +# +# We could figure out the valid adjacencies between the tiles by hand and WFC would work just fine---in fact, Gumin's original SimpleTile model is exactly that. But lets bring automation on our side and calculate the adjacencies instead. +# +# To do this, we're going to use Gumin's abstraction of patterns. A pattern is simply a region of tiles, in this case in the original image. Instead of talking about adjacencies between patterns, we will use adjacencies between _patterns_. This lets us automatically capture more expressive information about the source image. +# +# The size of the patterns influences the results. 1x1 patterns behave exactly as tiles. 2x2 and 3x3 are common in generating pixel art. Larger patterns are better at capturing relationships but need more valid samples to retain the same flexibility. Which also leads to larger patterns rapidly getting more and more computationally expensive. + +# ## Extracting Patterns + +# In[12]: + + +# def unique_patterns_2d(a, k): +# assert(k >= 1) +# +# a = np.pad(a, ((0,k-1),(0,k-1),*(((0,0),)*(len(a.shape)-2))), mode='wrap') +# +# patches = np.lib.stride_tricks.as_strided( +# a, +# (a.shape[0]-k+1,a.shape[1]-k+1,k,k,*a.shape[2:]), +# a.strides[:2] + a.strides[:2] + a.strides[2:], +# writeable=False) +# +# patch_codes = hash_downto(patches,2) + + +# uc, ui = np.unique(patch_codes, return_index=True) +# locs = np.unravel_index(ui, patch_codes.shape) +# up = patches[locs[0],locs[1]] +# ids = np.vectorize({c: i for i,c in enumerate(uc)}.get)(patch_codes) + +# return ids, up + +# def make_pattern_catalog(img_ns): + +# ns.number_of_rotations = 4# TODO: take this as an argument +# more_grids = [img_ns.tile_grid] +# for rotation in range(1, ns.number_of_rotations): +# rot_a = np.rot90(img_ns.tile_grid, rotation) +# more_grids.append(rot_a) +# ref_v = np.flip(rot_a, 0) +# more_grids.append(ref_v) +# ref_h = np.flip(rot_a, 1) +# more_grids.append(ref_h) + +# all_pattern_catalog = {} +# all_pattern_weights = {} +# all_patterns = np.zeros((0,img_ns.pattern_width,img_ns.pattern_width)) +# for m_grid in more_grids: +# print(m_grid.shape) + +# pattern_codes, patterns = unique_patterns_2d(m_grid, img_ns.pattern_width) +# print(type(pattern_codes), pattern_codes.shape) +# print(type(patterns), patterns.shape) + +# logging.info(f'{len(patterns)} unique patterns') +# assert np.array_equal(m_grid, patterns[pattern_codes][:,:,0,0]) +# pattern_catalog = {i:x for i,x in enumerate(patterns)} +# logging.debug(f'\npattern_codes: {pattern_codes}') +# pattern_weights = {i:(pattern_codes == i).sum() for i,x in enumerate(patterns)} +# logging.debug(f'pattern_weights: {pattern_weights}') +# all_pattern_catalog = {**all_pattern_catalog, **pattern_catalog} +# all_patterns = np.concatenate(all_patterns, patterns) + +# return all_pattern_catalog, pattern_weights, patterns + + +# ns.pattern_catalog, ns.pattern_weights, ns.patterns = make_pattern_catalog(ns) + + +def unique_patterns_2d(a, k, periodic_input): + assert k >= 1 + if periodic_input: + a = np.pad( + a, ((0, k - 1), (0, k - 1), *(((0, 0),) * (len(a.shape) - 2))), mode="wrap" + ) + else: + # TODO: implement non-wrapped image handling + # a = np.pad(a, ((0,k-1),(0,k-1),*(((0,0),)*(len(a.shape)-2))), mode='constant', constant_values=None) + a = np.pad( + a, ((0, k - 1), (0, k - 1), *(((0, 0),) * (len(a.shape) - 2))), mode="wrap" + ) + + patches = np.lib.stride_tricks.as_strided( + a, + (a.shape[0] - k + 1, a.shape[1] - k + 1, k, k, *a.shape[2:]), + a.strides[:2] + a.strides[:2] + a.strides[2:], + writeable=False, + ) + patch_codes = hash_downto(patches, 2) + uc, ui = np.unique(patch_codes, return_index=True) + locs = np.unravel_index(ui, patch_codes.shape) + up = patches[locs[0], locs[1]] + ids = np.vectorize({c: i for i, c in enumerate(uc)}.get)(patch_codes) + wfc_logger.debug(ids) + return ids, up + + +def unique_patterns_brute_force(grid, size, periodic_input): + padded_grid = np.pad( + grid, + ((0, size - 1), (0, size - 1), *(((0, 0),) * (len(grid.shape) - 2))), + mode="wrap", + ) + patches = [] + for x in range(grid.shape[0]): + row_patches = [] + for y in range(grid.shape[1]): + row_patches.append( + np.ndarray.tolist(padded_grid[x : x + size, y : y + size]) + ) + patches.append(row_patches) + patches = np.array(patches) + patch_codes = hash_downto(patches, 2) + uc, ui = np.unique(patch_codes, return_index=True) + locs = np.unravel_index(ui, patch_codes.shape) + up = patches[locs[0], locs[1]] + ids = np.vectorize({c: i for i, c in enumerate(uc)}.get)(patch_codes) + wfc_logger.debug(ids) + return ids, up + + +def make_pattern_catalog_from_grid(tile_grid, pattern_width, periodic_input): + pattern_codes, patterns = unique_patterns_2d( + tile_grid, pattern_width, periodic_input + ) + + VERIFY = False + if VERIFY: + pattern_codes2, patterns2 = unique_patterns_brute_force( + tile_grid, pattern_width, periodic_input + ) + assert np.array_equal(pattern_codes, pattern_codes2) + assert np.array_equal(patterns, patterns2) + logging.info(f"{len(patterns)} unique patterns") + + assert np.array_equal(tile_grid, patterns[pattern_codes][:, :, 0, 0]) + pattern_catalog = {i: x for i, x in enumerate(patterns)} + logging.debug(f"\npattern_codes: {pattern_codes}") + pattern_weights = {i: (pattern_codes == i).sum() for i, x in enumerate(patterns)} + logging.debug(f"pattern_weights: {pattern_weights}") + + return pattern_catalog, pattern_weights, patterns, pattern_codes + + +def reflect_pattern(pattern): + # print(f"reflect:\n{pattern}\nto\n{np.fliplr(pattern).copy()}\n") + return np.fliplr(pattern).copy() + + +def rotate_pattern(pattern): + # print(f"rotate:\n{pattern}\nto\n{np.rot90(pattern).copy()}\n") + return np.rot90(pattern).copy() + + +def make_pattern_catalog_with_symmetry( + tile_grid, pattern_width, symmetry, periodic_input +): + ( + pattern_catalog, + pattern_weights, + patterns, + pattern_grid, + ) = make_pattern_catalog_from_grid(tile_grid, pattern_width, periodic_input) + # print(patterns.shape) + # print('~~~~~~~~~') + if symmetry > 1: + for i in list(pattern_catalog.keys()): + base_pattern = pattern_catalog[i].copy() + for sym_op in range(2, symmetry + 1): + # print(f"{sym_op} and {(sym_op % 2 == 0)}") + r_func = reflect_pattern if (sym_op % 2 == 0) else rotate_pattern + # print(r_func.__name__) + new_pattern = r_func(base_pattern) + try: + pattern_index = [ + np.array_equal(new_pattern, x) for x in pattern_catalog.values() + ].index(True) + pattern_weights[pattern_index] += 1 + except ValueError: + new_index = len(pattern_weights) + pattern_catalog[new_index] = new_pattern.copy() + pattern_weights[new_index] = 1 + patterns = np.append(patterns, [new_pattern.copy()], axis=0) + # print(f"added_pattern {new_index}") + base_pattern = new_pattern.copy() + logging.info(f"{len(patterns)} unique patterns after symmetry {symmetry}") + print(patterns.shape) + # assert False + # print(pattern_catalog) + # print(patterns) + # assert False + return pattern_catalog, pattern_weights, patterns, pattern_grid + + +def find_last_patterns(pattern_grid, number_of_last_patterns): + last_patterns = [] + for x in reversed(range(pattern_grid.shape[0])): + for y in reversed(range(pattern_grid.shape[1])): + current_pattern = pattern_grid[x][y] + if not (current_pattern in last_patterns): + last_patterns.append(current_pattern) + if len(last_patterns) >= abs(number_of_last_patterns): + return last_patterns + return last_patterns + + +def detect_ground(wfc_ns): + wfc_ns.last_patterns = [] + if wfc_ns.ground != 0: + wfc_ns.last_patterns = find_last_patterns(wfc_ns.pattern_grid, wfc_ns.ground) + return wfc_ns + + +def make_pattern_catalog_with_rotation(tile_grid, pattern_width): + the_pattern_catalog = {} + the_pattern_weights = {} + the_patterns = np.zeros((0, pattern_width, pattern_width), dtype=np.int64) + number_of_rotations = 4 + for rotation in range(0, number_of_rotations): + rotated_grid = np.rot90(tile_grid, rotation) + flipped_grid = np.flip(rotated_grid, 0) + for grid in [rotated_grid, flipped_grid]: + ( + a_pattern_catalog, + a_pattern_weights, + a_patterns, + ) = make_pattern_catalog_from_grid(tile_grid, pattern_width) + the_pattern_catalog = {**the_pattern_catalog, **a_pattern_catalog} + the_pattern_weights = { + x: (the_pattern_weights.get(x, 0) + a_pattern_weights.get(x, 0)) + for x in (the_pattern_weights.keys() | a_pattern_weights.keys()) + } + the_patterns = np.concatenate((the_patterns, a_patterns)) + return the_pattern_catalog, the_pattern_weights, the_patterns + + +def make_pattern_catalog(tile_grid, pattern_width): + pc, pw, patterns = make_pattern_catalog_with_rotation(tile_grid, pattern_width) + patterns = np.unique(patterns, axis=0) + for p in range(patterns.shape[0]): + assert np.array_equal(patterns[p], pc[p]) + return pc, pw, patterns + + +def make_pattern_catalog_no_rotations(tile_grid, pattern_width): + """ + >>> make_pattern_catalog_no_rotations(test_ns.tile_grid, test_ns.pattern_width) + ({0: array([[-8754995591521426669, 0], + [-8754995591521426669, -8754995591521426669]], dtype=int64), 1: array([[-8754995591521426669, 0], + [-8754995591521426669, 0]], dtype=int64), 2: array([[ 0, 0], + [-8754995591521426669, -8754995591521426669]], dtype=int64), 3: array([[8253868773529191888, 0], + [ 0, 0]], dtype=int64), 4: array([[-8754995591521426669, -8754995591521426669], + [ 0, -8754995591521426669]], dtype=int64), 5: array([[-8754995591521426669, -8754995591521426669], + [-8754995591521426669, 0]], dtype=int64), 6: array([[ 0, -8754995591521426669], + [-8754995591521426669, -8754995591521426669]], dtype=int64), 7: array([[ 0, 0], + [ 0, 8253868773529191888]], dtype=int64), 8: array([[ 0, 0], + [8253868773529191888, 0]], dtype=int64), 9: array([[-8754995591521426669, -8754995591521426669], + [ 0, 0]], dtype=int64), 10: array([[ 0, -8754995591521426669], + [ 0, -8754995591521426669]], dtype=int64), 11: array([[ 0, 8253868773529191888], + [ 0, 0]], dtype=int64)}, {0: 1, 1: 2, 2: 2, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 2, 10: 2, 11: 1}, array([[[-8754995591521426669, 0], + [-8754995591521426669, -8754995591521426669]], + + [[-8754995591521426669, 0], + [-8754995591521426669, 0]], + + [[ 0, 0], + [-8754995591521426669, -8754995591521426669]], + + [[ 8253868773529191888, 0], + [ 0, 0]], + + [[-8754995591521426669, -8754995591521426669], + [ 0, -8754995591521426669]], + + [[-8754995591521426669, -8754995591521426669], + [-8754995591521426669, 0]], + + [[ 0, -8754995591521426669], + [-8754995591521426669, -8754995591521426669]], + + [[ 0, 0], + [ 0, 8253868773529191888]], + + [[ 0, 0], + [ 8253868773529191888, 0]], + + [[-8754995591521426669, -8754995591521426669], + [ 0, 0]], + + [[ 0, -8754995591521426669], + [ 0, -8754995591521426669]], + + [[ 0, 8253868773529191888], + [ 0, 0]]], dtype=int64)) + """ + pc, pw, patterns = make_pattern_catalog_from_grid(tile_grid, pattern_width) + return pc, pw, patterns + + +# In[13]: + + +def version_check(minimum_ver): + minim = [int(v) for v in minimum_ver.split(".")] + checking = [int(v) for v in np.__version__.split(".")] + if not checking[0] >= minim[0]: + if not checking[1] >= minim[1]: + if not checking[2] >= minim[2]: + return True + return False + + +def render_pattern(render_pattern, nspace): + rp_iter = np.nditer(render_pattern, flags=["multi_index"]) + output = np.zeros(render_pattern.shape + (3,), dtype=np.uint32) + while not rp_iter.finished: + # Note that this truncates images with more than 3 channels down to just the channels in the output. + # If we want to have alpha channels, we'll need a different way to handle this. + output[rp_iter.multi_index] = np.resize( + nspace.tile_catalog[render_pattern[rp_iter.multi_index]], + output[rp_iter.multi_index].shape, + ) + rp_iter.iternext() + return output + + +def show_pattern_catalog(img_ns): + s_columns = 24 // min(24, img_ns.pattern_width) + s_rows = 1 + (int(len(img_ns.pattern_catalog)) // s_columns) + fig = figure(figsize=(s_columns, s_rows * 1.5)) + title("Extracted Patterns") + for i, tcode in img_ns.pattern_catalog.items(): + pat_cat = img_ns.pattern_catalog[i] + ptr = render_pattern(pat_cat, img_ns).astype(np.uint8) + sp = subplot(s_rows, s_columns, i + 1) + spi = sp.imshow(ptr) + spi.axes.xaxis.set_label_text(f"({img_ns.pattern_weights[i]})") + sp.set_title(i) + spi.axes.tick_params( + labelleft=False, labelbottom=False, left=False, bottom=False + ) + spi.axes.grid(False) + plt.savefig(img_ns.output_filename + "_patterns.pdf", bbox_inches="tight") + plt.close() + + +if __name__ == "__main__": + import types + import wfc_tiles + + test_ns = types.SimpleNamespace( + img_filename="red_maze.png", + seed=87386, + tile_size=1, + pattern_width=2, + channels=3, + adjacency_directions=dict( + enumerate( + [ + CoordXY(x=0, y=-1), + CoordXY(x=1, y=0), + CoordXY(x=0, y=1), + CoordXY(x=-1, y=0), + ] + ) + ), + periodic_input=True, + periodic_output=True, + generated_size=(3, 3), + screenshots=1, + iteration_limit=0, + allowed_attempts=1, + ) + test_ns = wfc_utilities.find_pattern_center(test_ns) + test_ns = wfc_utilities.load_visualizer(test_ns) + test_ns.img = wfc_tiles.load_source_image(test_ns.img_filename) + ( + test_ns.tile_catalog, + test_ns.tile_grid, + test_ns.code_list, + test_ns.unique_tiles, + ) = wfc_tiles.make_tile_catalog(test_ns) + test_ns.tile_ids = { + v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() + } + test_ns.tile_weights = { + a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) + } + ( + test_ns.pattern_catalog, + test_ns.pattern_weights, + test_ns.patterns, + ) = make_pattern_catalog_no_rotations(test_ns.tile_grid, test_ns.pattern_width) + import doctest + + doctest.testmod() diff --git a/wfc/wfc1/wfc_solver.py b/wfc/wfc1/wfc_solver.py new file mode 100644 index 0000000..c43d32a --- /dev/null +++ b/wfc/wfc1/wfc_solver.py @@ -0,0 +1,2131 @@ +# ## Current WFC Solver + +# In[22]: + +import matplotlib + +# matplotlib.use('Agg') + +import types +from wfc.wfc_adjacency import adjacency_extraction_consistent +import numpy as np +from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE +import matplotlib.pyplot +from matplotlib.pyplot import figure, subplot, subplots, title, matshow +from wfc.wfc_patterns import render_pattern +from wfc.wfc_adjacency import blit +from wfc.wfc_tiles import tiles_to_images +import wfc.wfc_utilities +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +import random +import copy +import time + +import imageio + +import logging + +logging.basicConfig(level=logging.INFO) +wfc_logger = logging.getLogger() + +import math + +import pdb + +# import moviepy.editor as mpy +# from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter + + +WFC_DEBUGGING = False +WFC_VISUALIZE = False + +WFC_FINISHED = -2 +WFC_FAILURE = -1 +WFC_TIMEDOUT = -3 +WFC_FAKE_FAILURE = -6 + +### Visualization Functions + +# In[23]: + + +# def pattern_to_tile(pattern, pattern_catalog, pattern_center): +# try: +# return pattern_catalog[pattern][pattern_center] +# except: +# return pattern_catalog[0][pattern_center] + + +# In[24]: + + +def status_print_helper(status_string): + # print(status_string) + pass + + +# In[25]: + + +def show_wfc_patterns(wfc_state, pattern_translations): + s_columns = 24 // min(24, wfc_state.wfc_ns.pattern_width) + s_rows = 1 + (int(len(pattern_translations)) // s_columns) + fig = figure(figsize=(32, 32)) + + title("Extracted Patterns") + for i, tcode in enumerate(pattern_translations): + pat_cat = pattern_translations[i] + ptr = render_pattern(pat_cat, wfc_state.wfc_ns).astype(np.uint8) + sp = subplot(s_rows, s_columns, i + 1) + spi = sp.imshow(ptr) + spi.axes.xaxis.set_label_text(f"({wfc_state.wfc_ns.pattern_weights[i]})") + spi.axes.tick_params( + labelleft=False, labelbottom=False, left=False, bottom=False + ) + spi.axes.grid(color="grey", linewidth=1.0) + for axis in [spi.axes.xaxis, spi.axes.yaxis]: + axis.set_ticks(np.arange(-0.5, wfc_state.wfc_ns.pattern_width + 0.5, 1)) + sp.set_title(i) + matplotlib.pyplot.close(fig) + + +# In[26]: + + +def visualize_propagator_matrix(p_matrix): + visual_stack = np.empty( + [p_matrix.shape[1], p_matrix.shape[2]], dtype=p_matrix.dtype + ) + visual_stack = ( + (p_matrix[0] * 1) + (p_matrix[1] * 2) + (p_matrix[2] * 4) + (p_matrix[3] * 8) + ) + matfig = figure(figsize=(9, 9)) + + matfig.tight_layout(pad=0) + ax = subplot(1, 1, 1) + + title("Propagator Matrix") + # ax.matshow(visual_stack,cmap='jet',fignum=matfig.number) + ax.matshow(visual_stack, cmap="jet") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + # ax.grid(color="black", linewidth=1.5) + # for axis in [ax.xaxis, ax.yaxis]: + # axis.set_ticks(np.arange(-0.5, p_matrix.shape[1] + 0.5, 1.0)) + matplotlib.pyplot.close(matfig) + + +def record_visualization(wfc_state, wfc_vis=None): + if None == wfc_vis: + wfc_vis = types.SimpleNamespace( + method_time_stack=[], + ones_time_stack=[], + output_time_stack=[], + partial_output_time_stack=[], + crystal_time_stack=[], + choices_recording_stack=[], + number_of_patterns=wfc_state.number_of_patterns, + pattern_center=wfc_state.wfc_ns.pattern_center, + rows=wfc_state.rows, + columns=wfc_state.columns, + pattern_catalog=wfc_state.wfc_ns.pattern_catalog, + wfc_ns=copy.deepcopy(wfc_state.wfc_ns), + wave_table_stack=[], + solver_recording_stack=[], + ) + wfc_vis.method_time_stack.append(np.copy(wfc_state.method_time)) + wfc_vis.ones_time_stack.append( + np.copy(np.count_nonzero(wfc_state.wave_table, axis=2)) + ) + wfc_vis.output_time_stack.append(np.copy(wfc_state.output_grid)) + wfc_vis.partial_output_time_stack.append(np.copy(wfc_state.partial_output_grid)) + wfc_vis.crystal_time_stack.append(np.copy(wfc_state.crystal_time)) + wfc_vis.choices_recording_stack.append(np.copy(wfc_state.choices_recording)) + wfc_vis.wave_table_stack.append(np.copy(wfc_state.wave_table)) + print(f"time stack length: {len(wfc_vis.method_time_stack)}") + return wfc_vis + + +def render_recorded_visualization(wfc_vis): + # wfc_vis = wfc_solution_to_vis.recorded_vis + + wfc_logger.info(f"time stack length: {len(wfc_vis.method_time_stack)}") + wfc_logger.info( + f"method time stack: {[x.sum() for x in wfc_vis.method_time_stack]}" + ) + for i in range(len(wfc_vis.method_time_stack)): + matfig = figure(figsize=(16, 8)) + + # matplotlib.pyplot.title(f"{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}", fontsize=14, fontweight='bold', y = -1) + matplotlib.pyplot.title(f"{i}", fontsize=14, fontweight="bold", y=0.6) + + ax = subplot(1, 5, 1) + title("Resolution Method") + ax.matshow(wfc_vis.method_time_stack[i], cmap="magma") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + ax = subplot(1, 5, 2) + title("Ones Matrix") + + ax.matshow( + wfc_vis.ones_time_stack[i], + cmap="plasma", + vmin=0, + vmax=wfc_vis.number_of_patterns, + ) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + # ax = subplot(1,5,3) + # title('Output Matrix') + + # ax.matshow(wfc_state.output_time_stack[i],cmap='inferno', vmin=0, vmax=wfc_state.number_of_patterns) + # ax.grid(None) + # ax.set_yticklabels([]) + # ax.set_xticklabels([]) + # ax.grid(None) + + ax = subplot(1, 5, 3) + title("Choices Matrix") + + ax.matshow( + wfc_vis.choices_recording_stack[i], + cmap="inferno", + vmin=0, + vmax=wfc_vis.number_of_patterns, + ) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + ax = subplot(1, 5, 4) + title("Crystal Matrix") + + ax.matshow( + wfc_vis.crystal_time_stack[i], + cmap="gist_rainbow", + vmin=0, + vmax=len(wfc_vis.crystal_time_stack), + ) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + pattern_grid = np.array(wfc_vis.output_time_stack[i], dtype=np.int64) + + has_gaps = np.any(np.count_nonzero(wfc_vis.wave_table_stack[i], axis=2) != 1) + if has_gaps: + pattern_grid = np.array( + wfc_vis.partial_output_time_stack[i], dtype=np.int64 + ) + render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) + pattern_center = wfc_vis.wfc_ns.pattern_center + for row in range(wfc_vis.rows): + for column in range(wfc_vis.columns): + if len(pattern_grid.shape) > 2: + pattern_list = [] + for z in range(wfc_vis.number_of_patterns): + pattern_list.append(pattern_grid[(row, column, z)]) + pattern_list = [ + pattern_grid[(row, column, z)] + for z in range(wfc_vis.number_of_patterns) + if (pattern_grid[(row, column, z)] != -1) + and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) + ] + for pl_count, the_pattern in enumerate(pattern_list): + the_pattern_tiles = wfc_vis.pattern_catalog[the_pattern][ + pattern_center[0] : pattern_center[0] + 1, + pattern_center[1] : pattern_center[1] + 1, + ] + render_grid = blit( + render_grid, + the_pattern_tiles, + (row, column), + layer=pl_count, + ) + else: + if WFC_NULL_VALUE != pattern_grid[(row, column)]: + the_pattern = wfc_vis.wfc_ns.pattern_catalog[ + pattern_grid[(row, column)] + ] + p_x = wfc_vis.wfc_ns.pattern_center[0] + p_y = wfc_vis.wfc_ns.pattern_center[1] + the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] + render_grid = blit(render_grid, the_pattern, (row, column)) + ptr = tiles_to_images( + wfc_vis.wfc_ns, + render_grid, + wfc_vis.wfc_ns.tile_catalog, + wfc_vis.wfc_ns.tile_size, + visualize=True, + partial=True, + ).astype(np.uint8) + + ax = subplot(1, 5, 5) + title("Output Matrix") + + ax.imshow(ptr) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + matplotlib.pyplot.savefig( + f"{wfc_vis.wfc_ns.output_path}crystal_preview_{wfc_vis.wfc_ns.output_file_number}_{backtrack_track_global}_{i}_{str(time.time())}.png", + bbox_inches="tight", + ) + + img_data = np.frombuffer(matfig.canvas.tostring_rgb(), dtype=np.uint8) + img_data = img_data.reshape(matfig.canvas.get_width_height() + (3,)) + + # print(f"img_data shape: {matfig.canvas.get_width_height()} {matfig.canvas.get_width_height()[::-1] + (3,)}") + + # temporarily disable the recording stack... + # wfc_vis.solver_recording_stack.append(img_data) + matplotlib.pyplot.close(fig=matfig) + wfc_logger.info(f"recording stack length: {len(wfc_vis.solver_recording_stack)}") + return wfc_vis + + +def visualize_entropies(wfc_state): + matfig = figure(figsize=(24, 24)) + + matplotlib.pyplot.title( + f"{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}", + fontsize=14, + fontweight="bold", + y=0.6, + ) + + ax = subplot(1, 5, 1) + title("Resolution Method") + for row in range(wfc_state.rows): + for column in range(wfc_state.columns): + try: + entropy_sum = wfc_state.sums_of_weights[row, column] + wfc_state.entropies[row, column] = (math.log(entropy_sum)) - ( + (wfc_state.sums_of_weight_log_weights[row, column]) / entropy_sum + ) + except: + pass + + ax.matshow(wfc_state.method_time, cmap="magma") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + ax = subplot(1, 5, 2) + title("Ones Matrix") + + ax.matshow( + np.count_nonzero(wfc_state.wave_table, axis=2), + cmap="plasma", + vmin=0, + vmax=wfc_state.number_of_patterns, + ) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + # ax = subplot(1,5,3) + # title('Output Matrix') + # + # ax.matshow(wfc_state.output_grid,cmap='inferno', vmin=0, vmax=wfc_state.number_of_patterns) + # ax.grid(None) + # ax.set_yticklabels([]) + # ax.set_xticklabels([]) + # ax.grid(None) + + ax = subplot(1, 5, 3) + title("Count of Choices") + + ax.matshow( + wfc_state.choices_recording, + cmap="magma", + vmin=0, + vmax=math.log(wfc_state.number_of_patterns), + ) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + ax = subplot(1, 5, 4) + title("Crystal Matrix") + + ax.matshow(wfc_state.crystal_time % 512.0, cmap="gist_rainbow", vmin=0, vmax=512) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) + + has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) + if has_gaps: + pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) + render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) + pattern_center = wfc_state.wfc_ns.pattern_center + for row in range(wfc_state.rows): + for column in range(wfc_state.columns): + if len(pattern_grid.shape) > 2: + pattern_list = [] + for z in range(wfc_state.number_of_patterns): + pattern_list.append(pattern_grid[(row, column, z)]) + pattern_list = [ + pattern_grid[(row, column, z)] + for z in range(wfc_state.number_of_patterns) + if (pattern_grid[(row, column, z)] != -1) + and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) + ] + for pl_count, the_pattern in enumerate(pattern_list): + the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[the_pattern][ + pattern_center[0] : pattern_center[0] + 1, + pattern_center[1] : pattern_center[1] + 1, + ] + render_grid = blit( + render_grid, the_pattern_tiles, (row, column), layer=pl_count + ) + else: + if WFC_NULL_VALUE != pattern_grid[(row, column)]: + the_pattern = wfc_state.wfc_ns.pattern_catalog[ + pattern_grid[(row, column)] + ] + p_x = wfc_state.wfc_ns.pattern_center[0] + p_y = wfc_state.wfc_ns.pattern_center[1] + the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] + render_grid = blit(render_grid, the_pattern, (row, column)) + ptr = tiles_to_images( + wfc_state.wfc_ns, + render_grid, + wfc_state.wfc_ns.tile_catalog, + wfc_state.wfc_ns.tile_size, + visualize=True, + partial=True, + ).astype(np.uint8) + + # ax.grid(color="magenta", linewidth=1.5) + # ax.tick_params(direction='in', bottom=False, left=False) + + # for axis, dim in zip([ax.xaxis, ax.yaxis],[wfc_state.columns, wfc_state.rows]): + # axis.set_ticks(np.arange(-0.5, dim + 0.5, 1)) + # axis.set_ticklabels([]) + + ax = subplot(1, 5, 5) + title("Output Matrix") + + ax.imshow(ptr) + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + + matplotlib.pyplot.savefig( + f"{wfc_state.wfc_ns.output_path}crystal_preview_{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}.png", + bbox_inches="tight", + ) + + img_data = np.frombuffer(matfig.canvas.tostring_rgb(), dtype=np.uint8) + img_data = img_data.reshape(matfig.canvas.get_width_height() + (3,)) + # print(f"img_data shape: {matfig.canvas.get_width_height()} {matfig.canvas.get_width_height()[::-1] + (3,)}") + matplotlib.pyplot.close(matfig) + # matplotlib.clear() + return img_data + + +# In[27]: + + +def show_pattern_adjacency(wfc_state): + s_columns = 4 # wfc_state.number_of_directions // 2 + s_rows = 1 # int(wfc_state.number_of_directions % 2) + cat_size = len(wfc_state.wfc_ns.pattern_catalog) + 1 + pat_adj_size = wfc_state.wfc_ns.pattern_width * cat_size + + fig = figure(figsize=(s_columns * 7.0, s_rows * 7.0)) + + title("Pattern Adjacency") + for d_index, d_offset in wfc_state.wfc_ns.adjacency_directions.items(): + + adj_preview = np.full((pat_adj_size, pat_adj_size), -1, dtype=np.int64) + for x in range(cat_size): + for y in range(cat_size): + the_pattern = None + if (0 == y) and x > 0: + the_pattern = wfc_state.wfc_ns.pattern_catalog[x - 1] + if (0 == x) and y > 0: + the_pattern = wfc_state.wfc_ns.pattern_catalog[y - 1] + if (x > 0) and (y > 0): + if wfc_state.propagator_matrix[d_index, y - 1, x - 1]: + the_pattern = np.array([[-2, -2], [-2, -2]], dtype=np.int64) + if type(None) != type(the_pattern): + adj_preview = blit( + adj_preview, + the_pattern, + ( + x * wfc_state.wfc_ns.pattern_width, + y * wfc_state.wfc_ns.pattern_width, + ), + ) + ptr = tiles_to_images( + wfc_state.wfc_ns, + adj_preview, + wfc_state.wfc_ns.tile_catalog, + wfc_state.wfc_ns.tile_size, + visualize=True, + ).astype(np.uint8) + ax = subplot(s_rows, s_columns, 1 + d_index) + ax.grid(color="magenta", linewidth=1.5) + im = ax.imshow(ptr) + for axis in [ax.xaxis, ax.yaxis]: + axis.set_ticks( + np.arange(-0.5, pat_adj_size + 0.5, wfc_state.wfc_ns.pattern_width) + ) + title( + "Direction {}\nr,c({})\nor x,y({})".format( + d_index, wfc_state.wfc_ns.adjacency_directions_rc[d_index], d_offset + ), + fontsize=15, + ) + matplotlib.pyplot.close(fig) + + +# In[28]: + + +import itertools +import math + + +def weight_log(val): + return val * math.log(val) + + +def wfc_init(prestate): + prestate.adjacency_directions_rc = { + i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() + } + prestate = wfc.wfc_utilities.find_pattern_center(prestate) + wfc_state = types.SimpleNamespace(wfc_ns=prestate) + + wfc_state.fake_failure = False + + wfc_state.result = None + wfc_state.adjacency_relations = adjacency_extraction_consistent( + wfc_state.wfc_ns, wfc_state.wfc_ns.patterns + ) + if WFC_DEBUGGING: + wfc_logger.debug( + f"wfc_state.adjacency_relations:\n{wfc_state.adjacency_relations}" + ) + # status_print_helper(f"wfc_state.wfc_ns.patterns {wfc_state.wfc_ns.patterns}") + + wfc_logger.debug("wfc_init():patterns") + + wfc_state.patterns = np.array(list(wfc_state.wfc_ns.pattern_catalog.keys())) + wfc_state.pattern_translations = list(wfc_state.wfc_ns.pattern_catalog.values()) + wfc_state.number_of_patterns = wfc_state.patterns.size + if WFC_DEBUGGING: + wfc_logger.debug("number_of_patterns: {}".format(wfc_state.number_of_patterns)) + wfc_logger.debug("patterns: {}".format(wfc_state.patterns)) + wfc_logger.debug( + "pattern translations: {}".format(wfc_state.pattern_translations) + ) + if WFC_VISUALIZE: + show_wfc_patterns(wfc_state, wfc_state.pattern_translations) + + wfc_state.number_of_directions = len(wfc_state.wfc_ns.adjacency_directions) + # wfc_state.reverse_adjacency_directions = make_reverse_adjacency_directions(wfc_state.wfc_ns.adjacency_directions) + # if WFC_DEBUGGING: + # status_print_helper("reverse_adjacency_directions: {}".format(wfc_state.reverse_adjacency_directions)) + + # The Propagator is a data structure that holds the adjacency information + # for the patterns, i.e. given a direction, which patterns are allowed to + # be placed next to the pattern that we're currently concerned with. + # This won't change over the course of using the solver, so the important + # thing here is fast lookup. + wfc_state.propagator_matrix = np.zeros( + ( + wfc_state.number_of_directions, + wfc_state.number_of_patterns, + wfc_state.number_of_patterns, + ), + dtype=np.bool_, + ) + + wfc_logger.debug("wfc_init():adjacency_relations") + + # While the adjacencies were stored as (x,y) pairs, we're going to use (row,column) pairs here. + # wfc_state.reversed_directions = [(r,c) for c,r in wfc_state.wfc_ns.adjacency_directions.values()] + # print(f"wfc_state.reversed_directions:\n{wfc_state.reversed_directions}") + for d, p1, p2 in wfc_state.adjacency_relations: + wfc_state.propagator_matrix[(d, p1, p2)] = True + + if WFC_VISUALIZE: + visualize_propagator_matrix(wfc_state.propagator_matrix) + show_pattern_adjacency(wfc_state) + + # The Wave Table is the boolean expression of which patterns are allowed + # in which cells of the solution we are calculating. + wfc_state.rows = wfc_state.wfc_ns.generated_size[0] + wfc_state.columns = wfc_state.wfc_ns.generated_size[1] + + wfc_state.solving_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.propagation_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + + wfc_state.wave_shape = [ + wfc_state.rows, + wfc_state.columns, + wfc_state.number_of_patterns, + ] + wfc_state.wave_table = np.full(wfc_state.wave_shape, True, dtype=np.bool_) + + # The compatible_count is a running count of the number of patterns that + # are still allowed to be next to this cell in a particular direction. + compatible_shape = [ + wfc_state.rows, + wfc_state.columns, + wfc_state.number_of_patterns, + wfc_state.number_of_directions, + ] + + wfc_logger.debug(f"compatible shape:{compatible_shape}") + wfc_state.compatible_count = np.full( + compatible_shape, wfc_state.number_of_patterns, dtype=np.int16 + ) # assumes that there are less than 65536 patterns + + wfc_logger.debug("wfc_init():weights") + + # The weights are how we manage the probabilities when we choose the next + # pattern to place. Rather than recalculating them from scratch each time, + # these let us incrementally update their values. + wfc_state.weights = np.array(list(wfc_state.wfc_ns.pattern_weights.values())) + wfc_state.weight_log_weights = np.vectorize(weight_log)(wfc_state.weights) + if WFC_DEBUGGING: + status_print_helper(f"wfc_state.weights {wfc_state.weights}") + status_print_helper( + f"wfc_state.weight_log_weights {wfc_state.weight_log_weights}" + ) + + wfc_state.sum_of_weights = np.sum(wfc_state.weights) + if WFC_DEBUGGING: + status_print_helper(f"wfc_state.sum_of_weights {wfc_state.sum_of_weights}") + wfc_state.sum_of_weight_log_weights = np.sum(wfc_state.weight_log_weights) + wfc_state.starting_entropy = math.log(wfc_state.sum_of_weights) - ( + wfc_state.sum_of_weight_log_weights / wfc_state.sum_of_weights + ) + + wfc_state.entropies = np.zeros( + [wfc_state.rows, wfc_state.columns], dtype=np.float64 + ) + # wfc_state.sums_of_ones = np.zeros([wfc_state.rows, wfc_state.columns], dtype = np.float64) + wfc_state.sums_of_weights = np.zeros( + [wfc_state.rows, wfc_state.columns], dtype=np.float64 + ) + + # Instead of updating all of the cells for every propagation, we use a queue + # that marks the dirty tiles to update. + wfc_state.observation_stack = collections.deque() + + wfc_state.output_grid = np.full( + [wfc_state.rows, wfc_state.columns], WFC_NULL_VALUE, dtype=np.int64 + ) + wfc_state.partial_output_grid = np.full( + [wfc_state.rows, wfc_state.columns, wfc_state.number_of_patterns], + -9, + dtype=np.int64, + ) + + wfc_logger.debug("wfc_init():observation") + + wfc_state.current_iteration_count_observation = 0 + wfc_state.current_iteration_count_propagation = 0 + wfc_state.current_iteration_count_last_touch = 0 + wfc_state.current_iteration_count_crystal = 0 + wfc_state.solving_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.ones_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.propagation_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.touch_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.crystal_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.method_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.choices_recording = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.float32 + ) + + # global recording_stack + # recording_stack.method_time_stack = [] + # recording_stack.ones_time_stack = [] + # recording_stack.solving_time_stack = [] + # recording_stack.propagation_time_stack = [] + # recording_stack.touch_time_stack = [] + # recording_stack.crystal_time_stack = [] + # recording_stack.output_time_stack = [] + # recording_stack.solver_recording_stack = [] + # recording_stack.choices_recording_stack = [] + + return wfc_state + + +# In[29]: + + +def visualize_entropy(wfc_state): + matfig = figure(figsize=(7, 7)) + + ax = subplot(1, 1, 1) + title("Sums of Weights") + ax.matshow(wfc_state.sums_of_weights, cmap="plasma") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + status_print_helper(f"sum_of_weights\n{wfc_state.sums_of_weights}") + matplotlib.pyplot.close(matfig) + + +# In[30]: + + +def wfc_clear(wfc_state): + # Crystal solving time matrix + wfc_state.current_iteration_count_observation = 0 + wfc_state.current_iteration_count_propagation = 0 + wfc_state.current_iteration_count_last_touch = 0 + wfc_state.current_iteration_count_crystal = 0 + + # global recording_stack + # recording_stack.method_time_stack = [] + # recording_stack.ones_time_stack = [] + # recording_stack.solving_time_stack = [] + # recording_stack.propagation_time_stack = [] + # recording_stack.touch_time_stack = [] + # recording_stack.crystal_time_stack = [] + # recording_stack.output_time_stack = [] + # recording_stack.solver_recording_stack = [] + # recording_stack.choices_recording_stack = [] + + wfc_state.solving_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.ones_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.propagation_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.touch_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.crystal_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.method_time = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 + ) + wfc_state.choices_recording = np.full( + (wfc_state.rows, wfc_state.columns), 0, dtype=np.float32 + ) + + wfc_logger.debug("reset wave table") + # Reset the wave table. + wfc_state.wave_table = np.full(wfc_state.wave_shape, True, dtype=np.bool_) + + compatible_shape = [ + wfc_state.rows, + wfc_state.columns, + wfc_state.number_of_patterns, + wfc_state.number_of_directions, + ] + # wfc_state_compatible_count = np.full(compatible_shape, wfc_state.number_of_patterns, dtype=np.int16) + wfc_logger.debug("Initialize the compatible count") + # Initialize the compatible count from the propagation matrix. This sets the + # maximum domain of possible neighbors for each cell node. + # for row in range(wfc_state.rows): + # print(row, end=',') + # for column in range(wfc_state.columns): + # for pattern in range(wfc_state.number_of_patterns): + # for direction in range(wfc_state.number_of_directions): + # p_matrix_sum = sum(wfc_state.propagator_matrix[(direction+2)%4][pattern]) # TODO: figure out why flipping directions is needed here, maybe fix things so it isn't + # wfc_state_compatible_count[row, column, pattern, direction] = p_matrix_sum + # # #print("{},{}\n".format(row, column)) + + def prop_compat(p, d): + # print(p,d,end=':') + # print(p) + # print(d) + # print('pm[{},{}]: {}'.format(d,p,wfc_state.propagator_matrix[d][p])) + return sum(wfc_state.propagator_matrix[(d + 2) % 4][p]) + + def comp_count(r, c, p, d): + return pattern_compatible_count[p][d] + + pcomp = np.vectorize(prop_compat) + ccount = np.vectorize(comp_count) + pattern_compatible_count = np.fromfunction( + pcomp, + (wfc_state.number_of_patterns, wfc_state.number_of_directions), + dtype=np.int16, + ) + wfc_state.compatible_count = np.fromfunction( + ccount, + ( + wfc_state.rows, + wfc_state.columns, + wfc_state.number_of_patterns, + wfc_state.number_of_directions, + ), + dtype=np.int16, + ) + + wfc_logger.debug("set the weights to their maximum values") + # Likewise, set the weights to their maximum values + # wfc_state.sums_of_ones = np.full([wfc_state.rows, wfc_state.columns], + # wfc_state.number_of_patterns, + # dtype = np.uint16) + wfc_state.sums_of_weights = np.full( + [wfc_state.rows, wfc_state.columns], wfc_state.sum_of_weights, dtype=np.float64 + ) + wfc_state.sums_of_weight_log_weights = np.full( + [wfc_state.rows, wfc_state.columns], + wfc_state.sum_of_weight_log_weights, + dtype=np.float64, + ) + wfc_state.entropies = np.full( + [wfc_state.rows, wfc_state.columns], + wfc_state.starting_entropy, + dtype=np.float64, + ) + if WFC_DEBUGGING: + status_print_helper(f"starting entropy: {wfc_state.starting_entropy}") + status_print_helper(f"wfc_state.entropies: {wfc_state.entropies}") + # status_print_helper(f"wfc_state.sums_of_ones: {wfc_state.sums_of_ones}") + status_print_helper(f"wfc_state.sums_of_weights: {wfc_state.sums_of_weights}") + + wfc_state.recorded_steps = [] + + wfc_state.observation_stack = collections.deque() + # TODO: add ground-banning of patterns / masking / etc + + # ground-banning + if wfc_state.wfc_ns.ground != 0 and False: # False => currently disabled + for p in wfc_state.wfc_ns.pattern_catalog.keys(): + for x in range(wfc_state.rows): + for y in range(wfc_state.columns): + ban_pattern = not (p in wfc_state.wfc_ns.last_patterns) and ( + y >= wfc_state.wfc_ns.generated_size[1] - 1 + ) # or ((p in wfc_state.wfc_ns.last_patterns) and (y < wfc_state.wfc_ns.generated_size[1] - 1)) + if ban_pattern: + wfc_state = Ban(wfc_state, CoordRC(row=y, column=x), p) + + wfc_state.previous_decisions = [] + + wfc_logger.debug("clear complete") + + return wfc_state + + +# We'll want to visualize the compatible count as the solver runs. This starts out as a uniform color (with everything at 100%) but quickly changes as individual cells start to resolve. + +# In[31]: + + +def visualize_compatible_count(wfc_state): + directions = wfc_state.number_of_directions + visual_stack = np.zeros(wfc_state.compatible_count.shape[:2]) + for i in range(visual_stack.shape[0]): + for j in range(visual_stack.shape[1]): + for k in range(wfc_state.compatible_count.shape[2]): + for l in range(wfc_state.compatible_count.shape[3]): + visual_stack[i, j] += wfc_state.compatible_count[i, j, k, l] + if WFC_DEBUGGING: + status_print_helper( + f"compatible: {i},{j},{k},{l} => {wfc_state.compatible_count[i,j,k,l]}" + ) + matfig = figure(figsize=(7, 7)) + + ax = subplot(1, 1, 1) + title("Compatible Count") + ax.matshow(visual_stack, cmap="viridis") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + status_print_helper(f"compatible_count\n{wfc_state.compatible_count}") + + matplotlib.pyplot.close(matfig) + + +# In[32]: + + +def wfc_partial_output(wfc_state): + wfc_state.partial_output_grid = np.full( + [wfc_state.rows, wfc_state.columns, wfc_state.number_of_patterns], + -9, + dtype=np.int64, + ) + + for row in range(wfc_state.rows): + for column in range(wfc_state.columns): + pattern_flags = wfc_state.wave_table[row, column] + # print(f"pattern_flags: {pattern_flags}") + p_list = [] + for pindex, pflag in enumerate(pattern_flags): + if pflag: + p_list.append(pindex) + # print(f"p_list: {p_list}\n") + for z, p in enumerate(p_list): + wfc_state.partial_output_grid[row, column, z] = p + # print(f"\n~~~ wfc_state.partial_output_grid ~~~\n{wfc_state.partial_output_grid}") + wfc_state.recorded_steps.append(wfc_state.partial_output_grid) + return wfc_state + + +# In[33]: + + +# A useful helper function which we use because we want numpy arrays instead of jagged arrays +# https://stackoverflow.com/questions/7632963/numpy-find-first-index-of-value-fast/7654768 +def find_first(item, vec): + """return the index of the first occurence of item in vec""" + for i in range(len(vec)): + if item == vec[i]: + return i + return -1 + + +# In[34]: + + +def recalculate_weights(state, parameters, coords_index, pattern_id): + state.sums_of_weights[coords_index.row, coords_index.column] -= parameters.weights[ + pattern_id + ] + state.sums_of_weight_log_weights[ + coords_index.row, coords_index.column + ] -= state.weight_log_weights[pattern_id] + entropy_sum = state.sums_of_weights[coords_index.row, coords_index.column] + try: + state.entropies[coords_index.row, coords_index.column] = ( + math.log(entropy_sum) + ) - ( + (state.sums_of_weight_log_weights[coords_index.row, coords_index.column]) + / entropy_sum + ) + except ValueError as e: + logging.debug(f"Contradiction when banning {coords_index} -> {pattern_id}: {e}") + state.result = WFC_FAILURE + return state + + +def RecalculateWeights(wfc_state, coords_index, pattern_id): + # uncomment to show all fails + # if np.count_nonzero(wfc_state.wave_table[coords_index.row, coords_index.column]) < 1: + # wfc_logger.warning(f"Sums of ones already below 1 at {coords_index}: {wfc_state.wave_table[coords_index.row, coords_index.column].sum()}") + + wfc_state.sums_of_weights[ + coords_index.row, coords_index.column + ] -= wfc_state.weights[pattern_id] + wfc_state.sums_of_weight_log_weights -= wfc_state.weight_log_weights[pattern_id] + + entropy_sum = wfc_state.sums_of_weights[coords_index.row, coords_index.column] + try: + wfc_state.entropies[coords_index.row, coords_index.column] = ( + math.log(entropy_sum) + ) - ( + ( + wfc_state.sums_of_weight_log_weights[ + coords_index.row, coords_index.column + ] + ) + / entropy_sum + ) + except ValueError as e: + logging.debug(f"Contradiction when banning {coords_index} -> {pattern_id}: {e}") + wfc_state.result = WFC_FAILURE + return wfc_state + return wfc_state + + +import collections + +BanEntry = collections.namedtuple( + "BanEntry", ["coords_row", "coords_column", "pattern_id"] +) + + +def BanAlreadyTried(wfc_state, coords_index, pattern_id): + wfc_state.wave_table[coords_index.row, coords_index.column, pattern_id] = False + for ( + direction_id, + direction_offset, + ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): + wfc_state.compatible_count[ + coords_index.row, coords_index.column, pattern_id, direction_id + ] = 0 + wfc_state.observation_stack.append( + BanEntry(coords_index.row, coords_index.column, pattern_id) + ) + wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) + + return wfc_state + + +def Ban(wfc_state, coords_index, pattern_id): + if wfc_state.logging: + with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"Banning: {pattern_id} at {coords_index}\n") + + # pdb.set_trace() + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): + print("overflow E") + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + + wfc_state.wave_table[coords_index.row, coords_index.column, pattern_id] = False + for ( + direction_id, + direction_offset, + ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): + wfc_state.compatible_count[ + coords_index.row, coords_index.column, pattern_id, direction_id + ] = 0 + wfc_state.observation_stack.append( + BanEntry(coords_index.row, coords_index.column, pattern_id) + ) + wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) + + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): + print("overflow F") + # print(wfc_state.sums_of_ones) + print("---") + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + + wfc_state.touch_time[ + coords_index.row, coords_index.column + ] = wfc_state.current_iteration_count_last_touch + if 1 == np.count_nonzero( + wfc_state.wave_table[coords_index.row, coords_index.column] + ): + if 0 == wfc_state.propagation_time[coords_index.row, coords_index.column]: + wfc_state.touch_time[ + coords_index.row, coords_index.column + ] = wfc_state.current_iteration_count_last_touch + wfc_state.propagation_time[ + coords_index.row, coords_index.column + ] = wfc_state.current_iteration_count_propagation + + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): + print("overflow G") + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + + # if WFC_FAILURE == wfc_state.result: + # return wfc_state + if 1 == np.count_nonzero( + wfc_state.wave_table[coords_index.row, coords_index.column] + ): + pattern_flags = wfc_state.wave_table[coords_index.row, coords_index.column] + wfc_state.output_grid[coords_index.row, coords_index.column] = find_first( + True, pattern_flags + ) # Update the output grid as we go... + wfc_state.crystal_time[ + coords_index.row, coords_index.column + ] = wfc_state.current_iteration_count_crystal + wfc_state.current_iteration_count_crystal += 1 + if 0 == wfc_state.method_time[coords_index.row, coords_index.column]: + wfc_state.method_time[ + coords_index.row, coords_index.column + ] = ( + wfc_state.current_iteration_count_crystal + ) # (coords_index.row * coords_index.column) + + + # if WFC_DEBUGGING: + # status_print_helper(f"wfc_state.entropies : {wfc_state.entropies}") + # status_print_helper(f"wfc_state.sums_of_ones: {wfc_state.sums_of_ones}") + ##print(f"Ban({coords_index}, {pattern_id})") + ##print_internals(wfc_state) + + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): + print("overflow D") + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + + return wfc_state + + +# In[35]: + + +def print_internals(wfc_state): + show_rendered_patterns(wfc_state, partial=True) + y = wfc_state.wave_table.shape[0] + x = wfc_state.wave_table.shape[1] + print("sums_of_ones") + for i in range(y): + for j in range(x): + print("{0: >02d}".format(wfc_state.wave_table[i, j].sum()), end=" ") + print() + print("observation_stack") + for i in wfc_state.observation_stack: + print(i.coords_row, i.coords_column, i.pattern_id) + print("output_grid") + for i in range(y): + for j in range(x): + if wfc_state.wave_table[i, j].sum() > 1: + print("**", end=" ") + else: + try: + if len(wfc_state.partial_output_grid.shape) > 2: + for k in range(wfc_state.partial_output_grid.shape[2]): + if wfc_state.partial_output_grid[i, j, k] != -9: + print( + "{0: >02d}".format( + wfc_state.partial_output_grid[i, j, k] + ), + end="+", + ) + print(" ", end="") + else: + print( + "{0: >02d}".format(wfc_state.partial_output_grid[i, j]), + end=" ", + ) + except: + print("??", end=" ") + print() + + +# In[36]: + +import pdb + + +def find_upper_left_entropy(wfc_state, random_variation): + print(wfc_state.wave_table) + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + print(np.argmax(np.count_nonzero(wfc_state.wave_table, axis=2))) + pdb.set_trace() + chosen_cell = np.argmax(np.count_nonzero(wfc_state.wave_table, axis=2)) + if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): + status_print_helper("FINISHED") + if WFC_DEBUGGING: + status_print_helper(wfc_state.wave_table) + return WFC_FINISHED + cell_index = np.unravel_index( + chosen_cell, [wfc_state.wave_table.shape[0], wfc_state.wave_table[1]] + ) + return CoordRC(row=cell_index[0], column=cell_index[1]) + + +def find_upper_left_unresolved(wfc_state, random_variation): + unresolved_cells = np.count_nonzero(wfc_state.wave_table, axis=2) > 1 + unresolved_indices = np.where(unresolved_cells) + cell_index = (unresolved_indices[0][0], unresolved_indices[1][0]) + return CoordRC(row=cell_index[0], column=cell_index[1]) + + +def find_random_unresolved(wfc_state, random_variation): + global temp_track_number_of_finishes + # if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): + # print("FAIL") + # return WFC_FAILURE + + # the_result = check_completion(wfc_state) + # if 0 != the_result: + # return the_result + + noise_level = 1e-6 + entropy_map = random_variation * noise_level + unresolved_cells = np.count_nonzero(wfc_state.wave_table, axis=2) > 1 + entropy_map = entropy_map.flatten() * (0 == unresolved_cells.flatten()) + + chosen_cell = np.argmax(entropy_map) + + cell_index = np.unravel_index( + chosen_cell, + [ + np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], + np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], + ], + ) + return CoordRC(row=cell_index[0], column=cell_index[1]) + + +temp_track_number_of_finishes = 0 + + +def check_completion(wfc_state): + if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): + # Require that every pattern be use at least once + pattern_set = set(np.argmax(wfc_state.wave_table, axis=2).flatten()) + # Force a test to encourage backtracking - temporary addition + if ( + len(pattern_set) != wfc_state.number_of_patterns + ) and wfc_state.wfc_ns.force_use_all_patterns: + print("Some patterns were not used") + return WFC_FAILURE + + # Force a test to encourage backtracking - temporary addition + if (not (57 in pattern_set)) and False: + print("Ground not used") + return WFC_FAILURE + + # Just force a failure the first time for testing purposes + temp_track_number_of_finishes += 1 + if ( + temp_track_number_of_finishes < 2 + and wfc_state.wfc_ns.force_fail_first_solution + ): + print("Force fake failure to test backtracking") + return WFC_FAILURE + + status_print_helper("FINISHED") + if WFC_DEBUGGING: + status_print_helper(wfc_state.wave_table) + return WFC_FINISHED + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) < 1): + return WFC_FAILURE + return None + + +def find_upper_left_relevant(wave_table, random_variation): + unresolved_cells = np.count_nonzero(wave_table, axis=-1) > 1 + rows, cols = np.where(unresolved_cells) + return CoordRC(row=rows[0], column=cols[0]) + + +def find_random_unresolved_relevant(wave_table, random_variation): + unresolved_cell_mask = np.count_nonzero(wave_table, axis=2) > 1 + cell_weights = np.where(unresolved_cell_mask, random_variation, np.inf) + row, col = np.unravel_index(np.argmin(cell_weights), cell_weights.shape) + return CoordRC(row=row, column=col) + + +def find_minimum_entropy_relevant(wave_table, random_variation): + unresolved_cell_mask = np.count_nonzero(wave_table, axis=2) > 1 + cell_weights = np.where( + unresolved_cell_mask, + random_variation + (np.count_nonzero(wave_table, axis=2)), + np.inf, + ) + row, col = np.unravel_index(np.argmin(cell_weights), cell_weights.shape) + return CoordRC(row=row, column=col) + + +def find_minimum_entropy(wfc_state, random_variation): + global temp_track_number_of_finishes + noise_level = 1e-6 + entropy_map = random_variation * noise_level + entropy_map = entropy_map.flatten() + wfc_state.entropies.flatten() + # TODO: add boundary check for non-wrapping generation + + minimum_cell = np.argmin(entropy_map) + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): + print("FAIL") + # print(np.count_nonzero(wfc_state.wave_table, axis=2)) + print(f"previous decisions: {len(wfc_state.previous_decisions)}") + + return WFC_FAILURE + if 0 == np.count_nonzero(wfc_state.wave_table, axis=2).flatten()[minimum_cell]: + if WFC_DEBUGGING: + print_internals(wfc_state) + return WFC_FAILURE + + higher_than_threshold = np.ma.MaskedArray( + entropy_map, np.count_nonzero(wfc_state.wave_table, axis=2).flatten() <= 1 + ) + minimum_cell = higher_than_threshold.argmin( + fill_value=999999.9 + ) # np.ma.maximum_fill_value(1)) + maximum_cell = higher_than_threshold.argmax(fill_value=0.0) + chosen_cell = maximum_cell + + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): + wfc_logger.debug("A zero-state node has been found.") + + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 65534): + wfc_logger.error("overflow A") + wfc_logger.error(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + + if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): + print("DETECTED FINISH") + print("nonzero count: {np.count_nonzero(wfc_state.wave_table, axis=2)}") + + # the_result = check_completion(wfc_state) + # if 0 != the_result: + # return the_result + + cell_index = np.unravel_index( + chosen_cell, + [ + np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], + np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], + ], + ) + return CoordRC(row=cell_index[0], column=cell_index[1]) + + +def FinalizeObservedWaves(wfc_state): + for row in range(wfc_state.rows): + for column in range(wfc_state.columns): + pattern_flags = wfc_state.wave_table[row, column] + wfc_state.output_grid[row, column] = find_first( + True, pattern_flags + ) # TODO: This line is probably overkill? + wfc_state.result = WFC_FINISHED + return wfc_state + + +def make_observation_relevant( + state, + parameters, + instrumentation, + dirty_cell_list, + cell_to_observe, + random_number_generator, +): + row, column = cell_to_observe + distribution = np.zeros((parameters.number_of_patterns,), dtype=np.float64) + for wave_pat in range(parameters.number_of_patterns): + if state.wave_table[row, column, wave_pat]: + distribution[wave_pat] = parameters.weights[wave_pat] + + cell_weight_sum = sum(distribution) + if cell_weight_sum <= 0: + wfc_logger.info( + f"Tried to observe cell with no valid weights: {cell_to_observe} is {cell_weight_sum}" + ) + return state, instrumentation, dirty_cell_list + normalized = [float(i) / cell_weight_sum for i in distribution] + + choice_count = sum([1 for i in distribution if i > 0]) + chosen_pattern = parameters.patterns[0] + chosen_pattern = random_number_generator.choice( + parameters.patterns, 1, p=normalized + )[0] + instrumentation.choices_recording[row, column] = math.log(choice_count) + + if parameters.visualizing_output: + instrumentation.output_grid[row, column] = chosen_pattern + instrumentation.solving_time[ + row, column + ] = wfc_state.current_iteration_count_observation + instrumentation.touch_time[ + row, column + ] = wfc_state.current_iteration_count_last_touch + + for wave_pat in range(parameters.number_of_patterns): + if state.wave_table[row, column][wave_pat] != (wave_pat == chosen_pattern): + state.wave_table[ + cell_to_observe.row, cell_to_observe.column, chosen_pattern + ] = False + for ( + direction_id, + direction_offset, + ) in parameters.wfc_ns.adjacency_directions_rc.items(): + state.compatible_count[row, column, chosen_pattern, direction_id] = 0 + state.observation_stack.append(BanEntry(row, column, chosen_pattern)) + state = recalculate_weights( + state, parameters, cell_to_observe, chosen_pattern + ) + + return state, instrumentation, dirty_cell_list + + +def make_observation(wfc_state, cell_to_observe, random_number_generator): + print(dir(wfc_state)) + assert False + row, column = cell_to_observe + distribution = np.zeros((wfc_state.number_of_patterns,), dtype=np.float64) + for wave_pat in range(wfc_state.number_of_patterns): + if wfc_state.wave_table[row, column, wave_pat]: + distribution[wave_pat] = wfc_state.weights[wave_pat] + + cell_weight_sum = sum(distribution) + normalized = [float(i) / cell_weight_sum for i in distribution] + if np.any(np.isnan(normalized)): + print(normalized) + print(distribution) + print(cell_weight_sum) + + # assert not(np.any(np.isnan(normalized))) + + choice_count = sum([1 for i in distribution if i > 0]) + chosen_pattern = wfc_state.patterns[0] + try: + chosen_pattern = random_number_generator.choice( + wfc_state.patterns, 1, p=normalized + )[0] + wfc_state.choices_recording[row, column] = math.log(choice_count) + except ValueError as e: + print("observation ValueError") + print(e) + print(normalized) + if WFC_DEBUGGING: + print("chosen_pattern: {0}".format(chosen_pattern)) + print( + "wfc_state.patterns[chosen_pattern]: {0}".format( + wfc_state.patterns[chosen_pattern] + ) + ) + + if wfc_state.visualizing_output: + wfc_state.output_grid[row, column] = chosen_pattern + wfc_state.solving_time[ + row, column + ] = wfc_state.current_iteration_count_observation + wfc_state.touch_time[row, column] = wfc_state.current_iteration_count_last_touch + wfc_state.method_time[row, column] = ( + 1000 + wfc_state.current_iteration_count_observation + ) + + # wave = wfc_state.wave_table[row, column] + for wave_pat in range(wfc_state.number_of_patterns): + if wfc_state.wave_table[row, column][wave_pat] != (wave_pat == chosen_pattern): + # wfc_state = Ban(wfc_state, cell_to_observe, wave_pat) + wfc_state.wave_table[ + cell_to_observe.row, cell_to_observe.column, pattern_id + ] = False + for ( + direction_id, + direction_offset, + ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): + wfc_state.compatible_count[ + coords_index.row, coords_index.column, pattern_id, direction_id + ] = 0 + wfc_state.observation_stack.append( + BanEntry(coords_index.row, coords_index.column, pattern_id) + ) + wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) + + wfc_state.wfc_ns.stats_tracking["observations"] += 1 + global ongoing_observations + ongoing_observations += 1 + wfc_state.wfc_ns.stats_tracking["total_observations"] = ongoing_observations + if wfc_state.wfc_ns.backtracking: + wfc_state.previous_decisions.append((cell_to_observe, wave_pat,)) + + if wfc_state.logging: + with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"making observation at: {cell_to_observe}\n") + stats_file.write(f"{wave_pat}\n") + + return wfc_state + + +def wfc_observe(wfc_state, random_variation, random_number_generator): + wfc_state.current_iteration_count_observation += 1 + + the_result = None + if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): + status_print_helper("FINISHED") + if WFC_DEBUGGING: + status_print_helper(wfc_state.wave_table) + the_result = WFC_FINISHED + if None == the_result: + the_result = check_completion(wfc_state) + + cell = None + if None == the_result: + cell = find_minimum_entropy(wfc_state, random_variation) + # cell = find_upper_left_entropy(wfc_state, random_variation) + # cell = find_upper_left_unresolved(wfc_state, random_variation) + # cell = find_random_unresolved(wfc_state, random_variation) + + if cell == WFC_FAILURE: + the_result = cell + + if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): + status_print_helper("FINISHED") + if WFC_DEBUGGING: + status_print_helper(wfc_state.wave_table) + the_result = WFC_FINISHED + if None == the_result: + the_result = check_completion(wfc_state) + + # print(f"&&& We are observing cell: {cell}") + # if the_result != 0: + # print(f"result: {the_result}") + if WFC_FAKE_FAILURE == the_result: + wfc_state.result = WFC_FAILURE + wfc_state.fake_failure = True + return wfc_state + if WFC_FAILURE == the_result: + wfc_state.result = WFC_FAILURE + return wfc_state + if WFC_FINISHED == the_result: + return FinalizeObservedWaves(wfc_state) + + return make_observation(wfc_state, cell, random_number_generator) + + +# In[37]: + + +def show_crystal_time(wfc_state, partial=False): + # wfc_state.solving_time + # wfc_state.propagation_time + # pl = matshow(wfc_state.solving_time, cmap='gist_ncar', extent=(0, wfc_state.rows, wfc_state.columns, 0)) + # pl.axes.grid(None) + # pl = matshow(wfc_state.propagation_time, cmap='gist_ncar', extent=(0, wfc_state.rows, wfc_state.columns, 0)) + # pl.axes.grid(None) + + matfig_obsv = figure(figsize=(9, 9)) + + ax = subplot(1, 1, 1) + title("Observation Time") + ax.matshow(wfc_state.solving_time, cmap="viridis") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + # matfig_obsv.colorbar() + print(f"solving time: {wfc_state.solving_time}") + + matfig_prop = figure(figsize=(9, 9)) + ax = subplot(1, 1, 1) + title("Propagation Time") + ax.matshow(wfc_state.propagation_time, cmap="viridis") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + # matfig_prop.colorbar() + print(f"propagation time: {wfc_state.propagation_time}") + + matfig_touch = figure(figsize=(9, 9)) + ax = subplot(1, 1, 1) + title("Last Altered Time") + ax.matshow(wfc_state.touch_time, cmap="viridis") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + # matfig_prop.colorbar() + print(f"touch time: {wfc_state.touch_time}") + + matfig_touch2 = figure(figsize=(9, 9)) + ax = subplot(1, 1, 1) + title("Crystal Time") + ax.matshow(wfc_state.crystal_time, cmap="viridis") + ax.grid(None) + ax.set_yticklabels([]) + ax.set_xticklabels([]) + ax.grid(None) + # matfig_prop.colorbar() + print(f"touch_time: {wfc_state.touch_time}") + matplotlib.pyplot.close(matfig_obsv) + matplotlib.pyplot.close(matfig_prop) + matplotlib.pyplot.close(matfig_touch) + matplotlib.pyplot.close(matfig_touch2) + + +def show_rendered_patterns(wfc_state, partial=False): + partial = True + pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) + preview_size = ( + wfc_state.wfc_ns.pattern_width * wfc_state.rows, + wfc_state.wfc_ns.pattern_width * wfc_state.columns, + ) + has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) + # print(wfc_state.sums_of_ones != 1) + # print(f"has gaps: {has_gaps}", end=', ') + print( + f"remaining nodes: {np.count_nonzero(np.count_nonzero(wfc_state.wave_table, axis=2) != 1)}" + ) + + # Temporarily only look at the final few node resulrt... + # if np.count_nonzero(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) > 10: + # return + + if has_gaps: + pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) + preview_size = ( + wfc_state.wfc_ns.pattern_width * wfc_state.rows, + wfc_state.wfc_ns.pattern_width * wfc_state.columns, + wfc_state.partial_output_grid.shape[2], + ) + grid_preview = np.full(preview_size, WFC_PARTIAL_BLANK, dtype=np.int64) + # pattern_center = (math.floor((wfc_state.wfc_ns.pattern_width - 1) / 2), math.floor((wfc_state.wfc_ns.pattern_width - 1) / 2)) + pattern_center = wfc_state.wfc_ns.pattern_center + + special_count_tiles = np.full( + (wfc_state.wfc_ns.pattern_width, wfc_state.wfc_ns.pattern_width), + 1, + dtype=np.int64, + ) + # WFC_DEBUGGING = True + # print(len(pattern_grid.shape)) + if WFC_DEBUGGING: + print("show rendered patterns") + for row in range(wfc_state.rows): + # print() + if WFC_DEBUGGING: + print() + for column in range(wfc_state.columns): + if len(pattern_grid.shape) > 2: + if WFC_DEBUGGING: + print("[", end="") + pattern_list = [] + for z in range(wfc_state.number_of_patterns): + pattern_list.append(pattern_grid[(row, column, z)]) + pl_count = 0 + for the_pattern in pattern_list: + if WFC_DEBUGGING: + print(the_pattern, end="") + if (the_pattern != -1) and (the_pattern != WFC_NULL_VALUE): + if WFC_DEBUGGING: + print("!", end="") + the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[ + the_pattern + ] + grid_preview = blit( + grid_preview, + the_pattern_tiles, + ( + row * wfc_state.wfc_ns.pattern_width, + column * wfc_state.wfc_ns.pattern_width, + ), + layer=pl_count, + ) + pl_count += 1 + else: + if WFC_DEBUGGING: + print(" ", end="") + if WFC_DEBUGGING: + print("", end=" ") + if WFC_DEBUGGING: + print("]", end="") + else: + if WFC_DEBUGGING: + print(pattern_grid) + # print(pattern_grid[(row,column)],end=' ') + if WFC_NULL_VALUE != pattern_grid[(row, column)]: + the_pattern = wfc_state.wfc_ns.pattern_catalog[ + pattern_grid[(row, column)] + ] + # if not partial: + # p_x = wfc_state.wfc_ns.pattern_center[0] + # p_y = wfc_state.wfc_ns.pattern_center[1] + # the_pattern = the_pattern[p_x:p_x+1, p_y:p_y+1] + # print(f"the_pattern: {the_pattern}") + grid_preview = blit( + grid_preview, + the_pattern, + ( + row * wfc_state.wfc_ns.pattern_width, + column * wfc_state.wfc_ns.pattern_width, + ), + ) + if WFC_DEBUGGING: + print(f"\ngrid_preview:\n{grid_preview}") + ptr = tiles_to_images( + wfc_state.wfc_ns, + grid_preview, + wfc_state.wfc_ns.tile_catalog, + wfc_state.wfc_ns.tile_size, + visualize=True, + partial=partial, + ).astype(np.uint8) + if WFC_DEBUGGING: + print(f"ptr: {ptr}") + fig, ax = subplots(figsize=(16, 16)) + ax.grid(color="magenta", linewidth=1.5) + ax.tick_params(direction="in", bottom=False, left=False) + + im = ax.imshow(ptr) + for axis, dim in zip([ax.xaxis, ax.yaxis], [wfc_state.columns, wfc_state.rows]): + axis.set_ticks( + np.arange( + -0.5, + (wfc_state.wfc_ns.pattern_width * dim) + 0.5, + wfc_state.wfc_ns.pattern_width, + ) + ) + axis.set_ticklabels([]) + + +# In[38]: + + +def render_patterns_to_output(wfc_state, partial=False, visualize=True): + pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) + + has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) + if has_gaps: + pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) + render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) + pattern_center = wfc_state.wfc_ns.pattern_center + for row in range(wfc_state.rows): + if WFC_DEBUGGING: + print() + for column in range(wfc_state.columns): + if len(pattern_grid.shape) > 2: + if WFC_DEBUGGING: + print("[", end="") + pattern_list = [] + for z in range(wfc_state.number_of_patterns): + pattern_list.append(pattern_grid[(row, column, z)]) + pattern_list = [ + pattern_grid[(row, column, z)] + for z in range(wfc_state.number_of_patterns) + if (pattern_grid[(row, column, z)] != -1) + and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) + ] + for pl_count, the_pattern in enumerate(pattern_list): + if WFC_DEBUGGING: + print(the_pattern, end="") + the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[the_pattern][ + pattern_center[0] : pattern_center[0] + 1, + pattern_center[1] : pattern_center[1] + 1, + ] + if WFC_DEBUGGING: + print(the_pattern_tiles, end=" ") + render_grid = blit( + render_grid, the_pattern_tiles, (row, column), layer=pl_count + ) + if WFC_DEBUGGING: + print("]", end=" ") + else: + if WFC_DEBUGGING: + print(pattern_grid[(row, column)], end=",") + if WFC_NULL_VALUE != pattern_grid[(row, column)]: + the_pattern = wfc_state.wfc_ns.pattern_catalog[ + pattern_grid[(row, column)] + ] + p_x = wfc_state.wfc_ns.pattern_center[0] + p_y = wfc_state.wfc_ns.pattern_center[1] + the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] + render_grid = blit(render_grid, the_pattern, (row, column)) + if WFC_DEBUGGING: + print("\nrender grid") + print(render_grid) + ptr = tiles_to_images( + wfc_state.wfc_ns, + render_grid, + wfc_state.wfc_ns.tile_catalog, + wfc_state.wfc_ns.tile_size, + visualize=True, + partial=partial, + ).astype(np.uint8) + if WFC_DEBUGGING: + print(f"ptr {ptr}") + + if visualize: + fig, ax = subplots(figsize=(16, 16)) + # ax.grid(color="magenta", linewidth=1.5) + ax.tick_params(direction="in", bottom=False, left=False) + + im = ax.imshow(ptr) + for axis, dim in zip([ax.xaxis, ax.yaxis], [wfc_state.columns, wfc_state.rows]): + axis.set_ticks(np.arange(-0.5, dim + 0.5, 1)) + axis.set_ticklabels([]) + # print(ptr) + imageio.imwrite(wfc_state.wfc_ns.output_filename, ptr) + + +# In[41]: + + +def is_cell_on_boundary(wfc_state, wfc_coords): + if not wfc_state.wfc_ns.periodic_output: + return False + # otherwise... + return False # TODO + + +def wrap_coords(wfc_state, cell_coords): + r = (cell_coords.row + wfc_state.wfc_ns.generated_size[0]) % ( + wfc_state.wfc_ns.generated_size[0] + ) + c = (cell_coords.column + wfc_state.wfc_ns.generated_size[1]) % ( + wfc_state.wfc_ns.generated_size[1] + ) + return CoordRC(row=r, column=c) + + +def wfc_propagate(wfc_state): + + # while len(wfc_state.observation_stack) > 0: + # element = wfc_state.observation_stack.pop() + # print(f"element: {element}") + # for direction_id, direction_offset in wfc_state.wfc_ns.adjacency_directions.items(): + # neighbor_coords = CoordRC(row=element.coords_row + direction_offset.y, column=element.coords_column + direction_offset.x) + # if not is_cell_on_boundary(wfc_state, neighbor_coords): + # neighbor_coords = wrap_coords(wfc_state, neighbor_coords) + # compatible_pattern_list = wfc_state.propagator_matrix[direction_id][element.pattern_id] + # for pat_id, pat_value in enumerate(compatible_pattern_list): + # if pat_value: + # wfc_state.compatible_count[neighbor_coords[0], neighbor_coords[1], pat_id, direction_id] -= 1 + # if 0 == wfc_state.compatible_count[neighbor_coords[0], neighbor_coords[1], pat_id, direction_id]: + # wfc_state = Ban(wfc_state, neighbor_coords, pat_id) + # return wfc_state + + # wfc_state = wfc_observe(wfc_state, random_variation, random_number_generator) + # wfc_state = wfc_partial_output(wfc_state) + # show_rendered_patterns(wfc_state, True) + # render_patterns_to_output(wfc_state, True) + + # print(f"Propagating. Current solver result: {wfc_state.result}") + assert wfc_state.result == None + # print(wfc_state.compatible_count.shape) + while len(wfc_state.observation_stack) > 0: + # print(wfc_state.observation_stack) + if wfc_state.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): + print("overflow K") + print(np.count_nonzero(wfc_state.wave_table, axis=2)) + assert False + + wfc_state.current_iteration_count_propagation += 1 + + element = wfc_state.observation_stack.pop() + # print(f"element: {element}") + for ( + direction_id, + direction_offset, + ) in wfc_state.wfc_ns.adjacency_directions.items(): + neighbor_coords = CoordRC( + row=element.coords_row + direction_offset.y, + column=element.coords_column + direction_offset.x, + ) + neighbor_coords = wrap_coords(wfc_state, neighbor_coords) + # print(f" {direction_offset} -> {neighbor_coords}") + compatible_pattern_list = wfc_state.propagator_matrix[ + direction_id, element.pattern_id + ] + # print(compatible_pattern_list) + for pat_id, pat_value in enumerate(compatible_pattern_list): + if pat_value: + # print(neighbor_coords) + wfc_state.compatible_count[ + neighbor_coords.row, + neighbor_coords.column, + pat_id, + direction_id, + ] -= 1 + if ( + 0 + == wfc_state.compatible_count[ + neighbor_coords.row, + neighbor_coords.column, + pat_id, + direction_id, + ] + ): + wfc_state = Ban(wfc_state, neighbor_coords, pat_id) + + # print(f"*** stack length: {len(wfc_state.observation_stack)}") + # print(f"~~~ compatible count:\n{wfc_state.compatible_count}") + # print(f"~~~ wave table:\n{wfc_state.wave_table}") + + # wfc_state = wfc_partial_output(wfc_state) + # show_rendered_patterns(wfc_state, True) + # render_patterns_to_output(wfc_state, True) + wfc_state.wfc_ns.stats_tracking["propagations"] += 1 + return wfc_state + + +import pdb + +backtrack_track_global = 0 + + +def wfc_backtrack(current_wfc_state, list_of_old_wfc_states): + global backtrack_track_global + backtrack_track_global += 1 + print(f"backtrack {backtrack_track_global}") + # current_wfc_state.wave_table = copy.deepcopy(old_wfc_state.wave_table) + # current_wfc_state.compatible_count = copy.deepcopy(old_wfc_state.compatible_count) + # current_wfc_state.sums_of_ones = copy.deepcopy(old_wfc_state.sums_of_ones) + # current_wfc_state.sums_of_weights = copy.deepcopy(old_wfc_state.sums_of_weights) + # current_wfc_state.sums_of_weight_log_weights = copy.deepcopy(old_wfc_state.sums_of_weight_log_weights) + # current_wfc_state.entropies = copy.deepcopy(old_wfc_state.entropies) + # current_wfc_state.observation_stack = copy.deepcopy(old_wfc_state.observation_stack) + before_len = len(current_wfc_state.previous_decisions) + forbidden = current_wfc_state.previous_decisions.pop() + assert before_len != len(current_wfc_state.previous_decisions) + wfc_logger.warning(f"Backtracking from {forbidden}") + + if current_wfc_state.logging: + with open(current_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"Backtracking #{backtrack_track_global}\n") + stats_file.write("Past state list:\n") + past_state_list = list( + zip( + [ + np.count_nonzero( + np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1 + ) + for o_wfc_state in list_of_old_wfc_states + ], + [ + np.count_nonzero( + np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1 + ) + for o_wfc_state in list_of_old_wfc_states + ], + ) + ) + stats_file.write(f"{past_state_list}\n") + + # print("Past state list: ", end='') + # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1) for o_wfc_state in list_of_old_wfc_states], end=' | ') + # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1) for o_wfc_state in list_of_old_wfc_states]) + old_wfc_state = list_of_old_wfc_states.pop() + # print(np.count_nonzero(old_wfc_state.wave_table, axis=2)) + # print(f"remaining nodes: {np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) != 1)}") + # print(f"invalid nodes: {(np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) > 60000))}") + + # print("vvvvv") + try: + old_wfc_state = list_of_old_wfc_states.pop() + except IndexError as e: + wfc_logger.info("stack of previous states is empty") + if old_wfc_state.logging: + with open(old_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"stack of previous states is empty\n") + # print(np.count_nonzero(old_wfc_state.wave_table, axis=2)) + # print(f"remaining nodes: {np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) != 1)}") + # print(f"invalid nodes: {(np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) > 60000))}") + + # print("Past state list: ", end='') + # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1) for o_wfc_state in list_of_old_wfc_states], end = ' ') + # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1) for o_wfc_state in list_of_old_wfc_states]) + + nold_wfc_state = copy.deepcopy(old_wfc_state) + # wfc_logger.warning(f"nold ones: {nold_wfc_state.sums_of_ones}") + + # current_wfc_state = Ban(current_wfc_state, forbidden[0], forbidden[1]) + # current_wfc_state.result = None + new_wfc_state = Ban(nold_wfc_state, forbidden[0], forbidden[1]) + # print(f"new invalid nodes: {(np.count_nonzero(np.count_nonzero(new_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(new_wfc_state.wave_table, axis=2) > 60000))}") + # wfc_logger.warning(f"new ones: {new_wfc_state.sums_of_ones}") + if new_wfc_state.overflow_check: + if np.any(np.count_nonzero(new_wfc_state.wave_table, axis=2) > 60000): + print("overflow J") + print(np.count_nonzero(new_wfc_state.wave_table, axis=2)) + assert False + # wfc_logger.warning(f"obstack len {len(new_wfc_state.observation_stack)}") + # new_wfc_state.observation_stack.pop() + # wfc_logger.warning(f"obstack len {len(new_wfc_state.observation_stack)}") + + # pdb.set_trace() + + new_wfc_state.result = None + + if new_wfc_state.logging: + with open(new_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"After backtracking:\n") + stats_file.write(f"stack length:{len(list_of_old_wfc_states)}\n") + stats_file.write("remaining wave table choices:\n") + stats_file.write( + f"{(np.count_nonzero(new_wfc_state.wave_table, axis=2))}\n" + ) + + return new_wfc_state, list_of_old_wfc_states + + +# from lucid_serialize_array import _normalize_array + +import cProfile, pstats +import logging + +ongoing_observations = 0 + + +def reset_backtracking_count(): + global backtrack_track_global + backtrack_track_global = 0 + + +def wfc_run(wfc_seed_state, visualize=False, logging=False): + wfc_logger.info("wfc_run()") + global WFC_VISUALIZE + if visualize: + WFC_VISUALIZE = True + else: + WFC_VISUALIZE = False + + global ongoing_observations + ongoing_observations = 0 + + wfc_state = wfc_init(wfc_seed_state) + + # print("Profiling clear...") + # pr = cProfile.Profile() + # pr.enable() + wfc_state = wfc_clear(wfc_state) + # pr.disable() + # print("Profiling complete...") + # profile_filename = "" + str(wfc_state.wfc_ns.output_path) + "" + "clear_" + str(wfc_state.wfc_ns.output_file_number) + "_" + str(wfc_state.wfc_ns.seed) + "_" + str(time.time()) + ".profile" + # with open(profile_filename, 'w') as profile_file: + # ps = pstats.Stats(pr, stream=profile_file) + # ps.print_stats() + # print("...profile saved") + + wfc_state.logging = logging + wfc_state.overflow_check = False + + if visualize: + show_pattern_adjacency(wfc_state) + visualize_propagator_matrix(wfc_state.propagator_matrix) + random_number_generator = np.random.RandomState(wfc_state.wfc_ns.seed) + random_variation = random_number_generator.random_sample(wfc_state.entropies.size) + + recorded_vis = None + if visualize: + recorded_vis = record_visualization(wfc_state, recorded_vis) + vis_stack = [] + + backtracking_stack = [] + backtracking_count = 0 + print(wfc_state.patterns) + + iterations = 0 + while (iterations < wfc_state.wfc_ns.iteration_limit) or ( + 0 == wfc_state.wfc_ns.iteration_limit + ): + wfc_state.backtracking_count = backtracking_count + wfc_state.backtracking_stack_length = len(backtracking_stack) + wfc_state.backtracking_total = backtrack_track_global + if visualize: + recorded_vis = record_visualization(wfc_state, recorded_vis) + wfc_state.current_iteration_count_last_touch += 1 + wfc_state = wfc_observe(wfc_state, random_variation, random_number_generator) + + # Add a time-out on the number of total observations + # print(f"Observations so far: {wfc_state.wfc_ns.stats_tracking['total_observations']}") + if wfc_state.wfc_ns.stats_tracking["total_observations"] > 3000: + wfc_state.result = WFC_TIMEDOUT + return wfc_state + + # print(f"wfc_state.entropies : {wfc_state.entropies}") + if visualize: + vis_stack.append(visualize_entropies(wfc_state)) + if iterations % 50 == 0: + print( + iterations, end=" " + ) # print(np.count_nonzero(wfc_state.wave_table, axis=2)) + # print(np.argmax(wfc_state.wave_table, axis=2)) + # print(wfc_state.result) + # print_internals(wfc_state) + + if wfc_state.logging: + with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"\n=====\n") + stats_file.write(f"result: {wfc_state.result}\n") + stats_file.write( + f"total observations: {wfc_state.wfc_ns.stats_tracking['total_observations']}\n" + ) + stats_file.write( + f"On backtracking {backtracking_count}, with stack size {len(backtracking_stack)}\n" + ) + stats_file.write(f"{wfc_state.result}\n") + stats_file.write("remaining wave table choices:\n") + stats_file.write( + f"{(np.count_nonzero(wfc_state.wave_table, axis=2))}\n" + ) + + if WFC_FINISHED == wfc_state.result: + wfc_state.wfc_ns.stats_tracking["success"] = True + wfc_state.recorded_vis = recorded_vis + return wfc_state + if WFC_FAILURE == wfc_state.result: + # print(np.count_nonzero(wfc_state.wave_table, axis=2)) + if not wfc_state.wfc_ns.backtracking: + wfc_state.recorded_vis = recorded_vis + return wfc_state + if len(backtracking_stack) <= 0: + wfc_state.recorded_vis = recorded_vis + return wfc_state + if len(wfc_state.previous_decisions) <= 0: + wfc_state.recorded_vis = recorded_vis + return wfc_state + backtracking_count += 1 + wfc_logger.warning( + f"Backtracking {backtracking_count}, stack size {len(backtracking_stack)}" + ) + if backtracking_count > 450: # Time out on too many backtracks + wfc_state.result = WFC_TIMEDOUT + wfc_state.recorded_vis = recorded_vis + return wfc_state + + # for ix, bs in enumerate(backtracking_stack): + # print(ix, end=',') + # print(bs.sums_of_ones) + # last_backtracking_added = backtracking_stack.pop() + wfc_state, backtracking_stack = wfc_backtrack(wfc_state, backtracking_stack) + wfc_logger.warning(f"stack size {len(backtracking_stack)}") + # print(f"wfc_state.sums_of_ones\n{wfc_state.sums_of_ones}") + print(f"current result code: {wfc_state.result}") + # if len(backtracking_stack) <= 0: + # #wfc_state.recorded_vis = recorded_vis + # #return wfc_state + # print(f"backtracking stack empty") + + if visualize: + wfc_state = wfc_partial_output(wfc_state) + visualize_compatible_count(wfc_state) + visualize_entropy(wfc_state) + show_rendered_patterns(wfc_state, True) + + wfc_state = wfc_propagate(wfc_state) + if visualize: + wfc_state = wfc_partial_output(wfc_state) + # show_rendered_patterns(wfc_state, partial=True) + # render_patterns_to_output(wfc_state, partial=True) + iterations += 1 + # print(iterations, end = ' ') + # wfc_logger.info("iterations: " + str(iterations)) + backtracking_stack.append(copy.deepcopy(wfc_state)) + wfc_state.result = WFC_TIMEDOUT + + wfc_state.recorded_vis = recorded_vis + return wfc_state + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/wfc/wfc1/wfc_solver_two.py b/wfc/wfc1/wfc_solver_two.py new file mode 100644 index 0000000..6ab5ab0 --- /dev/null +++ b/wfc/wfc1/wfc_solver_two.py @@ -0,0 +1,535 @@ +# + +import types +import collections +import logging +import math +import pdb +import numpy as np + + +from wfc.wfc_adjacency import adjacency_extraction_consistent + +from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE + +# import matplotlib.pyplot +# from matplotlib.pyplot import figure, subplot, subplots, title, matshow +# from wfc.wfc_patterns import render_pattern +# from wfc.wfc_adjacency import blit +# from wfc.wfc_tiles import tiles_to_images +import wfc.wfc_utilities +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center + +# import random +# import copy +# import time + +# import imageio + +logging.basicConfig(level=logging.INFO) +WFC_LOGGER = logging.getLogger() + + +WFC_FINISHED = -2 +WFC_FAILURE = -1 +WFC_TIMEDOUT = -3 +WFC_FAKE_FAILURE = -6 + + +def weight_log(val): + """Return the log of the weight value, used in calculating updated weights.""" + return val * math.log(val) + + +def wfc_init(prestate): + """ + Initialize the WFC solver, returning the fixed and mutable data structures needed. + """ + prestate.adjacency_directions_rc = { + i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() + } # + prestate = wfc.wfc_utilities.find_pattern_center(prestate) + parameters = types.SimpleNamespace(wfc_ns=prestate) + + state = types.SimpleNamespace() + state.result = None + + parameters.heuristic = ( + 0 # TODO: Implement control code to choose between heuristics + ) + + parameters.adjacency_relations = adjacency_extraction_consistent( + parameters.wfc_ns, parameters.wfc_ns.patterns + ) + parameters.patterns = np.array(list(parameters.wfc_ns.pattern_catalog.keys())) + parameters.pattern_translations = list(parameters.wfc_ns.pattern_catalog.values()) + parameters.number_of_patterns = parameters.patterns.size + parameters.number_of_directions = len(parameters.wfc_ns.adjacency_directions) + + # The Propagator is a data structure that holds the adjacency information + # for the patterns, i.e. given a direction, which patterns are allowed to + # be placed next to the pattern that we're currently concerned with. + # This won't change over the course of using the solver, so the important + # thing here is fast lookup. + parameters.propagator_matrix = np.zeros( + ( + parameters.number_of_directions, + parameters.number_of_patterns, + parameters.number_of_patterns, + ), + dtype=np.bool_, + ) + for direction, pattern_one, pattern_two in parameters.adjacency_relations: + parameters.propagator_matrix[(direction, pattern_one, pattern_two)] = True + + output = types.SimpleNamespace() + + # The Wave Table is the boolean expression table of which patterns are allowed + # in which cells of the solution we are calculating. + parameters.rows = parameters.wfc_ns.generated_size[0] + parameters.columns = parameters.wfc_ns.generated_size[1] + + output.solving_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.propagation_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + + parameters.wave_shape = [ + parameters.rows, + parameters.columns, + parameters.number_of_patterns, + ] + state.wave_table = np.full(parameters.wave_shape, True, dtype=np.bool_) + + # The compatible_count is a running count of the number of patterns that + # are still allowed to be next to this cell in a particular direction. + compatible_shape = [ + parameters.rows, + parameters.columns, + parameters.number_of_patterns, + parameters.number_of_directions, + ] + + WFC_LOGGER.debug(f"compatible shape:{compatible_shape}") + state.compatible_count = np.full( + compatible_shape, parameters.number_of_patterns, dtype=np.int16 + ) # assumes that there are less than 65536 patterns + + # The weights are how we manage the probabilities when we choose the next + # pattern to place. Rather than recalculating them from scratch each time, + # these let us incrementally update their values. + state.weights = np.array(list(parameters.wfc_ns.pattern_weights.values())) + state.weight_log_weights = np.vectorize(weight_log)(state.weights) + state.sum_of_weights = np.sum(state.weights) + + state.sum_of_weight_log_weights = np.sum(state.weight_log_weights) + state.starting_entropy = math.log(state.sum_of_weights) - ( + state.sum_of_weight_log_weights / state.sum_of_weights + ) + + state.entropies = np.zeros([parameters.rows, parameters.columns], dtype=np.float64) + state.sums_of_weights = np.zeros( + [parameters.rows, parameters.columns], dtype=np.float64 + ) + + # Instead of updating all of the cells for every propagation, we use a queue + # that marks the dirty tiles to update. + state.observation_stack = collections.deque() + + output.output_grid = np.full( + [parameters.rows, parameters.columns], WFC_NULL_VALUE, dtype=np.int64 + ) + output.partial_output_grid = np.full( + [parameters.rows, parameters.columns, parameters.number_of_patterns], + -9, + dtype=np.int64, + ) + + output.current_iteration_count_observation = 0 + output.current_iteration_count_propagation = 0 + output.current_iteration_count_last_touch = 0 + output.current_iteration_count_crystal = 0 + output.solving_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.ones_time = np.full((parameters.rows, parameters.columns), 0, dtype=np.int32) + output.propagation_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.touch_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.crystal_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.method_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.choices_recording = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.float32 + ) + + output.stats_tracking = prestate.stats_tracking.copy() + + return parameters, state, output + + +def wfc_clear(parameters, state, output): + """Given an initialized WFC solver state, clear it out for the beginning for the solving.""" + # Crystal solving time matrix + output.current_iteration_count_observation = 0 + output.current_iteration_count_propagation = 0 + output.current_iteration_count_last_touch = 0 + output.current_iteration_count_crystal = 0 + + output.solving_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.ones_time = np.full((parameters.rows, parameters.columns), 0, dtype=np.int32) + output.propagation_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.touch_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.crystal_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.method_time = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.int32 + ) + output.choices_recording = np.full( + (parameters.rows, parameters.columns), 0, dtype=np.float32 + ) + output.stats_tracking["total_observations"] = 0 + + state.wave_table = np.full(parameters.wave_shape, True, dtype=np.bool_) + + compatible_shape = [ + parameters.rows, + parameters.columns, + parameters.number_of_patterns, + parameters.number_of_directions, + ] + + # Initialize the compatible count from the propagation matrix. This sets the + # maximum domain of possible neighbors for each cell node. + + def prop_compat(p, d): + return sum(parameters.propagator_matrix[(d + 2) % 4][p]) + + def comp_count(r, c, p, d): + return pattern_compatible_count[p][d] + + pcomp = np.vectorize(prop_compat) + ccount = np.vectorize(comp_count) + pattern_compatible_count = np.fromfunction( + pcomp, + (parameters.number_of_patterns, parameters.number_of_directions), + dtype=np.int16, + ) + state.compatible_count = np.fromfunction( + ccount, + ( + parameters.rows, + parameters.columns, + parameters.number_of_patterns, + parameters.number_of_directions, + ), + dtype=np.int16, + ) + + # Likewise, set the weights to their maximum values + state.sums_of_weights = np.full( + [parameters.rows, parameters.columns], state.sum_of_weights, dtype=np.float64 + ) + state.sums_of_weight_log_weights = np.full( + [parameters.rows, parameters.columns], + state.sum_of_weight_log_weights, + dtype=np.float64, + ) + state.entropies = np.full( + [parameters.rows, parameters.columns], state.starting_entropy, dtype=np.float64 + ) + + state.recorded_steps = [] + state.observation_stack = collections.deque() + + # ground banning goes here + if parameters.wfc_ns.ground != 0 and False: + pass + + output.previous_decisions = [] + state.previous_decisions = [] + WFC_LOGGER.debug("clear complete") + + return state, output + + +# A useful helper function which we use because we want numpy arrays instead of jagged arrays +# https://stackoverflow.com/questions/7632963/numpy-find-first-index-of-value-fast/7654768 +def find_first(item, vec): + """return the index of the first occurence of item in vec""" + for i in range(len(vec)): + if item == vec[i]: + return i + return -1 + + +def find_minimum_entropy(wfc_state, random_variation): + return None + + +def find_upper_left_entropy(wfc_state, random_variation): + return None + + +def find_upper_left_unresolved(wfc_state, random_variation): + return None + + +def find_random_unresolved(wfc_state, random_variation): + noise_level = 1e-6 + entropy_map = random_variation * noise_level + entropy_map = entropy_map.flatten() + wfc_state.entropies.flatten() + minimum_cell = np.argmin(entropy_map) + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): + WFC_LOGGER.warning("Solver FAIL") + WFC_LOGGER.debug(f"previous decisions: {len(wfc_state.previous_decisions)}") + return WFC_FAILURE + if np.count_nonzero(wfc_state.wave_table, axis=2).flatten()[minimum_cell] == 0: + WFC_LOGGER.debug(f"previous decisions: {wfc_state}") + return WFC_FAILURE + return None + + higher_than_threshold = np.ma.MaskedArray( + entropy_map, np.count_nonzero(wfc_state.wave_table, axis=2).flatten() <= 1 + ) + minimum_cell = higher_than_threshold.argmin(fill_value=999999.9) + maximum_cell = higher_than_threshold.argmax(fill_value=0.0) + chosen_cell = maximum_cell + + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): + WFC_LOGGER.debug("A zero-state node has been found.") + + if wfc_parameters.overflow_check: + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 65534): + WFC_LOGGER.error("Overflow A") + WFC_LOGGER.error(np.count_nonzero(wfc_state.wave_table, axis=2)) + pdb.set_trace() + assert False + if np.all(np.count_nonzero(wfc_state.wave_table, axis=2) == 1): + WFC_LOGGER.info("DETECTED FINISH") + WFC_LOGGER.info( + f"nonzero count: {np.count_nonzero(wfc_state.wave_table, axis=2)}" + ) + + cell_index = np.unravel_index( + chosen_cell, + [ + np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], + np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], + ], + ) + return CoordRC(row=cell_index[0], column=cell_index[1]) + + +def check_completion(wfc_parameters, wfc_state): + """Check to see if the solver has failed, found a solution, or should keep going.""" + if np.all(np.count_nonzero(wfc_state.wave_table, axis=2) == 1): + # Require that every pattern be use at least once? + pattern_set = set(np.argmax(wfc_state.wave_table, axis=2).flatten()) + # Force a test to encourage backtracking - temporary addition + if ( + len(pattern_set) != wfc_state.number_of_patterns + ) and wfc_parameters.wfc_ns.force_use_all_patterns: + WFC_LOGGER.info("Some patterns were not used") + return WFC_FAILURE + WFC_LOGGER.info("Check complete: Solver FINISHED") + return WFC_FINISHED + if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) < 1): + return WFC_FAILURE + return None + + +def finalized_observed_waves(parameters, state, output): + """The solver is finished, process the solution for consumption as part of the output.""" + for row in range(parameters.rows): + for column in range(parameters.columns): + pattern_flags = state.wave_table[row, column] + output.output_grid[row, column] = find_first( + True, pattern_flags + ) # TODO: this line is probably overkill? + state.result = WFC_FINISHED + return state, output + + +def make_observation(state, cell, random_number_generator, output): + return state, output + + +def wfc_observe(state, random_variation, random_number_generator, parameters, output): + output.current_iteration_count_observation += 1 + the_result = None + if np.all(np.count_nonzero(state.wave_table, axis=2) == 1): + WFC_LOGGER.info("FINISHED") + WFC_LOGGER.debug(state.wave_table) + the_result = WFC_FINISHED + + if the_result is None: + the_result = check_completion(parameters, state) + + cell = None + if the_result is None: + if parameters.heuristic == 0: + cell = find_minimum_entropy(state, random_variation) + if parameters.heuristic == 1: + cell = find_upper_left_entropy(state, random_variation) + if parameters.heuristic == 2: + cell = find_upper_left_unresolved(state, random_variation) + if parameters.heuristic == 3: + cell = find_random_unresolved(state, random_variation) + + if cell is WFC_FAILURE: + the_result = cell + if np.all(np.count_nonzero(state.wave_table, axis=2) == 1): + WFC_LOGGER.info("Solver FINISHED") + WFC_LOGGER.debug(state.wave_table) + the_result = WFC_FINISHED + if the_result is None: + the_result = check_completion(parameters, state) + if WFC_FAKE_FAILURE is the_result: + state.result = WFC_FAILURE + state.fake_failure = True + return state, output + if WFC_FAILURE == the_result: + state.result = WFC_FAILURE + return state, output + if WFC_FINISHED == the_result: + return finalized_observed_waves(parameters, state, output) + + return make_observation(state, cell, random_number_generator, output) + + +def is_cell_on_boundary(wfc_parameters, wfc_coords): + if not wfc_parameters.wfc_ns.periodic_output: + return False + # otherwise... + return False # TODO + + +def wrap_coords(wfc_parameters, cell_coords): + r = (cell_coords.row + wfc_parameters.wfc_ns.generated_size[0]) % ( + wfc_parameters.wfc_ns.generated_size[0] + ) + c = (cell_coords.column + wfc_parameters.wfc_ns.generated_size[1]) % ( + wfc_parameters.wfc_ns.generated_size[1] + ) + return CoordRC(row=r, column=c) + + +def wfc_propagate(parameters, state, output): + return state, output + + +def wfc_backtrack(state, output_stack): + return state, output_stack + + +BACKTRACK_TRACK_GLOBAL = 0 + + +def reset_backtracking_count(): + global BACKTRACK_TRACK_GLOBAL + BACKTRACK_TRACK_GLOBAL = 0 + + +def wfc_run(wfc_seed_state, visualize=False, logging=False): + WFC_LOGGER.info("wfc_run()") + wfc_output_stack = [] + backtracking_stack = [] + backtracking_count = 0 + + wfc_output = types.SimpleNamespace() + + wfc_parameters, wfc_state, wfc_output = wfc_init(wfc_seed_state) + wfc_parameters.visualize = (visualize,) + wfc_parameters.logging = logging + wfc_parameters.timeout = 3000 + + wfc_state, wfc_output = wfc_clear(wfc_parameters, wfc_state, wfc_output) + # if wfc_status.visualize: + # show_pattern_adjacency(wfc_state) + # visualize_propagator_matrix(wfc_state.propagator_matrix) + random_number_generator = np.random.RandomState(wfc_parameters.wfc_ns.seed) + random_variation = random_number_generator.random_sample(wfc_state.entropies.size) + # record_visualization() + iterations = 0 + + while (iterations < wfc_parameters.wfc_ns.iteration_limit) or ( + 0 == wfc_parameters.wfc_ns.iteration_limit + ): + wfc_state.backtracking_count = backtracking_count + wfc_state.backtracking_stack_length = len(backtracking_stack) + wfc_state.backtracking_total = BACKTRACK_TRACK_GLOBAL + # if parameters.visualize: + # recorded_vis = record_visualization(wfc_state, recorded_vis) + # wfc_state.current_iteration_count_last_touch += 1 + wfc_state, wfc_output = wfc_observe( + wfc_state, + random_variation, + random_number_generator, + wfc_parameters, + wfc_output, + ) + + # Add a time-out on the number of total observations + print(wfc_output.stats_tracking) + if wfc_output.stats_tracking["total_observations"] > wfc_parameters.timeout: + wfc_state.result = WFC_TIMEDOUT + return wfc_state + + if iterations % 50 == 0: + print(iterations, end=" ") + + if wfc_parameters.logging: + with open(wfc_parameters.wfc_ns.debug_log_filename, "a") as stats_file: + stats_file.write(f"\n=====\n") + stats_file.write(f"result: {wfc_state.result}\n") + # stats_file.write(f"total observations: {wfc_state.wfc_ns.stats_tracking['total_observations']}\n") + stats_file.write( + f"On backtracking {backtracking_count}, with stack size {len(backtracking_stack)}\n" + ) + stats_file.write(f"{wfc_state.result}\n") + stats_file.write("remaining wave table choices:\n") + stats_file.write( + f"{(np.count_nonzero(wfc_state.wave_table, axis=2))}\n" + ) + + if WFC_FINISHED == wfc_state.result: + wfc_output.stats_tracking["success"] = True + # wfc_output.recorded_vis = recorded_vis + return wfc_state, wfc_output + if WFC_FAILURE == wfc_state.result: + if not wfc_parameters.wfc_ns.backtracking: + return wfc_state, wfc_output + backtracking_count += 1 + WFC_LOGGER.warning( + f"Backtracking {backtracking_count}, stack size {len(backtracking_stack)}" + ) + if backtracking_count > wfc_parameters.wfc_ns.backtracking_limit: + if wfc_parameters.wfc_ns.backtracking_limit > 0: + wfc_state.result = WFC_TIMEDOUT + return wfc_state, wfc_output + wfc_state, backtracking_stack = wfc_backtrack(wfc_state, backtracking_stack) + wfc_state, wfc_output = wfc_propagate(wfc_parameters, wfc_state, wfc_output) + iterations += 1 + print(dir(wfc_parameters)) + print("===") + print(dir(wfc_state)) + assert False + wfc_state.result = WFC_TIMEDOUT + print(wfc_state) + return wfc_state, wfc_output diff --git a/wfc/wfc1/wfc_tiles.py b/wfc/wfc1/wfc_tiles.py new file mode 100644 index 0000000..de3eb04 --- /dev/null +++ b/wfc/wfc1/wfc_tiles.py @@ -0,0 +1,453 @@ +import wfc.wfc_utilities +from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE +from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center +import matplotlib.pyplot as plt +from matplotlib.pyplot import figure, subplot, subplots, title, matshow +import math +import numpy as np +import logging + +import logging + +logging.basicConfig(level=logging.INFO) +wfc_logger = logging.getLogger() + +## Helper functions +RGB_CHANNELS = 3 + + +def rgb_to_int(rgb_in): + """"Takes RGB triple, returns integer representation.""" + return struct.unpack( + "I", struct.pack("<" + "B" * 4, *(rgb_in + [0] * (4 - len(rgb_in)))) + )[0] + + +def int_to_rgb(val): + return [x for x in val.to_bytes(RGB_CHANNELS, "little")] + + +# In[8]: + + +import imageio + + +def load_source_image(filename): + return imageio.imread(filename) + + +def image_to_tiles(img, tile_size): + """ + Takes an images, divides it into tiles, return an array of tiles. + >>> image_to_tiles(test_ns.img, test_ns.tile_size) + array([[[[[255, 255, 255]]], + + + [[[255, 255, 255]]], + + + [[[255, 255, 255]]], + + + [[[255, 255, 255]]]], + + + + [[[[255, 255, 255]]], + + + [[[ 0, 0, 0]]], + + + [[[ 0, 0, 0]]], + + + [[[ 0, 0, 0]]]], + + + + [[[[255, 255, 255]]], + + + [[[ 0, 0, 0]]], + + + [[[255, 0, 0]]], + + + [[[ 0, 0, 0]]]], + + + + [[[[255, 255, 255]]], + + + [[[ 0, 0, 0]]], + + + [[[ 0, 0, 0]]], + + + [[[ 0, 0, 0]]]]], dtype=uint8) + """ + padding_argument = [(0, 0), (0, 0), (0, 0)] + for input_dim in [0, 1]: + padding_argument[input_dim] = ( + 0, + (tile_size - img.shape[input_dim]) % tile_size, + ) + img = np.pad(img, padding_argument, mode="constant") + tiles = img.reshape( + ( + img.shape[0] // tile_size, + tile_size, + img.shape[1] // tile_size, + tile_size, + img.shape[2], + ) + ).swapaxes(1, 2) + return tiles + + +def tile_to_image(tile, tile_catalog, tile_size, visualize=False): + """ + Takes a single tile and returns the pixel image representation. + """ + new_img = np.zeros((tile_size, tile_size, 3), dtype=np.int64) + for u in range(tile_size): + for v in range(tile_size): + ## If we want to display a partial pattern, it is helpful to + ## be able to show empty cells. Therefore, in visualize mode, + ## we use -1 as a magic number for a non-existant tile. + pixel = [200, 0, 200] + if (visualize) and ((-1 == tile) or (WFC_PARTIAL_BLANK == tile)): + if 0 == (u + v) % 2: + pixel = [255, 0, 255] + else: + if (visualize) and -2 == tile: + pixel = [0, 255, 255] + else: + pixel = tile_catalog[tile][u, v] + new_img[u, v] = pixel + + +def tiles_to_images( + wfc_ns, + tile_grid, + tile_catalog, + tile_size, + visualize=False, + partial=False, + grid_count=None, +): + """ + Takes a tile_grid and transforms it into an image, using the information + in tile_catalog. We use tile_size to figure out the size the new image + should be, and visualize for displaying partial tile patterns. + """ + new_img = np.zeros( + ( + tile_grid.shape[0] * tile_size, + tile_grid.shape[1] * tile_size, + wfc_ns.channels, + ), + dtype=np.int64, + ) + if partial and (len(tile_grid.shape) > 2): + for i in range(tile_grid.shape[0]): + for j in range(tile_grid.shape[1]): + for u in range(wfc_ns.tile_size): + for v in range(wfc_ns.tile_size): + pixel_merge_list = [] + for k in range(tile_grid.shape[2]): + tile = tile_grid[i, j, k] + ## If we want to display a partial pattern, it is helpful to + ## be able to show empty cells. Therefore, in visualize mode, + ## we use -1 as a magic number for a non-existant tile. + pixel = None # [200, 0, 200] + # print(tile) + if (visualize) and ((-1 == tile) or (-2 == tile)): + if -1 == tile: + pixel = [200, 0, 200] + if 0 == (i + j) % 2: + pixel = [255, 0, 255] + else: + pixel = [0, 255, 255] + else: + if (WFC_PARTIAL_BLANK != tile) and ( + WFC_NULL_VALUE != tile + ): # TODO: instead of -3, use MaskedArrays + pixel = tile_catalog[tile][u, v] + if not (pixel is None): + pixel_merge_list.append(pixel) + if len(pixel_merge_list) == 0: + if 0 == (i + j) % 2: + pixel_merge_list.append([255, 0, 255]) + else: + pixel_merge_list.append([0, 172, 172]) + + if len(pixel_merge_list) > 0: + pixel_to_add = pixel_merge_list[0] + if len(pixel_merge_list) > 1: + pixel_to_add = [ + round(sum(x) / len(pixel_merge_list)) + for x in zip(*pixel_merge_list) + ] + try: + while len(pixel_to_add) < wfc_ns.channels: + pixel_to_add.append(255) + new_img[ + (i * wfc_ns.tile_size) + u, + (j * wfc_ns.tile_size) + v, + ] = pixel_to_add + except TypeError as e: + wfc_logger.warning(e) + wfc_logger.warning( + "Tried to add {} from {}".format( + pixel_to_add, pixel_merge_list + ) + ) + else: + for i in range(tile_grid.shape[0]): + for j in range(tile_grid.shape[1]): + tile = tile_grid[i, j] + for u in range(wfc_ns.tile_size): + for v in range(wfc_ns.tile_size): + ## If we want to display a partial pattern, it is helpful to + ## be able to show empty cells. Therefore, in visualize mode, + ## we use -1 as a magic number for a non-existant tile. + pixel = [200, 0, 200] + # print(f"tile: {tile}") + if (visualize) and ((-1 == tile) or (-2 == tile)): + if -1 == tile: + if 0 == (i + j) % 2: + pixel = [255, 0, 255] + if -2 == tile: + pixel = [0, 255, 255] + else: + if WFC_PARTIAL_BLANK != tile: + pixel = tile_catalog[tile][u, v] + # Watch out for images with more than 3 channels! + new_img[ + (i * wfc_ns.tile_size) + u, (j * wfc_ns.tile_size) + v + ] = np.resize( + pixel, + new_img[ + (i * wfc_ns.tile_size) + u, (j * wfc_ns.tile_size) + v + ].shape, + ) + logging.debug("Output image shape is", new_img.shape) + return new_img + + +# Past this point, WFC itself doesn't care about the exact content of the image, just that it exists. So we're going to pack all that information away behind some data structures: a dictionary of the tiles and a matrix with the tiles in the input image. The `tile_grid` is the most important thing for automatically figuring out adjacencies, while the `tile_catalog` is what will use at the end to render the final results. +# +# Here's the default tile cataloger. Take the big bag of tiles that we've got and arrange them in a dictionary that categorizes similar tiles under the same key. +# +# `tile_catalog`: dictionary to translate the hash-key ID of the tile to the image representation of the tile. We won't need this again until we do the output render. +# `tile_grid`: the original input image, only this time expressed as a 2D array of tile IDs. +# `code_list`: 1D array of the tile IDs. +# `unique_tiles`: 1D array of the unique tiles in the tile grid. +# +# You can modify this to make your own tile cataloger, with different behavor. For example, if you wanted each tile to have its own id even if it had the same image, or contrariwise if you wanted to group all of the background tiles under the same heading. +# + +# In[9]: + + +def make_tile_catalog(nspace): + """ + """ + tiles = image_to_tiles(nspace.img, nspace.tile_size) + logging.info(f"The shape of the input image is {tiles.shape}") + # print(f'The shape of the input image is {tiles.shape}') + # print(tiles) + print( + ( + tiles.shape[0] * tiles.shape[1], + nspace.tile_size, + nspace.tile_size, + nspace.channels, + ) + ) + + tile_list = np.array(tiles).reshape( + ( + tiles.shape[0] * tiles.shape[1], + nspace.tile_size, + nspace.tile_size, + nspace.channels, + ) + ) + ## Make Tile Catalog + code_list = np.array(hash_downto(tiles, 2)).reshape( + (tiles.shape[0] * tiles.shape[1]) + ) + tile_grid = np.array(hash_downto(tiles, 2), dtype=np.int64) + unique_tiles = np.unique(tile_grid, return_counts=True) + + tile_catalog = {} + for i, j in enumerate(code_list): + tile_catalog[j] = tile_list[i] + return tile_catalog, tile_grid, code_list, unique_tiles + + +# Let's visualize what we have so far. We'll load an image, turn it into tiles, and then render those tiles into an image. If everything is working right, we should have two identical images. + +# In[10]: + + +def show_input_to_output(img_ns): + """ + Does the input equal the output? + + >>> [show_input_to_output(test_ns), load_source_image(test_ns.img_filename)] + [[[255 255 255] + [255 255 255] + [255 255 255] + [255 255 255]] + + [[255 255 255] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0]] + + [[255 255 255] + [ 0 0 0] + [255 0 0] + [ 0 0 0]] + + [[255 255 255] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0]]] + [None, Image([[[255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255]], + + [[255, 255, 255], + [ 0, 0, 0], + [ 0, 0, 0], + [ 0, 0, 0]], + + [[255, 255, 255], + [ 0, 0, 0], + [255, 0, 0], + [ 0, 0, 0]], + + [[255, 255, 255], + [ 0, 0, 0], + [ 0, 0, 0], + [ 0, 0, 0]]], dtype=uint8)] + """ + figure() + + sp = subplot(1, 2, 1).imshow(img_ns.img) + sp.axes.grid(False) + sp.axes.tick_params( + bottom=False, + left=False, + which="both", + labelleft=False, + labelbottom=False, + length=0, + ) + title("Input Image", fontsize=10) + outimg = tiles_to_images( + img_ns, img_ns.tile_grid, img_ns.tile_catalog, img_ns.tile_size + ) + sp = subplot(1, 2, 2).imshow(outimg.astype(np.uint8)) + sp.axes.tick_params( + bottom=False, + left=False, + which="both", + labelleft=False, + labelbottom=False, + length=0, + ) + title("Output Image From Tiles", fontsize=10) + sp.axes.grid(False) + # print(outimg.astype(np.uint8)) + # print(img_ns) + plt.savefig(img_ns.output_filename + "_input_to_output.pdf", bbox_inches="tight") + plt.close() + + +def show_extracted_tiles(img_ns): + figure(figsize=(4, 4), edgecolor="k", frameon=True) + title("Extracted Tiles") + s = math.ceil(math.sqrt(len(img_ns.unique_tiles))) + 1 + # print(s) + for tcode, i in img_ns.tile_ids.items(): + sp = subplot(s, s, i + 1).imshow(img_ns.tile_catalog[tcode]) + sp.axes.tick_params(labelleft=False, labelbottom=False, length=0) + title(i, fontsize=10) + sp.axes.grid(False) + + plt.close() + + +# A nice little diagram of our palette of tiles. +# +# And, just to check that our internal `tile_grid` representation has reasonable values, let's look at it directly. This will be useful if we have to debug the inner workings of our propagator. + +# In[11]: + + +def show_false_color_tile_grid(img_ns): + pl = matshow( + img_ns.tile_grid, + cmap="gist_ncar", + extent=(0, img_ns.tile_grid.shape[1], img_ns.tile_grid.shape[0], 0), + ) + title("False Color Map of Tiles in Input Image") + pl.axes.grid(None) + # edgecolor('black') + + +if __name__ == "__main__": + import types + + test_ns = types.SimpleNamespace( + img_filename="red_maze.png", + seed=87386, + tile_size=1, + pattern_width=2, + channels=3, + adjacency_directions=dict( + enumerate( + [ + CoordXY(x=0, y=-1), + CoordXY(x=1, y=0), + CoordXY(x=0, y=1), + CoordXY(x=-1, y=0), + ] + ) + ), + periodic_input=True, + periodic_output=True, + generated_size=(3, 3), + screenshots=1, + iteration_limit=0, + allowed_attempts=1, + ) + test_ns = wfc_utilities.find_pattern_center(test_ns) + test_ns = wfc_utilities.load_visualizer(test_ns) + test_ns.img = load_source_image(test_ns.img_filename) + ( + test_ns.tile_catalog, + test_ns.tile_grid, + test_ns.code_list, + test_ns.unique_tiles, + ) = make_tile_catalog(test_ns) + test_ns.tile_ids = { + v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() + } + test_ns.tile_weights = { + a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) + } + import doctest + + doctest.testmod() diff --git a/wfc/wfc1/wfc_utilities.py b/wfc/wfc1/wfc_utilities.py new file mode 100644 index 0000000..4cb87bc --- /dev/null +++ b/wfc/wfc1/wfc_utilities.py @@ -0,0 +1,64 @@ +# In[4]: + +import collections +import numpy as np +import math +import logging + +CoordXY = collections.namedtuple("coords_xy", ["x", "y"]) +CoordRC = collections.namedtuple("coords_rc", ["row", "column"]) + +WFC_PARTIAL_BLANK = -3 +WFC_NULL_VALUE = -9 + + +def hash_downto(a, rank, seed=0): + state = np.random.RandomState(seed) + assert rank < len(a.shape) + u = a.reshape((np.prod(a.shape[:rank]), -1)) + v = state.randint(1 - (1 << 63), 1 << 63, np.prod(a.shape[rank:]), dtype="int64") + return np.inner(u, v).reshape(a.shape[:rank]).astype("int64") + + +# In[5]: + + +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False + + +# In[6]: + + +# get_ipython().run_line_magic('pylab', 'inline') +def load_visualizer(wfc_ns): + if IN_COLAB: + from google.colab import files + + uploaded = files.upload() + for fn in uploaded.keys(): + print( + 'User uploaded file "{name}" with length {length} bytes'.format( + name=fn, length=len(uploaded[fn]) + ) + ) + else: + import matplotlib + import matplotlib.pylab + from matplotlib.pyplot import figure + from matplotlib.pyplot import subplot + from matplotlib.pyplot import title + from matplotlib.pyplot import matshow + + wfc_ns.img_filename = f"images/{wfc_ns.img_filename}" + return wfc_ns + + +def find_pattern_center(wfc_ns): + # wfc_ns.pattern_center = (math.floor((wfc_ns.pattern_width - 1) / 2), math.floor((wfc_ns.pattern_width - 1) / 2)) + wfc_ns.pattern_center = (0, 0) + return wfc_ns diff --git a/wfc/wfc_run.py b/wfc/wfc_run.py new file mode 100644 index 0000000..e293acb --- /dev/null +++ b/wfc/wfc_run.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +"""Base code to load commands from xml and run them.""" + +import time +import wfc.wfc_control as wfc_control +import xml.etree.ElementTree as ET +import os + + +def string2bool(strn): + if isinstance(strn, bool): + return strn + return strn.lower() in ["true"] + + +def run_default(run_experiment=False): + log_filename = f"log_{time.time()}" + xdoc = ET.ElementTree(file="samples_reference.xml") + default_allowed_attempts = 10 + default_backtracking = False + log_stats_to_output = wfc_control.make_log_stats() + + for xnode in xdoc.getroot(): + name = xnode.get("name", "NAME") + if "overlapping" == xnode.tag: + # seed = 3262 + tile_size = int(xnode.get("tile_size", 1)) + # seed for random generation, can be any number + tile_size = int(xnode.get("tile_size", 1)) # size of tile, in pixels + pattern_width = int(xnode.get("N", 2)) # Size of the patterns we want. + # 2x2 is the minimum, larger scales get slower fast. + + symmetry = int(xnode.get("symmetry", 8)) + ground = int(xnode.get("ground", 0)) + periodic_input = string2bool( + xnode.get("periodic", False) + ) # Does the input wrap? + periodic_output = string2bool( + xnode.get("periodic", False) + ) # Do we want the output to wrap? + generated_size = (int(xnode.get("width", 48)), int(xnode.get("height", 48))) + screenshots = int( + xnode.get("screenshots", 3) + ) # Number of times to run the algorithm, will produce this many distinct outputs + iteration_limit = int( + xnode.get("iteration_limit", 0) + ) # After this many iterations, time out. 0 = never time out. + allowed_attempts = int( + xnode.get("allowed_attempts", default_allowed_attempts) + ) # Give up after this many contradictions + backtracking = string2bool(xnode.get("backtracking", default_backtracking)) + visualize_experiment = False + + run_instructions = [ + { + "loc": "entropy", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + } + ] + # run_instructions = [{"loc": "entropy", "choice": "weighted", "backtracking": True, "global": "allpatterns"}] + if run_experiment: + run_instructions = [ + { + "loc": "lexical", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": False, + "global": None, + }, + { + "loc": "lexical", + "choice": "random", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "entropy", + "choice": "random", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "random", + "choice": "random", + "backtracking": False, + "global": None, + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": True, + "global": None, + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": True, + "global": None, + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": True, + "global": "allpatterns", + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": True, + "global": "allpatterns", + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": False, + "global": "allpatterns", + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": False, + "global": "allpatterns", + }, + ] + if run_experiment == "heuristic": + run_instructions = [ + { + "loc": "hilbert", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "spiral", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "anti-entropy", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "simple", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": backtracking, + "global": None, + }, + ] + if run_experiment == "backtracking": + run_instructions = [ + { + "loc": "entropy", + "choice": "weighted", + "backtracking": True, + "global": "allpatterns", + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": False, + "global": "allpatterns", + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": True, + "global": None, + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": False, + "global": None, + }, + ] + if run_experiment == "backtracking_heuristic": + run_instructions = [ + { + "loc": "lexical", + "choice": "weighted", + "backtracking": True, + "global": "allpatterns", + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": False, + "global": "allpatterns", + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": True, + "global": None, + }, + { + "loc": "lexical", + "choice": "weighted", + "backtracking": False, + "global": None, + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": True, + "global": "allpatterns", + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": False, + "global": "allpatterns", + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": True, + "global": None, + }, + { + "loc": "random", + "choice": "weighted", + "backtracking": False, + "global": None, + }, + ] + if run_experiment == "choices": + run_instructions = [ + { + "loc": "entropy", + "choice": "rarest", + "backtracking": False, + "global": None, + }, + { + "loc": "entropy", + "choice": "weighted", + "backtracking": False, + "global": None, + }, + { + "loc": "entropy", + "choice": "random", + "backtracking": False, + "global": None, + }, + ] + + for experiment in run_instructions: + for x in range(screenshots): + print(f"-: {name} > {x}") + solution = wfc_control.execute_wfc( + name, + tile_size=tile_size, + pattern_width=pattern_width, + rotations=symmetry, + output_size=generated_size, + ground=ground, + attempt_limit=allowed_attempts, + output_periodic=periodic_output, + input_periodic=periodic_input, + loc_heuristic=experiment["loc"], + choice_heuristic=experiment["choice"], + backtracking=experiment["backtracking"], + global_constraint=experiment["global"], + log_filename=log_filename, + log_stats_to_output=log_stats_to_output, + visualize=visualize_experiment, + logging=True, + ) + if solution is None: + print(None) + else: + print(solution) + + # These are included for my colab experiments, remove them if you're not me + os.system( + 'cp -rf "/content/wfc/output/*.tsv" "/content/drive/My Drive/wfc_exper/2"' + ) + os.system( + 'cp -r "/content/wfc/output" "/content/drive/My Drive/wfc_exper/2"' + ) + + +run_default("choice") +run_default("backtracking") +run_default("heuristic") +run_default() +run_default("choices") +run_default("backtracking") From b83f7349eca55ad2d136253a7a011a914cd6db6d Mon Sep 17 00:00:00 2001 From: Isaac Karth Date: Fri, 14 Aug 2020 12:48:53 -0700 Subject: [PATCH 3/4] removed outdated wfc code --- wfc/wfc1/wfc_adjacency.py | 503 --------- wfc/wfc1/wfc_control.py | 408 ------- wfc/wfc1/wfc_example.py | 242 ---- wfc/wfc1/wfc_extra.py | 159 --- wfc/wfc1/wfc_minizinc.py | 114 -- wfc/wfc1/wfc_patterns.py | 412 ------- wfc/wfc1/wfc_solver.py | 2131 ------------------------------------ wfc/wfc1/wfc_solver_two.py | 535 --------- wfc/wfc1/wfc_tiles.py | 453 -------- wfc/wfc1/wfc_utilities.py | 64 -- 10 files changed, 5021 deletions(-) delete mode 100644 wfc/wfc1/wfc_adjacency.py delete mode 100644 wfc/wfc1/wfc_control.py delete mode 100644 wfc/wfc1/wfc_example.py delete mode 100644 wfc/wfc1/wfc_extra.py delete mode 100644 wfc/wfc1/wfc_minizinc.py delete mode 100644 wfc/wfc1/wfc_patterns.py delete mode 100644 wfc/wfc1/wfc_solver.py delete mode 100644 wfc/wfc1/wfc_solver_two.py delete mode 100644 wfc/wfc1/wfc_tiles.py delete mode 100644 wfc/wfc1/wfc_utilities.py diff --git a/wfc/wfc1/wfc_adjacency.py b/wfc/wfc1/wfc_adjacency.py deleted file mode 100644 index b5144d5..0000000 --- a/wfc/wfc1/wfc_adjacency.py +++ /dev/null @@ -1,503 +0,0 @@ -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -from wfc.wfc_tiles import tiles_to_images -import matplotlib.pyplot as plt -from matplotlib.pyplot import figure, subplot, subplots, title, matshow -import numpy as np -import itertools -import math -import matplotlib - -# In[15]: - - -# def is_valid_overlap_xy(d, p1, p2, pattern_catalog, pattern_width, adjacency_directions): -# """Given a direction, two patterns, and a pattern catalog, return True -# if we overlap pattern two on top of pattern one and the intersection -# of the two patterns is an exact match.""" -# dimensions = (1,0) -# not_a_number = -1 -# adjacency_directions_inverted = CoordXY(x = 0 - adjacency_directions[d].x, y = 0 - adjacency_directions[d].y) - -# ##TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... -# shifted = np.roll(np.pad(pattern_catalog[p2], pattern_width, mode='constant', constant_values = not_a_number), adjacency_directions[d], dimensions) -# #print("*") -# #print(shifted) -# compare = shifted[pattern_width:pattern_width+pattern_width, pattern_width:pattern_width+pattern_width] -# left = max(0,0 + adjacency_directions_inverted.x) -# right = min(pattern_width, pattern_width + adjacency_directions_inverted.x) -# top = max(0,0 + adjacency_directions_inverted.y) -# bottom = min(pattern_width, pattern_width + adjacency_directions_inverted.y) -# a = pattern_catalog[p1][top:bottom,left:right] -# b = compare[top:bottom,left:right] - -# #a = pattern_catalog[p1] -# #b = pattern_catalog[p2] -# #b_shift = np.roll(b, (adjacency_directions[d].y,adjacency_directions[d].x), (0,1)) -# #a_slice = a[0+adjacency_directions[d].y:pattern_width+1+adjacency_directions[d].y, 0+adjacency_directions[d].x:pattern_width+1+adjacency_directions[d].x] -# #b_slice = b_shift[0+adjacency_directions[d].y:pattern_width+1+adjacency_directions[d].y, 0+adjacency_directions[d].x:pattern_width+1+adjacency_directions[d].x] - -# print(a) -# print(b) -# print(compare) -# print(shifted) -# res = np.array_equal(a,b) - -# print(f"is_valid_overlap: {p1}+{p2} at {d}{adjacency_directions[d]} = {res}") -# #print(f"\n{a}\n = \n{b}\n{b_shift}") -# #print(a_slice) -# #print('<-') -# #print(b_slice) - -# #shifted = np.roll(np.pad(pattern_catalog[p2], -# # pattern_width, -# # mode='constant', -# # constant_values = not_a_number), -# # adjacency_directions[d], dimensions) -# #print(a,b) - -# return res - - -def is_valid_overlap_xy( - dir_id, p1, p2, pattern_catalog, pattern_width, adjacency_directions -): - """Given a direction, two patterns, and a pattern catalog, return True - if we overlap pattern two on top of pattern one and the intersection - of the two patterns is an exact match.""" - # dir_corrected = (0 - adjacency_directions[dir_id].x, 0 - adjacency_directions[dir_id].y) - dir_corrected = ( - 0 + adjacency_directions[dir_id].x, - 0 + adjacency_directions[dir_id].y, - ) - dimensions = (1, 0) - not_a_number = -1 - # TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... - # print(d, p2, p1) - shifted = np.roll( - np.pad( - pattern_catalog[p2], - pattern_width, - mode="constant", - constant_values=not_a_number, - ), - dir_corrected, - dimensions, - ) - compare = shifted[ - pattern_width : pattern_width + pattern_width, - pattern_width : pattern_width + pattern_width, - ] - left = max(0, 0 + dir_corrected[0]) - right = min(pattern_width, pattern_width + dir_corrected[0]) - top = max(0, 0 + dir_corrected[1]) - bottom = min(pattern_width, pattern_width + dir_corrected[1]) - a = pattern_catalog[p1][top:bottom, left:right] - b = compare[top:bottom, left:right] - res = np.array_equal(a, b) - # print(f"res: {res}") - return res - - -def valid_overlap(d, p1, p2): - dimensions = (1, 0) - not_a_number = 0 - # TODO: can probably speed this up by using the right slices, rather than rolling the whole pattern... - shifted = numpy.roll( - numpy.pad( - pattern_catalog[p2], - max(patternsize), - mode="constant", - constant_values=not_a_number, - ), - d, - dimensions, - ) - compare = shifted[ - patternsize[0] : patternsize[0] + patternsize[0], - patternsize[1] : patternsize[1] + patternsize[1], - ] - left = max(0, 0 + d[0]) - right = min(patternsize[0], patternsize[0] + d[0]) - top = max(0, 0 + d[1]) - bottom = min(patternsize[1], patternsize[1] + d[1]) - - a = pattern_catalog[p1][top:bottom, left:right] - b = compare[top:bottom, left:right] - res = numpy.array_equal(a, b) - return res - - -def adjacency_extraction_consistent(wfc_ns, pattern_cat): - """Takes a pattern catalog, returns a list of all legal adjacencies.""" - # This is a brute force implementation. We should really use the adjacency list we've already calculated... - legal = [] - # print(f"pattern_cat\n{pattern_cat}") - for p1, pattern1 in enumerate(pattern_cat): - for d_index, d in enumerate(wfc_ns.adjacency_directions): - for p2, pattern2 in enumerate(pattern_cat): - if is_valid_overlap_xy( - d, - p1, - p2, - pattern_cat, - wfc_ns.pattern_width, - wfc_ns.adjacency_directions, - ): - legal.append((d_index, p1, p2)) - return legal - - -# .. .. *. .* .. -# .* *. .. .. .. - -# 1+0 -# ... -# .*. - -# 1+1 -# ... -# *X. - -# 1+2 -# .X. -# .X. - -# 1+3 -# .X. -# .X. - -# 1+4 -# ... -# .X. - - -# In[16]: - - -# def adjacency_efficent_extraction_observed(codes): -# adjacency_relations = list() -# -# #if mode == 'observed': # just pairing seen in the input image -# for i,adj_dir in ns.adjacency_directions.items(): -# #print(codes) -# u = adj_dir.x -# v = adj_dir.y -# a = codes[max(0,0+u):codes.shape[0]+u,max(0,0+v):codes.shape[1]+v] -# b = codes[max(0,0-u):codes.shape[0]-u,max(0,0-v):codes.shape[1]-v] -# triples = [(i,ns.tile_ids[j],ns.tile_ids[k]) for j,k in zip(a.ravel(),b.ravel())] -# adjacency_relations.extend(set(triples)) -# return adjacency_relations - -import collections -import itertools - - -def adjacency_efficent_extraction_consistent(patterns): - assert ns.pattern_width > 0 - adjacency_relations = list() - for i, adj_dir in ns.adjacency_directions.items(): - a = hash_downto( - patterns[ - :, - max(0, 0 + adj_dir.x) : ns.pattern_width + adj_dir.x, - max(0, 0 + adj_dir.y) : ns.pattern_width + adj_dir.y, - ], - 1, - ) - b = hash_downto( - patterns[ - :, - max(0, 0 - adj_dir.x) : ns.pattern_width - adj_dir.x, - max(0, 0 - adj_dir.y) : ns.pattern_width - adj_dir.y, - ], - 1, - ) - rel = collections.defaultdict(lambda: ([], [])) - for ia, key in enumerate(a): - rel[key][0].append(ia) - for ib, key in enumerate(b): - rel[key][1].append(ib) - - triples = [] - for (ias, ibs) in rel.values(): - adjacency_relations.extend( - [(i, ia, ib) for ia, ib in itertools.product(ias, ibs)] - ) - - return adjacency_relations - - -# -# """%%time -# adjacency_relations2 = adjacency_efficent_extraction_observed(ns.patterns) -# adjacency_relations3 = adjacency_efficent_extraction_consistent(ns.patterns) -# print(adjacency_relations2) -# """ - - -# In[17]: - - -# test = np.array([[0,1,2,3],[4,5,6,7]]) -# print(test) -# print() -# print(np.repeat(test[:,:,np.newaxis], 4, axis=2)) - - -# In[18]: - - -def blit(destination, sprite, upper_left, layer=False, check=False): - """ - Blits one multidimensional array into another numpy array. - """ - lower_right = [ - ((a + b) if ((a + b) < c) else c) - for a, b, c in zip(upper_left, sprite.shape, destination.shape) - ] - if min(lower_right) < 0: - return - - for i_index, i in enumerate(range(upper_left[0], lower_right[0])): - for j_index, j in enumerate(range(upper_left[1], lower_right[1])): - if (i >= 0) and (j >= 0): - if len(destination.shape) > 2: - destination[i, j, layer] = sprite[i_index, j_index] - else: - if check: - if ( - (destination[i, j] == sprite[i_index, j_index]) - or (destination[i, j] == -1) - or {sprite[i_index, j_index] == -1} - ): - destination[i, j] = sprite[i_index, j_index] - else: - print( - "ERROR, mismatch: destination[{i},{j}] = {destination[i, j]}, sprite[{i_index}, {j_index}] = {sprite[i_index, j_index]}" - ) - else: - destination[i, j] = sprite[i_index, j_index] - return destination - - -# In[19]: - - -import pprint - - -def show_adjacencies(wfc_ns, adjacency_relations_list): - try: - figadj = figure(figsize=(12, 1 + len(adjacency_relations_list)), edgecolor="b") - title("Adjacencies") - max_offset = max( - [ - abs(x) - for x in list( - itertools.chain.from_iterable(wfc_ns.adjacency_directions.values()) - ) - ] - ) - - for i, adj_rel in enumerate(adjacency_relations_list): - preview_size = wfc_ns.pattern_width + max_offset * 2 - preview_adj = np.full((preview_size, preview_size), -1, dtype=np.int64) - upper_left_of_center = CoordXY( - x=max_offset, y=max_offset - ) # (ns.pattern_width, ns.pattern_width) - # print(f"adj_rel: {adj_rel}") - blit( - preview_adj, - wfc_ns.patterns[adj_rel[1]], - upper_left_of_center, - check=True, - ) - blit( - preview_adj, - wfc_ns.patterns[adj_rel[2]], - ( - upper_left_of_center.y + wfc_ns.adjacency_directions[adj_rel[0]].y, - upper_left_of_center.x + wfc_ns.adjacency_directions[adj_rel[0]].x, - ), - check=True, - ) - - ptr = tiles_to_images( - wfc_ns, - preview_adj, - wfc_ns.tile_catalog, - wfc_ns.tile_size, - visualize=True, - ).astype(np.uint8) - - subp = subplot(math.ceil(len(adjacency_relations_list) / 4), 4, i + 1) - spi = subp.imshow(ptr) - spi.axes.tick_params( - left=False, bottom=False, labelleft=False, labelbottom=False - ) - title( - f"{i}: ({adj_rel[1]} + {adj_rel[2]}) by\n{wfc_ns.adjacency_directions[adj_rel[0]]}", - fontsize=10, - ) - - indicator_rect = matplotlib.patches.Rectangle( - (upper_left_of_center.y - 0.51, upper_left_of_center.x - 0.51), - wfc_ns.pattern_width, - wfc_ns.pattern_width, - Fill=False, - edgecolor="b", - linewidth=3.0, - linestyle=":", - ) - - spi.axes.add_artist(indicator_rect) - spi.axes.grid(False) - plt.savefig(wfc_ns.output_filename + "_adjacency.pdf", bbox_inches="tight") - plt.close() - except ValueError as e: - print(e) - - -# # Solvers - -# ## Adjacency Grid - -# In[20]: - - -def make_adjacency_grid(shape: tuple, directions): - adj_grid_shape = ((len(directions)), *shape) - - def within_bounds(x, limit): - while x < 0: - x += limit - while x > limit - 1: - x -= limit - return x - - def add_offset(a, b): - offset = [sum(x) for x in zip(a, b)] - return (within_bounds(offset[0], shape[0]), within_bounds(offset[1], shape[1])) - - adj_grid = np.zeros(adj_grid_shape, dtype=np.uint32) - - # TODO: grids bigger than max(uint32 - 1) can't index the entire array - # Therefore, output shapes should not exceed approximately 65535 x 65535 - for d_idx, d in directions.items(): - for y in range(shape[0]): - for x in range(shape[1]): - cell = (y, x) - offset_cell = add_offset(d, cell) - adj_grid[d_idx, y, x] = offset_cell[0] + (offset_cell[1] * shape[0]) - # Adds one to the index so we can use zero in MiniZinc to - # indicate non-edges for non-wrapping output images and similar - # adj_grid[d_idx,x,y] = 1 + (offset_cell[0] + (offset_cell[1] * shape[0])) - return adj_grid - - -def make_reverse_adjacency_directions(adjacency_directions): - reverse_adjacency_directions_val = {} - for k, v in adjacency_directions.items(): - reverse_adjacency_directions_val[k] = tuple([(i * -1) for i in v]) - # print(f"reverse_adjacency direction {v} to {reverse_adjacency_directions_val[k]}") - return reverse_adjacency_directions_val # , reverse_directions_by_index - - -def make_reverse_adjacency_grid(shape, directions): - reverse_directions = make_reverse_adjacency_directions(directions) - return make_adjacency_grid(shape, reverse_directions) - - -def adjacency_index(shape, index_num): - x = math.floor((index_num) % shape[0]) - y = math.floor((index_num) / shape[0]) - return (x, y) - - -def reverse_direction_index(directions): - rev_directions = make_reverse_adjacency_directions( - directions - ) # [tuple([(i * -1) for i in o]) for o in directions.values()] - inverted_offset = {} - # Match with first offset value that matches - for rev_key, rev_val in rev_directions.items(): - for key, val in directions.items(): - if val == rev_val: - inverted_offset[rev_key] = key - break - return inverted_offset - - -def get_direction_from_offset(rdirections, offset): - # TODO: Currently assumes a one-to-one mapping, which dictionaries do not enforce. - for rev_key, rev_val in rdirections.items(): - if rev_val == offset: - return rev_key - raise ValueError("Offset not found in directions.") - - -# In[21]: - - -# adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) -# print(wfc_ns_chess.adjacency_directions) -# reverse_adjacency_directions = make_reverse_adjacency_directions(wfc_ns_chess.adjacency_directions) -# print(reverse_adjacency_directions) -# reverse_adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, reverse_adjacency_directions) -# adjacency_grid = make_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) -# reverse_adjacency_grid = make_reverse_adjacency_grid(wfc_ns_chess.generated_size, wfc_ns_chess.adjacency_directions) - -# print(f"reverse: {reverse_direction_index(wfc_ns_chess.adjacency_directions)}") - -# print(wfc_ns_chess.adjacency_directions) -##print(adjacency_grid) -##print(reverse_adjacency_grid) -# print(adjacency_index(wfc_ns_chess.generated_size, 2)) -# print(reverse_direction_index(wfc_ns_chess.adjacency_directions)) -# print(reverse_direction_index(wfc_ns_chess.adjacency_directions)[get_direction_from_offset(wfc_ns_chess.adjacency_directions, (0,-1))]) - - -if __name__ == "__main__": - import types - - test_ns = types.SimpleNamespace( - img_filename="red_maze.png", - seed=87386, - tile_size=1, - pattern_width=2, - channels=3, - adjacency_directions=dict( - enumerate( - [ - CoordXY(x=0, y=-1), - CoordXY(x=1, y=0), - CoordXY(x=0, y=1), - CoordXY(x=-1, y=0), - ] - ) - ), - periodic_input=True, - periodic_output=True, - generated_size=(3, 3), - screenshots=1, - iteration_limit=0, - allowed_attempts=1, - ) - test_ns = wfc_utilities.find_pattern_center(test_ns) - test_ns = wfc_utilities.load_visualizer(test_ns) - test_ns.img = load_source_image(test_ns.img_filename) - ( - test_ns.tile_catalog, - test_ns.tile_grid, - test_ns.code_list, - test_ns.unique_tiles, - ) = make_tile_catalog(test_ns) - test_ns.tile_ids = { - v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() - } - test_ns.tile_weights = { - a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) - } - import doctest - - doctest.testmod() diff --git a/wfc/wfc1/wfc_control.py b/wfc/wfc1/wfc_control.py deleted file mode 100644 index 96d2f46..0000000 --- a/wfc/wfc1/wfc_control.py +++ /dev/null @@ -1,408 +0,0 @@ -# -*- coding: utf-8 -*- - - -import types -import wfc.wfc_utilities -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE -from wfc.wfc_tiles import ( - load_source_image, - image_to_tiles, - make_tile_catalog, - show_input_to_output, - show_extracted_tiles, - show_false_color_tile_grid, -) -import wfc.wfc_patterns -from wfc.wfc_patterns import ( - make_pattern_catalog_no_rotations, - show_pattern_catalog, - make_pattern_catalog_with_symmetry, -) -from wfc.wfc_adjacency import adjacency_extraction_consistent, show_adjacencies -import wfc.wfc_solver -from wfc.wfc_solver import wfc_run -from wfc.wfc_solver import ( - wfc_init, - show_wfc_patterns, - show_pattern_adjacency, - visualize_propagator_matrix, - visualize_entropy, - wfc_clear, - visualize_compatible_count, - show_rendered_patterns, - render_patterns_to_output, - wfc_observe, - wfc_partial_output, - wrap_coords, - show_crystal_time, -) - -# from wfc.wfc_minizinc import mz_run - -import logging - -logging.basicConfig(level=logging.INFO) -wfc_logger = logging.getLogger() - -import numpy as np - -wfc_logger.info(f"Using numpy version {np.__version__}") - -np.set_printoptions(threshold=np.inf) - -import xml.etree.ElementTree as ET - -import cProfile, pstats -import time - -import moviepy.editor as mpy -from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter - - -import copy - - -def string2bool(strn): - if isinstance(strn, bool): - return strn - return strn.lower() in ["true"] - - -def wfc_execute(WFC_VISUALIZE=False, WFC_PROFILE=False, WFC_LOGGING=False): - - solver_to_use = "default" # "minizinc" - - wfc_stats_tracking = { - "observations": 0, - "propagations": 0, - "time_start": None, - "time_end": None, - "choices_before_success": 0, - "choices_per_run": [], - "success": False, - } - wfc_stats_data = [] - stats_file_name = f"output/stats_{time.time()}.tsv" - - with open(stats_file_name, "a+") as stats_file: - stats_file.write( - "id\tname\tsuccess?\tattempts\tobservations\tpropagations\tchoices_to_solution\ttotal_observations_before_solution_in_last_restart\ttotal_choices_before_success_across_restarts\tbacktracking_total\ttime_passed\ttime_start\ttime_end\tfinal_time_end\tgenerated_size\tpattern_count\tseed\tbacktracking?\tallowed_restarts\tforce_the_use_of_all_patterns?\toutput_filename\n" - ) - - default_backtracking = False - default_allowed_attempts = 10 - default_force_use_all_patterns = False - - xdoc = ET.ElementTree(file="samples_original.xml") - counter = 0 - choices_before_success = 0 - for xnode in xdoc.getroot(): - counter += 1 - choices_before_success = 0 - if "#comment" == xnode.tag: - continue - - name = xnode.get("name", "NAME") - global hackstring - hackstring = name - print("< {0} ".format(name), end="") - if "backtracking_on" == xnode.tag: - default_backtracking = True - if "backtracking_off" == xnode.tag: - default_backtracking = False - if "one_allowed_attempts" == xnode.tag: - default_allowed_attempts = 1 - if "ten_allowed_attempts" == xnode.tag: - default_allowed_attempts = 10 - if "force_use_all_patterns" == xnode.tag: - default_force_use_all_patterns = True - if "overlapping" == xnode.tag: - choices_before_success = 0 - print("beginning...") - print(xnode.attrib) - current_output_file_number = 97000 + (counter * 10) - wfc_ns = types.SimpleNamespace( - output_path="output/", - img_filename="samples/" - + xnode.get("name", "NAME") - + ".png", # name of the input file - output_file_number=current_output_file_number, - operation_name=xnode.get("name", "NAME"), - output_filename="output/" - + xnode.get("name", "NAME") - + "_" - + str(current_output_file_number) - + "_" - + str(time.time()) - + ".png", # name of the output file - debug_log_filename="output/" - + xnode.get("name", "NAME") - + "_" - + str(current_output_file_number) - + "_" - + str(time.time()) - + ".log", - seed=11975, # seed for random generation, can be any number - tile_size=int(xnode.get("tile_size", 1)), # size of tile, in pixels - pattern_width=int( - xnode.get("N", 2) - ), # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - symmetry=int(xnode.get("symmetry", 8)), - ground=int(xnode.get("ground", 0)), - adjacency_directions=dict( - enumerate( - [ - CoordXY(x=0, y=-1), - CoordXY(x=1, y=0), - CoordXY(x=0, y=1), - CoordXY(x=-1, y=0), - ] - ) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=string2bool( - xnode.get("periodicInput", True) - ), # Does the input wrap? - periodic_output=string2bool( - xnode.get("periodicOutput", False) - ), # Do we want the output to wrap? - generated_size=( - int(xnode.get("width", 48)), - int(xnode.get("height", 48)), - ), # Size of the final image - screenshots=int( - xnode.get("screenshots", 3) - ), # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=int( - xnode.get("iteration_limit", 0) - ), # After this many iterations, time out. 0 = never time out. - allowed_attempts=int( - xnode.get("allowed_attempts", default_allowed_attempts) - ), # Give up after this many contradictions - stats_tracking=wfc_stats_tracking.copy(), - backtracking=string2bool( - xnode.get("backtracking", default_backtracking) - ), - force_use_all_patterns=default_force_use_all_patterns, - force_fail_first_solution=False, - ) - wfc_ns.stats_tracking["choices_before_success"] += choices_before_success - wfc_ns.stats_tracking["time_start"] = time.time() - pr = cProfile.Profile() - pr.enable() - wfc_ns = find_pattern_center(wfc_ns) - wfc_ns = wfc.wfc_utilities.load_visualizer(wfc_ns) - ## - ## Load image and make tile data structures - ## - wfc_ns.img = load_source_image(wfc_ns.img_filename) - wfc_ns.channels = wfc_ns.img.shape[ - -1 - ] # detect if it uses channels other than RGB... - wfc_ns.tiles = image_to_tiles(wfc_ns.img, wfc_ns.tile_size) - ( - wfc_ns.tile_catalog, - wfc_ns.tile_grid, - wfc_ns.code_list, - wfc_ns.unique_tiles, - ) = make_tile_catalog(wfc_ns) - wfc_ns.tile_ids = { - v: k for k, v in dict(enumerate(wfc_ns.unique_tiles[0])).items() - } - wfc_ns.tile_weights = { - a: b for a, b in zip(wfc_ns.unique_tiles[0], wfc_ns.unique_tiles[1]) - } - - if WFC_VISUALIZE: - show_input_to_output(wfc_ns) - show_extracted_tiles(wfc_ns) - show_false_color_tile_grid(wfc_ns) - - ( - wfc_ns.pattern_catalog, - wfc_ns.pattern_weights, - wfc_ns.patterns, - wfc_ns.pattern_grid, - ) = make_pattern_catalog_with_symmetry( - wfc_ns.tile_grid, - wfc_ns.pattern_width, - wfc_ns.symmetry, - wfc_ns.periodic_input, - ) - if WFC_VISUALIZE: - show_pattern_catalog(wfc_ns) - adjacency_relations = adjacency_extraction_consistent( - wfc_ns, wfc_ns.patterns - ) - if WFC_VISUALIZE: - show_adjacencies(wfc_ns, adjacency_relations[:256]) - wfc_ns = wfc.wfc_patterns.detect_ground(wfc_ns) - pr.disable() - - screenshots_collected = 0 - while screenshots_collected < wfc_ns.screenshots: - wfc_logger.info(f"Starting solver #{screenshots_collected}") - screenshots_collected += 1 - wfc_ns.seed += 100 - - choice_before_success = 0 - # wfc_ns.stats_tracking["choices_before_success"] = 0# += choices_before_success - wfc_ns.stats_tracking["time_start"] = time.time() - wfc_ns.stats_tracking["final_time_end"] = None - - # update output name so each iteration has a unique filename - output_filename = ( - "output/" - + xnode.get("name", "NAME") - + "_" - + str(current_output_file_number) - + "_" - + str(time.time()) - + "_" - + str(wfc_ns.seed) - + ".png", - ) # name of the output file - - profile_filename = ( - "" - + str(wfc_ns.output_path) - + "setup_" - + str(wfc_ns.output_file_number) - + "_" - + str(wfc_ns.seed) - + "_" - + str(time.time()) - + "_" - + str(wfc_ns.seed) - + ".profile" - ) - if WFC_PROFILE: - with open(profile_filename, "w") as profile_file: - ps = pstats.Stats(pr, stream=profile_file) - ps.sort_stats("cumtime", "ncalls") - ps.print_stats(20) - solution = None - - if "minizinc" == solver_to_use: - attempt_count = 0 - # while attempt_count < wfc_ns.allowed_attempts: - # attempt_count += 1 - # solution = mz_run(wfc_ns) - # solution.wfc_ns.stats_tracking["attempt_count"] = attempt_count - # solution.wfc_ns.stats_tracking["choices_before_success"] += solution.wfc_ns.stats_tracking["observations"] - - else: - if True: - attempt_count = 0 - # print("allowed attempts: " + str(wfc_ns.allowed_attempts)) - attempt_wfc_ns = copy.deepcopy(wfc_ns) - attempt_wfc_ns.stats_tracking["time_start"] = time.time() - attempt_wfc_ns.stats_tracking["choices_before_success"] = 0 - attempt_wfc_ns.stats_tracking[ - "total_observations_before_success" - ] = 0 - wfc.wfc_solver.reset_backtracking_count() # reset the count of how many times we've backtracked, because multiple attempts are handled here instead of there - while attempt_count < wfc_ns.allowed_attempts: - attempt_count += 1 - print(attempt_count, end=" ") - attempt_wfc_ns.seed += 7 # change seed for each attempt... - solution = wfc_run( - attempt_wfc_ns, - visualize=WFC_VISUALIZE, - logging=WFC_LOGGING, - ) - solution.wfc_ns.stats_tracking[ - "attempt_count" - ] = attempt_count - solution.wfc_ns.stats_tracking[ - "choices_before_success" - ] += solution.wfc_ns.stats_tracking["observations"] - attempt_wfc_ns.stats_tracking[ - "total_observations_before_success" - ] += solution.wfc_ns.stats_tracking["total_observations"] - wfc_logger.info( - "result: {} is {}".format( - attempt_count, solution.result - ) - ) - if solution.result == -2: - attempt_count = wfc_ns.allowed_attempts - solution.wfc_ns.stats_tracking["time_end"] = time.time() - wfc_stats_data.append(solution.wfc_ns.stats_tracking.copy()) - solution.wfc_ns.stats_tracking["final_time_end"] = time.time() - print("tracking choices before success...") - choices_before_success = solution.wfc_ns.stats_tracking[ - "choices_before_success" - ] - time_passed = None - if None != solution.wfc_ns.stats_tracking["time_end"]: - time_passed = ( - solution.wfc_ns.stats_tracking["time_end"] - - solution.wfc_ns.stats_tracking["time_start"] - ) - else: - if None != solution.wfc_ns.stats_tracking["final_time_end"]: - time_passed = ( - solution.wfc_ns.stats_tracking["final_time_end"] - - solution.wfc_ns.stats_tracking["time_start"] - ) - - print("...finished calculating time passed") - # print(wfc_stats_data) - print("writing stats...", end="") - - with open(stats_file_name, "a+") as stats_file: - stats_file.write( - f"{solution.wfc_ns.output_file_number}\t{solution.wfc_ns.operation_name}\t{solution.wfc_ns.stats_tracking['success']}\t{solution.wfc_ns.stats_tracking['attempt_count']}\t{solution.wfc_ns.stats_tracking['observations']}\t{solution.wfc_ns.stats_tracking['propagations']}\t{solution.wfc_ns.stats_tracking['choices_before_success']}\t{solution.wfc_ns.stats_tracking['total_observations']}\t{attempt_wfc_ns.stats_tracking['total_observations_before_success']}\t{solution.backtracking_total}\t{time_passed}\t{solution.wfc_ns.stats_tracking['time_start']}\t{solution.wfc_ns.stats_tracking['time_end']}\t{solution.wfc_ns.stats_tracking['final_time_end']}\t{solution.wfc_ns.generated_size}\t{len(solution.wfc_ns.pattern_weights.keys())}\t{solution.wfc_ns.seed}\t{solution.wfc_ns.backtracking}\t{solution.wfc_ns.allowed_attempts}\t{solution.wfc_ns.force_use_all_patterns}\t{solution.wfc_ns.output_filename}\n" - ) - print("done") - - if WFC_VISUALIZE: - print("visualize") - if None == solution: - print("n u l l") - # print(solution) - print(1) - solution_vis = wfc.wfc_solver.render_recorded_visualization( - solution.recorded_vis - ) - # print(solution) - print(2) - - video_fn = f"{solution.wfc_ns.output_path}/crystal_example_{solution.wfc_ns.output_file_number}_{time.time()}.mp4" - wfc_logger.info("*****************************") - wfc_logger.warning(video_fn) - print( - f"solver recording stack len - {len(solution_vis.solver_recording_stack)}" - ) - print(solution_vis.solver_recording_stack[0].shape) - if len(solution_vis.solver_recording_stack) > 0: - wfc_logger.info(solution_vis.solver_recording_stack[0].shape) - writer = FFMPEG_VideoWriter( - video_fn, - [ - solution_vis.solver_recording_stack[0].shape[0], - solution_vis.solver_recording_stack[0].shape[1], - ], - 12.0, - ) - for img_data in solution_vis.solver_recording_stack: - writer.write_frame(img_data) - print("!", end="") - writer.close() - mpy.ipython_display(video_fn, height=700) - print("recording done") - if WFC_VISUALIZE: - solution = wfc_partial_output(solution) - show_rendered_patterns(solution, True) - print("render to output") - render_patterns_to_output(solution, True, False) - print("completed") - print("\n{0} >".format(name)) - - elif "simpletiled" == xnode.tag: - print("> ", end="\n") - continue - else: - continue diff --git a/wfc/wfc1/wfc_example.py b/wfc/wfc1/wfc_example.py deleted file mode 100644 index adc6a33..0000000 --- a/wfc/wfc1/wfc_example.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 11 12:08:10 2019 - -@author: Isaac -""" - -import matplotlib - -# matplotlib.use('agg') - -import types -import math -from IPython.core.debugger import set_trace -import collections - -import matplotlib.pyplot as plt - -# from matplotlib.pyplot import figure, subplot, subplots, title, matshow - - -import wfc_utilities -from wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -from wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE - -import wfc_tiles -from wfc_tiles import ( - load_source_image, - image_to_tiles, - tiles_to_images, - make_tile_catalog, - show_input_to_output, - show_extracted_tiles, - show_false_color_tile_grid, -) - -import wfc_patterns -from wfc_patterns import ( - make_pattern_catalog_no_rotations, - render_pattern, - show_pattern_catalog, -) - -from wfc_adjacency import ( - adjacency_extraction_consistent, - is_valid_overlap_xy, - blit, - show_adjacencies, -) - -import wfc_solver -from wfc_solver import ( - wfc_init, - wfc_run, - show_wfc_patterns, - show_pattern_adjacency, - visualize_propagator_matrix, - visualize_entropy, - wfc_clear, - visualize_compatible_count, - show_rendered_patterns, - render_patterns_to_output, - wfc_observe, - wfc_partial_output, - wrap_coords, - show_crystal_time, -) -import logging -import numpy as np - -print(f"Using numpy version {np.__version__}") - -np.set_printoptions(threshold=np.inf) - -## -## Set up the namespace -## - - -wfc_ns_chess = types.SimpleNamespace( - output_path="output/", - img_filename="samples/Red Maze.png", # name of the input file - output_file_number=40, - seed=587386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate( - [ - CoordXY(x=0, y=-1), - CoordXY(x=1, y=0), - CoordXY(x=0, y=1), - CoordXY(x=-1, y=0), - ] - ) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(16, 8), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=1, -) # Give up after this many contradictions - -wfc_ns_chess = find_pattern_center(wfc_ns_chess) - -wfc_ns_chess = wfc_utilities.load_visualizer(wfc_ns_chess) - -## -## Load image and make tile data structures -## - -wfc_ns_chess.img = load_source_image(wfc_ns_chess.img_filename) -wfc_ns_chess.tiles = image_to_tiles(wfc_ns_chess.img, wfc_ns_chess.tile_size) -( - wfc_ns_chess.tile_catalog, - wfc_ns_chess.tile_grid, - wfc_ns_chess.code_list, - wfc_ns_chess.unique_tiles, -) = make_tile_catalog(wfc_ns_chess) -wfc_ns_chess.tile_ids = { - v: k for k, v in dict(enumerate(wfc_ns_chess.unique_tiles[0])).items() -} -print(wfc_ns_chess.unique_tiles) -wfc_ns_chess.tile_weights = { - a: b for a, b in zip(wfc_ns_chess.unique_tiles[0], wfc_ns_chess.unique_tiles[1]) -} -print(wfc_ns_chess.tile_weights) - -# print("wfc_ns_chess.tile_catalog") -# print(wfc_ns_chess.tile_catalog) -# print("wfc_ns_chess.tile_grid") -# print(wfc_ns_chess.tile_grid) -# print("wfc_ns_chess.code_list") -# print(wfc_ns_chess.code_list) -# print("wfc_ns_chess.unique_tiles") -# print(wfc_ns_chess.unique_tiles) - -# assert False - -show_input_to_output(wfc_ns_chess) -show_extracted_tiles(wfc_ns_chess) -show_false_color_tile_grid(wfc_ns_chess) - -# im = np.array([[[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]], -# [[255, 0, 0], [255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0],[255, 0, 0]]]) - -# plt.imshow(im) -# plt.show() - - -# def adjacency_extraction_consistent(wfc_ns, pattern_cat): -# """Takes a pattern catalog, returns a list of all legal adjacencies.""" -# # This is a brute force implementation. We should really use the adjacency list we've already calculated... -# legal = [] -# #print(f"pattern_cat\n{pattern_cat}") -# for p1, pattern1 in enumerate(pattern_cat): -# for d_index, d in enumerate(wfc_ns.adjacency_directions): -# for p2, pattern2 in enumerate(pattern_cat): -# if is_valid_overlap_xy(d, p1, p2, pattern_cat, wfc_ns.pattern_width, wfc_ns.adjacency_directions): -# legal.append((d_index, p1, p2)) -# return legal - -# from pycallgraph import PyCallGraph -# from pycallgraph.output import GraphvizOutput -# with PyCallGraph(output=GraphvizOutput()): - - -## -## Patterns -## - -( - wfc_ns_chess.pattern_catalog, - wfc_ns_chess.pattern_weights, - wfc_ns_chess.patterns, -) = make_pattern_catalog_no_rotations( - wfc_ns_chess.tile_grid, wfc_ns_chess.pattern_width -) -show_pattern_catalog(wfc_ns_chess) -adjacency_relations = adjacency_extraction_consistent( - wfc_ns_chess, wfc_ns_chess.patterns -) -# print(adjacency_relations) -show_adjacencies(wfc_ns_chess, adjacency_relations[:256]) -# print(wfc_ns_chess.patterns) - - -## -## Run the solver -## - -solution = wfc_run(wfc_ns_chess, visualize=False) - -## -## Output the results -## - -import moviepy.editor as mpy -from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter - -# from lucid_serialize_array import _normalize_array - -solution = wfc_solver.render_recorded_visualization(solution) - -video_fn = f"{solution.wfc_ns.output_path}/crystal_example_{solution.wfc_ns.output_file_number}.mp4" -print("*****************************") -print(solution.solver_recording_stack[0].shape) -writer = FFMPEG_VideoWriter(video_fn, solution.solver_recording_stack[0].shape, 12.0) -# for i in range(24): -# writer.write_frame(solution.solver_recording_stack[0]) -for img_data in solution.solver_recording_stack: - writer.write_frame(img_data) - # print(_normalize_array(img_data)) - print("!", end="") -# for i in range(24): -# writer.write_frame(solution.solver_recording_stack[-1]) -# for i in range(24): -# writer.write_frame(solution.solver_recording_stack[0]) -# for img_data in solution.solver_recording_stack: -# writer.write_frame(img_data) -# #print(_normalize_array(img_data)) -# print('!',end='') -# for i in range(24): -# writer.write_frame(solution.solver_recording_stack[-1]) - -writer.close() - -mpy.ipython_display(video_fn, height=700) - -solution = wfc_partial_output(solution) -show_rendered_patterns(solution, True) -render_patterns_to_output(solution, True) -# show_crystal_time(solution, True) -# show_rendered_patterns(solution, False) -# render_patterns_to_output(solution, False) diff --git a/wfc/wfc1/wfc_extra.py b/wfc/wfc1/wfc_extra.py deleted file mode 100644 index db73ad1..0000000 --- a/wfc/wfc1/wfc_extra.py +++ /dev/null @@ -1,159 +0,0 @@ -# In[ ]: - - -wfc_ns_chess48 = types.SimpleNamespace( - img_filename="Chess.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_chess48 = prepare_wfc_namespace(wfc_ns_chess48, visualize=True) -wfc_main(wfc_ns_chess48) - - -# In[ ]: - - -wfc_ns_chess47 = types.SimpleNamespace( - img_filename="Chess.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(47, 47), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_chess47 = prepare_wfc_namespace(wfc_ns_chess47, visualize=True) -wfc_main(wfc_ns_chess47) - - -# In[ ]: - - -wfc_ns_blackdots = types.SimpleNamespace( - img_filename="blackdots.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_blackdots = prepare_wfc_namespace(wfc_ns_blackdots, visualize=True) -wfc_main(wfc_ns_blackdots) - - -# In[ ]: - - -wfc_ns_blackdotsred = types.SimpleNamespace( - img_filename="blackdotsred.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_blackdotsred = prepare_wfc_namespace(wfc_ns_blackdotsred, visualize=True) -wfc_main(wfc_ns_blackdotsred) - - -# In[ ]: - - -wfc_ns_blackdotsstripe = types.SimpleNamespace( - img_filename="blackdotsstripe.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_blackdotsstripe = prepare_wfc_namespace(wfc_ns_blackdotsstripe, visualize=True) -wfc_main(wfc_ns_blackdotsstripe) - - -# In[ ]: - - -wfc_ns_Skyline = types.SimpleNamespace( - img_filename="Skyline.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=2, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_Skyline = prepare_wfc_namespace(wfc_ns_Skyline, visualize=True) -wfc_main(wfc_ns_Skyline) - - -# In[ ]: - - -wfc_ns_flowers = types.SimpleNamespace( - img_filename="Flowers.png", # name of the input file - seed=87386, # seed for random generation, can be any number - tile_size=1, # size of tile, in pixels - pattern_width=3, # Size of the patterns we want. 2x2 is the minimum, larger scales get slower fast. - channels=3, # Color channels in the image (usually 3 for RGB) - adjacency_directions=dict( - enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)]) - ), # The list of adjacencies that we care about - these will be turned into the edges of the graph - periodic_input=True, # Does the input wrap? - periodic_output=True, # Do we want the output to wrap? - generated_size=(48, 48), # Size of the final image - screenshots=1, # Number of times to run the algorithm, will produce this many distinct outputs - iteration_limit=0, # After this many iterations, time out. 0 = never time out. - allowed_attempts=2, -) # Give up after this many contradictions -wfc_ns_flowers = prepare_wfc_namespace(wfc_ns_flowers, visualize=True) -wfc_main(wfc_ns_flowers) diff --git a/wfc/wfc1/wfc_minizinc.py b/wfc/wfc1/wfc_minizinc.py deleted file mode 100644 index bf09873..0000000 --- a/wfc/wfc1/wfc_minizinc.py +++ /dev/null @@ -1,114 +0,0 @@ -# An interface to other solvers via MiniZinc -import pymzn -import time -import wfc.wfc_solver -from wfc.wfc_adjacency import adjacency_extraction_consistent -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center - - -import logging - -logging.basicConfig(level=logging.INFO) -wfc_logger = logging.getLogger() - -import pdb - - -def calculate_adjacency_grid(shape: tuple, directions): - adj_grid_shape = ((len(directions)), *shape) - - def within_bounds(x, limit): - while x < 0: - x += limit - while x > limit - 1: - x -= limit - return x - - def add_offset(a, b): - offset = [sum(x) for x in zip(a, b)] - return (within_bounds(offset[0], shape[0]), within_bounds(offset[1], shape[1])) - - adj_grid = numpy.zeros(adj_grid_shape, dtype=numpy.uint32) - # TODO: grids bigger than max(uint32 - 1) can't index the entire array - # Therefore, output shapes should not exceed approximately 65535 x 65535 - for d_idx, d in enumerate(directions): - for x in range(shape[0]): - for y in range(shape[1]): - cell = (x, y) - offset_cell = add_offset(d, cell) - # Adds one to the index so we can use zero in MiniZinc to - # indicate non-edges for non-wrapping output images and similar - adj_grid[d_idx, y, x] = 1 + ( - offset_cell[0] + (offset_cell[1] * shape[0]) - ) - return adj_grid - - -solns = pymzn.minizinc("knapsack.mzn", "knapsack.dzn", data={"capacity": 20}) -print(solns) - -pymzn.dict2dzn( - { - "w": 8, - "h": 8, - "pattern_count": 95, - "direction_count": 4, - "adjacency_count": 992, - "pattern_names": [], - "relation_matrix": [], - "adjaceny_table": [], - "adjacency_matrix": [], - }, - fout="test.dzn", -) - - -def mz_init(prestate): - prestate.adjacency_directions_rc = { - i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() - } - prestate = wfc.wfc_utilities.find_pattern_center(prestate) - wfc_state = types.SimpleNamespace(wfc_ns=prestate) - - wfc_state.result = None - wfc_state.adjacency_relations = adjacency_extraction_consistent( - wfc_state.wfc_ns, wfc_state.wfc_ns.patterns - ) - wfc_state.patterns = np.array(list(wfc_state.wfc_ns.pattern_catalog.keys())) - wfc_state.pattern_translations = list(wfc_state.wfc_ns.pattern_catalog.values()) - wfc_state.number_of_patterns = wfc_state.patterns.size - wfc_state.number_of_directions = len(wfc_state.wfc_ns.adjacency_directions) - - wfc_state.propagator_matrix = np.zeros( - ( - wfc_state.number_of_directions, - wfc_state.number_of_patterns, - wfc_state.number_of_patterns, - ), - dtype=np.bool_, - ) - for d, p1, p2 in wfc_state.adjacency_relations: - wfc_state.propagator_matrix[(d, p1, p2)] = True - - wfc_state.mz_dzn = { - "w": wfc_state.wfc_ns.generated_size[0], - "h": wfc_state.wfc_ns.generated_size[1], - "pattern_count": wfc_state.number_of_patterns, - "direction_count": wfc_state.number_of_directions, - "adjacency_count": len(wfc_state.adjacency_relations), - "pattern_names": list( - wfc_state.patterns, zip(range(wfc_state.number_of_patterns)) - ), - "relation_matrix": calculate_adjacency_grid( - wfc_state.wfc_ns.generated_size, wfc_state.wfc_ns.adjacency_directions - ), - "adjaceny_table": [], - "adjacency_matrix": [], - } - - -def mz_run(wfc_seed_state): - wfc_logger.info("Invoking MiniZinc solver") - wfc_state = mz_init(wfc_seed_state) - - return wfc_state diff --git a/wfc/wfc1/wfc_patterns.py b/wfc/wfc1/wfc_patterns.py deleted file mode 100644 index 06ac170..0000000 --- a/wfc/wfc1/wfc_patterns.py +++ /dev/null @@ -1,412 +0,0 @@ -import logging - -# from matplotlib.pyplot import figure, subplot, subplots, title, matshow -import wfc.wfc_utilities -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.pyplot import figure, subplot, subplots, title, matshow - -import logging - -logging.basicConfig(level=logging.INFO) -wfc_logger = logging.getLogger() - -# # Patterns -# -# We could figure out the valid adjacencies between the tiles by hand and WFC would work just fine---in fact, Gumin's original SimpleTile model is exactly that. But lets bring automation on our side and calculate the adjacencies instead. -# -# To do this, we're going to use Gumin's abstraction of patterns. A pattern is simply a region of tiles, in this case in the original image. Instead of talking about adjacencies between patterns, we will use adjacencies between _patterns_. This lets us automatically capture more expressive information about the source image. -# -# The size of the patterns influences the results. 1x1 patterns behave exactly as tiles. 2x2 and 3x3 are common in generating pixel art. Larger patterns are better at capturing relationships but need more valid samples to retain the same flexibility. Which also leads to larger patterns rapidly getting more and more computationally expensive. - -# ## Extracting Patterns - -# In[12]: - - -# def unique_patterns_2d(a, k): -# assert(k >= 1) -# -# a = np.pad(a, ((0,k-1),(0,k-1),*(((0,0),)*(len(a.shape)-2))), mode='wrap') -# -# patches = np.lib.stride_tricks.as_strided( -# a, -# (a.shape[0]-k+1,a.shape[1]-k+1,k,k,*a.shape[2:]), -# a.strides[:2] + a.strides[:2] + a.strides[2:], -# writeable=False) -# -# patch_codes = hash_downto(patches,2) - - -# uc, ui = np.unique(patch_codes, return_index=True) -# locs = np.unravel_index(ui, patch_codes.shape) -# up = patches[locs[0],locs[1]] -# ids = np.vectorize({c: i for i,c in enumerate(uc)}.get)(patch_codes) - -# return ids, up - -# def make_pattern_catalog(img_ns): - -# ns.number_of_rotations = 4# TODO: take this as an argument -# more_grids = [img_ns.tile_grid] -# for rotation in range(1, ns.number_of_rotations): -# rot_a = np.rot90(img_ns.tile_grid, rotation) -# more_grids.append(rot_a) -# ref_v = np.flip(rot_a, 0) -# more_grids.append(ref_v) -# ref_h = np.flip(rot_a, 1) -# more_grids.append(ref_h) - -# all_pattern_catalog = {} -# all_pattern_weights = {} -# all_patterns = np.zeros((0,img_ns.pattern_width,img_ns.pattern_width)) -# for m_grid in more_grids: -# print(m_grid.shape) - -# pattern_codes, patterns = unique_patterns_2d(m_grid, img_ns.pattern_width) -# print(type(pattern_codes), pattern_codes.shape) -# print(type(patterns), patterns.shape) - -# logging.info(f'{len(patterns)} unique patterns') -# assert np.array_equal(m_grid, patterns[pattern_codes][:,:,0,0]) -# pattern_catalog = {i:x for i,x in enumerate(patterns)} -# logging.debug(f'\npattern_codes: {pattern_codes}') -# pattern_weights = {i:(pattern_codes == i).sum() for i,x in enumerate(patterns)} -# logging.debug(f'pattern_weights: {pattern_weights}') -# all_pattern_catalog = {**all_pattern_catalog, **pattern_catalog} -# all_patterns = np.concatenate(all_patterns, patterns) - -# return all_pattern_catalog, pattern_weights, patterns - - -# ns.pattern_catalog, ns.pattern_weights, ns.patterns = make_pattern_catalog(ns) - - -def unique_patterns_2d(a, k, periodic_input): - assert k >= 1 - if periodic_input: - a = np.pad( - a, ((0, k - 1), (0, k - 1), *(((0, 0),) * (len(a.shape) - 2))), mode="wrap" - ) - else: - # TODO: implement non-wrapped image handling - # a = np.pad(a, ((0,k-1),(0,k-1),*(((0,0),)*(len(a.shape)-2))), mode='constant', constant_values=None) - a = np.pad( - a, ((0, k - 1), (0, k - 1), *(((0, 0),) * (len(a.shape) - 2))), mode="wrap" - ) - - patches = np.lib.stride_tricks.as_strided( - a, - (a.shape[0] - k + 1, a.shape[1] - k + 1, k, k, *a.shape[2:]), - a.strides[:2] + a.strides[:2] + a.strides[2:], - writeable=False, - ) - patch_codes = hash_downto(patches, 2) - uc, ui = np.unique(patch_codes, return_index=True) - locs = np.unravel_index(ui, patch_codes.shape) - up = patches[locs[0], locs[1]] - ids = np.vectorize({c: i for i, c in enumerate(uc)}.get)(patch_codes) - wfc_logger.debug(ids) - return ids, up - - -def unique_patterns_brute_force(grid, size, periodic_input): - padded_grid = np.pad( - grid, - ((0, size - 1), (0, size - 1), *(((0, 0),) * (len(grid.shape) - 2))), - mode="wrap", - ) - patches = [] - for x in range(grid.shape[0]): - row_patches = [] - for y in range(grid.shape[1]): - row_patches.append( - np.ndarray.tolist(padded_grid[x : x + size, y : y + size]) - ) - patches.append(row_patches) - patches = np.array(patches) - patch_codes = hash_downto(patches, 2) - uc, ui = np.unique(patch_codes, return_index=True) - locs = np.unravel_index(ui, patch_codes.shape) - up = patches[locs[0], locs[1]] - ids = np.vectorize({c: i for i, c in enumerate(uc)}.get)(patch_codes) - wfc_logger.debug(ids) - return ids, up - - -def make_pattern_catalog_from_grid(tile_grid, pattern_width, periodic_input): - pattern_codes, patterns = unique_patterns_2d( - tile_grid, pattern_width, periodic_input - ) - - VERIFY = False - if VERIFY: - pattern_codes2, patterns2 = unique_patterns_brute_force( - tile_grid, pattern_width, periodic_input - ) - assert np.array_equal(pattern_codes, pattern_codes2) - assert np.array_equal(patterns, patterns2) - logging.info(f"{len(patterns)} unique patterns") - - assert np.array_equal(tile_grid, patterns[pattern_codes][:, :, 0, 0]) - pattern_catalog = {i: x for i, x in enumerate(patterns)} - logging.debug(f"\npattern_codes: {pattern_codes}") - pattern_weights = {i: (pattern_codes == i).sum() for i, x in enumerate(patterns)} - logging.debug(f"pattern_weights: {pattern_weights}") - - return pattern_catalog, pattern_weights, patterns, pattern_codes - - -def reflect_pattern(pattern): - # print(f"reflect:\n{pattern}\nto\n{np.fliplr(pattern).copy()}\n") - return np.fliplr(pattern).copy() - - -def rotate_pattern(pattern): - # print(f"rotate:\n{pattern}\nto\n{np.rot90(pattern).copy()}\n") - return np.rot90(pattern).copy() - - -def make_pattern_catalog_with_symmetry( - tile_grid, pattern_width, symmetry, periodic_input -): - ( - pattern_catalog, - pattern_weights, - patterns, - pattern_grid, - ) = make_pattern_catalog_from_grid(tile_grid, pattern_width, periodic_input) - # print(patterns.shape) - # print('~~~~~~~~~') - if symmetry > 1: - for i in list(pattern_catalog.keys()): - base_pattern = pattern_catalog[i].copy() - for sym_op in range(2, symmetry + 1): - # print(f"{sym_op} and {(sym_op % 2 == 0)}") - r_func = reflect_pattern if (sym_op % 2 == 0) else rotate_pattern - # print(r_func.__name__) - new_pattern = r_func(base_pattern) - try: - pattern_index = [ - np.array_equal(new_pattern, x) for x in pattern_catalog.values() - ].index(True) - pattern_weights[pattern_index] += 1 - except ValueError: - new_index = len(pattern_weights) - pattern_catalog[new_index] = new_pattern.copy() - pattern_weights[new_index] = 1 - patterns = np.append(patterns, [new_pattern.copy()], axis=0) - # print(f"added_pattern {new_index}") - base_pattern = new_pattern.copy() - logging.info(f"{len(patterns)} unique patterns after symmetry {symmetry}") - print(patterns.shape) - # assert False - # print(pattern_catalog) - # print(patterns) - # assert False - return pattern_catalog, pattern_weights, patterns, pattern_grid - - -def find_last_patterns(pattern_grid, number_of_last_patterns): - last_patterns = [] - for x in reversed(range(pattern_grid.shape[0])): - for y in reversed(range(pattern_grid.shape[1])): - current_pattern = pattern_grid[x][y] - if not (current_pattern in last_patterns): - last_patterns.append(current_pattern) - if len(last_patterns) >= abs(number_of_last_patterns): - return last_patterns - return last_patterns - - -def detect_ground(wfc_ns): - wfc_ns.last_patterns = [] - if wfc_ns.ground != 0: - wfc_ns.last_patterns = find_last_patterns(wfc_ns.pattern_grid, wfc_ns.ground) - return wfc_ns - - -def make_pattern_catalog_with_rotation(tile_grid, pattern_width): - the_pattern_catalog = {} - the_pattern_weights = {} - the_patterns = np.zeros((0, pattern_width, pattern_width), dtype=np.int64) - number_of_rotations = 4 - for rotation in range(0, number_of_rotations): - rotated_grid = np.rot90(tile_grid, rotation) - flipped_grid = np.flip(rotated_grid, 0) - for grid in [rotated_grid, flipped_grid]: - ( - a_pattern_catalog, - a_pattern_weights, - a_patterns, - ) = make_pattern_catalog_from_grid(tile_grid, pattern_width) - the_pattern_catalog = {**the_pattern_catalog, **a_pattern_catalog} - the_pattern_weights = { - x: (the_pattern_weights.get(x, 0) + a_pattern_weights.get(x, 0)) - for x in (the_pattern_weights.keys() | a_pattern_weights.keys()) - } - the_patterns = np.concatenate((the_patterns, a_patterns)) - return the_pattern_catalog, the_pattern_weights, the_patterns - - -def make_pattern_catalog(tile_grid, pattern_width): - pc, pw, patterns = make_pattern_catalog_with_rotation(tile_grid, pattern_width) - patterns = np.unique(patterns, axis=0) - for p in range(patterns.shape[0]): - assert np.array_equal(patterns[p], pc[p]) - return pc, pw, patterns - - -def make_pattern_catalog_no_rotations(tile_grid, pattern_width): - """ - >>> make_pattern_catalog_no_rotations(test_ns.tile_grid, test_ns.pattern_width) - ({0: array([[-8754995591521426669, 0], - [-8754995591521426669, -8754995591521426669]], dtype=int64), 1: array([[-8754995591521426669, 0], - [-8754995591521426669, 0]], dtype=int64), 2: array([[ 0, 0], - [-8754995591521426669, -8754995591521426669]], dtype=int64), 3: array([[8253868773529191888, 0], - [ 0, 0]], dtype=int64), 4: array([[-8754995591521426669, -8754995591521426669], - [ 0, -8754995591521426669]], dtype=int64), 5: array([[-8754995591521426669, -8754995591521426669], - [-8754995591521426669, 0]], dtype=int64), 6: array([[ 0, -8754995591521426669], - [-8754995591521426669, -8754995591521426669]], dtype=int64), 7: array([[ 0, 0], - [ 0, 8253868773529191888]], dtype=int64), 8: array([[ 0, 0], - [8253868773529191888, 0]], dtype=int64), 9: array([[-8754995591521426669, -8754995591521426669], - [ 0, 0]], dtype=int64), 10: array([[ 0, -8754995591521426669], - [ 0, -8754995591521426669]], dtype=int64), 11: array([[ 0, 8253868773529191888], - [ 0, 0]], dtype=int64)}, {0: 1, 1: 2, 2: 2, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 2, 10: 2, 11: 1}, array([[[-8754995591521426669, 0], - [-8754995591521426669, -8754995591521426669]], - - [[-8754995591521426669, 0], - [-8754995591521426669, 0]], - - [[ 0, 0], - [-8754995591521426669, -8754995591521426669]], - - [[ 8253868773529191888, 0], - [ 0, 0]], - - [[-8754995591521426669, -8754995591521426669], - [ 0, -8754995591521426669]], - - [[-8754995591521426669, -8754995591521426669], - [-8754995591521426669, 0]], - - [[ 0, -8754995591521426669], - [-8754995591521426669, -8754995591521426669]], - - [[ 0, 0], - [ 0, 8253868773529191888]], - - [[ 0, 0], - [ 8253868773529191888, 0]], - - [[-8754995591521426669, -8754995591521426669], - [ 0, 0]], - - [[ 0, -8754995591521426669], - [ 0, -8754995591521426669]], - - [[ 0, 8253868773529191888], - [ 0, 0]]], dtype=int64)) - """ - pc, pw, patterns = make_pattern_catalog_from_grid(tile_grid, pattern_width) - return pc, pw, patterns - - -# In[13]: - - -def version_check(minimum_ver): - minim = [int(v) for v in minimum_ver.split(".")] - checking = [int(v) for v in np.__version__.split(".")] - if not checking[0] >= minim[0]: - if not checking[1] >= minim[1]: - if not checking[2] >= minim[2]: - return True - return False - - -def render_pattern(render_pattern, nspace): - rp_iter = np.nditer(render_pattern, flags=["multi_index"]) - output = np.zeros(render_pattern.shape + (3,), dtype=np.uint32) - while not rp_iter.finished: - # Note that this truncates images with more than 3 channels down to just the channels in the output. - # If we want to have alpha channels, we'll need a different way to handle this. - output[rp_iter.multi_index] = np.resize( - nspace.tile_catalog[render_pattern[rp_iter.multi_index]], - output[rp_iter.multi_index].shape, - ) - rp_iter.iternext() - return output - - -def show_pattern_catalog(img_ns): - s_columns = 24 // min(24, img_ns.pattern_width) - s_rows = 1 + (int(len(img_ns.pattern_catalog)) // s_columns) - fig = figure(figsize=(s_columns, s_rows * 1.5)) - title("Extracted Patterns") - for i, tcode in img_ns.pattern_catalog.items(): - pat_cat = img_ns.pattern_catalog[i] - ptr = render_pattern(pat_cat, img_ns).astype(np.uint8) - sp = subplot(s_rows, s_columns, i + 1) - spi = sp.imshow(ptr) - spi.axes.xaxis.set_label_text(f"({img_ns.pattern_weights[i]})") - sp.set_title(i) - spi.axes.tick_params( - labelleft=False, labelbottom=False, left=False, bottom=False - ) - spi.axes.grid(False) - plt.savefig(img_ns.output_filename + "_patterns.pdf", bbox_inches="tight") - plt.close() - - -if __name__ == "__main__": - import types - import wfc_tiles - - test_ns = types.SimpleNamespace( - img_filename="red_maze.png", - seed=87386, - tile_size=1, - pattern_width=2, - channels=3, - adjacency_directions=dict( - enumerate( - [ - CoordXY(x=0, y=-1), - CoordXY(x=1, y=0), - CoordXY(x=0, y=1), - CoordXY(x=-1, y=0), - ] - ) - ), - periodic_input=True, - periodic_output=True, - generated_size=(3, 3), - screenshots=1, - iteration_limit=0, - allowed_attempts=1, - ) - test_ns = wfc_utilities.find_pattern_center(test_ns) - test_ns = wfc_utilities.load_visualizer(test_ns) - test_ns.img = wfc_tiles.load_source_image(test_ns.img_filename) - ( - test_ns.tile_catalog, - test_ns.tile_grid, - test_ns.code_list, - test_ns.unique_tiles, - ) = wfc_tiles.make_tile_catalog(test_ns) - test_ns.tile_ids = { - v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() - } - test_ns.tile_weights = { - a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) - } - ( - test_ns.pattern_catalog, - test_ns.pattern_weights, - test_ns.patterns, - ) = make_pattern_catalog_no_rotations(test_ns.tile_grid, test_ns.pattern_width) - import doctest - - doctest.testmod() diff --git a/wfc/wfc1/wfc_solver.py b/wfc/wfc1/wfc_solver.py deleted file mode 100644 index c43d32a..0000000 --- a/wfc/wfc1/wfc_solver.py +++ /dev/null @@ -1,2131 +0,0 @@ -# ## Current WFC Solver - -# In[22]: - -import matplotlib - -# matplotlib.use('Agg') - -import types -from wfc.wfc_adjacency import adjacency_extraction_consistent -import numpy as np -from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE -import matplotlib.pyplot -from matplotlib.pyplot import figure, subplot, subplots, title, matshow -from wfc.wfc_patterns import render_pattern -from wfc.wfc_adjacency import blit -from wfc.wfc_tiles import tiles_to_images -import wfc.wfc_utilities -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -import random -import copy -import time - -import imageio - -import logging - -logging.basicConfig(level=logging.INFO) -wfc_logger = logging.getLogger() - -import math - -import pdb - -# import moviepy.editor as mpy -# from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter - - -WFC_DEBUGGING = False -WFC_VISUALIZE = False - -WFC_FINISHED = -2 -WFC_FAILURE = -1 -WFC_TIMEDOUT = -3 -WFC_FAKE_FAILURE = -6 - -### Visualization Functions - -# In[23]: - - -# def pattern_to_tile(pattern, pattern_catalog, pattern_center): -# try: -# return pattern_catalog[pattern][pattern_center] -# except: -# return pattern_catalog[0][pattern_center] - - -# In[24]: - - -def status_print_helper(status_string): - # print(status_string) - pass - - -# In[25]: - - -def show_wfc_patterns(wfc_state, pattern_translations): - s_columns = 24 // min(24, wfc_state.wfc_ns.pattern_width) - s_rows = 1 + (int(len(pattern_translations)) // s_columns) - fig = figure(figsize=(32, 32)) - - title("Extracted Patterns") - for i, tcode in enumerate(pattern_translations): - pat_cat = pattern_translations[i] - ptr = render_pattern(pat_cat, wfc_state.wfc_ns).astype(np.uint8) - sp = subplot(s_rows, s_columns, i + 1) - spi = sp.imshow(ptr) - spi.axes.xaxis.set_label_text(f"({wfc_state.wfc_ns.pattern_weights[i]})") - spi.axes.tick_params( - labelleft=False, labelbottom=False, left=False, bottom=False - ) - spi.axes.grid(color="grey", linewidth=1.0) - for axis in [spi.axes.xaxis, spi.axes.yaxis]: - axis.set_ticks(np.arange(-0.5, wfc_state.wfc_ns.pattern_width + 0.5, 1)) - sp.set_title(i) - matplotlib.pyplot.close(fig) - - -# In[26]: - - -def visualize_propagator_matrix(p_matrix): - visual_stack = np.empty( - [p_matrix.shape[1], p_matrix.shape[2]], dtype=p_matrix.dtype - ) - visual_stack = ( - (p_matrix[0] * 1) + (p_matrix[1] * 2) + (p_matrix[2] * 4) + (p_matrix[3] * 8) - ) - matfig = figure(figsize=(9, 9)) - - matfig.tight_layout(pad=0) - ax = subplot(1, 1, 1) - - title("Propagator Matrix") - # ax.matshow(visual_stack,cmap='jet',fignum=matfig.number) - ax.matshow(visual_stack, cmap="jet") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - # ax.grid(color="black", linewidth=1.5) - # for axis in [ax.xaxis, ax.yaxis]: - # axis.set_ticks(np.arange(-0.5, p_matrix.shape[1] + 0.5, 1.0)) - matplotlib.pyplot.close(matfig) - - -def record_visualization(wfc_state, wfc_vis=None): - if None == wfc_vis: - wfc_vis = types.SimpleNamespace( - method_time_stack=[], - ones_time_stack=[], - output_time_stack=[], - partial_output_time_stack=[], - crystal_time_stack=[], - choices_recording_stack=[], - number_of_patterns=wfc_state.number_of_patterns, - pattern_center=wfc_state.wfc_ns.pattern_center, - rows=wfc_state.rows, - columns=wfc_state.columns, - pattern_catalog=wfc_state.wfc_ns.pattern_catalog, - wfc_ns=copy.deepcopy(wfc_state.wfc_ns), - wave_table_stack=[], - solver_recording_stack=[], - ) - wfc_vis.method_time_stack.append(np.copy(wfc_state.method_time)) - wfc_vis.ones_time_stack.append( - np.copy(np.count_nonzero(wfc_state.wave_table, axis=2)) - ) - wfc_vis.output_time_stack.append(np.copy(wfc_state.output_grid)) - wfc_vis.partial_output_time_stack.append(np.copy(wfc_state.partial_output_grid)) - wfc_vis.crystal_time_stack.append(np.copy(wfc_state.crystal_time)) - wfc_vis.choices_recording_stack.append(np.copy(wfc_state.choices_recording)) - wfc_vis.wave_table_stack.append(np.copy(wfc_state.wave_table)) - print(f"time stack length: {len(wfc_vis.method_time_stack)}") - return wfc_vis - - -def render_recorded_visualization(wfc_vis): - # wfc_vis = wfc_solution_to_vis.recorded_vis - - wfc_logger.info(f"time stack length: {len(wfc_vis.method_time_stack)}") - wfc_logger.info( - f"method time stack: {[x.sum() for x in wfc_vis.method_time_stack]}" - ) - for i in range(len(wfc_vis.method_time_stack)): - matfig = figure(figsize=(16, 8)) - - # matplotlib.pyplot.title(f"{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}", fontsize=14, fontweight='bold', y = -1) - matplotlib.pyplot.title(f"{i}", fontsize=14, fontweight="bold", y=0.6) - - ax = subplot(1, 5, 1) - title("Resolution Method") - ax.matshow(wfc_vis.method_time_stack[i], cmap="magma") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - ax = subplot(1, 5, 2) - title("Ones Matrix") - - ax.matshow( - wfc_vis.ones_time_stack[i], - cmap="plasma", - vmin=0, - vmax=wfc_vis.number_of_patterns, - ) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - # ax = subplot(1,5,3) - # title('Output Matrix') - - # ax.matshow(wfc_state.output_time_stack[i],cmap='inferno', vmin=0, vmax=wfc_state.number_of_patterns) - # ax.grid(None) - # ax.set_yticklabels([]) - # ax.set_xticklabels([]) - # ax.grid(None) - - ax = subplot(1, 5, 3) - title("Choices Matrix") - - ax.matshow( - wfc_vis.choices_recording_stack[i], - cmap="inferno", - vmin=0, - vmax=wfc_vis.number_of_patterns, - ) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - ax = subplot(1, 5, 4) - title("Crystal Matrix") - - ax.matshow( - wfc_vis.crystal_time_stack[i], - cmap="gist_rainbow", - vmin=0, - vmax=len(wfc_vis.crystal_time_stack), - ) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - pattern_grid = np.array(wfc_vis.output_time_stack[i], dtype=np.int64) - - has_gaps = np.any(np.count_nonzero(wfc_vis.wave_table_stack[i], axis=2) != 1) - if has_gaps: - pattern_grid = np.array( - wfc_vis.partial_output_time_stack[i], dtype=np.int64 - ) - render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) - pattern_center = wfc_vis.wfc_ns.pattern_center - for row in range(wfc_vis.rows): - for column in range(wfc_vis.columns): - if len(pattern_grid.shape) > 2: - pattern_list = [] - for z in range(wfc_vis.number_of_patterns): - pattern_list.append(pattern_grid[(row, column, z)]) - pattern_list = [ - pattern_grid[(row, column, z)] - for z in range(wfc_vis.number_of_patterns) - if (pattern_grid[(row, column, z)] != -1) - and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) - ] - for pl_count, the_pattern in enumerate(pattern_list): - the_pattern_tiles = wfc_vis.pattern_catalog[the_pattern][ - pattern_center[0] : pattern_center[0] + 1, - pattern_center[1] : pattern_center[1] + 1, - ] - render_grid = blit( - render_grid, - the_pattern_tiles, - (row, column), - layer=pl_count, - ) - else: - if WFC_NULL_VALUE != pattern_grid[(row, column)]: - the_pattern = wfc_vis.wfc_ns.pattern_catalog[ - pattern_grid[(row, column)] - ] - p_x = wfc_vis.wfc_ns.pattern_center[0] - p_y = wfc_vis.wfc_ns.pattern_center[1] - the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] - render_grid = blit(render_grid, the_pattern, (row, column)) - ptr = tiles_to_images( - wfc_vis.wfc_ns, - render_grid, - wfc_vis.wfc_ns.tile_catalog, - wfc_vis.wfc_ns.tile_size, - visualize=True, - partial=True, - ).astype(np.uint8) - - ax = subplot(1, 5, 5) - title("Output Matrix") - - ax.imshow(ptr) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - matplotlib.pyplot.savefig( - f"{wfc_vis.wfc_ns.output_path}crystal_preview_{wfc_vis.wfc_ns.output_file_number}_{backtrack_track_global}_{i}_{str(time.time())}.png", - bbox_inches="tight", - ) - - img_data = np.frombuffer(matfig.canvas.tostring_rgb(), dtype=np.uint8) - img_data = img_data.reshape(matfig.canvas.get_width_height() + (3,)) - - # print(f"img_data shape: {matfig.canvas.get_width_height()} {matfig.canvas.get_width_height()[::-1] + (3,)}") - - # temporarily disable the recording stack... - # wfc_vis.solver_recording_stack.append(img_data) - matplotlib.pyplot.close(fig=matfig) - wfc_logger.info(f"recording stack length: {len(wfc_vis.solver_recording_stack)}") - return wfc_vis - - -def visualize_entropies(wfc_state): - matfig = figure(figsize=(24, 24)) - - matplotlib.pyplot.title( - f"{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}", - fontsize=14, - fontweight="bold", - y=0.6, - ) - - ax = subplot(1, 5, 1) - title("Resolution Method") - for row in range(wfc_state.rows): - for column in range(wfc_state.columns): - try: - entropy_sum = wfc_state.sums_of_weights[row, column] - wfc_state.entropies[row, column] = (math.log(entropy_sum)) - ( - (wfc_state.sums_of_weight_log_weights[row, column]) / entropy_sum - ) - except: - pass - - ax.matshow(wfc_state.method_time, cmap="magma") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - ax = subplot(1, 5, 2) - title("Ones Matrix") - - ax.matshow( - np.count_nonzero(wfc_state.wave_table, axis=2), - cmap="plasma", - vmin=0, - vmax=wfc_state.number_of_patterns, - ) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - # ax = subplot(1,5,3) - # title('Output Matrix') - # - # ax.matshow(wfc_state.output_grid,cmap='inferno', vmin=0, vmax=wfc_state.number_of_patterns) - # ax.grid(None) - # ax.set_yticklabels([]) - # ax.set_xticklabels([]) - # ax.grid(None) - - ax = subplot(1, 5, 3) - title("Count of Choices") - - ax.matshow( - wfc_state.choices_recording, - cmap="magma", - vmin=0, - vmax=math.log(wfc_state.number_of_patterns), - ) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - ax = subplot(1, 5, 4) - title("Crystal Matrix") - - ax.matshow(wfc_state.crystal_time % 512.0, cmap="gist_rainbow", vmin=0, vmax=512) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) - - has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) - if has_gaps: - pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) - render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) - pattern_center = wfc_state.wfc_ns.pattern_center - for row in range(wfc_state.rows): - for column in range(wfc_state.columns): - if len(pattern_grid.shape) > 2: - pattern_list = [] - for z in range(wfc_state.number_of_patterns): - pattern_list.append(pattern_grid[(row, column, z)]) - pattern_list = [ - pattern_grid[(row, column, z)] - for z in range(wfc_state.number_of_patterns) - if (pattern_grid[(row, column, z)] != -1) - and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) - ] - for pl_count, the_pattern in enumerate(pattern_list): - the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[the_pattern][ - pattern_center[0] : pattern_center[0] + 1, - pattern_center[1] : pattern_center[1] + 1, - ] - render_grid = blit( - render_grid, the_pattern_tiles, (row, column), layer=pl_count - ) - else: - if WFC_NULL_VALUE != pattern_grid[(row, column)]: - the_pattern = wfc_state.wfc_ns.pattern_catalog[ - pattern_grid[(row, column)] - ] - p_x = wfc_state.wfc_ns.pattern_center[0] - p_y = wfc_state.wfc_ns.pattern_center[1] - the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] - render_grid = blit(render_grid, the_pattern, (row, column)) - ptr = tiles_to_images( - wfc_state.wfc_ns, - render_grid, - wfc_state.wfc_ns.tile_catalog, - wfc_state.wfc_ns.tile_size, - visualize=True, - partial=True, - ).astype(np.uint8) - - # ax.grid(color="magenta", linewidth=1.5) - # ax.tick_params(direction='in', bottom=False, left=False) - - # for axis, dim in zip([ax.xaxis, ax.yaxis],[wfc_state.columns, wfc_state.rows]): - # axis.set_ticks(np.arange(-0.5, dim + 0.5, 1)) - # axis.set_ticklabels([]) - - ax = subplot(1, 5, 5) - title("Output Matrix") - - ax.imshow(ptr) - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - - matplotlib.pyplot.savefig( - f"{wfc_state.wfc_ns.output_path}crystal_preview_{wfc_state.wfc_ns.output_file_number}_{backtrack_track_global}_{wfc_state.current_iteration_count_last_touch}.png", - bbox_inches="tight", - ) - - img_data = np.frombuffer(matfig.canvas.tostring_rgb(), dtype=np.uint8) - img_data = img_data.reshape(matfig.canvas.get_width_height() + (3,)) - # print(f"img_data shape: {matfig.canvas.get_width_height()} {matfig.canvas.get_width_height()[::-1] + (3,)}") - matplotlib.pyplot.close(matfig) - # matplotlib.clear() - return img_data - - -# In[27]: - - -def show_pattern_adjacency(wfc_state): - s_columns = 4 # wfc_state.number_of_directions // 2 - s_rows = 1 # int(wfc_state.number_of_directions % 2) - cat_size = len(wfc_state.wfc_ns.pattern_catalog) + 1 - pat_adj_size = wfc_state.wfc_ns.pattern_width * cat_size - - fig = figure(figsize=(s_columns * 7.0, s_rows * 7.0)) - - title("Pattern Adjacency") - for d_index, d_offset in wfc_state.wfc_ns.adjacency_directions.items(): - - adj_preview = np.full((pat_adj_size, pat_adj_size), -1, dtype=np.int64) - for x in range(cat_size): - for y in range(cat_size): - the_pattern = None - if (0 == y) and x > 0: - the_pattern = wfc_state.wfc_ns.pattern_catalog[x - 1] - if (0 == x) and y > 0: - the_pattern = wfc_state.wfc_ns.pattern_catalog[y - 1] - if (x > 0) and (y > 0): - if wfc_state.propagator_matrix[d_index, y - 1, x - 1]: - the_pattern = np.array([[-2, -2], [-2, -2]], dtype=np.int64) - if type(None) != type(the_pattern): - adj_preview = blit( - adj_preview, - the_pattern, - ( - x * wfc_state.wfc_ns.pattern_width, - y * wfc_state.wfc_ns.pattern_width, - ), - ) - ptr = tiles_to_images( - wfc_state.wfc_ns, - adj_preview, - wfc_state.wfc_ns.tile_catalog, - wfc_state.wfc_ns.tile_size, - visualize=True, - ).astype(np.uint8) - ax = subplot(s_rows, s_columns, 1 + d_index) - ax.grid(color="magenta", linewidth=1.5) - im = ax.imshow(ptr) - for axis in [ax.xaxis, ax.yaxis]: - axis.set_ticks( - np.arange(-0.5, pat_adj_size + 0.5, wfc_state.wfc_ns.pattern_width) - ) - title( - "Direction {}\nr,c({})\nor x,y({})".format( - d_index, wfc_state.wfc_ns.adjacency_directions_rc[d_index], d_offset - ), - fontsize=15, - ) - matplotlib.pyplot.close(fig) - - -# In[28]: - - -import itertools -import math - - -def weight_log(val): - return val * math.log(val) - - -def wfc_init(prestate): - prestate.adjacency_directions_rc = { - i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() - } - prestate = wfc.wfc_utilities.find_pattern_center(prestate) - wfc_state = types.SimpleNamespace(wfc_ns=prestate) - - wfc_state.fake_failure = False - - wfc_state.result = None - wfc_state.adjacency_relations = adjacency_extraction_consistent( - wfc_state.wfc_ns, wfc_state.wfc_ns.patterns - ) - if WFC_DEBUGGING: - wfc_logger.debug( - f"wfc_state.adjacency_relations:\n{wfc_state.adjacency_relations}" - ) - # status_print_helper(f"wfc_state.wfc_ns.patterns {wfc_state.wfc_ns.patterns}") - - wfc_logger.debug("wfc_init():patterns") - - wfc_state.patterns = np.array(list(wfc_state.wfc_ns.pattern_catalog.keys())) - wfc_state.pattern_translations = list(wfc_state.wfc_ns.pattern_catalog.values()) - wfc_state.number_of_patterns = wfc_state.patterns.size - if WFC_DEBUGGING: - wfc_logger.debug("number_of_patterns: {}".format(wfc_state.number_of_patterns)) - wfc_logger.debug("patterns: {}".format(wfc_state.patterns)) - wfc_logger.debug( - "pattern translations: {}".format(wfc_state.pattern_translations) - ) - if WFC_VISUALIZE: - show_wfc_patterns(wfc_state, wfc_state.pattern_translations) - - wfc_state.number_of_directions = len(wfc_state.wfc_ns.adjacency_directions) - # wfc_state.reverse_adjacency_directions = make_reverse_adjacency_directions(wfc_state.wfc_ns.adjacency_directions) - # if WFC_DEBUGGING: - # status_print_helper("reverse_adjacency_directions: {}".format(wfc_state.reverse_adjacency_directions)) - - # The Propagator is a data structure that holds the adjacency information - # for the patterns, i.e. given a direction, which patterns are allowed to - # be placed next to the pattern that we're currently concerned with. - # This won't change over the course of using the solver, so the important - # thing here is fast lookup. - wfc_state.propagator_matrix = np.zeros( - ( - wfc_state.number_of_directions, - wfc_state.number_of_patterns, - wfc_state.number_of_patterns, - ), - dtype=np.bool_, - ) - - wfc_logger.debug("wfc_init():adjacency_relations") - - # While the adjacencies were stored as (x,y) pairs, we're going to use (row,column) pairs here. - # wfc_state.reversed_directions = [(r,c) for c,r in wfc_state.wfc_ns.adjacency_directions.values()] - # print(f"wfc_state.reversed_directions:\n{wfc_state.reversed_directions}") - for d, p1, p2 in wfc_state.adjacency_relations: - wfc_state.propagator_matrix[(d, p1, p2)] = True - - if WFC_VISUALIZE: - visualize_propagator_matrix(wfc_state.propagator_matrix) - show_pattern_adjacency(wfc_state) - - # The Wave Table is the boolean expression of which patterns are allowed - # in which cells of the solution we are calculating. - wfc_state.rows = wfc_state.wfc_ns.generated_size[0] - wfc_state.columns = wfc_state.wfc_ns.generated_size[1] - - wfc_state.solving_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.propagation_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - - wfc_state.wave_shape = [ - wfc_state.rows, - wfc_state.columns, - wfc_state.number_of_patterns, - ] - wfc_state.wave_table = np.full(wfc_state.wave_shape, True, dtype=np.bool_) - - # The compatible_count is a running count of the number of patterns that - # are still allowed to be next to this cell in a particular direction. - compatible_shape = [ - wfc_state.rows, - wfc_state.columns, - wfc_state.number_of_patterns, - wfc_state.number_of_directions, - ] - - wfc_logger.debug(f"compatible shape:{compatible_shape}") - wfc_state.compatible_count = np.full( - compatible_shape, wfc_state.number_of_patterns, dtype=np.int16 - ) # assumes that there are less than 65536 patterns - - wfc_logger.debug("wfc_init():weights") - - # The weights are how we manage the probabilities when we choose the next - # pattern to place. Rather than recalculating them from scratch each time, - # these let us incrementally update their values. - wfc_state.weights = np.array(list(wfc_state.wfc_ns.pattern_weights.values())) - wfc_state.weight_log_weights = np.vectorize(weight_log)(wfc_state.weights) - if WFC_DEBUGGING: - status_print_helper(f"wfc_state.weights {wfc_state.weights}") - status_print_helper( - f"wfc_state.weight_log_weights {wfc_state.weight_log_weights}" - ) - - wfc_state.sum_of_weights = np.sum(wfc_state.weights) - if WFC_DEBUGGING: - status_print_helper(f"wfc_state.sum_of_weights {wfc_state.sum_of_weights}") - wfc_state.sum_of_weight_log_weights = np.sum(wfc_state.weight_log_weights) - wfc_state.starting_entropy = math.log(wfc_state.sum_of_weights) - ( - wfc_state.sum_of_weight_log_weights / wfc_state.sum_of_weights - ) - - wfc_state.entropies = np.zeros( - [wfc_state.rows, wfc_state.columns], dtype=np.float64 - ) - # wfc_state.sums_of_ones = np.zeros([wfc_state.rows, wfc_state.columns], dtype = np.float64) - wfc_state.sums_of_weights = np.zeros( - [wfc_state.rows, wfc_state.columns], dtype=np.float64 - ) - - # Instead of updating all of the cells for every propagation, we use a queue - # that marks the dirty tiles to update. - wfc_state.observation_stack = collections.deque() - - wfc_state.output_grid = np.full( - [wfc_state.rows, wfc_state.columns], WFC_NULL_VALUE, dtype=np.int64 - ) - wfc_state.partial_output_grid = np.full( - [wfc_state.rows, wfc_state.columns, wfc_state.number_of_patterns], - -9, - dtype=np.int64, - ) - - wfc_logger.debug("wfc_init():observation") - - wfc_state.current_iteration_count_observation = 0 - wfc_state.current_iteration_count_propagation = 0 - wfc_state.current_iteration_count_last_touch = 0 - wfc_state.current_iteration_count_crystal = 0 - wfc_state.solving_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.ones_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.propagation_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.touch_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.crystal_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.method_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.choices_recording = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.float32 - ) - - # global recording_stack - # recording_stack.method_time_stack = [] - # recording_stack.ones_time_stack = [] - # recording_stack.solving_time_stack = [] - # recording_stack.propagation_time_stack = [] - # recording_stack.touch_time_stack = [] - # recording_stack.crystal_time_stack = [] - # recording_stack.output_time_stack = [] - # recording_stack.solver_recording_stack = [] - # recording_stack.choices_recording_stack = [] - - return wfc_state - - -# In[29]: - - -def visualize_entropy(wfc_state): - matfig = figure(figsize=(7, 7)) - - ax = subplot(1, 1, 1) - title("Sums of Weights") - ax.matshow(wfc_state.sums_of_weights, cmap="plasma") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - status_print_helper(f"sum_of_weights\n{wfc_state.sums_of_weights}") - matplotlib.pyplot.close(matfig) - - -# In[30]: - - -def wfc_clear(wfc_state): - # Crystal solving time matrix - wfc_state.current_iteration_count_observation = 0 - wfc_state.current_iteration_count_propagation = 0 - wfc_state.current_iteration_count_last_touch = 0 - wfc_state.current_iteration_count_crystal = 0 - - # global recording_stack - # recording_stack.method_time_stack = [] - # recording_stack.ones_time_stack = [] - # recording_stack.solving_time_stack = [] - # recording_stack.propagation_time_stack = [] - # recording_stack.touch_time_stack = [] - # recording_stack.crystal_time_stack = [] - # recording_stack.output_time_stack = [] - # recording_stack.solver_recording_stack = [] - # recording_stack.choices_recording_stack = [] - - wfc_state.solving_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.ones_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.propagation_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.touch_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.crystal_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.method_time = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.int32 - ) - wfc_state.choices_recording = np.full( - (wfc_state.rows, wfc_state.columns), 0, dtype=np.float32 - ) - - wfc_logger.debug("reset wave table") - # Reset the wave table. - wfc_state.wave_table = np.full(wfc_state.wave_shape, True, dtype=np.bool_) - - compatible_shape = [ - wfc_state.rows, - wfc_state.columns, - wfc_state.number_of_patterns, - wfc_state.number_of_directions, - ] - # wfc_state_compatible_count = np.full(compatible_shape, wfc_state.number_of_patterns, dtype=np.int16) - wfc_logger.debug("Initialize the compatible count") - # Initialize the compatible count from the propagation matrix. This sets the - # maximum domain of possible neighbors for each cell node. - # for row in range(wfc_state.rows): - # print(row, end=',') - # for column in range(wfc_state.columns): - # for pattern in range(wfc_state.number_of_patterns): - # for direction in range(wfc_state.number_of_directions): - # p_matrix_sum = sum(wfc_state.propagator_matrix[(direction+2)%4][pattern]) # TODO: figure out why flipping directions is needed here, maybe fix things so it isn't - # wfc_state_compatible_count[row, column, pattern, direction] = p_matrix_sum - # # #print("{},{}\n".format(row, column)) - - def prop_compat(p, d): - # print(p,d,end=':') - # print(p) - # print(d) - # print('pm[{},{}]: {}'.format(d,p,wfc_state.propagator_matrix[d][p])) - return sum(wfc_state.propagator_matrix[(d + 2) % 4][p]) - - def comp_count(r, c, p, d): - return pattern_compatible_count[p][d] - - pcomp = np.vectorize(prop_compat) - ccount = np.vectorize(comp_count) - pattern_compatible_count = np.fromfunction( - pcomp, - (wfc_state.number_of_patterns, wfc_state.number_of_directions), - dtype=np.int16, - ) - wfc_state.compatible_count = np.fromfunction( - ccount, - ( - wfc_state.rows, - wfc_state.columns, - wfc_state.number_of_patterns, - wfc_state.number_of_directions, - ), - dtype=np.int16, - ) - - wfc_logger.debug("set the weights to their maximum values") - # Likewise, set the weights to their maximum values - # wfc_state.sums_of_ones = np.full([wfc_state.rows, wfc_state.columns], - # wfc_state.number_of_patterns, - # dtype = np.uint16) - wfc_state.sums_of_weights = np.full( - [wfc_state.rows, wfc_state.columns], wfc_state.sum_of_weights, dtype=np.float64 - ) - wfc_state.sums_of_weight_log_weights = np.full( - [wfc_state.rows, wfc_state.columns], - wfc_state.sum_of_weight_log_weights, - dtype=np.float64, - ) - wfc_state.entropies = np.full( - [wfc_state.rows, wfc_state.columns], - wfc_state.starting_entropy, - dtype=np.float64, - ) - if WFC_DEBUGGING: - status_print_helper(f"starting entropy: {wfc_state.starting_entropy}") - status_print_helper(f"wfc_state.entropies: {wfc_state.entropies}") - # status_print_helper(f"wfc_state.sums_of_ones: {wfc_state.sums_of_ones}") - status_print_helper(f"wfc_state.sums_of_weights: {wfc_state.sums_of_weights}") - - wfc_state.recorded_steps = [] - - wfc_state.observation_stack = collections.deque() - # TODO: add ground-banning of patterns / masking / etc - - # ground-banning - if wfc_state.wfc_ns.ground != 0 and False: # False => currently disabled - for p in wfc_state.wfc_ns.pattern_catalog.keys(): - for x in range(wfc_state.rows): - for y in range(wfc_state.columns): - ban_pattern = not (p in wfc_state.wfc_ns.last_patterns) and ( - y >= wfc_state.wfc_ns.generated_size[1] - 1 - ) # or ((p in wfc_state.wfc_ns.last_patterns) and (y < wfc_state.wfc_ns.generated_size[1] - 1)) - if ban_pattern: - wfc_state = Ban(wfc_state, CoordRC(row=y, column=x), p) - - wfc_state.previous_decisions = [] - - wfc_logger.debug("clear complete") - - return wfc_state - - -# We'll want to visualize the compatible count as the solver runs. This starts out as a uniform color (with everything at 100%) but quickly changes as individual cells start to resolve. - -# In[31]: - - -def visualize_compatible_count(wfc_state): - directions = wfc_state.number_of_directions - visual_stack = np.zeros(wfc_state.compatible_count.shape[:2]) - for i in range(visual_stack.shape[0]): - for j in range(visual_stack.shape[1]): - for k in range(wfc_state.compatible_count.shape[2]): - for l in range(wfc_state.compatible_count.shape[3]): - visual_stack[i, j] += wfc_state.compatible_count[i, j, k, l] - if WFC_DEBUGGING: - status_print_helper( - f"compatible: {i},{j},{k},{l} => {wfc_state.compatible_count[i,j,k,l]}" - ) - matfig = figure(figsize=(7, 7)) - - ax = subplot(1, 1, 1) - title("Compatible Count") - ax.matshow(visual_stack, cmap="viridis") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - status_print_helper(f"compatible_count\n{wfc_state.compatible_count}") - - matplotlib.pyplot.close(matfig) - - -# In[32]: - - -def wfc_partial_output(wfc_state): - wfc_state.partial_output_grid = np.full( - [wfc_state.rows, wfc_state.columns, wfc_state.number_of_patterns], - -9, - dtype=np.int64, - ) - - for row in range(wfc_state.rows): - for column in range(wfc_state.columns): - pattern_flags = wfc_state.wave_table[row, column] - # print(f"pattern_flags: {pattern_flags}") - p_list = [] - for pindex, pflag in enumerate(pattern_flags): - if pflag: - p_list.append(pindex) - # print(f"p_list: {p_list}\n") - for z, p in enumerate(p_list): - wfc_state.partial_output_grid[row, column, z] = p - # print(f"\n~~~ wfc_state.partial_output_grid ~~~\n{wfc_state.partial_output_grid}") - wfc_state.recorded_steps.append(wfc_state.partial_output_grid) - return wfc_state - - -# In[33]: - - -# A useful helper function which we use because we want numpy arrays instead of jagged arrays -# https://stackoverflow.com/questions/7632963/numpy-find-first-index-of-value-fast/7654768 -def find_first(item, vec): - """return the index of the first occurence of item in vec""" - for i in range(len(vec)): - if item == vec[i]: - return i - return -1 - - -# In[34]: - - -def recalculate_weights(state, parameters, coords_index, pattern_id): - state.sums_of_weights[coords_index.row, coords_index.column] -= parameters.weights[ - pattern_id - ] - state.sums_of_weight_log_weights[ - coords_index.row, coords_index.column - ] -= state.weight_log_weights[pattern_id] - entropy_sum = state.sums_of_weights[coords_index.row, coords_index.column] - try: - state.entropies[coords_index.row, coords_index.column] = ( - math.log(entropy_sum) - ) - ( - (state.sums_of_weight_log_weights[coords_index.row, coords_index.column]) - / entropy_sum - ) - except ValueError as e: - logging.debug(f"Contradiction when banning {coords_index} -> {pattern_id}: {e}") - state.result = WFC_FAILURE - return state - - -def RecalculateWeights(wfc_state, coords_index, pattern_id): - # uncomment to show all fails - # if np.count_nonzero(wfc_state.wave_table[coords_index.row, coords_index.column]) < 1: - # wfc_logger.warning(f"Sums of ones already below 1 at {coords_index}: {wfc_state.wave_table[coords_index.row, coords_index.column].sum()}") - - wfc_state.sums_of_weights[ - coords_index.row, coords_index.column - ] -= wfc_state.weights[pattern_id] - wfc_state.sums_of_weight_log_weights -= wfc_state.weight_log_weights[pattern_id] - - entropy_sum = wfc_state.sums_of_weights[coords_index.row, coords_index.column] - try: - wfc_state.entropies[coords_index.row, coords_index.column] = ( - math.log(entropy_sum) - ) - ( - ( - wfc_state.sums_of_weight_log_weights[ - coords_index.row, coords_index.column - ] - ) - / entropy_sum - ) - except ValueError as e: - logging.debug(f"Contradiction when banning {coords_index} -> {pattern_id}: {e}") - wfc_state.result = WFC_FAILURE - return wfc_state - return wfc_state - - -import collections - -BanEntry = collections.namedtuple( - "BanEntry", ["coords_row", "coords_column", "pattern_id"] -) - - -def BanAlreadyTried(wfc_state, coords_index, pattern_id): - wfc_state.wave_table[coords_index.row, coords_index.column, pattern_id] = False - for ( - direction_id, - direction_offset, - ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): - wfc_state.compatible_count[ - coords_index.row, coords_index.column, pattern_id, direction_id - ] = 0 - wfc_state.observation_stack.append( - BanEntry(coords_index.row, coords_index.column, pattern_id) - ) - wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) - - return wfc_state - - -def Ban(wfc_state, coords_index, pattern_id): - if wfc_state.logging: - with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"Banning: {pattern_id} at {coords_index}\n") - - # pdb.set_trace() - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): - print("overflow E") - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - - wfc_state.wave_table[coords_index.row, coords_index.column, pattern_id] = False - for ( - direction_id, - direction_offset, - ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): - wfc_state.compatible_count[ - coords_index.row, coords_index.column, pattern_id, direction_id - ] = 0 - wfc_state.observation_stack.append( - BanEntry(coords_index.row, coords_index.column, pattern_id) - ) - wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) - - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): - print("overflow F") - # print(wfc_state.sums_of_ones) - print("---") - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - - wfc_state.touch_time[ - coords_index.row, coords_index.column - ] = wfc_state.current_iteration_count_last_touch - if 1 == np.count_nonzero( - wfc_state.wave_table[coords_index.row, coords_index.column] - ): - if 0 == wfc_state.propagation_time[coords_index.row, coords_index.column]: - wfc_state.touch_time[ - coords_index.row, coords_index.column - ] = wfc_state.current_iteration_count_last_touch - wfc_state.propagation_time[ - coords_index.row, coords_index.column - ] = wfc_state.current_iteration_count_propagation - - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): - print("overflow G") - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - - # if WFC_FAILURE == wfc_state.result: - # return wfc_state - if 1 == np.count_nonzero( - wfc_state.wave_table[coords_index.row, coords_index.column] - ): - pattern_flags = wfc_state.wave_table[coords_index.row, coords_index.column] - wfc_state.output_grid[coords_index.row, coords_index.column] = find_first( - True, pattern_flags - ) # Update the output grid as we go... - wfc_state.crystal_time[ - coords_index.row, coords_index.column - ] = wfc_state.current_iteration_count_crystal - wfc_state.current_iteration_count_crystal += 1 - if 0 == wfc_state.method_time[coords_index.row, coords_index.column]: - wfc_state.method_time[ - coords_index.row, coords_index.column - ] = ( - wfc_state.current_iteration_count_crystal - ) # (coords_index.row * coords_index.column) + - - # if WFC_DEBUGGING: - # status_print_helper(f"wfc_state.entropies : {wfc_state.entropies}") - # status_print_helper(f"wfc_state.sums_of_ones: {wfc_state.sums_of_ones}") - ##print(f"Ban({coords_index}, {pattern_id})") - ##print_internals(wfc_state) - - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): - print("overflow D") - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - - return wfc_state - - -# In[35]: - - -def print_internals(wfc_state): - show_rendered_patterns(wfc_state, partial=True) - y = wfc_state.wave_table.shape[0] - x = wfc_state.wave_table.shape[1] - print("sums_of_ones") - for i in range(y): - for j in range(x): - print("{0: >02d}".format(wfc_state.wave_table[i, j].sum()), end=" ") - print() - print("observation_stack") - for i in wfc_state.observation_stack: - print(i.coords_row, i.coords_column, i.pattern_id) - print("output_grid") - for i in range(y): - for j in range(x): - if wfc_state.wave_table[i, j].sum() > 1: - print("**", end=" ") - else: - try: - if len(wfc_state.partial_output_grid.shape) > 2: - for k in range(wfc_state.partial_output_grid.shape[2]): - if wfc_state.partial_output_grid[i, j, k] != -9: - print( - "{0: >02d}".format( - wfc_state.partial_output_grid[i, j, k] - ), - end="+", - ) - print(" ", end="") - else: - print( - "{0: >02d}".format(wfc_state.partial_output_grid[i, j]), - end=" ", - ) - except: - print("??", end=" ") - print() - - -# In[36]: - -import pdb - - -def find_upper_left_entropy(wfc_state, random_variation): - print(wfc_state.wave_table) - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - print(np.argmax(np.count_nonzero(wfc_state.wave_table, axis=2))) - pdb.set_trace() - chosen_cell = np.argmax(np.count_nonzero(wfc_state.wave_table, axis=2)) - if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): - status_print_helper("FINISHED") - if WFC_DEBUGGING: - status_print_helper(wfc_state.wave_table) - return WFC_FINISHED - cell_index = np.unravel_index( - chosen_cell, [wfc_state.wave_table.shape[0], wfc_state.wave_table[1]] - ) - return CoordRC(row=cell_index[0], column=cell_index[1]) - - -def find_upper_left_unresolved(wfc_state, random_variation): - unresolved_cells = np.count_nonzero(wfc_state.wave_table, axis=2) > 1 - unresolved_indices = np.where(unresolved_cells) - cell_index = (unresolved_indices[0][0], unresolved_indices[1][0]) - return CoordRC(row=cell_index[0], column=cell_index[1]) - - -def find_random_unresolved(wfc_state, random_variation): - global temp_track_number_of_finishes - # if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): - # print("FAIL") - # return WFC_FAILURE - - # the_result = check_completion(wfc_state) - # if 0 != the_result: - # return the_result - - noise_level = 1e-6 - entropy_map = random_variation * noise_level - unresolved_cells = np.count_nonzero(wfc_state.wave_table, axis=2) > 1 - entropy_map = entropy_map.flatten() * (0 == unresolved_cells.flatten()) - - chosen_cell = np.argmax(entropy_map) - - cell_index = np.unravel_index( - chosen_cell, - [ - np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], - np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], - ], - ) - return CoordRC(row=cell_index[0], column=cell_index[1]) - - -temp_track_number_of_finishes = 0 - - -def check_completion(wfc_state): - if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): - # Require that every pattern be use at least once - pattern_set = set(np.argmax(wfc_state.wave_table, axis=2).flatten()) - # Force a test to encourage backtracking - temporary addition - if ( - len(pattern_set) != wfc_state.number_of_patterns - ) and wfc_state.wfc_ns.force_use_all_patterns: - print("Some patterns were not used") - return WFC_FAILURE - - # Force a test to encourage backtracking - temporary addition - if (not (57 in pattern_set)) and False: - print("Ground not used") - return WFC_FAILURE - - # Just force a failure the first time for testing purposes - temp_track_number_of_finishes += 1 - if ( - temp_track_number_of_finishes < 2 - and wfc_state.wfc_ns.force_fail_first_solution - ): - print("Force fake failure to test backtracking") - return WFC_FAILURE - - status_print_helper("FINISHED") - if WFC_DEBUGGING: - status_print_helper(wfc_state.wave_table) - return WFC_FINISHED - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) < 1): - return WFC_FAILURE - return None - - -def find_upper_left_relevant(wave_table, random_variation): - unresolved_cells = np.count_nonzero(wave_table, axis=-1) > 1 - rows, cols = np.where(unresolved_cells) - return CoordRC(row=rows[0], column=cols[0]) - - -def find_random_unresolved_relevant(wave_table, random_variation): - unresolved_cell_mask = np.count_nonzero(wave_table, axis=2) > 1 - cell_weights = np.where(unresolved_cell_mask, random_variation, np.inf) - row, col = np.unravel_index(np.argmin(cell_weights), cell_weights.shape) - return CoordRC(row=row, column=col) - - -def find_minimum_entropy_relevant(wave_table, random_variation): - unresolved_cell_mask = np.count_nonzero(wave_table, axis=2) > 1 - cell_weights = np.where( - unresolved_cell_mask, - random_variation + (np.count_nonzero(wave_table, axis=2)), - np.inf, - ) - row, col = np.unravel_index(np.argmin(cell_weights), cell_weights.shape) - return CoordRC(row=row, column=col) - - -def find_minimum_entropy(wfc_state, random_variation): - global temp_track_number_of_finishes - noise_level = 1e-6 - entropy_map = random_variation * noise_level - entropy_map = entropy_map.flatten() + wfc_state.entropies.flatten() - # TODO: add boundary check for non-wrapping generation - - minimum_cell = np.argmin(entropy_map) - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): - print("FAIL") - # print(np.count_nonzero(wfc_state.wave_table, axis=2)) - print(f"previous decisions: {len(wfc_state.previous_decisions)}") - - return WFC_FAILURE - if 0 == np.count_nonzero(wfc_state.wave_table, axis=2).flatten()[minimum_cell]: - if WFC_DEBUGGING: - print_internals(wfc_state) - return WFC_FAILURE - - higher_than_threshold = np.ma.MaskedArray( - entropy_map, np.count_nonzero(wfc_state.wave_table, axis=2).flatten() <= 1 - ) - minimum_cell = higher_than_threshold.argmin( - fill_value=999999.9 - ) # np.ma.maximum_fill_value(1)) - maximum_cell = higher_than_threshold.argmax(fill_value=0.0) - chosen_cell = maximum_cell - - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): - wfc_logger.debug("A zero-state node has been found.") - - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 65534): - wfc_logger.error("overflow A") - wfc_logger.error(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - - if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): - print("DETECTED FINISH") - print("nonzero count: {np.count_nonzero(wfc_state.wave_table, axis=2)}") - - # the_result = check_completion(wfc_state) - # if 0 != the_result: - # return the_result - - cell_index = np.unravel_index( - chosen_cell, - [ - np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], - np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], - ], - ) - return CoordRC(row=cell_index[0], column=cell_index[1]) - - -def FinalizeObservedWaves(wfc_state): - for row in range(wfc_state.rows): - for column in range(wfc_state.columns): - pattern_flags = wfc_state.wave_table[row, column] - wfc_state.output_grid[row, column] = find_first( - True, pattern_flags - ) # TODO: This line is probably overkill? - wfc_state.result = WFC_FINISHED - return wfc_state - - -def make_observation_relevant( - state, - parameters, - instrumentation, - dirty_cell_list, - cell_to_observe, - random_number_generator, -): - row, column = cell_to_observe - distribution = np.zeros((parameters.number_of_patterns,), dtype=np.float64) - for wave_pat in range(parameters.number_of_patterns): - if state.wave_table[row, column, wave_pat]: - distribution[wave_pat] = parameters.weights[wave_pat] - - cell_weight_sum = sum(distribution) - if cell_weight_sum <= 0: - wfc_logger.info( - f"Tried to observe cell with no valid weights: {cell_to_observe} is {cell_weight_sum}" - ) - return state, instrumentation, dirty_cell_list - normalized = [float(i) / cell_weight_sum for i in distribution] - - choice_count = sum([1 for i in distribution if i > 0]) - chosen_pattern = parameters.patterns[0] - chosen_pattern = random_number_generator.choice( - parameters.patterns, 1, p=normalized - )[0] - instrumentation.choices_recording[row, column] = math.log(choice_count) - - if parameters.visualizing_output: - instrumentation.output_grid[row, column] = chosen_pattern - instrumentation.solving_time[ - row, column - ] = wfc_state.current_iteration_count_observation - instrumentation.touch_time[ - row, column - ] = wfc_state.current_iteration_count_last_touch - - for wave_pat in range(parameters.number_of_patterns): - if state.wave_table[row, column][wave_pat] != (wave_pat == chosen_pattern): - state.wave_table[ - cell_to_observe.row, cell_to_observe.column, chosen_pattern - ] = False - for ( - direction_id, - direction_offset, - ) in parameters.wfc_ns.adjacency_directions_rc.items(): - state.compatible_count[row, column, chosen_pattern, direction_id] = 0 - state.observation_stack.append(BanEntry(row, column, chosen_pattern)) - state = recalculate_weights( - state, parameters, cell_to_observe, chosen_pattern - ) - - return state, instrumentation, dirty_cell_list - - -def make_observation(wfc_state, cell_to_observe, random_number_generator): - print(dir(wfc_state)) - assert False - row, column = cell_to_observe - distribution = np.zeros((wfc_state.number_of_patterns,), dtype=np.float64) - for wave_pat in range(wfc_state.number_of_patterns): - if wfc_state.wave_table[row, column, wave_pat]: - distribution[wave_pat] = wfc_state.weights[wave_pat] - - cell_weight_sum = sum(distribution) - normalized = [float(i) / cell_weight_sum for i in distribution] - if np.any(np.isnan(normalized)): - print(normalized) - print(distribution) - print(cell_weight_sum) - - # assert not(np.any(np.isnan(normalized))) - - choice_count = sum([1 for i in distribution if i > 0]) - chosen_pattern = wfc_state.patterns[0] - try: - chosen_pattern = random_number_generator.choice( - wfc_state.patterns, 1, p=normalized - )[0] - wfc_state.choices_recording[row, column] = math.log(choice_count) - except ValueError as e: - print("observation ValueError") - print(e) - print(normalized) - if WFC_DEBUGGING: - print("chosen_pattern: {0}".format(chosen_pattern)) - print( - "wfc_state.patterns[chosen_pattern]: {0}".format( - wfc_state.patterns[chosen_pattern] - ) - ) - - if wfc_state.visualizing_output: - wfc_state.output_grid[row, column] = chosen_pattern - wfc_state.solving_time[ - row, column - ] = wfc_state.current_iteration_count_observation - wfc_state.touch_time[row, column] = wfc_state.current_iteration_count_last_touch - wfc_state.method_time[row, column] = ( - 1000 + wfc_state.current_iteration_count_observation - ) - - # wave = wfc_state.wave_table[row, column] - for wave_pat in range(wfc_state.number_of_patterns): - if wfc_state.wave_table[row, column][wave_pat] != (wave_pat == chosen_pattern): - # wfc_state = Ban(wfc_state, cell_to_observe, wave_pat) - wfc_state.wave_table[ - cell_to_observe.row, cell_to_observe.column, pattern_id - ] = False - for ( - direction_id, - direction_offset, - ) in wfc_state.wfc_ns.adjacency_directions_rc.items(): - wfc_state.compatible_count[ - coords_index.row, coords_index.column, pattern_id, direction_id - ] = 0 - wfc_state.observation_stack.append( - BanEntry(coords_index.row, coords_index.column, pattern_id) - ) - wfc_state = RecalculateWeights(wfc_state, coords_index, pattern_id) - - wfc_state.wfc_ns.stats_tracking["observations"] += 1 - global ongoing_observations - ongoing_observations += 1 - wfc_state.wfc_ns.stats_tracking["total_observations"] = ongoing_observations - if wfc_state.wfc_ns.backtracking: - wfc_state.previous_decisions.append((cell_to_observe, wave_pat,)) - - if wfc_state.logging: - with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"making observation at: {cell_to_observe}\n") - stats_file.write(f"{wave_pat}\n") - - return wfc_state - - -def wfc_observe(wfc_state, random_variation, random_number_generator): - wfc_state.current_iteration_count_observation += 1 - - the_result = None - if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): - status_print_helper("FINISHED") - if WFC_DEBUGGING: - status_print_helper(wfc_state.wave_table) - the_result = WFC_FINISHED - if None == the_result: - the_result = check_completion(wfc_state) - - cell = None - if None == the_result: - cell = find_minimum_entropy(wfc_state, random_variation) - # cell = find_upper_left_entropy(wfc_state, random_variation) - # cell = find_upper_left_unresolved(wfc_state, random_variation) - # cell = find_random_unresolved(wfc_state, random_variation) - - if cell == WFC_FAILURE: - the_result = cell - - if np.all(1 == np.count_nonzero(wfc_state.wave_table, axis=2)): - status_print_helper("FINISHED") - if WFC_DEBUGGING: - status_print_helper(wfc_state.wave_table) - the_result = WFC_FINISHED - if None == the_result: - the_result = check_completion(wfc_state) - - # print(f"&&& We are observing cell: {cell}") - # if the_result != 0: - # print(f"result: {the_result}") - if WFC_FAKE_FAILURE == the_result: - wfc_state.result = WFC_FAILURE - wfc_state.fake_failure = True - return wfc_state - if WFC_FAILURE == the_result: - wfc_state.result = WFC_FAILURE - return wfc_state - if WFC_FINISHED == the_result: - return FinalizeObservedWaves(wfc_state) - - return make_observation(wfc_state, cell, random_number_generator) - - -# In[37]: - - -def show_crystal_time(wfc_state, partial=False): - # wfc_state.solving_time - # wfc_state.propagation_time - # pl = matshow(wfc_state.solving_time, cmap='gist_ncar', extent=(0, wfc_state.rows, wfc_state.columns, 0)) - # pl.axes.grid(None) - # pl = matshow(wfc_state.propagation_time, cmap='gist_ncar', extent=(0, wfc_state.rows, wfc_state.columns, 0)) - # pl.axes.grid(None) - - matfig_obsv = figure(figsize=(9, 9)) - - ax = subplot(1, 1, 1) - title("Observation Time") - ax.matshow(wfc_state.solving_time, cmap="viridis") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - # matfig_obsv.colorbar() - print(f"solving time: {wfc_state.solving_time}") - - matfig_prop = figure(figsize=(9, 9)) - ax = subplot(1, 1, 1) - title("Propagation Time") - ax.matshow(wfc_state.propagation_time, cmap="viridis") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - # matfig_prop.colorbar() - print(f"propagation time: {wfc_state.propagation_time}") - - matfig_touch = figure(figsize=(9, 9)) - ax = subplot(1, 1, 1) - title("Last Altered Time") - ax.matshow(wfc_state.touch_time, cmap="viridis") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - # matfig_prop.colorbar() - print(f"touch time: {wfc_state.touch_time}") - - matfig_touch2 = figure(figsize=(9, 9)) - ax = subplot(1, 1, 1) - title("Crystal Time") - ax.matshow(wfc_state.crystal_time, cmap="viridis") - ax.grid(None) - ax.set_yticklabels([]) - ax.set_xticklabels([]) - ax.grid(None) - # matfig_prop.colorbar() - print(f"touch_time: {wfc_state.touch_time}") - matplotlib.pyplot.close(matfig_obsv) - matplotlib.pyplot.close(matfig_prop) - matplotlib.pyplot.close(matfig_touch) - matplotlib.pyplot.close(matfig_touch2) - - -def show_rendered_patterns(wfc_state, partial=False): - partial = True - pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) - preview_size = ( - wfc_state.wfc_ns.pattern_width * wfc_state.rows, - wfc_state.wfc_ns.pattern_width * wfc_state.columns, - ) - has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) - # print(wfc_state.sums_of_ones != 1) - # print(f"has gaps: {has_gaps}", end=', ') - print( - f"remaining nodes: {np.count_nonzero(np.count_nonzero(wfc_state.wave_table, axis=2) != 1)}" - ) - - # Temporarily only look at the final few node resulrt... - # if np.count_nonzero(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) > 10: - # return - - if has_gaps: - pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) - preview_size = ( - wfc_state.wfc_ns.pattern_width * wfc_state.rows, - wfc_state.wfc_ns.pattern_width * wfc_state.columns, - wfc_state.partial_output_grid.shape[2], - ) - grid_preview = np.full(preview_size, WFC_PARTIAL_BLANK, dtype=np.int64) - # pattern_center = (math.floor((wfc_state.wfc_ns.pattern_width - 1) / 2), math.floor((wfc_state.wfc_ns.pattern_width - 1) / 2)) - pattern_center = wfc_state.wfc_ns.pattern_center - - special_count_tiles = np.full( - (wfc_state.wfc_ns.pattern_width, wfc_state.wfc_ns.pattern_width), - 1, - dtype=np.int64, - ) - # WFC_DEBUGGING = True - # print(len(pattern_grid.shape)) - if WFC_DEBUGGING: - print("show rendered patterns") - for row in range(wfc_state.rows): - # print() - if WFC_DEBUGGING: - print() - for column in range(wfc_state.columns): - if len(pattern_grid.shape) > 2: - if WFC_DEBUGGING: - print("[", end="") - pattern_list = [] - for z in range(wfc_state.number_of_patterns): - pattern_list.append(pattern_grid[(row, column, z)]) - pl_count = 0 - for the_pattern in pattern_list: - if WFC_DEBUGGING: - print(the_pattern, end="") - if (the_pattern != -1) and (the_pattern != WFC_NULL_VALUE): - if WFC_DEBUGGING: - print("!", end="") - the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[ - the_pattern - ] - grid_preview = blit( - grid_preview, - the_pattern_tiles, - ( - row * wfc_state.wfc_ns.pattern_width, - column * wfc_state.wfc_ns.pattern_width, - ), - layer=pl_count, - ) - pl_count += 1 - else: - if WFC_DEBUGGING: - print(" ", end="") - if WFC_DEBUGGING: - print("", end=" ") - if WFC_DEBUGGING: - print("]", end="") - else: - if WFC_DEBUGGING: - print(pattern_grid) - # print(pattern_grid[(row,column)],end=' ') - if WFC_NULL_VALUE != pattern_grid[(row, column)]: - the_pattern = wfc_state.wfc_ns.pattern_catalog[ - pattern_grid[(row, column)] - ] - # if not partial: - # p_x = wfc_state.wfc_ns.pattern_center[0] - # p_y = wfc_state.wfc_ns.pattern_center[1] - # the_pattern = the_pattern[p_x:p_x+1, p_y:p_y+1] - # print(f"the_pattern: {the_pattern}") - grid_preview = blit( - grid_preview, - the_pattern, - ( - row * wfc_state.wfc_ns.pattern_width, - column * wfc_state.wfc_ns.pattern_width, - ), - ) - if WFC_DEBUGGING: - print(f"\ngrid_preview:\n{grid_preview}") - ptr = tiles_to_images( - wfc_state.wfc_ns, - grid_preview, - wfc_state.wfc_ns.tile_catalog, - wfc_state.wfc_ns.tile_size, - visualize=True, - partial=partial, - ).astype(np.uint8) - if WFC_DEBUGGING: - print(f"ptr: {ptr}") - fig, ax = subplots(figsize=(16, 16)) - ax.grid(color="magenta", linewidth=1.5) - ax.tick_params(direction="in", bottom=False, left=False) - - im = ax.imshow(ptr) - for axis, dim in zip([ax.xaxis, ax.yaxis], [wfc_state.columns, wfc_state.rows]): - axis.set_ticks( - np.arange( - -0.5, - (wfc_state.wfc_ns.pattern_width * dim) + 0.5, - wfc_state.wfc_ns.pattern_width, - ) - ) - axis.set_ticklabels([]) - - -# In[38]: - - -def render_patterns_to_output(wfc_state, partial=False, visualize=True): - pattern_grid = np.array(wfc_state.output_grid, dtype=np.int64) - - has_gaps = np.any(np.count_nonzero(wfc_state.wave_table, axis=2) != 1) - if has_gaps: - pattern_grid = np.array(wfc_state.partial_output_grid, dtype=np.int64) - render_grid = np.full(pattern_grid.shape, WFC_PARTIAL_BLANK, dtype=np.int64) - pattern_center = wfc_state.wfc_ns.pattern_center - for row in range(wfc_state.rows): - if WFC_DEBUGGING: - print() - for column in range(wfc_state.columns): - if len(pattern_grid.shape) > 2: - if WFC_DEBUGGING: - print("[", end="") - pattern_list = [] - for z in range(wfc_state.number_of_patterns): - pattern_list.append(pattern_grid[(row, column, z)]) - pattern_list = [ - pattern_grid[(row, column, z)] - for z in range(wfc_state.number_of_patterns) - if (pattern_grid[(row, column, z)] != -1) - and (pattern_grid[(row, column, z)] != WFC_NULL_VALUE) - ] - for pl_count, the_pattern in enumerate(pattern_list): - if WFC_DEBUGGING: - print(the_pattern, end="") - the_pattern_tiles = wfc_state.wfc_ns.pattern_catalog[the_pattern][ - pattern_center[0] : pattern_center[0] + 1, - pattern_center[1] : pattern_center[1] + 1, - ] - if WFC_DEBUGGING: - print(the_pattern_tiles, end=" ") - render_grid = blit( - render_grid, the_pattern_tiles, (row, column), layer=pl_count - ) - if WFC_DEBUGGING: - print("]", end=" ") - else: - if WFC_DEBUGGING: - print(pattern_grid[(row, column)], end=",") - if WFC_NULL_VALUE != pattern_grid[(row, column)]: - the_pattern = wfc_state.wfc_ns.pattern_catalog[ - pattern_grid[(row, column)] - ] - p_x = wfc_state.wfc_ns.pattern_center[0] - p_y = wfc_state.wfc_ns.pattern_center[1] - the_pattern = the_pattern[p_x : p_x + 1, p_y : p_y + 1] - render_grid = blit(render_grid, the_pattern, (row, column)) - if WFC_DEBUGGING: - print("\nrender grid") - print(render_grid) - ptr = tiles_to_images( - wfc_state.wfc_ns, - render_grid, - wfc_state.wfc_ns.tile_catalog, - wfc_state.wfc_ns.tile_size, - visualize=True, - partial=partial, - ).astype(np.uint8) - if WFC_DEBUGGING: - print(f"ptr {ptr}") - - if visualize: - fig, ax = subplots(figsize=(16, 16)) - # ax.grid(color="magenta", linewidth=1.5) - ax.tick_params(direction="in", bottom=False, left=False) - - im = ax.imshow(ptr) - for axis, dim in zip([ax.xaxis, ax.yaxis], [wfc_state.columns, wfc_state.rows]): - axis.set_ticks(np.arange(-0.5, dim + 0.5, 1)) - axis.set_ticklabels([]) - # print(ptr) - imageio.imwrite(wfc_state.wfc_ns.output_filename, ptr) - - -# In[41]: - - -def is_cell_on_boundary(wfc_state, wfc_coords): - if not wfc_state.wfc_ns.periodic_output: - return False - # otherwise... - return False # TODO - - -def wrap_coords(wfc_state, cell_coords): - r = (cell_coords.row + wfc_state.wfc_ns.generated_size[0]) % ( - wfc_state.wfc_ns.generated_size[0] - ) - c = (cell_coords.column + wfc_state.wfc_ns.generated_size[1]) % ( - wfc_state.wfc_ns.generated_size[1] - ) - return CoordRC(row=r, column=c) - - -def wfc_propagate(wfc_state): - - # while len(wfc_state.observation_stack) > 0: - # element = wfc_state.observation_stack.pop() - # print(f"element: {element}") - # for direction_id, direction_offset in wfc_state.wfc_ns.adjacency_directions.items(): - # neighbor_coords = CoordRC(row=element.coords_row + direction_offset.y, column=element.coords_column + direction_offset.x) - # if not is_cell_on_boundary(wfc_state, neighbor_coords): - # neighbor_coords = wrap_coords(wfc_state, neighbor_coords) - # compatible_pattern_list = wfc_state.propagator_matrix[direction_id][element.pattern_id] - # for pat_id, pat_value in enumerate(compatible_pattern_list): - # if pat_value: - # wfc_state.compatible_count[neighbor_coords[0], neighbor_coords[1], pat_id, direction_id] -= 1 - # if 0 == wfc_state.compatible_count[neighbor_coords[0], neighbor_coords[1], pat_id, direction_id]: - # wfc_state = Ban(wfc_state, neighbor_coords, pat_id) - # return wfc_state - - # wfc_state = wfc_observe(wfc_state, random_variation, random_number_generator) - # wfc_state = wfc_partial_output(wfc_state) - # show_rendered_patterns(wfc_state, True) - # render_patterns_to_output(wfc_state, True) - - # print(f"Propagating. Current solver result: {wfc_state.result}") - assert wfc_state.result == None - # print(wfc_state.compatible_count.shape) - while len(wfc_state.observation_stack) > 0: - # print(wfc_state.observation_stack) - if wfc_state.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 60000): - print("overflow K") - print(np.count_nonzero(wfc_state.wave_table, axis=2)) - assert False - - wfc_state.current_iteration_count_propagation += 1 - - element = wfc_state.observation_stack.pop() - # print(f"element: {element}") - for ( - direction_id, - direction_offset, - ) in wfc_state.wfc_ns.adjacency_directions.items(): - neighbor_coords = CoordRC( - row=element.coords_row + direction_offset.y, - column=element.coords_column + direction_offset.x, - ) - neighbor_coords = wrap_coords(wfc_state, neighbor_coords) - # print(f" {direction_offset} -> {neighbor_coords}") - compatible_pattern_list = wfc_state.propagator_matrix[ - direction_id, element.pattern_id - ] - # print(compatible_pattern_list) - for pat_id, pat_value in enumerate(compatible_pattern_list): - if pat_value: - # print(neighbor_coords) - wfc_state.compatible_count[ - neighbor_coords.row, - neighbor_coords.column, - pat_id, - direction_id, - ] -= 1 - if ( - 0 - == wfc_state.compatible_count[ - neighbor_coords.row, - neighbor_coords.column, - pat_id, - direction_id, - ] - ): - wfc_state = Ban(wfc_state, neighbor_coords, pat_id) - - # print(f"*** stack length: {len(wfc_state.observation_stack)}") - # print(f"~~~ compatible count:\n{wfc_state.compatible_count}") - # print(f"~~~ wave table:\n{wfc_state.wave_table}") - - # wfc_state = wfc_partial_output(wfc_state) - # show_rendered_patterns(wfc_state, True) - # render_patterns_to_output(wfc_state, True) - wfc_state.wfc_ns.stats_tracking["propagations"] += 1 - return wfc_state - - -import pdb - -backtrack_track_global = 0 - - -def wfc_backtrack(current_wfc_state, list_of_old_wfc_states): - global backtrack_track_global - backtrack_track_global += 1 - print(f"backtrack {backtrack_track_global}") - # current_wfc_state.wave_table = copy.deepcopy(old_wfc_state.wave_table) - # current_wfc_state.compatible_count = copy.deepcopy(old_wfc_state.compatible_count) - # current_wfc_state.sums_of_ones = copy.deepcopy(old_wfc_state.sums_of_ones) - # current_wfc_state.sums_of_weights = copy.deepcopy(old_wfc_state.sums_of_weights) - # current_wfc_state.sums_of_weight_log_weights = copy.deepcopy(old_wfc_state.sums_of_weight_log_weights) - # current_wfc_state.entropies = copy.deepcopy(old_wfc_state.entropies) - # current_wfc_state.observation_stack = copy.deepcopy(old_wfc_state.observation_stack) - before_len = len(current_wfc_state.previous_decisions) - forbidden = current_wfc_state.previous_decisions.pop() - assert before_len != len(current_wfc_state.previous_decisions) - wfc_logger.warning(f"Backtracking from {forbidden}") - - if current_wfc_state.logging: - with open(current_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"Backtracking #{backtrack_track_global}\n") - stats_file.write("Past state list:\n") - past_state_list = list( - zip( - [ - np.count_nonzero( - np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1 - ) - for o_wfc_state in list_of_old_wfc_states - ], - [ - np.count_nonzero( - np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1 - ) - for o_wfc_state in list_of_old_wfc_states - ], - ) - ) - stats_file.write(f"{past_state_list}\n") - - # print("Past state list: ", end='') - # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1) for o_wfc_state in list_of_old_wfc_states], end=' | ') - # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1) for o_wfc_state in list_of_old_wfc_states]) - old_wfc_state = list_of_old_wfc_states.pop() - # print(np.count_nonzero(old_wfc_state.wave_table, axis=2)) - # print(f"remaining nodes: {np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) != 1)}") - # print(f"invalid nodes: {(np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) > 60000))}") - - # print("vvvvv") - try: - old_wfc_state = list_of_old_wfc_states.pop() - except IndexError as e: - wfc_logger.info("stack of previous states is empty") - if old_wfc_state.logging: - with open(old_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"stack of previous states is empty\n") - # print(np.count_nonzero(old_wfc_state.wave_table, axis=2)) - # print(f"remaining nodes: {np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) != 1)}") - # print(f"invalid nodes: {(np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(old_wfc_state.wave_table, axis=2) > 60000))}") - - # print("Past state list: ", end='') - # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) > 1) for o_wfc_state in list_of_old_wfc_states], end = ' ') - # print([np.count_nonzero(np.count_nonzero(o_wfc_state.wave_table, axis=2) < 1) for o_wfc_state in list_of_old_wfc_states]) - - nold_wfc_state = copy.deepcopy(old_wfc_state) - # wfc_logger.warning(f"nold ones: {nold_wfc_state.sums_of_ones}") - - # current_wfc_state = Ban(current_wfc_state, forbidden[0], forbidden[1]) - # current_wfc_state.result = None - new_wfc_state = Ban(nold_wfc_state, forbidden[0], forbidden[1]) - # print(f"new invalid nodes: {(np.count_nonzero(np.count_nonzero(new_wfc_state.wave_table, axis=2) < 1)) + (np.count_nonzero(np.count_nonzero(new_wfc_state.wave_table, axis=2) > 60000))}") - # wfc_logger.warning(f"new ones: {new_wfc_state.sums_of_ones}") - if new_wfc_state.overflow_check: - if np.any(np.count_nonzero(new_wfc_state.wave_table, axis=2) > 60000): - print("overflow J") - print(np.count_nonzero(new_wfc_state.wave_table, axis=2)) - assert False - # wfc_logger.warning(f"obstack len {len(new_wfc_state.observation_stack)}") - # new_wfc_state.observation_stack.pop() - # wfc_logger.warning(f"obstack len {len(new_wfc_state.observation_stack)}") - - # pdb.set_trace() - - new_wfc_state.result = None - - if new_wfc_state.logging: - with open(new_wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"After backtracking:\n") - stats_file.write(f"stack length:{len(list_of_old_wfc_states)}\n") - stats_file.write("remaining wave table choices:\n") - stats_file.write( - f"{(np.count_nonzero(new_wfc_state.wave_table, axis=2))}\n" - ) - - return new_wfc_state, list_of_old_wfc_states - - -# from lucid_serialize_array import _normalize_array - -import cProfile, pstats -import logging - -ongoing_observations = 0 - - -def reset_backtracking_count(): - global backtrack_track_global - backtrack_track_global = 0 - - -def wfc_run(wfc_seed_state, visualize=False, logging=False): - wfc_logger.info("wfc_run()") - global WFC_VISUALIZE - if visualize: - WFC_VISUALIZE = True - else: - WFC_VISUALIZE = False - - global ongoing_observations - ongoing_observations = 0 - - wfc_state = wfc_init(wfc_seed_state) - - # print("Profiling clear...") - # pr = cProfile.Profile() - # pr.enable() - wfc_state = wfc_clear(wfc_state) - # pr.disable() - # print("Profiling complete...") - # profile_filename = "" + str(wfc_state.wfc_ns.output_path) + "" + "clear_" + str(wfc_state.wfc_ns.output_file_number) + "_" + str(wfc_state.wfc_ns.seed) + "_" + str(time.time()) + ".profile" - # with open(profile_filename, 'w') as profile_file: - # ps = pstats.Stats(pr, stream=profile_file) - # ps.print_stats() - # print("...profile saved") - - wfc_state.logging = logging - wfc_state.overflow_check = False - - if visualize: - show_pattern_adjacency(wfc_state) - visualize_propagator_matrix(wfc_state.propagator_matrix) - random_number_generator = np.random.RandomState(wfc_state.wfc_ns.seed) - random_variation = random_number_generator.random_sample(wfc_state.entropies.size) - - recorded_vis = None - if visualize: - recorded_vis = record_visualization(wfc_state, recorded_vis) - vis_stack = [] - - backtracking_stack = [] - backtracking_count = 0 - print(wfc_state.patterns) - - iterations = 0 - while (iterations < wfc_state.wfc_ns.iteration_limit) or ( - 0 == wfc_state.wfc_ns.iteration_limit - ): - wfc_state.backtracking_count = backtracking_count - wfc_state.backtracking_stack_length = len(backtracking_stack) - wfc_state.backtracking_total = backtrack_track_global - if visualize: - recorded_vis = record_visualization(wfc_state, recorded_vis) - wfc_state.current_iteration_count_last_touch += 1 - wfc_state = wfc_observe(wfc_state, random_variation, random_number_generator) - - # Add a time-out on the number of total observations - # print(f"Observations so far: {wfc_state.wfc_ns.stats_tracking['total_observations']}") - if wfc_state.wfc_ns.stats_tracking["total_observations"] > 3000: - wfc_state.result = WFC_TIMEDOUT - return wfc_state - - # print(f"wfc_state.entropies : {wfc_state.entropies}") - if visualize: - vis_stack.append(visualize_entropies(wfc_state)) - if iterations % 50 == 0: - print( - iterations, end=" " - ) # print(np.count_nonzero(wfc_state.wave_table, axis=2)) - # print(np.argmax(wfc_state.wave_table, axis=2)) - # print(wfc_state.result) - # print_internals(wfc_state) - - if wfc_state.logging: - with open(wfc_state.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"\n=====\n") - stats_file.write(f"result: {wfc_state.result}\n") - stats_file.write( - f"total observations: {wfc_state.wfc_ns.stats_tracking['total_observations']}\n" - ) - stats_file.write( - f"On backtracking {backtracking_count}, with stack size {len(backtracking_stack)}\n" - ) - stats_file.write(f"{wfc_state.result}\n") - stats_file.write("remaining wave table choices:\n") - stats_file.write( - f"{(np.count_nonzero(wfc_state.wave_table, axis=2))}\n" - ) - - if WFC_FINISHED == wfc_state.result: - wfc_state.wfc_ns.stats_tracking["success"] = True - wfc_state.recorded_vis = recorded_vis - return wfc_state - if WFC_FAILURE == wfc_state.result: - # print(np.count_nonzero(wfc_state.wave_table, axis=2)) - if not wfc_state.wfc_ns.backtracking: - wfc_state.recorded_vis = recorded_vis - return wfc_state - if len(backtracking_stack) <= 0: - wfc_state.recorded_vis = recorded_vis - return wfc_state - if len(wfc_state.previous_decisions) <= 0: - wfc_state.recorded_vis = recorded_vis - return wfc_state - backtracking_count += 1 - wfc_logger.warning( - f"Backtracking {backtracking_count}, stack size {len(backtracking_stack)}" - ) - if backtracking_count > 450: # Time out on too many backtracks - wfc_state.result = WFC_TIMEDOUT - wfc_state.recorded_vis = recorded_vis - return wfc_state - - # for ix, bs in enumerate(backtracking_stack): - # print(ix, end=',') - # print(bs.sums_of_ones) - # last_backtracking_added = backtracking_stack.pop() - wfc_state, backtracking_stack = wfc_backtrack(wfc_state, backtracking_stack) - wfc_logger.warning(f"stack size {len(backtracking_stack)}") - # print(f"wfc_state.sums_of_ones\n{wfc_state.sums_of_ones}") - print(f"current result code: {wfc_state.result}") - # if len(backtracking_stack) <= 0: - # #wfc_state.recorded_vis = recorded_vis - # #return wfc_state - # print(f"backtracking stack empty") - - if visualize: - wfc_state = wfc_partial_output(wfc_state) - visualize_compatible_count(wfc_state) - visualize_entropy(wfc_state) - show_rendered_patterns(wfc_state, True) - - wfc_state = wfc_propagate(wfc_state) - if visualize: - wfc_state = wfc_partial_output(wfc_state) - # show_rendered_patterns(wfc_state, partial=True) - # render_patterns_to_output(wfc_state, partial=True) - iterations += 1 - # print(iterations, end = ' ') - # wfc_logger.info("iterations: " + str(iterations)) - backtracking_stack.append(copy.deepcopy(wfc_state)) - wfc_state.result = WFC_TIMEDOUT - - wfc_state.recorded_vis = recorded_vis - return wfc_state - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/wfc/wfc1/wfc_solver_two.py b/wfc/wfc1/wfc_solver_two.py deleted file mode 100644 index 6ab5ab0..0000000 --- a/wfc/wfc1/wfc_solver_two.py +++ /dev/null @@ -1,535 +0,0 @@ -# - -import types -import collections -import logging -import math -import pdb -import numpy as np - - -from wfc.wfc_adjacency import adjacency_extraction_consistent - -from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE - -# import matplotlib.pyplot -# from matplotlib.pyplot import figure, subplot, subplots, title, matshow -# from wfc.wfc_patterns import render_pattern -# from wfc.wfc_adjacency import blit -# from wfc.wfc_tiles import tiles_to_images -import wfc.wfc_utilities -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center - -# import random -# import copy -# import time - -# import imageio - -logging.basicConfig(level=logging.INFO) -WFC_LOGGER = logging.getLogger() - - -WFC_FINISHED = -2 -WFC_FAILURE = -1 -WFC_TIMEDOUT = -3 -WFC_FAKE_FAILURE = -6 - - -def weight_log(val): - """Return the log of the weight value, used in calculating updated weights.""" - return val * math.log(val) - - -def wfc_init(prestate): - """ - Initialize the WFC solver, returning the fixed and mutable data structures needed. - """ - prestate.adjacency_directions_rc = { - i: CoordRC(a.y, a.x) for i, a in prestate.adjacency_directions.items() - } # - prestate = wfc.wfc_utilities.find_pattern_center(prestate) - parameters = types.SimpleNamespace(wfc_ns=prestate) - - state = types.SimpleNamespace() - state.result = None - - parameters.heuristic = ( - 0 # TODO: Implement control code to choose between heuristics - ) - - parameters.adjacency_relations = adjacency_extraction_consistent( - parameters.wfc_ns, parameters.wfc_ns.patterns - ) - parameters.patterns = np.array(list(parameters.wfc_ns.pattern_catalog.keys())) - parameters.pattern_translations = list(parameters.wfc_ns.pattern_catalog.values()) - parameters.number_of_patterns = parameters.patterns.size - parameters.number_of_directions = len(parameters.wfc_ns.adjacency_directions) - - # The Propagator is a data structure that holds the adjacency information - # for the patterns, i.e. given a direction, which patterns are allowed to - # be placed next to the pattern that we're currently concerned with. - # This won't change over the course of using the solver, so the important - # thing here is fast lookup. - parameters.propagator_matrix = np.zeros( - ( - parameters.number_of_directions, - parameters.number_of_patterns, - parameters.number_of_patterns, - ), - dtype=np.bool_, - ) - for direction, pattern_one, pattern_two in parameters.adjacency_relations: - parameters.propagator_matrix[(direction, pattern_one, pattern_two)] = True - - output = types.SimpleNamespace() - - # The Wave Table is the boolean expression table of which patterns are allowed - # in which cells of the solution we are calculating. - parameters.rows = parameters.wfc_ns.generated_size[0] - parameters.columns = parameters.wfc_ns.generated_size[1] - - output.solving_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.propagation_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - - parameters.wave_shape = [ - parameters.rows, - parameters.columns, - parameters.number_of_patterns, - ] - state.wave_table = np.full(parameters.wave_shape, True, dtype=np.bool_) - - # The compatible_count is a running count of the number of patterns that - # are still allowed to be next to this cell in a particular direction. - compatible_shape = [ - parameters.rows, - parameters.columns, - parameters.number_of_patterns, - parameters.number_of_directions, - ] - - WFC_LOGGER.debug(f"compatible shape:{compatible_shape}") - state.compatible_count = np.full( - compatible_shape, parameters.number_of_patterns, dtype=np.int16 - ) # assumes that there are less than 65536 patterns - - # The weights are how we manage the probabilities when we choose the next - # pattern to place. Rather than recalculating them from scratch each time, - # these let us incrementally update their values. - state.weights = np.array(list(parameters.wfc_ns.pattern_weights.values())) - state.weight_log_weights = np.vectorize(weight_log)(state.weights) - state.sum_of_weights = np.sum(state.weights) - - state.sum_of_weight_log_weights = np.sum(state.weight_log_weights) - state.starting_entropy = math.log(state.sum_of_weights) - ( - state.sum_of_weight_log_weights / state.sum_of_weights - ) - - state.entropies = np.zeros([parameters.rows, parameters.columns], dtype=np.float64) - state.sums_of_weights = np.zeros( - [parameters.rows, parameters.columns], dtype=np.float64 - ) - - # Instead of updating all of the cells for every propagation, we use a queue - # that marks the dirty tiles to update. - state.observation_stack = collections.deque() - - output.output_grid = np.full( - [parameters.rows, parameters.columns], WFC_NULL_VALUE, dtype=np.int64 - ) - output.partial_output_grid = np.full( - [parameters.rows, parameters.columns, parameters.number_of_patterns], - -9, - dtype=np.int64, - ) - - output.current_iteration_count_observation = 0 - output.current_iteration_count_propagation = 0 - output.current_iteration_count_last_touch = 0 - output.current_iteration_count_crystal = 0 - output.solving_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.ones_time = np.full((parameters.rows, parameters.columns), 0, dtype=np.int32) - output.propagation_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.touch_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.crystal_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.method_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.choices_recording = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.float32 - ) - - output.stats_tracking = prestate.stats_tracking.copy() - - return parameters, state, output - - -def wfc_clear(parameters, state, output): - """Given an initialized WFC solver state, clear it out for the beginning for the solving.""" - # Crystal solving time matrix - output.current_iteration_count_observation = 0 - output.current_iteration_count_propagation = 0 - output.current_iteration_count_last_touch = 0 - output.current_iteration_count_crystal = 0 - - output.solving_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.ones_time = np.full((parameters.rows, parameters.columns), 0, dtype=np.int32) - output.propagation_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.touch_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.crystal_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.method_time = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.int32 - ) - output.choices_recording = np.full( - (parameters.rows, parameters.columns), 0, dtype=np.float32 - ) - output.stats_tracking["total_observations"] = 0 - - state.wave_table = np.full(parameters.wave_shape, True, dtype=np.bool_) - - compatible_shape = [ - parameters.rows, - parameters.columns, - parameters.number_of_patterns, - parameters.number_of_directions, - ] - - # Initialize the compatible count from the propagation matrix. This sets the - # maximum domain of possible neighbors for each cell node. - - def prop_compat(p, d): - return sum(parameters.propagator_matrix[(d + 2) % 4][p]) - - def comp_count(r, c, p, d): - return pattern_compatible_count[p][d] - - pcomp = np.vectorize(prop_compat) - ccount = np.vectorize(comp_count) - pattern_compatible_count = np.fromfunction( - pcomp, - (parameters.number_of_patterns, parameters.number_of_directions), - dtype=np.int16, - ) - state.compatible_count = np.fromfunction( - ccount, - ( - parameters.rows, - parameters.columns, - parameters.number_of_patterns, - parameters.number_of_directions, - ), - dtype=np.int16, - ) - - # Likewise, set the weights to their maximum values - state.sums_of_weights = np.full( - [parameters.rows, parameters.columns], state.sum_of_weights, dtype=np.float64 - ) - state.sums_of_weight_log_weights = np.full( - [parameters.rows, parameters.columns], - state.sum_of_weight_log_weights, - dtype=np.float64, - ) - state.entropies = np.full( - [parameters.rows, parameters.columns], state.starting_entropy, dtype=np.float64 - ) - - state.recorded_steps = [] - state.observation_stack = collections.deque() - - # ground banning goes here - if parameters.wfc_ns.ground != 0 and False: - pass - - output.previous_decisions = [] - state.previous_decisions = [] - WFC_LOGGER.debug("clear complete") - - return state, output - - -# A useful helper function which we use because we want numpy arrays instead of jagged arrays -# https://stackoverflow.com/questions/7632963/numpy-find-first-index-of-value-fast/7654768 -def find_first(item, vec): - """return the index of the first occurence of item in vec""" - for i in range(len(vec)): - if item == vec[i]: - return i - return -1 - - -def find_minimum_entropy(wfc_state, random_variation): - return None - - -def find_upper_left_entropy(wfc_state, random_variation): - return None - - -def find_upper_left_unresolved(wfc_state, random_variation): - return None - - -def find_random_unresolved(wfc_state, random_variation): - noise_level = 1e-6 - entropy_map = random_variation * noise_level - entropy_map = entropy_map.flatten() + wfc_state.entropies.flatten() - minimum_cell = np.argmin(entropy_map) - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): - WFC_LOGGER.warning("Solver FAIL") - WFC_LOGGER.debug(f"previous decisions: {len(wfc_state.previous_decisions)}") - return WFC_FAILURE - if np.count_nonzero(wfc_state.wave_table, axis=2).flatten()[minimum_cell] == 0: - WFC_LOGGER.debug(f"previous decisions: {wfc_state}") - return WFC_FAILURE - return None - - higher_than_threshold = np.ma.MaskedArray( - entropy_map, np.count_nonzero(wfc_state.wave_table, axis=2).flatten() <= 1 - ) - minimum_cell = higher_than_threshold.argmin(fill_value=999999.9) - maximum_cell = higher_than_threshold.argmax(fill_value=0.0) - chosen_cell = maximum_cell - - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) == 0): - WFC_LOGGER.debug("A zero-state node has been found.") - - if wfc_parameters.overflow_check: - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) > 65534): - WFC_LOGGER.error("Overflow A") - WFC_LOGGER.error(np.count_nonzero(wfc_state.wave_table, axis=2)) - pdb.set_trace() - assert False - if np.all(np.count_nonzero(wfc_state.wave_table, axis=2) == 1): - WFC_LOGGER.info("DETECTED FINISH") - WFC_LOGGER.info( - f"nonzero count: {np.count_nonzero(wfc_state.wave_table, axis=2)}" - ) - - cell_index = np.unravel_index( - chosen_cell, - [ - np.count_nonzero(wfc_state.wave_table, axis=2).shape[0], - np.count_nonzero(wfc_state.wave_table, axis=2).shape[1], - ], - ) - return CoordRC(row=cell_index[0], column=cell_index[1]) - - -def check_completion(wfc_parameters, wfc_state): - """Check to see if the solver has failed, found a solution, or should keep going.""" - if np.all(np.count_nonzero(wfc_state.wave_table, axis=2) == 1): - # Require that every pattern be use at least once? - pattern_set = set(np.argmax(wfc_state.wave_table, axis=2).flatten()) - # Force a test to encourage backtracking - temporary addition - if ( - len(pattern_set) != wfc_state.number_of_patterns - ) and wfc_parameters.wfc_ns.force_use_all_patterns: - WFC_LOGGER.info("Some patterns were not used") - return WFC_FAILURE - WFC_LOGGER.info("Check complete: Solver FINISHED") - return WFC_FINISHED - if np.any(np.count_nonzero(wfc_state.wave_table, axis=2) < 1): - return WFC_FAILURE - return None - - -def finalized_observed_waves(parameters, state, output): - """The solver is finished, process the solution for consumption as part of the output.""" - for row in range(parameters.rows): - for column in range(parameters.columns): - pattern_flags = state.wave_table[row, column] - output.output_grid[row, column] = find_first( - True, pattern_flags - ) # TODO: this line is probably overkill? - state.result = WFC_FINISHED - return state, output - - -def make_observation(state, cell, random_number_generator, output): - return state, output - - -def wfc_observe(state, random_variation, random_number_generator, parameters, output): - output.current_iteration_count_observation += 1 - the_result = None - if np.all(np.count_nonzero(state.wave_table, axis=2) == 1): - WFC_LOGGER.info("FINISHED") - WFC_LOGGER.debug(state.wave_table) - the_result = WFC_FINISHED - - if the_result is None: - the_result = check_completion(parameters, state) - - cell = None - if the_result is None: - if parameters.heuristic == 0: - cell = find_minimum_entropy(state, random_variation) - if parameters.heuristic == 1: - cell = find_upper_left_entropy(state, random_variation) - if parameters.heuristic == 2: - cell = find_upper_left_unresolved(state, random_variation) - if parameters.heuristic == 3: - cell = find_random_unresolved(state, random_variation) - - if cell is WFC_FAILURE: - the_result = cell - if np.all(np.count_nonzero(state.wave_table, axis=2) == 1): - WFC_LOGGER.info("Solver FINISHED") - WFC_LOGGER.debug(state.wave_table) - the_result = WFC_FINISHED - if the_result is None: - the_result = check_completion(parameters, state) - if WFC_FAKE_FAILURE is the_result: - state.result = WFC_FAILURE - state.fake_failure = True - return state, output - if WFC_FAILURE == the_result: - state.result = WFC_FAILURE - return state, output - if WFC_FINISHED == the_result: - return finalized_observed_waves(parameters, state, output) - - return make_observation(state, cell, random_number_generator, output) - - -def is_cell_on_boundary(wfc_parameters, wfc_coords): - if not wfc_parameters.wfc_ns.periodic_output: - return False - # otherwise... - return False # TODO - - -def wrap_coords(wfc_parameters, cell_coords): - r = (cell_coords.row + wfc_parameters.wfc_ns.generated_size[0]) % ( - wfc_parameters.wfc_ns.generated_size[0] - ) - c = (cell_coords.column + wfc_parameters.wfc_ns.generated_size[1]) % ( - wfc_parameters.wfc_ns.generated_size[1] - ) - return CoordRC(row=r, column=c) - - -def wfc_propagate(parameters, state, output): - return state, output - - -def wfc_backtrack(state, output_stack): - return state, output_stack - - -BACKTRACK_TRACK_GLOBAL = 0 - - -def reset_backtracking_count(): - global BACKTRACK_TRACK_GLOBAL - BACKTRACK_TRACK_GLOBAL = 0 - - -def wfc_run(wfc_seed_state, visualize=False, logging=False): - WFC_LOGGER.info("wfc_run()") - wfc_output_stack = [] - backtracking_stack = [] - backtracking_count = 0 - - wfc_output = types.SimpleNamespace() - - wfc_parameters, wfc_state, wfc_output = wfc_init(wfc_seed_state) - wfc_parameters.visualize = (visualize,) - wfc_parameters.logging = logging - wfc_parameters.timeout = 3000 - - wfc_state, wfc_output = wfc_clear(wfc_parameters, wfc_state, wfc_output) - # if wfc_status.visualize: - # show_pattern_adjacency(wfc_state) - # visualize_propagator_matrix(wfc_state.propagator_matrix) - random_number_generator = np.random.RandomState(wfc_parameters.wfc_ns.seed) - random_variation = random_number_generator.random_sample(wfc_state.entropies.size) - # record_visualization() - iterations = 0 - - while (iterations < wfc_parameters.wfc_ns.iteration_limit) or ( - 0 == wfc_parameters.wfc_ns.iteration_limit - ): - wfc_state.backtracking_count = backtracking_count - wfc_state.backtracking_stack_length = len(backtracking_stack) - wfc_state.backtracking_total = BACKTRACK_TRACK_GLOBAL - # if parameters.visualize: - # recorded_vis = record_visualization(wfc_state, recorded_vis) - # wfc_state.current_iteration_count_last_touch += 1 - wfc_state, wfc_output = wfc_observe( - wfc_state, - random_variation, - random_number_generator, - wfc_parameters, - wfc_output, - ) - - # Add a time-out on the number of total observations - print(wfc_output.stats_tracking) - if wfc_output.stats_tracking["total_observations"] > wfc_parameters.timeout: - wfc_state.result = WFC_TIMEDOUT - return wfc_state - - if iterations % 50 == 0: - print(iterations, end=" ") - - if wfc_parameters.logging: - with open(wfc_parameters.wfc_ns.debug_log_filename, "a") as stats_file: - stats_file.write(f"\n=====\n") - stats_file.write(f"result: {wfc_state.result}\n") - # stats_file.write(f"total observations: {wfc_state.wfc_ns.stats_tracking['total_observations']}\n") - stats_file.write( - f"On backtracking {backtracking_count}, with stack size {len(backtracking_stack)}\n" - ) - stats_file.write(f"{wfc_state.result}\n") - stats_file.write("remaining wave table choices:\n") - stats_file.write( - f"{(np.count_nonzero(wfc_state.wave_table, axis=2))}\n" - ) - - if WFC_FINISHED == wfc_state.result: - wfc_output.stats_tracking["success"] = True - # wfc_output.recorded_vis = recorded_vis - return wfc_state, wfc_output - if WFC_FAILURE == wfc_state.result: - if not wfc_parameters.wfc_ns.backtracking: - return wfc_state, wfc_output - backtracking_count += 1 - WFC_LOGGER.warning( - f"Backtracking {backtracking_count}, stack size {len(backtracking_stack)}" - ) - if backtracking_count > wfc_parameters.wfc_ns.backtracking_limit: - if wfc_parameters.wfc_ns.backtracking_limit > 0: - wfc_state.result = WFC_TIMEDOUT - return wfc_state, wfc_output - wfc_state, backtracking_stack = wfc_backtrack(wfc_state, backtracking_stack) - wfc_state, wfc_output = wfc_propagate(wfc_parameters, wfc_state, wfc_output) - iterations += 1 - print(dir(wfc_parameters)) - print("===") - print(dir(wfc_state)) - assert False - wfc_state.result = WFC_TIMEDOUT - print(wfc_state) - return wfc_state, wfc_output diff --git a/wfc/wfc1/wfc_tiles.py b/wfc/wfc1/wfc_tiles.py deleted file mode 100644 index de3eb04..0000000 --- a/wfc/wfc1/wfc_tiles.py +++ /dev/null @@ -1,453 +0,0 @@ -import wfc.wfc_utilities -from wfc.wfc_utilities import WFC_PARTIAL_BLANK, WFC_NULL_VALUE -from wfc.wfc_utilities import CoordXY, CoordRC, hash_downto, find_pattern_center -import matplotlib.pyplot as plt -from matplotlib.pyplot import figure, subplot, subplots, title, matshow -import math -import numpy as np -import logging - -import logging - -logging.basicConfig(level=logging.INFO) -wfc_logger = logging.getLogger() - -## Helper functions -RGB_CHANNELS = 3 - - -def rgb_to_int(rgb_in): - """"Takes RGB triple, returns integer representation.""" - return struct.unpack( - "I", struct.pack("<" + "B" * 4, *(rgb_in + [0] * (4 - len(rgb_in)))) - )[0] - - -def int_to_rgb(val): - return [x for x in val.to_bytes(RGB_CHANNELS, "little")] - - -# In[8]: - - -import imageio - - -def load_source_image(filename): - return imageio.imread(filename) - - -def image_to_tiles(img, tile_size): - """ - Takes an images, divides it into tiles, return an array of tiles. - >>> image_to_tiles(test_ns.img, test_ns.tile_size) - array([[[[[255, 255, 255]]], - - - [[[255, 255, 255]]], - - - [[[255, 255, 255]]], - - - [[[255, 255, 255]]]], - - - - [[[[255, 255, 255]]], - - - [[[ 0, 0, 0]]], - - - [[[ 0, 0, 0]]], - - - [[[ 0, 0, 0]]]], - - - - [[[[255, 255, 255]]], - - - [[[ 0, 0, 0]]], - - - [[[255, 0, 0]]], - - - [[[ 0, 0, 0]]]], - - - - [[[[255, 255, 255]]], - - - [[[ 0, 0, 0]]], - - - [[[ 0, 0, 0]]], - - - [[[ 0, 0, 0]]]]], dtype=uint8) - """ - padding_argument = [(0, 0), (0, 0), (0, 0)] - for input_dim in [0, 1]: - padding_argument[input_dim] = ( - 0, - (tile_size - img.shape[input_dim]) % tile_size, - ) - img = np.pad(img, padding_argument, mode="constant") - tiles = img.reshape( - ( - img.shape[0] // tile_size, - tile_size, - img.shape[1] // tile_size, - tile_size, - img.shape[2], - ) - ).swapaxes(1, 2) - return tiles - - -def tile_to_image(tile, tile_catalog, tile_size, visualize=False): - """ - Takes a single tile and returns the pixel image representation. - """ - new_img = np.zeros((tile_size, tile_size, 3), dtype=np.int64) - for u in range(tile_size): - for v in range(tile_size): - ## If we want to display a partial pattern, it is helpful to - ## be able to show empty cells. Therefore, in visualize mode, - ## we use -1 as a magic number for a non-existant tile. - pixel = [200, 0, 200] - if (visualize) and ((-1 == tile) or (WFC_PARTIAL_BLANK == tile)): - if 0 == (u + v) % 2: - pixel = [255, 0, 255] - else: - if (visualize) and -2 == tile: - pixel = [0, 255, 255] - else: - pixel = tile_catalog[tile][u, v] - new_img[u, v] = pixel - - -def tiles_to_images( - wfc_ns, - tile_grid, - tile_catalog, - tile_size, - visualize=False, - partial=False, - grid_count=None, -): - """ - Takes a tile_grid and transforms it into an image, using the information - in tile_catalog. We use tile_size to figure out the size the new image - should be, and visualize for displaying partial tile patterns. - """ - new_img = np.zeros( - ( - tile_grid.shape[0] * tile_size, - tile_grid.shape[1] * tile_size, - wfc_ns.channels, - ), - dtype=np.int64, - ) - if partial and (len(tile_grid.shape) > 2): - for i in range(tile_grid.shape[0]): - for j in range(tile_grid.shape[1]): - for u in range(wfc_ns.tile_size): - for v in range(wfc_ns.tile_size): - pixel_merge_list = [] - for k in range(tile_grid.shape[2]): - tile = tile_grid[i, j, k] - ## If we want to display a partial pattern, it is helpful to - ## be able to show empty cells. Therefore, in visualize mode, - ## we use -1 as a magic number for a non-existant tile. - pixel = None # [200, 0, 200] - # print(tile) - if (visualize) and ((-1 == tile) or (-2 == tile)): - if -1 == tile: - pixel = [200, 0, 200] - if 0 == (i + j) % 2: - pixel = [255, 0, 255] - else: - pixel = [0, 255, 255] - else: - if (WFC_PARTIAL_BLANK != tile) and ( - WFC_NULL_VALUE != tile - ): # TODO: instead of -3, use MaskedArrays - pixel = tile_catalog[tile][u, v] - if not (pixel is None): - pixel_merge_list.append(pixel) - if len(pixel_merge_list) == 0: - if 0 == (i + j) % 2: - pixel_merge_list.append([255, 0, 255]) - else: - pixel_merge_list.append([0, 172, 172]) - - if len(pixel_merge_list) > 0: - pixel_to_add = pixel_merge_list[0] - if len(pixel_merge_list) > 1: - pixel_to_add = [ - round(sum(x) / len(pixel_merge_list)) - for x in zip(*pixel_merge_list) - ] - try: - while len(pixel_to_add) < wfc_ns.channels: - pixel_to_add.append(255) - new_img[ - (i * wfc_ns.tile_size) + u, - (j * wfc_ns.tile_size) + v, - ] = pixel_to_add - except TypeError as e: - wfc_logger.warning(e) - wfc_logger.warning( - "Tried to add {} from {}".format( - pixel_to_add, pixel_merge_list - ) - ) - else: - for i in range(tile_grid.shape[0]): - for j in range(tile_grid.shape[1]): - tile = tile_grid[i, j] - for u in range(wfc_ns.tile_size): - for v in range(wfc_ns.tile_size): - ## If we want to display a partial pattern, it is helpful to - ## be able to show empty cells. Therefore, in visualize mode, - ## we use -1 as a magic number for a non-existant tile. - pixel = [200, 0, 200] - # print(f"tile: {tile}") - if (visualize) and ((-1 == tile) or (-2 == tile)): - if -1 == tile: - if 0 == (i + j) % 2: - pixel = [255, 0, 255] - if -2 == tile: - pixel = [0, 255, 255] - else: - if WFC_PARTIAL_BLANK != tile: - pixel = tile_catalog[tile][u, v] - # Watch out for images with more than 3 channels! - new_img[ - (i * wfc_ns.tile_size) + u, (j * wfc_ns.tile_size) + v - ] = np.resize( - pixel, - new_img[ - (i * wfc_ns.tile_size) + u, (j * wfc_ns.tile_size) + v - ].shape, - ) - logging.debug("Output image shape is", new_img.shape) - return new_img - - -# Past this point, WFC itself doesn't care about the exact content of the image, just that it exists. So we're going to pack all that information away behind some data structures: a dictionary of the tiles and a matrix with the tiles in the input image. The `tile_grid` is the most important thing for automatically figuring out adjacencies, while the `tile_catalog` is what will use at the end to render the final results. -# -# Here's the default tile cataloger. Take the big bag of tiles that we've got and arrange them in a dictionary that categorizes similar tiles under the same key. -# -# `tile_catalog`: dictionary to translate the hash-key ID of the tile to the image representation of the tile. We won't need this again until we do the output render. -# `tile_grid`: the original input image, only this time expressed as a 2D array of tile IDs. -# `code_list`: 1D array of the tile IDs. -# `unique_tiles`: 1D array of the unique tiles in the tile grid. -# -# You can modify this to make your own tile cataloger, with different behavor. For example, if you wanted each tile to have its own id even if it had the same image, or contrariwise if you wanted to group all of the background tiles under the same heading. -# - -# In[9]: - - -def make_tile_catalog(nspace): - """ - """ - tiles = image_to_tiles(nspace.img, nspace.tile_size) - logging.info(f"The shape of the input image is {tiles.shape}") - # print(f'The shape of the input image is {tiles.shape}') - # print(tiles) - print( - ( - tiles.shape[0] * tiles.shape[1], - nspace.tile_size, - nspace.tile_size, - nspace.channels, - ) - ) - - tile_list = np.array(tiles).reshape( - ( - tiles.shape[0] * tiles.shape[1], - nspace.tile_size, - nspace.tile_size, - nspace.channels, - ) - ) - ## Make Tile Catalog - code_list = np.array(hash_downto(tiles, 2)).reshape( - (tiles.shape[0] * tiles.shape[1]) - ) - tile_grid = np.array(hash_downto(tiles, 2), dtype=np.int64) - unique_tiles = np.unique(tile_grid, return_counts=True) - - tile_catalog = {} - for i, j in enumerate(code_list): - tile_catalog[j] = tile_list[i] - return tile_catalog, tile_grid, code_list, unique_tiles - - -# Let's visualize what we have so far. We'll load an image, turn it into tiles, and then render those tiles into an image. If everything is working right, we should have two identical images. - -# In[10]: - - -def show_input_to_output(img_ns): - """ - Does the input equal the output? - - >>> [show_input_to_output(test_ns), load_source_image(test_ns.img_filename)] - [[[255 255 255] - [255 255 255] - [255 255 255] - [255 255 255]] - - [[255 255 255] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0]] - - [[255 255 255] - [ 0 0 0] - [255 0 0] - [ 0 0 0]] - - [[255 255 255] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0]]] - [None, Image([[[255, 255, 255], - [255, 255, 255], - [255, 255, 255], - [255, 255, 255]], - - [[255, 255, 255], - [ 0, 0, 0], - [ 0, 0, 0], - [ 0, 0, 0]], - - [[255, 255, 255], - [ 0, 0, 0], - [255, 0, 0], - [ 0, 0, 0]], - - [[255, 255, 255], - [ 0, 0, 0], - [ 0, 0, 0], - [ 0, 0, 0]]], dtype=uint8)] - """ - figure() - - sp = subplot(1, 2, 1).imshow(img_ns.img) - sp.axes.grid(False) - sp.axes.tick_params( - bottom=False, - left=False, - which="both", - labelleft=False, - labelbottom=False, - length=0, - ) - title("Input Image", fontsize=10) - outimg = tiles_to_images( - img_ns, img_ns.tile_grid, img_ns.tile_catalog, img_ns.tile_size - ) - sp = subplot(1, 2, 2).imshow(outimg.astype(np.uint8)) - sp.axes.tick_params( - bottom=False, - left=False, - which="both", - labelleft=False, - labelbottom=False, - length=0, - ) - title("Output Image From Tiles", fontsize=10) - sp.axes.grid(False) - # print(outimg.astype(np.uint8)) - # print(img_ns) - plt.savefig(img_ns.output_filename + "_input_to_output.pdf", bbox_inches="tight") - plt.close() - - -def show_extracted_tiles(img_ns): - figure(figsize=(4, 4), edgecolor="k", frameon=True) - title("Extracted Tiles") - s = math.ceil(math.sqrt(len(img_ns.unique_tiles))) + 1 - # print(s) - for tcode, i in img_ns.tile_ids.items(): - sp = subplot(s, s, i + 1).imshow(img_ns.tile_catalog[tcode]) - sp.axes.tick_params(labelleft=False, labelbottom=False, length=0) - title(i, fontsize=10) - sp.axes.grid(False) - - plt.close() - - -# A nice little diagram of our palette of tiles. -# -# And, just to check that our internal `tile_grid` representation has reasonable values, let's look at it directly. This will be useful if we have to debug the inner workings of our propagator. - -# In[11]: - - -def show_false_color_tile_grid(img_ns): - pl = matshow( - img_ns.tile_grid, - cmap="gist_ncar", - extent=(0, img_ns.tile_grid.shape[1], img_ns.tile_grid.shape[0], 0), - ) - title("False Color Map of Tiles in Input Image") - pl.axes.grid(None) - # edgecolor('black') - - -if __name__ == "__main__": - import types - - test_ns = types.SimpleNamespace( - img_filename="red_maze.png", - seed=87386, - tile_size=1, - pattern_width=2, - channels=3, - adjacency_directions=dict( - enumerate( - [ - CoordXY(x=0, y=-1), - CoordXY(x=1, y=0), - CoordXY(x=0, y=1), - CoordXY(x=-1, y=0), - ] - ) - ), - periodic_input=True, - periodic_output=True, - generated_size=(3, 3), - screenshots=1, - iteration_limit=0, - allowed_attempts=1, - ) - test_ns = wfc_utilities.find_pattern_center(test_ns) - test_ns = wfc_utilities.load_visualizer(test_ns) - test_ns.img = load_source_image(test_ns.img_filename) - ( - test_ns.tile_catalog, - test_ns.tile_grid, - test_ns.code_list, - test_ns.unique_tiles, - ) = make_tile_catalog(test_ns) - test_ns.tile_ids = { - v: k for k, v in dict(enumerate(test_ns.unique_tiles[0])).items() - } - test_ns.tile_weights = { - a: b for a, b in zip(test_ns.unique_tiles[0], test_ns.unique_tiles[1]) - } - import doctest - - doctest.testmod() diff --git a/wfc/wfc1/wfc_utilities.py b/wfc/wfc1/wfc_utilities.py deleted file mode 100644 index 4cb87bc..0000000 --- a/wfc/wfc1/wfc_utilities.py +++ /dev/null @@ -1,64 +0,0 @@ -# In[4]: - -import collections -import numpy as np -import math -import logging - -CoordXY = collections.namedtuple("coords_xy", ["x", "y"]) -CoordRC = collections.namedtuple("coords_rc", ["row", "column"]) - -WFC_PARTIAL_BLANK = -3 -WFC_NULL_VALUE = -9 - - -def hash_downto(a, rank, seed=0): - state = np.random.RandomState(seed) - assert rank < len(a.shape) - u = a.reshape((np.prod(a.shape[:rank]), -1)) - v = state.randint(1 - (1 << 63), 1 << 63, np.prod(a.shape[rank:]), dtype="int64") - return np.inner(u, v).reshape(a.shape[:rank]).astype("int64") - - -# In[5]: - - -try: - import google.colab - - IN_COLAB = True -except: - IN_COLAB = False - - -# In[6]: - - -# get_ipython().run_line_magic('pylab', 'inline') -def load_visualizer(wfc_ns): - if IN_COLAB: - from google.colab import files - - uploaded = files.upload() - for fn in uploaded.keys(): - print( - 'User uploaded file "{name}" with length {length} bytes'.format( - name=fn, length=len(uploaded[fn]) - ) - ) - else: - import matplotlib - import matplotlib.pylab - from matplotlib.pyplot import figure - from matplotlib.pyplot import subplot - from matplotlib.pyplot import title - from matplotlib.pyplot import matshow - - wfc_ns.img_filename = f"images/{wfc_ns.img_filename}" - return wfc_ns - - -def find_pattern_center(wfc_ns): - # wfc_ns.pattern_center = (math.floor((wfc_ns.pattern_width - 1) / 2), math.floor((wfc_ns.pattern_width - 1) / 2)) - wfc_ns.pattern_center = (0, 0) - return wfc_ns From 83d73cfdd016cfb58f56ce9ee57d57630221b717 Mon Sep 17 00:00:00 2001 From: Isaac Karth Date: Fri, 14 Aug 2020 12:55:16 -0700 Subject: [PATCH 4/4] removed wfc tests --- requirements.txt | 2 + wfc/tests/__init__.py | 0 wfc/tests/conftest.py | 13 -- wfc/tests/test_wfc_adjacency.py | 35 ------ wfc/tests/test_wfc_patterns.py | 47 -------- wfc/tests/test_wfc_solver.py | 203 -------------------------------- wfc/tests/test_wfc_tiles.py | 28 ----- 7 files changed, 2 insertions(+), 326 deletions(-) delete mode 100644 wfc/tests/__init__.py delete mode 100644 wfc/tests/conftest.py delete mode 100644 wfc/tests/test_wfc_adjacency.py delete mode 100644 wfc/tests/test_wfc_patterns.py delete mode 100644 wfc/tests/test_wfc_solver.py delete mode 100644 wfc/tests/test_wfc_tiles.py diff --git a/requirements.txt b/requirements.txt index 0136aea..ab6a79a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,5 @@ wincertstore==0.2 moviepy==1.0.3 tracery==0.1.1 arrdem.datalog==2.0.1 +scipy +numpy diff --git a/wfc/tests/__init__.py b/wfc/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wfc/tests/conftest.py b/wfc/tests/conftest.py deleted file mode 100644 index e0cf714..0000000 --- a/wfc/tests/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -import os.path -import pytest - -PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) - -class Resources: - def get_image(self, image): - return os.path.join(PROJECT_ROOT, "images", image) - - -@pytest.fixture("session") -def resources(): - return Resources() \ No newline at end of file diff --git a/wfc/tests/test_wfc_adjacency.py b/wfc/tests/test_wfc_adjacency.py deleted file mode 100644 index e8a743d..0000000 --- a/wfc/tests/test_wfc_adjacency.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Convert input data to adjacency information""" - -import imageio -from wfc import wfc_tiles -from wfc import wfc_patterns -from wfc import wfc_adjacency - - -def test_adjacency_extraction(resources): - # TODO: generalize this to more than the four cardinal directions - direction_offsets = list(enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)])) - - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tile_size = 1 - pattern_width = 2 - rotations = 0 - _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) - pattern_catalog, _pattern_weights, _pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( - tile_grid, pattern_width, rotations - ) - adjacency_relations = wfc_adjacency.adjacency_extraction( - pattern_grid, pattern_catalog, direction_offsets - ) - assert ((0, -1), -6150964001204120324, -4042134092912931260) in adjacency_relations - assert ((-1, 0), -4042134092912931260, 3069048847358774683) in adjacency_relations - assert ((1, 0), -3950451988873469076, -3950451988873469076) in adjacency_relations - assert ((-1, 0), -3950451988873469076, -3950451988873469076) in adjacency_relations - assert ((0, 1), -3950451988873469076, 3336256675067683735) in adjacency_relations - assert ( - not ((0, -1), -3950451988873469076, -3950451988873469076) in adjacency_relations - ) - assert ( - not ((0, 1), -3950451988873469076, -3950451988873469076) in adjacency_relations - ) diff --git a/wfc/tests/test_wfc_patterns.py b/wfc/tests/test_wfc_patterns.py deleted file mode 100644 index 4c19a73..0000000 --- a/wfc/tests/test_wfc_patterns.py +++ /dev/null @@ -1,47 +0,0 @@ -import imageio -import numpy as np -from wfc import wfc_patterns -from wfc import wfc_tiles - - -def test_unique_patterns_2d(resources): - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tile_size = 1 - pattern_width = 2 - _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) - - _patterns_in_grid, pattern_contents_list, patch_codes = wfc_patterns.unique_patterns_2d( - tile_grid, pattern_width, True - ) - assert patch_codes[1][2] == 4867810695119132864 - assert pattern_contents_list[7][1][1] == 8253868773529191888 - - -def test_make_pattern_catalog(resources): - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tile_size = 1 - pattern_width = 2 - _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) - - pattern_catalog, pattern_weights, pattern_list, _pattern_grid = wfc_patterns.make_pattern_catalog( - tile_grid, pattern_width - ) - assert pattern_weights[-6150964001204120324] == 1 - assert pattern_list[3] == 2800765426490226432 - assert pattern_catalog[5177878755649963747][0][1] == -8754995591521426669 - - -def test_pattern_to_tile(resources): - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tile_size = 1 - pattern_width = 2 - _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) - - pattern_catalog, _pattern_weights, _pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( - tile_grid, pattern_width - ) - new_tile_grid = wfc_patterns.pattern_grid_to_tiles(pattern_grid, pattern_catalog) - assert np.array_equal(tile_grid, new_tile_grid) diff --git a/wfc/tests/test_wfc_solver.py b/wfc/tests/test_wfc_solver.py deleted file mode 100644 index 9cdeddc..0000000 --- a/wfc/tests/test_wfc_solver.py +++ /dev/null @@ -1,203 +0,0 @@ -import imageio -import numpy -from wfc import wfc_solver -from wfc import wfc_tiles -from wfc import wfc_patterns -from wfc import wfc_adjacency - - -def test_makeWave(): - wave = wfc_solver.makeWave(3, 10, 20, ground=[-1]) - # print(wave) - # print(wave.sum()) - # print((2*10*19) + (1*10*1)) - assert wave.sum() == (2 * 10 * 19) + (1 * 10 * 1) - assert wave[2, 5, 19] == True - assert wave[1, 5, 19] == False - - -def test_entropyLocationHeuristic(): - wave = numpy.ones((5, 3, 4), dtype=bool) # everthing is possible - wave[1:, 0, 0] = False # first cell is fully observed - wave[4, :, 2] = False - preferences = numpy.ones((3, 4), dtype=float) * 0.5 - preferences[1, 2] = 0.3 - preferences[1, 1] = 0.1 - heu = wfc_solver.makeEntropyLocationHeuristic(preferences) - result = heu(wave) - assert [1, 2] == result - - -def test_observe(): - - my_wave = numpy.ones((5, 3, 4), dtype=bool) - my_wave[0, 1, 2] = False - - def locHeu(wave): - assert numpy.array_equal(wave, my_wave) - return 1, 2 - - def patHeu(weights, wave): - assert numpy.array_equal(weights, my_wave[:, 1, 2]) - return 3 - - assert wfc_solver.observe(my_wave, locationHeuristic=locHeu, patternHeuristic=patHeu) == ( - 3, - 1, - 2, - ) - - -def test_propagate(): - wave = numpy.ones((3, 3, 4), dtype=bool) - adjLists = {} - # checkerboard #0/#1 or solid fill #2 - adjLists[(+1, 0)] = adjLists[(-1, 0)] = adjLists[(0, +1)] = adjLists[(0, -1)] = [ - [1], - [0], - [2], - ] - wave[:, 0, 0] = False - wave[0, 0, 0] = True - adj = wfc_solver.makeAdj(adjLists) - wfc_solver.propagate(wave, adj, periodic=False) - expected_result = numpy.array( - [ - [ - [True, False, True, False], - [False, True, False, True], - [True, False, True, False], - ], - [ - [False, True, False, True], - [True, False, True, False], - [False, True, False, True], - ], - [ - [False, False, False, False], - [False, False, False, False], - [False, False, False, False], - ], - ] - ) - assert numpy.array_equal(wave, expected_result) - - -def test_run(): - wave = wfc_solver.makeWave(3, 3, 4) - adjLists = {} - adjLists[(+1, 0)] = adjLists[(-1, 0)] = adjLists[(0, +1)] = adjLists[(0, -1)] = [ - [1], - [0], - [2], - ] - adj = wfc_solver.makeAdj(adjLists) - - first_result = wfc_solver.run( - wave.copy(), - adj, - locationHeuristic=wfc_solver.lexicalLocationHeuristic, - patternHeuristic=wfc_solver.lexicalPatternHeuristic, - periodic=False, - ) - - expected_first_result = numpy.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1]]) - - assert numpy.array_equal(first_result, expected_first_result) - - event_log = [] - - def onChoice(pattern, i, j): - event_log.append((pattern, i, j)) - - def onBacktrack(): - event_log.append("backtrack") - - second_result = wfc_solver.run( - wave.copy(), - adj, - locationHeuristic=wfc_solver.lexicalLocationHeuristic, - patternHeuristic=wfc_solver.lexicalPatternHeuristic, - periodic=True, - backtracking=True, - onChoice=onChoice, - onBacktrack=onBacktrack, - ) - - expected_second_result = numpy.array([[2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]) - - assert numpy.array_equal(second_result, expected_second_result) - print(event_log) - assert event_log == [(0, 0, 0), "backtrack", (2, 0, 0)] - - class Infeasible(Exception): - pass - - def explode(wave): - if wave.sum() < 20: - raise Infeasible - - try: - result = wfc_solver.run( - wave.copy(), - adj, - locationHeuristic=wfc_solver.lexicalLocationHeuristic, - patternHeuristic=wfc_solver.lexicalPatternHeuristic, - periodic=True, - backtracking=True, - checkFeasible=explode, - ) - print(result) - happy = False - except wfc_solver.Contradiction: - happy = True - - assert happy - - -def _test_recurse_vs_loop(resources): - # FIXME: run_recurse or run_loop do not exist anymore - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tile_size = 1 - pattern_width = 2 - rotations = 0 - output_size = [84, 84] - direction_offsets = list(enumerate([(0, -1), (1, 0), (0, 1), (-1, 0)])) - _tile_catalog, tile_grid, _code_list, _unique_tiles = wfc_tiles.make_tile_catalog(img, tile_size) - pattern_catalog, pattern_weights, pattern_list, pattern_grid = wfc_patterns.make_pattern_catalog( - tile_grid, pattern_width, rotations - ) - adjacency_relations = wfc_adjacency.adjacency_extraction( - pattern_grid, pattern_catalog, direction_offsets - ) - number_of_patterns = len(pattern_weights) - decode_patterns = {x: i for i, x in enumerate(pattern_list)} - adjacency_list = {} - for i, d in direction_offsets: - adjacency_list[d] = [set() for i in pattern_weights] - for i in adjacency_relations: - adjacency_list[i[0]][decode_patterns[i[1]]].add(decode_patterns[i[2]]) - wave = wfc_solver.makeWave(number_of_patterns, output_size[0], output_size[1]) - adjacency_matrix = wfc_solver.makeAdj(adjacency_list) - solution_loop = wfc_solver.run( - wave.copy(), - adjacency_matrix, - locationHeuristic=wfc_solver.lexicalLocationHeuristic, - patternHeuristic=wfc_solver.lexicalPatternHeuristic, - periodic=True, - backtracking=False, - onChoice=None, - onBacktrack=None, - ) - solution_recurse = wfc_solver.run_recurse( - wave.copy(), - adjacency_matrix, - locationHeuristic=wfc_solver.lexicalLocationHeuristic, - patternHeuristic=wfc_solver.lexicalPatternHeuristic, - periodic=True, - backtracking=False, - onChoice=None, - onBacktrack=None, - ) - assert numpy.array_equiv(solution_loop, solution_recurse) diff --git a/wfc/tests/test_wfc_tiles.py b/wfc/tests/test_wfc_tiles.py deleted file mode 100644 index 608d1fc..0000000 --- a/wfc/tests/test_wfc_tiles.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Breaks an image into consituant tiles.""" - -import imageio -from wfc import wfc_tiles - - -def test_image_to_tiles(resources): - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - tiles = wfc_tiles.image_to_tiles(img, 1) - assert tiles[2][2][0][0][0] == 255 - assert tiles[2][2][0][0][1] == 0 - - -def test_make_tile_catalog(resources): - filename = resources.get_image("samples/Red Maze.png") - img = imageio.imread(filename) - print(img) - tc, tg, cl, ut = wfc_tiles.make_tile_catalog(img, 1) - print("tile catalog") - print(tc) - print("tile grid") - print(tg) - print("code list") - print(cl) - print("unique tiles") - print(ut) - assert ut[1][0] == 7