Site icon R-bloggers

Pentomino Solution & Patchwork

[This article was first published on CHI(χ)-Files, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
< section id="pentomino-solution-as-igraph-object" class="level3">

Pentomino Solution as igraph object

In this post, I’m taking deeper dive into a dataset of Pentomino puzzle solutions using R. This time, I’m switching gears from the sf package, but to explore graph theory concepts with tidygraph and ggraph. Why? Because Pentomino solutions aren’t just puzzles; they’re networks waiting to be uncovered! 🌐

What’s on the menu? 🍴

< details class="code-fold"> < summary>Setups and Pakcages Used
# Load required libraries
library(tidyverse)    # Data wrangling and general utilities
library(ggraph)       # Graph visualization
library(tidygraph)    # Graph data structure (tbl_graph) + graph algorithms
library(ggforce)      # Extra geoms for ggplot2
library(cowplot)      # Additional plotting helpers
library(patchwork)    # Combine multiple ggplots effortlessly!
< section id="original-solution-dataset" class="level3">

Original Solution Dataset

I’ve saved solution as csv file from earlier blog posts. So just retriving the dataset. Solution looks like below.

< details class="code-fold"> < summary>Reading Solution Dataset
### Read solution data frame from Github 
pento_sol <- read_csv("https://raw.githubusercontent.com/chichacha/pentomino/refs/heads/main/pentomino_solution.csv")

sample_n(pento_sol, size=5) |>
  knitr::kable("markdown")
dim sol_text row_cnt col_cnt sol_idx
5×10 LLLLTTTNNN UUULWTNNFF UPUWWTYFFV PPWWYYYYFV PPIIIIIVVV 5 10 5427
6×10 IIIIIXYYYY VVVFXXXNYW VTFFFXNNWW VTTTFZNWWP UTUZZZNLPP UUUZLLLLPP 6 10 707
5×9 IIIIIFVVV UUPPFFFTV UPPPZZFTV UUNNZLTTT NNNZZLLLL 5 9 1024
8×8d .FFNNNX. FFNNYXXX IFYYYYXT ILLLLTTT IVVVLWWT IVZZWWPP IVZUWUPP .ZZUUUP. 8 8 133
5×11 LLLLFZZNTTT LXFFFVZNNTY XXXFPVZZNTY UXUPPVVVNYY UUUPPIIIIIY 5 11 2837
< section id="just-prepping-colors" class="level4">

Just prepping colors

To make our graphs visually fun, I’m just using retro-inspired color palette and assign unique colors to each Pentomino piece.

< details class="code-fold"> < summary>Prepping Color Palette
# Just Prepping some color palette
retro <-  c("#00A0B0", "#6A4A3C", "#CC333F", "#EB6841", "#EDC951")
retro12 <- colorRampPalette(retro)(12)
retro13 <- c(retro12,"#ffffff")

#Assigning names to each color allows direct mapping with scale_color_manual() or scale_fill_manual()
piece <- c("F","I","L","N","P","T","U","V","W","X","Y","Z")
names(retro12) <- piece
names(retro13) <- c(piece,".")

retro12 |> enframe() |>
  ggplot(aes(x=name,y=1)) +
  geom_tile(aes(fill=I(value))) +
  geom_text(aes(label=name), color="white",vjust=-0.5) +
  geom_text(aes(label=value), color="white",vjust=1, size=3) +
  theme_nothing()

< section id="converting-solution-to-xy-positions" class="level3">

Converting Solution to (x,y) Positions 📐

Next, we’ll transform each solution into (x, y) coordinates. This step is crucial for graph creation. I’ve listed a visualization & table of some 3×5 Pentomino solutions, mapped to their (x, y) position.

< details class="code-fold"> < summary>Converting Solution Texts to Data Frame with Coordinates
### Convert Solution to XY position data frame
pento_sol_df <- pento_sol |>
  mutate(solution_num = row_number()) |>
  mutate(sol_text = str_split(sol_text, " ")) |>
  unnest(sol_text) |>
  group_by(solution_num) |>
  mutate(y = row_number()) |>
  ungroup() |>
  mutate(sol_text = str_split(sol_text, "")) |>
  unnest(sol_text) |>
  group_by(solution_num, y) |>
  mutate(x = row_number()) |>
  ungroup()


# Split the data into chunks programmatically
tables <- pento_sol_df |> 
  filter(dim == "5×3", sol_idx == 1) |>
  select(dim, sol_text, x, y) |>
  group_split(x)

pento_sol_df |> 
  filter(dim == "5×3", sol_idx %in% c(1:6)) |>
  mutate(sol_idx=if_else(sol_idx==1, str_c("1. See Table Below"), as.character(sol_idx))) |>
  ggplot(aes(x=x,y=y)) +
  geom_point(aes(color=sol_text),size=10) +
  geom_text(aes(label=sol_text), color="#fffff3") +
  scale_color_manual(values=retro12, guide="none") +
  theme_minimal() +
  facet_wrap(~sol_idx,ncol=6) +
  coord_fixed() +
  scale_x_continuous(expand=expansion(add=1)) +
  scale_y_continuous(expand=expansion(add=1))

dim sol_text x y
5×3 L 1 1
5×3 L 1 2
5×3 L 1 3
5×3 L 1 4
5×3 V 1 5
dim sol_text x y
5×3 L 2 1
5×3 N 2 2
5×3 N 2 3
5×3 N 2 4
5×3 V 2 5
dim sol_text x y
5×3 N 3 1
5×3 N 3 2
5×3 V 3 3
5×3 V 3 4
5×3 V 3 5
< section id="storing-the-solution-in-nested-table" class="level3">

Storing the Solution in Nested Table

Use nest(.by = c(solution_num, dim)) to store each solution’s data in a list column. That way, each row in pento_min corresponds to one puzzle solution, containing the relevant (x, y, sol_text) data in a nested data frame.

< details class="code-fold"> < summary>Store solution in nested way
pento_min <- pento_sol_df |>
  filter(sol_text != ".") |>  # exclude the "." cells
  select(x, y, sol_text, solution_num, dim) |>
  nest(.by = c(solution_num, dim))
< section id="creating-graphs-with-tidygraph-ggraph" class="level3">

Creating Graphs with tidygraph & ggraph 📈

To visualize the connections between pieces, we’ll use tidygraph to create graph objects and ggraph for plotting.

< section id="function-data-frame-to-graph-conversion" class="level4">

Function: Data Frame to Graph Conversion

This function converts a solution’s data frame into a graph by identifying adjacent cells.

< details class="code-fold"> < summary>Convert Data Frame to igraph Object
df_to_graph <- function(data) {
  
  # cross join is expensive, but straightforward: 
  # generate all pairs of cells, then filter to pairs that are adjacent (Manhattan distance = 1).
  tmp <- data |>
    cross_join(data) |>
    filter((abs(x.x - x.y) + abs(y.x - y.y)) == 1) |>
    distinct()
  
  edges <- tmp |>
    transmute(
      from = paste(x.x, y.x, sol_text.x, sep = ","),
      to   = paste(x.y, y.y, sol_text.y, sep = ","),
      w    = if_else(sol_text.x == sol_text.y, 1, 0.1)
    )
  
  # Standardize from/to to avoid duplicate edges in undirected graph
  edges_unique <- edges %>%
    rowwise() %>%
    mutate(
      a = min(from, to),
      b = max(from, to)
    ) %>%
    ungroup() %>%
    distinct(a, b, .keep_all = TRUE) %>%
    select(-a, -b)
  
  graph <- tidygraph::as_tbl_graph(edges_unique, directed = FALSE)
  
  # Add node attributes
  graph <- graph |>
    mutate(
      x     = as.numeric(str_split_i(name, ",", 1)),
      y     = as.numeric(str_split_i(name, ",", 2)),
      piece = str_split_i(name, ",", 3)
    ) |>
    mutate(
      deg   = centrality_degree(weights = w),
      btwn  = centrality_betweenness(weights = w),
      sub_g = centrality_subgraph(),
      idx   = row_number()
    )
  
  return(graph)
}
< section id="function-graph-visualization" class="level4">

Function: Graph Visualization

This function plots the graph using ggraph and highlights adjacency within pieces.

< details class="code-fold"> < summary>Visualizing igraph object with ggraph
plot_graph <- function(g) {
  g |>
    activate("nodes") |>
    mutate(comm = group_louvain(weights = w)) |>
    ggraph(layout = "manual", x = x, y = y) +
    
    # 1) Thicker edges for cells in the same piece
    geom_edge_link(
      lineend = "round", linejoin = "mitre", alpha = 0.8,
      aes(edge_color = .N()$piece[from], edge_width = I(if_else(w == 1, 7, w)))
    ) +
    
    # 2) Another edge layer (white dash) just as decoration
    geom_edge_link(
      lineend = "square", linejoin = "round", alpha = 0.8,
      aes(edge_width = I(w)), color = "#ffffff", linetype = 3
    ) +
    
    coord_fixed() +
    theme_nothing() +
    scale_edge_color_manual(values = retro12) +
    scale_x_continuous(expand = expansion(add = 1)) +
    scale_y_reverse(expand = expansion(add = 1))
  
}
< section id="wrapping-it-up-with-patchwork" class="level3">

Wrapping It Up with patchwork 🖼️

Finally, let’s use patchwork to combine multiple graphs into a single layout.

< details class="code-fold"> < summary>Utilizing wrap_plot function
indexes <- c(14369:15378) |> sample(size = 24)

list_of_plots <- indexes %>%
  map(~ {
    pento_min$data[[.x]] %>%
      df_to_graph() %>%
      plot_graph()
  })

wrap_plots(list_of_plots, ncol = 3) +
  plot_annotation(
    title = "Collection of Pentomino Solutions (5x12)",
    theme = theme(text = element_text(family = "Roboto Condensed"))
  ) +
  plot_layout()

To leave a comment for the author, please follow the link and comment on their blog: CHI(χ)-Files.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Exit mobile version