http://mathling.com/wfc/overlapping-model  library module

http://mathling.com/wfc/overlapping-model


Wave Function Collapse
This is a port and rework of WFC
See https://github.com/mxgmn/WaveFunctionCollapse
Requires png/ppm modules, which require Saxon Java extension, EXPath file
respectively.

Copyright© Mary Holstege 2022-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)

July 2022
Status: Stable

Imports

http://mathling.com/core/array
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.xqy"
http://mathling.com/type/defref
import module namespace def="http://mathling.com/type/defref"
       at "../types/defref.xqy"
http://mathling.com/wfc/modeldef
import module namespace modeldef="http://mathling.com/wfc/modeldef"
       at "modeldef.xqy"
http://mathling.com/image/png
import module namespace png="http://mathling.com/image/png"
       at "../image/png.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/type/slot
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/image/matrix
import module namespace matrix="http://mathling.com/image/matrix"
       at "../image/matrix.xqy"
http://mathling.com/image/ppm
import module namespace ppm="http://mathling.com/image/ppm"
       at "../image/ppm.xqy"
http://mathling.com/colour/rgb
import module namespace rgb="http://mathling.com/colour/rgb"
       at "../colourspace/rgb.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/core/vector
import module namespace v="http://mathling.com/core/vector"
       at "../core/vector.xqy"
http://mathling.com/wfc/model
import module namespace model="http://mathling.com/wfc/model"
       at "model.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"

Functions

Function: sparse
declare function sparse($default as item()*) as map(*)

Params
  • default as item()*
Returns
  • map(*)
declare function this:sparse($default as item()*) as map(*)
{
  map {
    "kind": "sparse-propagator",
    "default": $default
  }
}

Function: set
declare function set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*)

Params
  • sparse3 as map(*)
  • x as xs:integer
  • y as xs:integer
  • z as xs:integer
  • val as item()*
Returns
  • map(*)
declare function this:set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*)
{
  let $xmap as map(*) := head(($sparse3($x), map{}))
  let $ymap as map(*) := head(($xmap($y), map{}))
  return
    $sparse3=>map:put($x,
      $xmap=>map:put($y,
        $ymap=>map:put($z, $val)
    )
  )
}

Function: set
declare function set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*)

Params
  • sparse2 as map(*)
  • x as xs:integer
  • y as xs:integer
  • val as item()*
Returns
  • map(*)
declare function this:set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*)
{
  $sparse2=>map:put($x,
    head(($sparse2($x),map{}))=>map:put($y, $val)
  )
}

Function: get
declare function get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()*

Params
  • sparse3 as map(*)
  • x as xs:integer
  • y as xs:integer
  • z as xs:integer
Returns
  • item()*
declare function this:get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()*
{
  let $xmap := $sparse3($x)
  return (
    if (empty($xmap)) then $sparse3("default") else (
      let $ymap := $xmap($y)
      return (
        if (empty($ymap)) then $sparse3("default") else (
          head(($ymap($z), $sparse3("default")))
        )
      )
    )
  )
}

Function: get
declare function get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()*

Params
  • sparse2 as map(*)
  • x as xs:integer
  • y as xs:integer
Returns
  • item()*
declare function this:get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()*
{
  let $xmap := $sparse2($x)
  return (
    if (empty($xmap)) then $sparse2("default") else (
      head(($xmap($y), $sparse2("default")))
    )
  )
}

Function: flatten
declare function flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()*

Params
  • sparse3 as map(*)
  • x as xs:integer
  • y as xs:integer
  • n as xs:integer
Returns
  • item()*
declare function this:flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()*
{
  let $xmap := $sparse3($x)
  return (
    if (empty($xmap)) then $sparse3("default") else (
      let $ymap := $xmap($y)
      return (
        if (empty($ymap)) then $sparse3("default") else (
          for $z in 1 to $n return head(($ymap($z), $sparse3("default")))
        )
      )
    )
  )
}

Function: agrees
declare function agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean


agrees()
Tiles are same if we overlap offset by a row or column in the given
direction, e.g.
x x o o x x
x x o agrees o x x with direction=4 (right)
x x o o x x

Params
  • m1 as map(*)
  • m2 as map(*)
  • dx as xs:integer
  • dy as xs:integer
Returns
  • xs:boolean
declare function this:agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean
{
  let $N as xs:integer := $m1=>arr:rows()
  let $N as xs:integer := (
    util:assert($m1=>arr:rows() = $m1=>arr:columns(), "m1.rows!=m1.columns"),
    util:assert($m2=>arr:rows() = $m2=>arr:columns(), "m2.rows!=m2.columns"),
    util:assert($m1=>arr:rows() = $m2=>arr:rows(), "m1.rows!=m2.rows"),
    $N
  )
  let $xmin as xs:integer := if ($dx < 0) then 1 else $dx + 1
  let $xmax as xs:integer := if ($dx < 0) then $dx + $N else $N
  let $ymin as xs:integer := if ($dy < 0) then 1 else $dy + 1
  let $ymax as xs:integer := if ($dy < 0) then $dy + $N else $N
  return (
    every $y in $ymin to $ymax satisfies (
      every $x in $xmin to $xmax satisfies (
        $m1=>arr:get($y, $x) = $m2=>arr:get($y - $dy, $x - $dx)
      )
    )
  )
}

