http://mathling.com/art/hyphae  library module

http://mathling.com/art/hyphae


HYPHAE:

Parameters:
hyphae.n: Max number of nodes, iteration limit
hyphae.seeds: Initial number of seeds
hyphae.branches.max: Max branch attempts from node
hyphae.radius.max: 1/2 max path width
hyphae.radius.fade: Radius changes this-fold each branch
hyphae.radius.function: f($k) adjustment to radius fade
hyphae.search-angle.range: Standard deviation on search angle
hyphae.search-angle.damping: damping in angle variation per generation
hyphea.tropism.mode:
default=damped: use search angle with increasing variability with each generation
adhoc: use function indicated by hyphae.tropism.function (no default)
the tropism function takes $randomizers, $parameters and returns
a tropism that takes an entry and returns an angle
hyphae.colour.mode: Colouring scheme
default=generation: generation
k: based on parent index
random: random colour
distance: distance from center
width: radius with
single: one (random) colour
order: based on point index
adhoc: use function indicated by hyphae.colour.function (no default)
the colour function takes $entries, $randomizers, $parameters and
returns a function that takes an entry and returns a stop number
hyphae.stop.max: Expected maximum value for colour stops (used for some colour schemes)
hyphae.fatness: Relative width of strokes (0,1]

Randomizers:
hyphae.init-angles: initial angles
hyphae.search-angles: branching angles
hyphae.init-radii: radius multipliers (multiplied by max-radius)

Rendering parameters:
background.fill
signature.colour
default-stroke-width
dot-shape
graph.gradient
graph.edge-mode: circles|sand|brush|graph|stroke
sand.grains
sand.opacity
sand.size

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

February 2023
Status: Stable

Imports

http://mathling.com/geometric/intersection
import module namespace inter="http://mathling.com/geometric/intersection"
       at "../geo/intersection.xqy"
http://mathling.com/type/distribution
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy"
http://mathling.com/type/wrapper
import module namespace wrapper="http://mathling.com/type/wrapper"
       at "../types/wrapper.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/geometric/graph
import module namespace graph="http://mathling.com/geometric/graph"
       at "../geo/graph.xqy"
http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/core/sparse
import module namespace sparse="http://mathling.com/core/sparse"
       at "../core/sparse.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"

Functions

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


Standard callback: Subcomponent map.

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

Function: rendering-parameters
declare function rendering-parameters($canvas as map(xs:string,item()*), $algorithm-parameters as map(xs:string,item()*)) as map(xs:string,item()*)


Standard callback: Set of rendering parameters.

Params
  • canvas as map(xs:string,item()*): drawing canvas
  • algorithm-parameters as map(xs:string,item()*): set of algorithm parameters
Returns
  • map(xs:string,item()*)
declare function this:rendering-parameters(
  $canvas as map(xs:string,item()*),
  $algorithm-parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "background.fill": "black",
    "signature.colour": "white",
    "default-stroke-width": 1,
    "dot-shape": "none",
    "graph.gradient": "plasma",
    "graph.edge-mode": "circles", (: circles|sand|brush|graph|stroke :)
    "sand.grains": 10,
    "sand.opacity": 0.5,
    "sand.size": 1
  }
}

Function: algorithm-mode-parameters
declare function algorithm-mode-parameters($mode as xs:string, $resolution as xs:string, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)


Standard callback: Set of algorithm parameters specific to a particular mode.

Params
  • mode as xs:string: selected mode
  • resolution as xs:string: defined resolution
  • canvas as map(xs:string,item()*): drawing canvas
Returns
  • map(xs:string,item()*)
declare function this:algorithm-mode-parameters(
  $mode as xs:string,
  $resolution as xs:string,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch ($mode)
  case "default" return map {}
  default return errors:error("ML-BADPARMS", ("mode", $mode))
}

Function: algorithm-parameters
declare function algorithm-parameters($resolution as xs:string, $canvas as map(xs:string, item()*)) as map(xs:string,item()*)


Standard callback: Set of default algorithm parameters.

Params
  • resolution as xs:string: defined resolution
  • canvas as map(xs:string,item()*): drawing canvas
Returns
  • map(xs:string,item()*)
