///|
/// Explicit neighbor lists, indexed [tile][east,south,west,north]. Unlike Model,
/// tile IDs are not packed into a signed machine integer.
pub(all) struct RuleModel {
labels : Array[String]
neighbors : Array[Array[Array[Int]]]
weights : Array[Double]
} derive(Debug, ToJson)
///|
fn RuleModel::checked(self : RuleModel) -> Unit raise SolveError {
let n = self.labels.length()
if n < 1 ||
n > 4096 ||
self.neighbors.length() != n ||
self.weights.length() != n {
raise Invalid("invalid rule model dimensions")
}
let mut edges = 0
let sets : Array[Map[Int, Bool]] = []
for t in 0.. 2147483647.0 ||
self.weights[t] != self.weights[t] {
// A valid 262144-pixel sample with eight symmetries can produce a
// frequency of 2097152. Keep all positive legacy Int weights valid too.
// This bound also keeps the aggregate weight and weight*log(weight)
// finite for the maximum 4096-pattern model.
raise Invalid("weights must be finite, positive and at most 2147483647")
}
if self.neighbors[t].length() != 4 {
raise Invalid("four neighbor lists required")
}
for row in self.neighbors[t] {
let seen : Map[Int, Bool] = Map([])
for other in row {
if other < 0 || other >= n || seen.contains(other) {
raise Invalid("invalid or duplicate neighbor")
}
seen[other] = true
}
edges += row.length()
if edges > 4000000 {
raise Invalid("adjacency size limit")
}
sets.push(seen)
}
}
for t in 0.. RuleModel raise SolveError {
let n = self.labels.length()
if n < 1 || n > 30 || self.allowed.length() != n {
raise Invalid("invalid legacy model dimensions")
}
let full = (1 << n) - 1
let neighbors = []
for row in self.allowed {
if row.length() != 4 {
raise Invalid("four directions required")
}
let lists = []
for mask in row {
if mask < 0 || (mask & full) != mask {
raise Invalid("invalid tile mask")
}
lists.push(Array::makei(n, i => i).filter(i => (mask & (1 << i)) != 0))
}
neighbors.push(lists)
}
let values = if weights.is_empty() {
Array::make(n, 1.0)
} else {
weights.map(w => w.to_double())
}
let result : RuleModel = {
labels: self.labels.copy(),
neighbors,
weights: values,
}
result.checked()
result
}
///|
fn grid_neighbor(
cell : Int,
direction : Int,
width : Int,
height : Int,
periodic : Bool,
) -> Int? {
let x = cell % width
let y = cell / width
match direction {
0 =>
if x + 1 < width {
Some(cell + 1)
} else if periodic {
Some(cell + 1 - width)
} else {
None
}
1 =>
if y + 1 < height {
Some(cell + width)
} else if periodic {
Some(x)
} else {
None
}
2 =>
if x > 0 {
Some(cell - 1)
} else if periodic {
Some(cell + width - 1)
} else {
None
}
_ =>
if y > 0 {
Some(cell - width)
} else if periodic {
Some((height - 1) * width + x)
} else {
None
}
}
}
///|
pub fn Solution::validate_rules(
self : Solution,
model : RuleModel,
pins? : Array[(Int, Int)] = [],
restrictions? : Array[(Int, Array[Int])] = [],
periodic? : Bool = false,
) -> Bool {
if self.width < 1 ||
self.width > 65536 ||
self.height < 1 ||
self.height > 65536 ||
self.width.to_int64() * self.height.to_int64() > 65536L ||
self.tiles.length() != self.width * self.height {
return false
}
model.checked() catch {
_ => return false
}
if self.tiles.iter().any(t => t < 0 || t >= model.labels.length()) {
return false
}
if pins.length() > 65536 || restrictions.length() > 65536 {
return false
}
for (cell, tile) in pins {
if cell < 0 || cell >= self.tiles.length() || self.tiles[cell] != tile {
return false
}
}
let mut candidates = 0
for (cell, allowed) in restrictions {
if cell < 0 ||
cell >= self.tiles.length() ||
allowed.length() > 2000000 - candidates {
return false
}
candidates += allowed.length()
if allowed.any(t => t < 0 || t >= model.labels.length()) ||
!allowed.contains(self.tiles[cell]) {
return false
}
}
for cell, tile in self.tiles {
for direction in 0..<4 {
if grid_neighbor(cell, direction, self.width, self.height, periodic)
is Some(other) {
if !model.neighbors[tile][direction].contains(self.tiles[other]) {
return false
}
}
}
}
true
}