Function: model
declare function model($model-def as map(xs:string,item()*), $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • model-def as map(xs:string,item()*)
  • width as xs:integer
  • height as xs:integer
  • options as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:model(
  $model-def as map(xs:string,item()*),
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image-type as xs:string := $model-def=>modeldef:image-type()
  let $symmetry as xs:integer := $model-def=>modeldef:symmetry()
  let $periodic-input as xs:boolean := $model-def=>modeldef:periodic-input()
  let $image as map(*) := $model-def=>modeldef:source-image()
  let $N as xs:integer := $model-def=>modeldef:tilesize()

  let $colours as xs:integer* :=
    distinct-values(($image=>matrix:data())!rgb:to-int(.))=>trace("colours")
  let $C as xs:integer := count($colours)
  let $W as xs:integer := math:pow($C, $N*$N) cast as xs:integer
  let $SX as xs:integer := $image=>matrix:columns()
  let $SY as xs:integer := $image=>matrix:rows()
  let $sample as map(*) :=
    let $data :=
      for $y in 1 to $SY
      for $x in 1 to $SX
      let $colour as xs:integer := $image=>matrix:get($y, $x)=>rgb:to-int()
      let $cix as xs:integer :=
        for $col at $i in $colours
        where $colour = $col
        return $i
      (: 
       : -1 because we are going to represent sample arrays as integers
       : base C, so we need 0s
       :)
      return ($cix - 1) cast as xs:double  
    return (
      arr:array($SY (:rows:), $SX(:columns:), $data)
    )

  let $pattern-from-index as function(xs:integer) as map(*) :=
    function($index as xs:integer) as map(*) {
      let $data as xs:integer* := (
        ($index=>util:as-base($C))!(. + 1)
      )
      let $data as xs:integer* := (
        for $i in 1 to $N*$N - count($data) return 1,
        $data
      )
      return (
        arr:array($N, $N, $data)
      )
    }
  let $weights as map(*) := (
    fold-left(
      for $y in 1 to (if ($periodic-input) then $SY else $SY - $N + 1)
      for $x in 1 to (if ($periodic-input) then $SX else $SX - $N + 1)
      return [$x, $y],
      map {},
      function($weights as map(*), $pair as array(xs:integer)) as map(*) {
        let $x as xs:integer := $pair(1)
        let $y as xs:integer := $pair(2)
        let $ps as map(*)* :=
          let $p0 as map(*) := $sample=>this:pattern-from-sample($x, $y, $N)
          let $p1 as map(*) := this:reflect($p0, $N)
          let $p2 as map(*) := this:rotate($p0, $N)
          let $p3 as map(*) := this:reflect($p2, $N)
          let $p4 as map(*) := this:rotate($p2, $N)
          let $p5 as map(*) := this:reflect($p4, $N)
          let $p6 as map(*) := this:rotate($p4, $N)
          let $p7 as map(*) := this:reflect($p6, $N)
          return ($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7)
        return (
          fold-left(1 to $symmetry, $weights,
            function($weights as map(*), $k as xs:integer) as map(*) {
              let $index as xs:integer := this:index($ps[$k], $C)
              return (
                if ($weights=>map:contains($index))
                then $weights=>util:map-increment($index)
                else (
                  $weights=>
                    map:put($index, 1)=>
                    util:map-append("ordering", $index)
                )
              )
            }
          )
        )
      }
    )
  )
  let $ordering as xs:integer* := $weights("ordering")
  let $T as xs:integer := $weights=>map:size() - 1
  let $weights as xs:double* := (
    for $w at $i in $ordering return xs:double($weights($w))
  )
  let $patterns as map(*)* :=
    for $w in $ordering return $pattern-from-index($w)
  let $propagator as array(*)* := (
    for $d in 1 to 4 return array {
      for $t1 in 1 to $T return array {
        let $list as xs:integer* :=
          for $t2 in 1 to $T
          where this:agrees($patterns[$t1], $patterns[$t2], $model:dx[$d], $model:dy[$d])
          return $t2
        return (
          $list
        )
      }
    }
  )

  let $weightLogWeights as xs:double* := 
    for $t in 1 to $T return $weights[$t] * math:log($weights[$t])

  return (
    (: util:log("T="||$T),
    util:log("propagator="||util:quote($propagator)), :)
    util:merge-into((
      model:model($width, $height, $options),
      map {
        "kind": "overlapping-model",
        "colours": $colours,
        "patterns": $patterns,
        "weights": $weights,
        "weightLogWeights": $weightLogWeights,
        "T": $T,
        "propagator": $propagator,
        "on-boundary": this:on-boundary#3
      }
    ))
  )
}

Function: model
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $periodic-input as xs:boolean, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • source-dir as xs:string
  • imagename as xs:string
  • image-type as xs:string
  • symmetry as xs:integer
  • periodic-input as xs:boolean
  • width as xs:integer
  • height as xs:integer
  • options as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $periodic-input as xs:boolean,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) := (
    modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry, $periodic-input)
  )
  return (
    this:model($model-def, $width, $height, $options)
  )
}

Function: model
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • source-dir as xs:string
  • imagename as xs:string
  • image-type as xs:string
  • symmetry as xs:integer
  • width as xs:integer
  • height as xs:integer
  • options as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry)
  return (
    this:model($model-def, $width, $height, $options)
  )
}

Function: model
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • source-dir as xs:string
  • imagename as xs:string
  • image-type as xs:string
  • width as xs:integer
  • height as xs:integer
  • options as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"), $image-type)
  return (
    this:model($model-def, $width, $height, $options)
  )
}