declare function this:algorithm-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string, item()*)
) as map(xs:string,item()*)
{
  map {
    "description": "Non-overlapping hyphae",
    "hyphae.n": 25000,             (: Max number of nodes :)
    "hyphae.seeds": 9,             (: Initial number of seeds :)
    "hyphae.branches.max": 15,     (: Max branch attempts from node :)
    "hyphae.radius.max": 40,       (: 1/2 max path width :)
    "hyphae.radius.fade": 0.92,    (: Radius change per branch:)
    "hyphae.radius.function": this:identity#1, (: f(k) adjustment :)
    "hyphae.search-angle.range": 180, (: Standard deviation in search angle :)
    "hyphae.search-angle.damping": 0.1, (: 1 - 1/pow(gen,damp) = multiplier of δangle :)
    "hyphea.fatness": 0.35,        (: Relative thickness of lines :)
    "hyphae.colour.mode": "generation", (: How to calculate colour stops :)
    "hyphae.stop.max": 512,         (: Constraint on colour stops :)
    "hyphae.tropism.mode": "damped" (: How to grow :)
  }
}

Function: randomizers
declare function randomizers($canvas as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)


Standard callback: Set of randomizers.

Params
  • canvas as map(xs:string,item()*): drawing canvas
  • parameters as map(xs:string,item()*): algorithm parameter bundle
Returns
  • map(xs:string,item()*)
declare function this:randomizers(
  $canvas as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "hyphae.init-angles": dist:uniform(0, math:pi()*2),
    "hyphae.search-angles":
      dist:normal(0, core:parameter("hyphae.search-angle.range", $parameters)),

    (: ($RAD + 0.2*$RAD*(1 - 2*rand:randomize($rand:STD-UNIFORM))) :)
    (: Multiply by $RAD to get results :)
    "hyphae.init-radii": dist:uniform(0.8, 1.2)
  }
}

Function: colophon
declare function colophon($parameters as map(xs:string,item()*)) as xs:string?


Standard callback: component colophon (string attached to signature).

Params
  • parameters as map(xs:string,item()*): algorithm parameter bundle
Returns
  • xs:string?
declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string?
{
  ()
}

Function: metadata
declare function metadata($canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*))


Standard callback: component metadata (in addition to dump of randomizers
and parameters)

Params
  • canvas as map(xs:string,item()*): drawing canvas
  • randomizers as map(xs:string,item()*): active randomizers for component
  • parameters as map(xs:string,item()*): active parameters for component
declare function this:metadata(
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
)
{
  ()
}

Function: identity
declare function identity($k as xs:integer) as xs:double


identity()
Default radius adjustment function (no adjustment).

Params
  • k as xs:integer: parent node ID
Returns
  • xs:double: adjustment
declare function this:identity($k as xs:integer) as xs:double
{
  $k cast as xs:double
}

Function: sin
declare function sin($k as xs:integer) as xs:double


sin()
Radius adjustment function that expands and contracts.

Params
  • k as xs:integer: parent node ID
Returns
  • xs:double: adjustment
declare function this:sin($k as xs:integer) as xs:double
{ 
  math:sin($k div 500)
}

Function: hyphae
declare function hyphae($init-boundaries as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)


hyphae()
Non-overlapping hyphae growth.

Params
  • init-boundaries as map(xs:string,item()*)*: bounding regions for initial seeds
  • boundaries as map(xs:string,item()*)*: bounding regions for growth
  • canvas as map(xs:string,item()*)
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:hyphae(
  $init-boundaries as map(xs:string,item()*)*,
  $boundaries as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $num_seeds := core:parameter("hyphae.seeds", $parameters)
  (:
   : All calculations are done in unit box [0,1]X[0,1] so scale
   : boundaries to that
   :)
  let $init-boundaries :=
    $init-boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $seeds := this:unit-seeds($num_seeds, $init-boundaries)
  let $boundaries :=
    $boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters)
  return (
    wrapper:wrapper(
      this:graph($nodes, $canvas, $randomizers, $parameters),
      map {
        "hyphae.init-boundaries": geom:quote($init-boundaries),
        "hyphae.boundaries": geom:quote($boundaries)
      }
    )
  )
}

Function: hyphae-seeded
declare function hyphae-seeded($seeds as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)


hyphae-seeded()
Non-overlapping hyphae growth with pre-determined seeds.

