///|
/// Graph and probabilistic dependency utilities for structured models.
pub struct GraphEdge {
  from : Int
  to : Int
  weight : Double
  label : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn GraphEdge::new(from : Int, to : Int, weight : Double) -> GraphEdge {
  { from, to, weight, label: "" }
}

///|
pub struct WeightedGraph {
  nodes : Int
  directed : Bool
  edges : Array[GraphEdge]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn WeightedGraph::new(nodes : Int, directed : Bool) -> WeightedGraph {
  if nodes < 0 {
    abort("WeightedGraph: nodes must be non-negative")
  }
  { nodes, directed, edges: [] }
}

///|
pub fn WeightedGraph::add_edge(
  self : WeightedGraph,
  edge : GraphEdge,
) -> WeightedGraph {
  if edge.from < 0 ||
    edge.from >= self.nodes ||
    edge.to < 0 ||
    edge.to >= self.nodes {
    abort("WeightedGraph: edge endpoint out of range")
  }
  let edges = self.edges.copy()
  edges.push(edge)
  if !self.directed && edge.from != edge.to {
    edges.push({
      from: edge.to,
      to: edge.from,
      weight: edge.weight,
      label: edge.label,
    })
  }
  { ..self, edges, }
}

///|
pub fn WeightedGraph::neighbors(
  self : WeightedGraph,
  node : Int,
) -> Array[(Int, Double)] {
  if node < 0 || node >= self.nodes {
    abort("WeightedGraph: node out of range")
  }
  let result : Array[(Int, Double)] = []
  for edge in self.edges.iter() {
    if edge.from == node {
      result.push((edge.to, edge.weight))
    }
  }
  result
}

///|
pub fn WeightedGraph::degree(self : WeightedGraph, node : Int) -> Int {
  self.neighbors(node).length()
}

///|
pub fn WeightedGraph::edge_count(self : WeightedGraph) -> Int {
  self.edges.length()
}

///|
pub fn WeightedGraph::has_edge(
  self : WeightedGraph,
  from : Int,
  to : Int,
) -> Bool {
  self.edges.any(fn(edge) { edge.from == from && edge.to == to })
}

///|
pub fn WeightedGraph::total_weight(self : WeightedGraph) -> Double {
  self.edges.fold(init=0.0, (total, edge) => total + edge.weight)
}

///|
pub fn WeightedGraph::adjacency_matrix(
  self : WeightedGraph,
) -> Array[Array[Double]] {
  let matrix : Array[Array[Double]] = []
  for _ in 0.. Array[(Int, Double)] {
  let result : Array[(Int, Double)] = []
  for edge in self.edges.iter() {
    if edge.to == node {
      result.push((edge.from, edge.weight))
    }
  }
  result
}

///|
pub fn WeightedGraph::outgoing_weight(
  self : WeightedGraph,
  node : Int,
) -> Double {
  self.neighbors(node).fold(init=0.0, (total, item) => total + item.1)
}

///|
pub fn graph_bfs(graph : WeightedGraph, start : Int) -> Array[Int] {
  let visited : Array[Bool] = []
  for _ in 0.. Array[Int] {
  let visited : Array[Bool] = []
  for _ in 0.. 0 {
    let node = stack[stack.length() - 1]
    let _ = stack.pop()
    if !visited[node] {
      visited[node] = true
      result.push(node)
      for item in graph.neighbors(node) {
        if !visited[item.0] {
          stack.push(item.0)
        }
      }
    }
  }
  result
}

///|
pub struct ShortestPath {
  distance : Double
  nodes : Array[Int]
  reachable : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn dijkstra(
  graph : WeightedGraph,
  start : Int,
  target : Int,
) -> ShortestPath {
  if start < 0 || target < 0 || start >= graph.nodes || target >= graph.nodes {
    abort("dijkstra: node out of range")
  }
  let distances : Array[Double] = []
  for _ in 0..= 0 {
    path.push(cursor)
    if cursor == start {
      break
    }
    cursor = previous[cursor]
  }
  for left in 0.. left && path[right] > -1 {
        let temp = path[left]
        path[left] = path[right]
        path[right] = temp
      }
    }
  }
  { distance: distances[target], nodes: path, reachable: true }
}

///|
pub fn pagerank(
  graph : WeightedGraph,
  damping : Double,
  iterations : Int,
) -> Array[Double] {
  if damping < 0.0 || damping > 1.0 || iterations < 0 {
    abort("pagerank: invalid inputs")
  }
  if graph.nodes == 0 {
    return []
  }
  let rank : Array[Double] = []
  for _ in 0.. 0.0 {
        next[edge.to] = next[edge.to] +
          damping * rank[edge.from] * edge.weight / weight
      }
    }
    for index in 0.. Double {
  let neighbors = graph.neighbors(node).map(fn(item) { item.0 })
  if neighbors.length() < 2 {
    return 0.0
  }
  let mut links = 0
  for left in 0.. MarkovChain {
  let states = transition.length()
  if states == 0 || initial.length() != states {
    abort("MarkovChain: dimension mismatch")
  }
  for row in transition.iter() {
    if row.length() != states {
      abort("MarkovChain: transition must be square")
    }
  }
  { states, transition, initial }
}

///|
pub fn MarkovChain::step(
  self : MarkovChain,
  distribution : Array[Double],
) -> Array[Double] {
  if distribution.length() != self.states {
    abort("MarkovChain: distribution dimension mismatch")
  }
  let result : Array[Double] = []
  for target in 0.. Array[Double] {
  if steps < 0 {
    abort("MarkovChain: steps must be non-negative")
  }
  let mut result = self.initial.copy()
  for _ in 0.. Array[Double] {
  let mut result = self.initial.copy()
  for _ in 0.. Double {
  let distribution = self.distribution(steps)
  if values.length() != self.states {
    abort("MarkovChain: state values dimension mismatch")
  }
  let mut result = 0.0
  for index in 0.. TransitionEstimate {
  if sequence.length() < 2 || states <= 0 || smoothing < 0.0 {
    abort("estimate_transitions: invalid inputs")
  }
  let counts : Array[Array[Int]] = []
  for _ in 0.. sum_value + value)
      .to_double() +
      smoothing * states.to_double()
    for target in 0.. Double {
  if sequence.length() < 2 {
    return 0.0
  }
  let mut result = 0.0
  for index in 1..