Function: model
declare function model($source-dir as xs:string, $imagename as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • source-dir as xs:string
  • imagename as xs:string
  • width as xs:integer
  • height as xs:integer
  • options as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, true())
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"))
  return (
    this:model($model-def, $width, $height, $options)
  )
}

Function: on-boundary
declare function on-boundary($model as map(xs:string,item()*), $x as xs:integer, $y as xs:integer) as xs:boolean

Params
  • model as map(xs:string,item()*)
  • x as xs:integer
  • y as xs:integer
Returns
  • xs:boolean
declare function this:on-boundary(
  $model as map(xs:string,item()*),
  $x as xs:integer,
  $y as xs:integer
) as xs:boolean
{
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $N as xs:integer := $model("N")
  let $periodic-output as xs:boolean := $model("periodic-output")
  return (
    not($periodic-output) and (
      $x - 1 < 0 or $y - 1 < 0 or $x - 1 + $N > $MX or $y - 1 + $N > $MY
    )
  )
}

Function: options
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer, $heuristic as xs:string) as map(xs:string,item()*)

Params
  • n as xs:integer
  • periodic-output as xs:boolean
  • background as xs:string
  • ground as xs:integer
  • heuristic as xs:string
Returns
  • map(xs:string,item()*)
declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string,
  $ground as xs:integer,
  $heuristic as xs:string
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background, $ground, $heuristic)
}

Function: options
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer) as map(xs:string,item()*)

Params
  • n as xs:integer
  • periodic-output as xs:boolean
  • background as xs:string
  • ground as xs:integer
Returns
  • map(xs:string,item()*)
declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string,
  $ground as xs:integer
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background, $ground)
}

Function: options
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string) as map(xs:string,item()*)

Params
  • n as xs:integer
  • periodic-output as xs:boolean
  • background as xs:string
Returns
  • map(xs:string,item()*)
declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background)
}

Function: options
declare function options($n as xs:integer, $periodic-output as xs:boolean) as map(xs:string,item()*)

Params
  • n as xs:integer
  • periodic-output as xs:boolean
Returns
  • map(xs:string,item()*)
declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output)
}

Function: options
declare function options($n as xs:integer) as map(xs:string,item()*)

Params
  • n as xs:integer
Returns
  • map(xs:string,item()*)
declare function this:options(
  $n as xs:integer
) as map(xs:string,item()*)
{
  model:options($n)
}

Function: options
declare function options() as map(xs:string,item()*)

Returns
  • map(xs:string,item()*)
declare function this:options(
) as map(xs:string,item()*)
{
  model:options()
}

Function: run
declare function run($model as map(xs:string,item()*), $limit as xs:integer) as map(xs:string,item()*)

Params
  • model as map(xs:string,item()*)
  • limit as xs:integer
Returns
  • map(xs:string,item()*)
declare function this:run(
  $model as map(xs:string,item()*),
  $limit as xs:integer
) as map(xs:string,item()*)
{
  $model=>model:run($limit, false())
}

Function: run
declare function run($model as map(xs:string,item()*), $limit as xs:integer, $forced as xs:boolean) as map(xs:string,item()*)

Params
  • model as map(xs:string,item()*)
  • limit as xs:integer
  • forced as xs:boolean
Returns
  • map(xs:string,item()*)
declare function this:run(
  $model as map(xs:string,item()*),
  $limit as xs:integer,
  $forced as xs:boolean
) as map(xs:string,item()*)
{
  $model=>model:run($limit, $forced)
}

Function: continue
declare function continue($model as map(xs:string,item()*), $run as map(xs:string,item()*), $limit as xs:integer) as map(xs:string,item()*)

Params
  • model as map(xs:string,item()*)
  • run as map(xs:string,item()*)
  • limit as xs:integer
Returns
  • map(xs:string,item()*)
declare function this:continue(
  $model as map(xs:string,item()*),
  $run as map(xs:string,item()*),
  $limit as xs:integer
) as map(xs:string,item()*)
{
  $model=>model:continue($run, $limit)
}

Function: graphics
declare function graphics($model as map(*), $run as map(*), $canvas as map(xs:string,item()*)) as map(*)

Params
  • model as map(*)
  • run as map(*)
  • canvas as map(xs:string,item()*)
Returns
  • map(*)