Params
  • seeds as map(xs:string,item()*)*: initial seeds
  • boundaries as map(xs:string,item()*)*: bounding regions for growth
  • canvas as map(xs:string,item()*)
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:hyphae-seeded(
  $seeds as map(xs:string,item()*)*,
  $boundaries as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  (:
   : All calculations are done in unit box [0,1]X[0,1] so scale
   : boundaries and seeds to that
   :)
  let $boundaries :=
    $boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $seeds :=
    $seeds=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters)
  return (
    wrapper:wrapper(
      this:graph($nodes, $canvas, $randomizers, $parameters),
      map {
        "hyphae.boundaries": geom:quote($boundaries)
      }
    )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : HYPHAE: 
 :
 : Parameters:
 : hyphae.n: Max number of nodes, iteration limit
 : hyphae.seeds: Initial number of seeds
 : hyphae.branches.max: Max branch attempts from node
 : hyphae.radius.max: 1/2 max path width
 : hyphae.radius.fade: Radius changes this-fold each branch
 : hyphae.radius.function: f($k) adjustment to radius fade
 : hyphae.search-angle.range: Standard deviation on search angle
 : hyphae.search-angle.damping: damping in angle variation per generation
 : hyphea.tropism.mode:
 :   default=damped: use search angle with increasing variability with each generation
 :   adhoc: use function indicated by hyphae.tropism.function (no default)
 :     the tropism function takes $randomizers, $parameters and returns
 :     a tropism that takes an entry and returns an angle
 : hyphae.colour.mode: Colouring scheme
 :   default=generation: generation
 :   k: based on parent index
 :   random: random colour
 :   distance: distance from center
 :   width: radius with
 :   single: one (random) colour
 :   order: based on point index
 :   adhoc: use function indicated by hyphae.colour.function (no default)
 :     the colour function takes $entries, $randomizers, $parameters and
 :     returns a function that takes an entry and returns a stop number
 : hyphae.stop.max: Expected maximum value for colour stops (used for some colour schemes)
 : hyphae.fatness: Relative width of strokes (0,1]
 :
 : Randomizers:
 : hyphae.init-angles: initial angles
 : hyphae.search-angles: branching angles
 : hyphae.init-radii: radius multipliers (multiplied by max-radius)
 :
 : Rendering parameters:
 : background.fill
 : signature.colour
 : default-stroke-width
 : dot-shape
 : graph.gradient
 : graph.edge-mode: circles|sand|brush|graph|stroke
 : sand.grains
 : sand.opacity
 : sand.size
 :
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since February 2023
 : @custom:Status Stable
 :)
module namespace this="http://mathling.com/art/hyphae";

import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace sparse="http://mathling.com/core/sparse"
       at "../core/sparse.xqy";
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy";
import module namespace wrapper="http://mathling.com/type/wrapper"
       at "../types/wrapper.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 inter="http://mathling.com/geometric/intersection"
       at "../geo/intersection.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace graph="http://mathling.com/geometric/graph"
       at "../geo/graph.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";

declare namespace svg="http://www.w3.org/2000/svg";
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 namespace art="http://mathling.com/art";

(:~
 : Standard callback: Subcomponent map.
 :)
declare function this:components() as map(xs:string, map(xs:string,item()*))
{
  map {
  }
};

(:~
 : Standard callback: Set of rendering parameters.
 : @param $canvas: drawing canvas
 : @param $algorithm-parameters: set of algorithm parameters
 :)
declare function this:rendering-parameters(
  $canvas as map(xs:string,item()*),
  $algorithm-parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "background.fill": "black",
    "signature.colour": "white",
    "default-stroke-width": 1,
    "dot-shape": "none",
    "graph.gradient": "plasma",
    "graph.edge-mode": "circles", (: circles|sand|brush|graph|stroke :)
    "sand.grains": 10,
    "sand.opacity": 0.5,
    "sand.size": 1
  }
};

(:~
 : Standard callback: Set of algorithm parameters specific to a particular mode.
 : @param $mode: selected mode
 : @param $resolution: defined resolution
 : @param $canvas: drawing canvas
 :)
declare function this:algorithm-mode-parameters(
  $mode as xs:string,
  $resolution as xs:string,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch ($mode)
  case "default" return map {}
  default return errors:error("ML-BADPARMS", ("mode", $mode))
};

(:~
 : Standard callback: Set of default algorithm parameters.
 : @param $resolution: defined resolution
 : @param $canvas: drawing canvas
 :)
declare function this:algorithm-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string, item()*)
) as map(xs:string,item()*)
{
  map {
    "description": "Non-overlapping hyphae",
    "hyphae.n": 25000,             (: Max number of nodes :)
    "hyphae.seeds": 9,             (: Initial number of seeds :)
    "hyphae.branches.max": 15,     (: Max branch attempts from node :)
    "hyphae.radius.max": 40,       (: 1/2 max path width :)
    "hyphae.radius.fade": 0.92,    (: Radius change per branch:)
    "hyphae.radius.function": this:identity#1, (: f(k) adjustment :)
    "hyphae.search-angle.range": 180, (: Standard deviation in search angle :)
    "hyphae.search-angle.damping": 0.1, (: 1 - 1/pow(gen,damp) = multiplier of δangle :)
    "hyphea.fatness": 0.35,        (: Relative thickness of lines :)
    "hyphae.colour.mode": "generation", (: How to calculate colour stops :)
    "hyphae.stop.max": 512,         (: Constraint on colour stops :)
    "hyphae.tropism.mode": "damped" (: How to grow :)
  }
};

(:~
 : Standard callback: Set of randomizers.
 : @param $canvas: drawing canvas
 : @param $parameters: algorithm parameter bundle
 :)
declare function this:randomizers(
  $canvas as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "hyphae.init-angles": dist:uniform(0, math:pi()*2),
    "hyphae.search-angles":
      dist:normal(0, core:parameter("hyphae.search-angle.range", $parameters)),

    (: ($RAD + 0.2*$RAD*(1 - 2*rand:randomize($rand:STD-UNIFORM))) :)
    (: Multiply by $RAD to get results :)
    "hyphae.init-radii": dist:uniform(0.8, 1.2)
  }
};

(:~
 : Standard callback: component colophon (string attached to signature).
 : @param $parameters: algorithm parameter bundle
 :)
declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string?
{
  ()
};

(:~
 : Standard callback: component metadata (in addition to dump of randomizers
 : and parameters)
 :
 : @param $canvas: drawing canvas
 : @param $randomizers: active randomizers for component
 : @param $parameters: active parameters for component
 :)
declare function this:metadata(
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
)
{
  ()
};

(: ======================================================================
 : Hyphae: non-overlapping growth
 : ====================================================================== :)

declare %private function this:near_zone_inds($pt as map(xs:string,item()*), $zones as xs:integer, $Z as array(*)) as xs:integer*
{
  (: 
   : Z is array of arrays of points: Z[z] = points in zone #z 
   : ZONES is the scaled zone width SIZE idiv ZONEWIDTH 
   : So i and j are the max offset from this point in a zone 
   : Z operates as a 2D array with enough space allocated for the maximum
   : span of points index (get_z) is (1+x*ZONES)*ZONES + 1+y*ZONES
   : so we're getting all zone combinations of (i-1,i,i+1) and (j-1,j,j+1)
   : i.e. adjacent points in the zone arrays
   :)
  distinct-values(
    let $i := (1 + point:px($pt)*$zones) cast as xs:integer
    let $j := (1 + point:py($pt)*$zones) cast as xs:integer
    for $a in ($i - 1, $i, $i + 1)
    for $b in ($j - 1, $j, $j + 1)
    let $ij := $a*$zones + $b
    where $ij > 1
    return array:flatten($Z($ij))
  )
};

(:~
 : The zone for this point 
 : ZONES is the scaled zone width SIZE idiv ZONEWIDTH 
 : So i and j are the max offset from this point in a zone 
 : i*ZONES+j is essentially treating the zone array as a 2D array allocating
 : enough space in each portion for the max zone size
 :)
declare %private function this:get_z($zones as xs:integer, $pt as map(xs:string,item()*)) as xs:integer
{
  let $i := (1 + point:px($pt)*$zones) cast as xs:integer
  let $j := (1 + point:py($pt)*$zones) cast as xs:integer
  return $i*$zones+$j
};