declare function this:graphics(
  $model as map(*),
  $run as map(*),
  $canvas as map(xs:string,item()*)
) as map(*) (: colour map :)
{
  (: util:log("model="||util:quote($model)),
  util:log("run="||util:quote($run)), :)
  let $bg as map(xs:string,item()*) := $model("background")
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $T as xs:integer := $model("T")
  let $N as xs:integer := $model("N")
  let $patterns as map(*)* := $model("patterns")
  let $colours as xs:integer* := $model("colours")
  let $weights as xs:double* := $model("weights")
  let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary")
  let $nodes as array(*) := $run("nodes")
  let $observed as array(*)? := $run("observed")
  let $forced as xs:boolean := $run("forced")
  return (
    if (exists($observed)) then (
      util:log("Collapsed"),
      let $colours as map(xs:string,item()*)* :=
        for $y as xs:integer in 1 to $MY
        let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1
        for $x as xs:integer in 1 to $MX
        let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1
        let $pattern as map(*) := $patterns[$observed($x - $dx + ($y - $dy - 1)*$MX)]
        return (
          rgb:from-int($colours[$pattern=>arr:get($dy + 1, $dx + 1)])
        )
      return matrix:array($MY, $MX, $colours)
    ) else (
      util:log("Uncollapsed"),
      let $colours as map(xs:string,item()*)* :=
        let $pairs as array(xs:integer)* := 
          for $dy as xs:integer in 0 to $N - 1
          for $dx as xs:integer in 0 to $N - 1
          return array {$dy, $dx}
        for $y as xs:integer in 0 to $MY - 1
        for $x as xs:integer in 0 to $MX - 1
        let $cva as item()* := 
          fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0),
            function ($cva as item()*, $pair as array(xs:integer)) as item()* {
              let $contributors as xs:integer := head($cva)
              let $cv as xs:double* := tail($cva)
              let $dy as xs:integer := $pair(1)
              let $dx as xs:integer := $pair(2)
              let $sx as xs:integer := $x - $dx
              let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx
              let $sy as xs:integer := $y - $dy
              let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy
              return (
                if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva
                else (
                  let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?*
                  let $amount as xs:integer := count($a[.=true()])
                  let $ts as xs:integer* := 
                    if ($forced)
                    then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t))
                    else for $t in 1 to $T where $a[$t] return $t
                  let $cv as xs:double* := 
                    fold-left($ts, $cv,
                      function($cv as xs:double*, $t as xs:integer) as xs:double* {
                        util:assert($a[$t], "Bad t"),
                        let $pattern as map(*) := $patterns[$t]
                        let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer
                        let $colour as map(*) := rgb:from-int($colours[$index])
                        let $tcv := $colour=>rgb:raw-coordinates()
                        return $cv=>v:add($tcv)
                      }
                    )
                  return (
                    if ($forced) then $contributors + 1
                    else $contributors + $amount,
                    $cv
                  )
                )
              )
            }
          )
        let $contributors as xs:integer := head($cva)
        let $cv as xs:double* := tail($cva)
        return (
          if ($contributors = 0) then $bg
          else (
            $cv=>v:times(1 div $contributors)=>rgb:to-rgb()
          )
        )
      return (
        matrix:array($MY, $MX, $colours)
      )
    )
  )
}

Function: draw
declare function draw($model as map(*), $run as map(*), $canvas as map(xs:string,item()*)) as item()*

Params
  • model as map(*)
  • run as map(*)
  • canvas as map(xs:string,item()*)
Returns
  • item()*
declare function this:draw(
  $model as map(*),
  $run as map(*),
  $canvas as map(xs:string,item()*)
) as item()*
{
  let $bg as map(xs:string,item()*) := $model("background")
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $T as xs:integer := $model("T")
  let $patterns as map(*)* := $model("patterns")
  let $N as xs:integer := $model("N")
  let $colours as xs:integer* := $model("colours")
  let $weights as xs:double* := $model("weights")
  let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary")
  let $nodes as array(*) := $run("nodes")
  let $observed as array(*)? := $run("observed")
  let $forced as xs:boolean := $run("forced")
  let $scale-x as xs:double := util:decimal(box:width($canvas) div $MX, 2)
  let $scale-y as xs:double := util:decimal(box:height($canvas) div $MY, 2)
  return (
    $canvas=>map:put("fill", rgb:to-string($bg)),
    if (exists($observed)) then (
      util:log("Collapsed"),
      (: We only use 1,1 N,1 1,N and N,N of the patterns :)
      for $colour-id as xs:integer in 
        distinct-values(
          for $i in distinct-values($observed?*)
          let $pattern := $patterns[$i]
          return (
            for $y in (1, $pattern=>arr:rows())
            for $x in (1, $pattern=>arr:columns())
            return $pattern=>arr:get($y, $x) cast as xs:integer
          )
        )
      let $colour as xs:string := $colours[$colour-id]=>rgb:from-int()=>rgb:to-string()
      return (
        def:def("px-"||$colour-id,
          point:point(0, 0)=>geom:with-properties(map {"colour": $colour})
        )
      )
      ,
      slot:slot(
        for $y in 1 to $MY
        let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1
        for $x in 1 to $MX
        let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1
        let $tile-id as xs:integer := $observed($x - $dx + ($y - $dy - 1)*$MX)
        let $colour-ix as xs:integer := 
          $patterns[$tile-id]=>arr:get($dy + 1, $dx + 1) cast as xs:integer
        return (
          def:ref("px-"||$colour-ix, point:point($x - 1, $y - 1), map {})
        )
      )=>slot:scale($scale-x, $scale-y)
    ) else (
      util:log("Uncollapsed"),
      slot:slot(
        let $pairs as array(xs:integer)* := 
          for $dy as xs:integer in 0 to $N - 1
          for $dx as xs:integer in 0 to $N - 1
          return array {$dy, $dx}
        for $y as xs:integer in 0 to $MY - 1
        for $x as xs:integer in 0 to $MX - 1
        let $cva as item()* := 
          fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0),
            function ($cva as item()*, $pair as array(xs:integer)) as item()* {
              let $contributors as xs:integer := head($cva)
              let $cv as xs:double* := tail($cva)
              let $dy as xs:integer := $pair(1)
              let $dx as xs:integer := $pair(2)
              let $sx as xs:integer := $x - $dx
              let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx
              let $sy as xs:integer := $y - $dy
              let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy
              return (
                if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva
                else (
                  let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?*
                  let $amount as xs:integer := count($a[.=true()])
                  let $ts as xs:integer* := 
                    if ($forced)
                    then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t))
                    else for $t in 1 to $T where $a[$t] return $t
                  let $cv as xs:double* := 
                    fold-left($ts, $cv,
                      function($cv as xs:double*, $t as xs:integer) as xs:double* {
                        util:assert($a[$t], "Bad t"),
                        let $pattern as map(*) := $patterns[$t]
                        let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer
                        let $colour as map(*) := rgb:from-int($colours[$index])
                        let $tcv := $colour=>rgb:raw-coordinates()
                        return $cv=>v:add($tcv)
                      }
                    )
                  return (
                    if ($forced) then $contributors + 1
                    else $contributors + $amount,
                    $cv
                  )
                )
              )
            }
          )
        let $contributors as xs:integer := head($cva)
        let $cv as xs:double* := tail($cva)
        return (
          if ($contributors = 0) then () (: background :)
          else (
            let $colour as xs:string := $cv=>v:times(1 div $contributors)=>rgb:to-rgb()=>rgb:to-string()
            return (
              point:point($x, $y)=>geom:with-properties(map {"colour": $colour})
            )
          )
        )
      )=>slot:scale($scale-x, $scale-y)
    )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : Wave Function Collapse
 : This is a port and rework of WFC
 : See https://github.com/mxgmn/WaveFunctionCollapse
 : Requires png/ppm modules, which require Saxon Java extension, EXPath file
 : respectively.
 :
 : Copyright© Mary Holstege 2022-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since July 2022
 : @custom:Status Stable
 :)
module namespace this="http://mathling.com/wfc/overlapping-model";

import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace def="http://mathling.com/type/defref"
       at "../types/defref.xqy";
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.xqy";
import module namespace v="http://mathling.com/core/vector"
       at "../core/vector.xqy";
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace matrix="http://mathling.com/image/matrix"
       at "../image/matrix.xqy";
import module namespace ppm="http://mathling.com/image/ppm"
       at "../image/ppm.xqy";
import module namespace png="http://mathling.com/image/png"
       at "../image/png.xqy";
import module namespace rgb="http://mathling.com/colour/rgb"
       at "../colourspace/rgb.xqy";
import module namespace model="http://mathling.com/wfc/model"
       at "model.xqy";
import module namespace modeldef="http://mathling.com/wfc/modeldef"
       at "modeldef.xqy";

declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";


declare function this:sparse($default as item()*) as map(*)
{
  map {
    "kind": "sparse-propagator",
    "default": $default
  }
};

declare function this:set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*)
{
  let $xmap as map(*) := head(($sparse3($x), map{}))
  let $ymap as map(*) := head(($xmap($y), map{}))
  return
    $sparse3=>map:put($x,
      $xmap=>map:put($y,
        $ymap=>map:put($z, $val)
    )
  )
};

declare function this:set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*)
{
  $sparse2=>map:put($x,
    head(($sparse2($x),map{}))=>map:put($y, $val)
  )
};

declare function this:get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()*
{
  let $xmap := $sparse3($x)
  return (
    if (empty($xmap)) then $sparse3("default") else (
      let $ymap := $xmap($y)
      return (
        if (empty($ymap)) then $sparse3("default") else (
          head(($ymap($z), $sparse3("default")))
        )
      )
    )
  )
};

declare function this:get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()*
{
  let $xmap := $sparse2($x)
  return (
    if (empty($xmap)) then $sparse2("default") else (
      head(($xmap($y), $sparse2("default")))
    )
  )
};

declare function this:flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()*
{
  let $xmap := $sparse3($x)
  return (
    if (empty($xmap)) then $sparse3("default") else (
      let $ymap := $xmap($y)
      return (
        if (empty($ymap)) then $sparse3("default") else (
          for $z in 1 to $n return head(($ymap($z), $sparse3("default")))
        )
      )
    )
  )
};

declare %private function this:rotate(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colour-idxs := $matrix=>arr:data()
  return $matrix=>arr:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colour-idxs[$tilesize - $y + 1 + ($x - 1) * $tilesize]
  )
};

declare %private function this:reflect(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colour-idxs := $matrix=>arr:data()
  return $matrix=>arr:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colour-idxs[$tilesize - $x + 1 + ($y - 1) * $tilesize]
  )
};

declare %private function this:pattern-from-sample(
  $matrix as map(*),
  $x as xs:integer,
  $y as xs:integer,
  $tilesize as xs:integer
) as map(*)
{
  let $data :=
    let $SX := $matrix=>arr:columns()
    let $SY := $matrix=>arr:rows()
    for $dy in 0 to $tilesize - 1
    for $dx in 0 to $tilesize - 1
    return $matrix=>arr:get(util:modix($y + $dy, $SY), util:modix($x + $dx, $SX))
  return arr:array($tilesize, $tilesize, $data)
};


declare %private function this:index(
  $matrix as map(*),
  $C as xs:integer
) as xs:integer
{
  (: Turn the array into a number base C, essentially :)
  let $data := reverse($matrix=>arr:data())
  let $l as xs:integer := count($data)
  let $powers as xs:integer* :=
    fold-left(1 to count($data), 1,
      function($powers as xs:integer*, $i as xs:integer) as xs:integer* {
        $powers, $powers[last()] * $C
      }
    )
  return (
    fold-left(1 to $l, 0,
      function($result as xs:integer, $i as xs:integer) as xs:integer {
        ($result + $powers[$i] * xs:integer($data[$i]))
      }
    )
  )
};