declare %private function this:node(
  $node-data as map(xs:string,item()*), 
  $k as xs:integer, (: parent to branch from :)
  $num as xs:integer, (: current number of nodes :)
  $boundaries as map(xs:string,item()*)*,
  $tropism as function (map(xs:string,item()*)) as xs:double
) as map(xs:string,item()*)
{
  let $one := $node-data("one")
  let $zones := $node-data("zones")
  let $max-branches := $node-data("max-branches")
  let $radius-fade := $node-data("radius-fade") * $node-data("radius-adjustment")($k)
  let $NODES := $node-data("NODES")
  let $Z := $node-data("Z")
  let $entry := $NODES=>sparse:get($k)
  let $entry := $entry=>map:put("br", $entry("br")+1)
  let $NODES := $NODES=>sparse:put($k, $entry)
  let $node-data := $node-data=>map:put("NODES", $NODES)
  return (
    if ($entry("br") > $max-branches) then (
      (: It's dead, Jim :)
      $node-data=>
        map:put("got-node", false())
    ) else (
      (: Alternative r := RAD + rand:randomize($rand:STD-UNIFORM)*ONE*R_RAND_SIZE :)
      let $Dk := $entry("d")
      let $Rk := $entry("r")
      let $r := if ($Dk > -1 ) then $Rk*$radius-fade else $Rk
      return (
        if ($r < $one) then (
          (: Kill this node :)
          $node-data=>
            map:put("NODES",
              $NODES=>sparse:put($k, 
                $entry=>map:put("br", $max-branches + 1)
              )
            )=>map:put("got-node", false())
        ) else (
          let $GEk := $entry("ge")
          let $ge := if ($Dk > -1) then $GEk+1 else $GEk
          let $θ := $tropism($entry)
          let $PTSk := $entry("pt")
          let $pt2 := point:destination($PTSk, $θ, $r)

          return (
            (: stop nodes outside of bounds :)
            (: remember to set initial node inside boundary :)
            let $good :=
              if (
                every $boundary in $boundaries
                satisfies not(inter:region-contains($boundary, $pt2))  
              ) then (
                (: node is outside bounds :)
                false()
              ) else (
                let $Pk := $entry("p")
                let $inds := this:near_zone_inds($pt2,$zones,$Z)
                let $inds := $inds[not(. = $k) and not(. = $Pk)]
                return (
                  if (count($inds) > 0) then (
                    every $ind in $inds satisfies (
                      let $node := $NODES=>sparse:get($ind)
                      let $PTSind := $node("pt")
                      let $Rind := $node("r")
                      return
                      (: i.e. the new point is a full circle away from the old :)
                        2 * point:distance($PTSind, $pt2) > $Rind + $r
                    )
                  ) else (
                    true()
                  )
                )
              )
            return (
              if ($good) then (
                let $new-entry :=
                  util:merge-into($NODES=>sparse:get($num),
                    map {
                      "pt": $pt2,
                      "r": $r,
                      "θ": $θ,
                      "ge": $ge,
                      "p": $k
                    }
                  )
                let $new-entry :=
                  if ($Dk < 0)
                  then $new-entry=>map:put("d", $num)
                  else $new-entry
                let $NODES := $NODES=>sparse:put($num, $new-entry)
                let $z := this:get_z($zones, $pt2)
                let $Z := $Z=>array:put($z, $Z=>array:get($z)=>array:append($num))
                let $node-data :=
                  $node-data=>
                    map:put("NODES", $NODES)=>
                    map:put("Z", $Z)=>
                    map:put("got-node", true())
                return $node-data
              ) else (
                $node-data=>
                  map:put("got-node", false())
              )
            )
          )
        ) (: else: r>1 :)
      )
    ) (: else: c < max-branches :)
  )
};

declare %private function this:nodes(
  $seeds as map(xs:string,item()*)*,
  $boundaries as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(*)
{
  let $iterations := core:parameter("hyphae.n", $parameters)
  let $search-angles := core:randomizer("hyphae.search-angles", $randomizers)
  let $max-branches := core:parameter("hyphae.branches.max", $parameters)
  let $radius-fade := core:parameter("hyphae.radius.fade", $parameters)
  let $radius-adjustment := core:parameter("hyphae.radius.function", $parameters)
  let $damping := core:parameter("hyphae.search-angle.damping", $parameters)
  let $size := box:diagonal($canvas)
  let $one := 1 div $size (: one scaled to canvas :)
  let $rad := core:parameter("hyphae.radius.max", $parameters) * $one
  let $zonewidth := 2 * ($rad div $one)
  let $zones := $size idiv $zonewidth
  let $n-seeds := count($seeds)  
  let $Z := array { for $i in 1 to ($zones+2)*($zones+2) return array{ () } }
  let $NODES :=
    sparse:array(
      map {
        "pt": $point:ORIGIN, (: location on the unit square :)
        "r": 0,              (: radius of line :)
        "θ": 0,              (: current angle :)
        "ge": 0,             (: generation :)
        "p": -1,             (: parent point index :)
        "br": 0,             (: branches attempted :)
        "d": -1              (: point index :)
      }
    )
  let $radii := core:randomize($n-seeds, "hyphae.init-radii", $randomizers)!(. *$rad)
  let $angles := core:randomize($n-seeds, "hyphae.init-angles", $randomizers)
  let $Z :=
    fold-left(1 to $n-seeds, $Z,
      function($Z as array(*), $i as xs:integer) as array(*) {
        let $z := this:get_z($zones, $seeds[$i])
        return $Z=>array:put($z, $Z=>array:get($z)=>array:append($i))
      }
    )
  let $NODES :=
    fold-left(1 to $n-seeds, $NODES,
      function($NODES as map(*), $i as xs:integer) as map(*) {
        $NODES=>sparse:put($i,
          map {
            "pt": $seeds[$i],
            "r": $radii[$i],
            "θ": $angles[$i],
            "ge": 1,
            "p": -1,
            "br": 0,
            "d": -1
          }
        )
      }
    )
  let $num := $n-seeds
  let $node-data :=
    map {
      "NODES": $NODES,
      "Z": $Z,
      "one": $one,
      "zones": $zones,
      "max-branches": $max-branches,
      "radius-fade": $radius-fade,
      "radius-adjustment": $radius-adjustment
    }
  let $tropism := (: entry => angle :)
    switch (core:parameter("hyphae.tropism.mode", $parameters))
    case "adhoc" return (
      core:parameter("hyphae.tropism.function", $parameters)($randomizers, $parameters)
    )
    default (: damped :) return (
      function ($entry as map(*)) as xs:double {
        let $Dk := $entry("d")
        let $GEk := $entry("ge")
        let $ge := if ($Dk > -1) then $GEk+1 else $GEk
        let $θk := $entry("θ")
        let $θ := $θk + (1 - 1 div math:pow($ge + 1, $damping))*rand:randomize($search-angles)
        return $θ
      }
    )
  let $node-data := (
    fold-left(1 to $iterations, ($n-seeds, $node-data),
      function($info as item()*, $i as xs:integer) as item()* {
        let $num := head($info)
        let $node-data := tail($info)
        let $k := rand:uniform(1, $num)=>rand:cast("integer")
        let $node-data := this:node($node-data, $k, $num, $boundaries, $tropism)
        return (
          if ($node-data("got-node")) then (
            $num + 1, $node-data
          ) else (
            $num, $node-data
          )
        )
      }
    )=>tail()
  )
  return $node-data("NODES")
};

declare %private function this:graph(
  $nodes as map(*),
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $scale-x := box:width($canvas)
  let $scale-y := box:height($canvas)
  let $fatness := core:parameter("hyphae.fatness", $parameters)
  let $rscale := (box:width($canvas) idiv 3) * $fatness
  let $entries := $nodes=>sparse:collection()
  let $stop-fn :=
    let $colour-mode := core:parameter("hyphae.colour.mode", $parameters)
    let $stop-max := core:parameter("hyphae.stop.max", $parameters)
    return
    switch ($colour-mode)
    case "distance" return (
      let $center := point:point(0.5, 0.5)
      return
      function($entry as map(*)) as xs:integer {
        util:intmix(1, $stop-max, point:distance($entry("pt"), $center) div 0.5)
      }
    )
    case "random" return (
      let $dist := dist:uniform(1, $stop-max)=>dist:cast("integer")
      return
      function($entry as map(*)) as xs:integer {
        rand:randomize($dist)
      }
    )
    case "single" return (
      let $stop := rand:uniform(1, $stop-max)=>rand:cast("integer")
      return
      function($entry as map(*)) as xs:integer {
        $stop
      }
    )
    case "width" return (
      let $radius-max := max($entries!(.("r")))
      let $radius-min := min($entries!(.("r")))
      let $radius-min := if ($radius-max = $radius-min) then 0 else $radius-min
      return
      function($entry as map(*)) as xs:integer {
        util:intmix(1, $stop-max, 1 - ($entry("r") - $radius-min) div ($radius-max - $radius-min))
      }
    )
    case "order" return (
      function($entry as map(*)) as xs:integer {
        $entry("d")
      }
    )
    case "k" return (
      function($entry as map(*)) as xs:integer {
        $entry("p")
      }
    )
    case "adhoc" return (
      core:parameter("hyphae.colour.function", $parameters)(
        $entries, $randomizers, $parameters
      )
    )
    default (: generation :) return (
      let $max-gen := max($entries!(.("ge")))
      return
      function($entry as map(*)) as xs:integer {
        util:intmix(1, $stop-max, $entry("ge") div $max-gen)
      }
    )
  let $edges :=
    for $entry in $entries
    let $k := $entry("p")
    let $parent := if ($k = -1) then () else ($nodes=>sparse:get($k))("pt")
    let $stop := $stop-fn($entry)
    where $k != -1
    return (
      edge:edge(
        $parent,
        $entry("pt"), 
        util:round($entry("r")*$rscale),
        map {"stop": $stop}
      )=>geom:scale($scale-x, $scale-y)
    )
  let $points :=
    for $entry in $entries return (
      $entry("pt")=>geom:scale($scale-x, $scale-y)
    )
  let $properties := map {"no-vertices": true()}
  return (
    util:log("nodes="||count($entries)||" edges="||count($edges)),
    graph:graph($points, $edges, $properties)
  )
};

declare %private function this:unit-seeds(
  $n-seeds as xs:integer,
  $bounds as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $unit := dist:uniform(0.0, 1.0) return
  if ($n-seeds = 0) then () else (
    let $seeds :=
      geom:random-points($n-seeds, $unit, $unit)[
        some $boundary in $bounds satisfies $boundary=>inter:region-contains(.)
      ]
    let $n := count($seeds)
    return (
      if ($n=$n-seeds) then $seeds else (
        $seeds, this:unit-seeds($n-seeds - $n, $bounds)
      )
    )
  )
};

(:~
 : identity()
 : Default radius adjustment function (no adjustment).
 : @param $k: parent node ID
 : @return adjustment
 :)
declare function this:identity($k as xs:integer) as xs:double
{
  $k cast as xs:double
};

(:~
 : sin()
 : Radius adjustment function that expands and contracts.
 : @param $k: parent node ID
 : @return adjustment
 :)
declare function this:sin($k as xs:integer) as xs:double
{ 
  math:sin($k div 500)
};

(:~
 : hyphae()
 : Non-overlapping hyphae growth.
 :
 : @param $init-boundaries: bounding regions for initial seeds
 : @param $boundaries: bounding regions for growth
 : @param: $canvas: drawing canvas
 : @param: $randomizers: randomizer bundle
 : @param: $parameters: parameter bundle
 :)
declare function this:hyphae(
  $init-boundaries as map(xs:string,item()*)*,
  $boundaries as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $num_seeds := core:parameter("hyphae.seeds", $parameters)
  (:
   : All calculations are done in unit box [0,1]X[0,1] so scale
   : boundaries to that
   :)
  let $init-boundaries :=
    $init-boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $seeds := this:unit-seeds($num_seeds, $init-boundaries)
  let $boundaries :=
    $boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters)
  return (
    wrapper:wrapper(
      this:graph($nodes, $canvas, $randomizers, $parameters),
      map {
        "hyphae.init-boundaries": geom:quote($init-boundaries),
        "hyphae.boundaries": geom:quote($boundaries)
      }
    )
  )
};

(:~
 : hyphae-seeded()
 : Non-overlapping hyphae growth with pre-determined seeds.
 :
 : @param $seeds: initial seeds
 : @param $boundaries: bounding regions for growth
 : @param: $canvas: drawing canvas
 : @param: $randomizers: randomizer bundle
 : @param: $parameters: parameter bundle
 :)
declare function this:hyphae-seeded(
  $seeds as map(xs:string,item()*)*,
  $boundaries as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  (:
   : All calculations are done in unit box [0,1]X[0,1] so scale
   : boundaries and seeds to that
   :)
  let $boundaries :=
    $boundaries=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $seeds :=
    $seeds=>
      geom:scale(1 div box:width($canvas), 1 div box:height($canvas))
  let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters)
  return (
    wrapper:wrapper(
      this:graph($nodes, $canvas, $randomizers, $parameters),
      map {
        "hyphae.boundaries": geom:quote($boundaries)
      }
    )
  )
};