(:~
 : agrees()
 : Tiles are same if we overlap offset by a row or column in the given 
 : direction, e.g.
 : x x o        o x x
 : x x o agrees o x x with direction=4 (right)
 : x x o        o x x
 :)
declare function this:agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean
{
  let $N as xs:integer := $m1=>arr:rows()
  let $N as xs:integer := (
    util:assert($m1=>arr:rows() = $m1=>arr:columns(), "m1.rows!=m1.columns"),
    util:assert($m2=>arr:rows() = $m2=>arr:columns(), "m2.rows!=m2.columns"),
    util:assert($m1=>arr:rows() = $m2=>arr:rows(), "m1.rows!=m2.rows"),
    $N
  )
  let $xmin as xs:integer := if ($dx < 0) then 1 else $dx + 1
  let $xmax as xs:integer := if ($dx < 0) then $dx + $N else $N
  let $ymin as xs:integer := if ($dy < 0) then 1 else $dy + 1
  let $ymax as xs:integer := if ($dy < 0) then $dy + $N else $N
  return (
    every $y in $ymin to $ymax satisfies (
      every $x in $xmin to $xmax satisfies (
        $m1=>arr:get($y, $x) = $m2=>arr:get($y - $dy, $x - $dx)
      )
    )
  )
}; (: agrees :)


declare %private function this:read-tile(
  $source-dir as xs:string,
  $tilename as xs:string,
  $png as xs:boolean
) as map(*)
{
  if ($png) then (
    png:png-array($source-dir||"/"||$tilename||".png")
  ) else (
    ppm:p3-array($source-dir||"/"||$tilename||".ppm")
  )
};

declare function this:model(
  $model-def as map(xs:string,item()*),
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image-type as xs:string := $model-def=>modeldef:image-type()
  let $symmetry as xs:integer := $model-def=>modeldef:symmetry()
  let $periodic-input as xs:boolean := $model-def=>modeldef:periodic-input()
  let $image as map(*) := $model-def=>modeldef:source-image()
  let $N as xs:integer := $model-def=>modeldef:tilesize()

  let $colours as xs:integer* :=
    distinct-values(($image=>matrix:data())!rgb:to-int(.))=>trace("colours")
  let $C as xs:integer := count($colours)
  let $W as xs:integer := math:pow($C, $N*$N) cast as xs:integer
  let $SX as xs:integer := $image=>matrix:columns()
  let $SY as xs:integer := $image=>matrix:rows()
  let $sample as map(*) :=
    let $data :=
      for $y in 1 to $SY
      for $x in 1 to $SX
      let $colour as xs:integer := $image=>matrix:get($y, $x)=>rgb:to-int()
      let $cix as xs:integer :=
        for $col at $i in $colours
        where $colour = $col
        return $i
      (: 
       : -1 because we are going to represent sample arrays as integers
       : base C, so we need 0s
       :)
      return ($cix - 1) cast as xs:double  
    return (
      arr:array($SY (:rows:), $SX(:columns:), $data)
    )

  let $pattern-from-index as function(xs:integer) as map(*) :=
    function($index as xs:integer) as map(*) {
      let $data as xs:integer* := (
        ($index=>util:as-base($C))!(. + 1)
      )
      let $data as xs:integer* := (
        for $i in 1 to $N*$N - count($data) return 1,
        $data
      )
      return (
        arr:array($N, $N, $data)
      )
    }
  let $weights as map(*) := (
    fold-left(
      for $y in 1 to (if ($periodic-input) then $SY else $SY - $N + 1)
      for $x in 1 to (if ($periodic-input) then $SX else $SX - $N + 1)
      return [$x, $y],
      map {},
      function($weights as map(*), $pair as array(xs:integer)) as map(*) {
        let $x as xs:integer := $pair(1)
        let $y as xs:integer := $pair(2)
        let $ps as map(*)* :=
          let $p0 as map(*) := $sample=>this:pattern-from-sample($x, $y, $N)
          let $p1 as map(*) := this:reflect($p0, $N)
          let $p2 as map(*) := this:rotate($p0, $N)
          let $p3 as map(*) := this:reflect($p2, $N)
          let $p4 as map(*) := this:rotate($p2, $N)
          let $p5 as map(*) := this:reflect($p4, $N)
          let $p6 as map(*) := this:rotate($p4, $N)
          let $p7 as map(*) := this:reflect($p6, $N)
          return ($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7)
        return (
          fold-left(1 to $symmetry, $weights,
            function($weights as map(*), $k as xs:integer) as map(*) {
              let $index as xs:integer := this:index($ps[$k], $C)
              return (
                if ($weights=>map:contains($index))
                then $weights=>util:map-increment($index)
                else (
                  $weights=>
                    map:put($index, 1)=>
                    util:map-append("ordering", $index)
                )
              )
            }
          )
        )
      }
    )
  )
  let $ordering as xs:integer* := $weights("ordering")
  let $T as xs:integer := $weights=>map:size() - 1
  let $weights as xs:double* := (
    for $w at $i in $ordering return xs:double($weights($w))
  )
  let $patterns as map(*)* :=
    for $w in $ordering return $pattern-from-index($w)
  let $propagator as array(*)* := (
    for $d in 1 to 4 return array {
      for $t1 in 1 to $T return array {
        let $list as xs:integer* :=
          for $t2 in 1 to $T
          where this:agrees($patterns[$t1], $patterns[$t2], $model:dx[$d], $model:dy[$d])
          return $t2
        return (
          $list
        )
      }
    }
  )

  let $weightLogWeights as xs:double* := 
    for $t in 1 to $T return $weights[$t] * math:log($weights[$t])

  return (
    (: util:log("T="||$T),
    util:log("propagator="||util:quote($propagator)), :)
    util:merge-into((
      model:model($width, $height, $options),
      map {
        "kind": "overlapping-model",
        "colours": $colours,
        "patterns": $patterns,
        "weights": $weights,
        "weightLogWeights": $weightLogWeights,
        "T": $T,
        "propagator": $propagator,
        "on-boundary": this:on-boundary#3
      }
    ))
  )
};

declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $periodic-input as xs:boolean,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) := (
    modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry, $periodic-input)
  )
  return (
    this:model($model-def, $width, $height, $options)
  )
};

declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry)
  return (
    this:model($model-def, $width, $height, $options)
  )
};

declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $image-type as xs:string,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"), $image-type)
  return (
    this:model($model-def, $width, $height, $options)
  )
};

declare function this:model(
  $source-dir as xs:string,
  $imagename as xs:string,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $image as map(*) := this:read-tile($source-dir, $imagename, true())
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $options("n"))
  return (
    this:model($model-def, $width, $height, $options)
  )
};

declare function this:on-boundary(
  $model as map(xs:string,item()*),
  $x as xs:integer,
  $y as xs:integer
) as xs:boolean
{
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $N as xs:integer := $model("N")
  let $periodic-output as xs:boolean := $model("periodic-output")
  return (
    not($periodic-output) and (
      $x - 1 < 0 or $y - 1 < 0 or $x - 1 + $N > $MX or $y - 1 + $N > $MY
    )
  )
};


declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string,
  $ground as xs:integer,
  $heuristic as xs:string
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background, $ground, $heuristic)
};

declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string,
  $ground as xs:integer
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background, $ground)
};

declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean,
  $background as xs:string
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output, $background)
};

declare function this:options(
  $n as xs:integer, 
  $periodic-output as xs:boolean
) as map(xs:string,item()*)
{
  model:options($n, $periodic-output)
};

declare function this:options(
  $n as xs:integer
) as map(xs:string,item()*)
{
  model:options($n)
};

declare function this:options(
) as map(xs:string,item()*)
{
  model:options()
};

declare function this:run(
  $model as map(xs:string,item()*),
  $limit as xs:integer
) as map(xs:string,item()*)
{
  $model=>model:run($limit, false())
};

declare function this:run(
  $model as map(xs:string,item()*),
  $limit as xs:integer,
  $forced as xs:boolean
) as map(xs:string,item()*)
{
  $model=>model:run($limit, $forced)
};

declare function this:continue(
  $model as map(xs:string,item()*),
  $run as map(xs:string,item()*),
  $limit as xs:integer
) as map(xs:string,item()*)
{
  $model=>model:continue($run, $limit)
};

declare function this:graphics(
  $model as map(*),
  $run as map(*),
  $canvas as map(xs:string,item()*)
) as map(*) (: colour map :)
{
  (: util:log("model="||util:quote($model)),
  util:log("run="||util:quote($run)), :)
  let $bg as map(xs:string,item()*) := $model("background")
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $T as xs:integer := $model("T")
  let $N as xs:integer := $model("N")
  let $patterns as map(*)* := $model("patterns")
  let $colours as xs:integer* := $model("colours")
  let $weights as xs:double* := $model("weights")
  let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary")
  let $nodes as array(*) := $run("nodes")
  let $observed as array(*)? := $run("observed")
  let $forced as xs:boolean := $run("forced")
  return (
    if (exists($observed)) then (
      util:log("Collapsed"),
      let $colours as map(xs:string,item()*)* :=
        for $y as xs:integer in 1 to $MY
        let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1
        for $x as xs:integer in 1 to $MX
        let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1
        let $pattern as map(*) := $patterns[$observed($x - $dx + ($y - $dy - 1)*$MX)]
        return (
          rgb:from-int($colours[$pattern=>arr:get($dy + 1, $dx + 1)])
        )
      return matrix:array($MY, $MX, $colours)
    ) else (
      util:log("Uncollapsed"),
      let $colours as map(xs:string,item()*)* :=
        let $pairs as array(xs:integer)* := 
          for $dy as xs:integer in 0 to $N - 1
          for $dx as xs:integer in 0 to $N - 1
          return array {$dy, $dx}
        for $y as xs:integer in 0 to $MY - 1
        for $x as xs:integer in 0 to $MX - 1
        let $cva as item()* := 
          fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0),
            function ($cva as item()*, $pair as array(xs:integer)) as item()* {
              let $contributors as xs:integer := head($cva)
              let $cv as xs:double* := tail($cva)
              let $dy as xs:integer := $pair(1)
              let $dx as xs:integer := $pair(2)
              let $sx as xs:integer := $x - $dx
              let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx
              let $sy as xs:integer := $y - $dy
              let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy
              return (
                if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva
                else (
                  let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?*
                  let $amount as xs:integer := count($a[.=true()])
                  let $ts as xs:integer* := 
                    if ($forced)
                    then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t))
                    else for $t in 1 to $T where $a[$t] return $t
                  let $cv as xs:double* := 
                    fold-left($ts, $cv,
                      function($cv as xs:double*, $t as xs:integer) as xs:double* {
                        util:assert($a[$t], "Bad t"),
                        let $pattern as map(*) := $patterns[$t]
                        let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer
                        let $colour as map(*) := rgb:from-int($colours[$index])
                        let $tcv := $colour=>rgb:raw-coordinates()
                        return $cv=>v:add($tcv)
                      }
                    )
                  return (
                    if ($forced) then $contributors + 1
                    else $contributors + $amount,
                    $cv
                  )
                )
              )
            }
          )
        let $contributors as xs:integer := head($cva)
        let $cv as xs:double* := tail($cva)
        return (
          if ($contributors = 0) then $bg
          else (
            $cv=>v:times(1 div $contributors)=>rgb:to-rgb()
          )
        )
      return (
        matrix:array($MY, $MX, $colours)
      )
    )
  )
}; (: graphics :)


declare function this:draw(
  $model as map(*),
  $run as map(*),
  $canvas as map(xs:string,item()*)
) as item()*
{
  let $bg as map(xs:string,item()*) := $model("background")
  let $MX as xs:integer := $model("MX")
  let $MY as xs:integer := $model("MY")
  let $T as xs:integer := $model("T")
  let $patterns as map(*)* := $model("patterns")
  let $N as xs:integer := $model("N")
  let $colours as xs:integer* := $model("colours")
  let $weights as xs:double* := $model("weights")
  let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary")
  let $nodes as array(*) := $run("nodes")
  let $observed as array(*)? := $run("observed")
  let $forced as xs:boolean := $run("forced")
  let $scale-x as xs:double := util:decimal(box:width($canvas) div $MX, 2)
  let $scale-y as xs:double := util:decimal(box:height($canvas) div $MY, 2)
  return (
    $canvas=>map:put("fill", rgb:to-string($bg)),
    if (exists($observed)) then (
      util:log("Collapsed"),
      (: We only use 1,1 N,1 1,N and N,N of the patterns :)
      for $colour-id as xs:integer in 
        distinct-values(
          for $i in distinct-values($observed?*)
          let $pattern := $patterns[$i]
          return (
            for $y in (1, $pattern=>arr:rows())
            for $x in (1, $pattern=>arr:columns())
            return $pattern=>arr:get($y, $x) cast as xs:integer
          )
        )
      let $colour as xs:string := $colours[$colour-id]=>rgb:from-int()=>rgb:to-string()
      return (
        def:def("px-"||$colour-id,
          point:point(0, 0)=>geom:with-properties(map {"colour": $colour})
        )
      )
      ,
      slot:slot(
        for $y in 1 to $MY
        let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1
        for $x in 1 to $MX
        let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1
        let $tile-id as xs:integer := $observed($x - $dx + ($y - $dy - 1)*$MX)
        let $colour-ix as xs:integer := 
          $patterns[$tile-id]=>arr:get($dy + 1, $dx + 1) cast as xs:integer
        return (
          def:ref("px-"||$colour-ix, point:point($x - 1, $y - 1), map {})
        )
      )=>slot:scale($scale-x, $scale-y)
    ) else (
      util:log("Uncollapsed"),
      slot:slot(
        let $pairs as array(xs:integer)* := 
          for $dy as xs:integer in 0 to $N - 1
          for $dx as xs:integer in 0 to $N - 1
          return array {$dy, $dx}
        for $y as xs:integer in 0 to $MY - 1
        for $x as xs:integer in 0 to $MX - 1
        let $cva as item()* := 
          fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0),
            function ($cva as item()*, $pair as array(xs:integer)) as item()* {
              let $contributors as xs:integer := head($cva)
              let $cv as xs:double* := tail($cva)
              let $dy as xs:integer := $pair(1)
              let $dx as xs:integer := $pair(2)
              let $sx as xs:integer := $x - $dx
              let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx
              let $sy as xs:integer := $y - $dy
              let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy
              return (
                if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva
                else (
                  let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?*
                  let $amount as xs:integer := count($a[.=true()])
                  let $ts as xs:integer* := 
                    if ($forced)
                    then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t))
                    else for $t in 1 to $T where $a[$t] return $t
                  let $cv as xs:double* := 
                    fold-left($ts, $cv,
                      function($cv as xs:double*, $t as xs:integer) as xs:double* {
                        util:assert($a[$t], "Bad t"),
                        let $pattern as map(*) := $patterns[$t]
                        let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer
                        let $colour as map(*) := rgb:from-int($colours[$index])
                        let $tcv := $colour=>rgb:raw-coordinates()
                        return $cv=>v:add($tcv)
                      }
                    )
                  return (
                    if ($forced) then $contributors + 1
                    else $contributors + $amount,
                    $cv
                  )
                )
              )
            }
          )
        let $contributors as xs:integer := head($cva)
        let $cv as xs:double* := tail($cva)
        return (
          if ($contributors = 0) then () (: background :)
          else (
            let $colour as xs:string := $cv=>v:times(1 div $contributors)=>rgb:to-rgb()=>rgb:to-string()
            return (
              point:point($x, $y)=>geom:with-properties(map {"colour": $colour})
            )
          )
        )
      )=>slot:scale($scale-x, $scale-y)
    )
  )
};