diff --git a/cmd/internal/markov/absorbing.go b/cmd/internal/markov/absorbing.go index ecb0fc1..3d233d7 100644 --- a/cmd/internal/markov/absorbing.go +++ b/cmd/internal/markov/absorbing.go @@ -29,14 +29,18 @@ import ( "gonum.org/v1/gonum/mat" ) +// AbsorbingMarkovChain is a graph representing an absorbing Markov chain. type AbsorbingMarkovChain struct { *multi.WeightedDirectedGraph } +// NewAbsorbingMarkovChain returns an absorbing Markov chain with no nodes or +// edges. func NewAbsorbingMarkovChain() *AbsorbingMarkovChain { return &AbsorbingMarkovChain{WeightedDirectedGraph: multi.NewWeightedDirectedGraph()} } +// IsValid returns true if the graph is a valid Markov chain. func (g *AbsorbingMarkovChain) IsValid() bool { for nodes := g.Nodes(); nodes.Next(); { u := nodes.Node() @@ -61,6 +65,8 @@ func (g *AbsorbingMarkovChain) outWeightSum(u graph.Node) float64 { return sum } +// AbsorbingNodes returns all the nodes in the Markov chain identified as +// absorbing nodes. func (g *AbsorbingMarkovChain) AbsorbingNodes() []graph.Node { absorbingNodes := make([]graph.Node, 0) for nodes := g.Nodes(); nodes.Next(); { @@ -80,6 +86,9 @@ func (g *AbsorbingMarkovChain) AbsorbingNodes() []graph.Node { return absorbingNodes } +// IsAbsorbing returns true if the Markov chain is an absorbing chain. This +// means at least one node is an absorbing node and that every node can reach +// an absorbing node. func (g *AbsorbingMarkovChain) IsAbsorbing() bool { absorbingNodes := g.AbsorbingNodes() if len(absorbingNodes) == 0 { @@ -106,6 +115,7 @@ func (g *AbsorbingMarkovChain) canBeAbsorbed(u graph.Node, absorbingNodes []grap return false } +// AdjacencyMatrix returns the graph's adjacency matrix. func (g *AbsorbingMarkovChain) AdjacencyMatrix() *mat.Dense { adj := simple.NewDirectedMatrix(g.Nodes().Len(), 0, 0, 0) for edges := g.WeightedEdges(); edges.Next(); { @@ -133,28 +143,40 @@ func (g *AbsorbingMarkovChain) AdjacencyMatrix() *mat.Dense { return a } +// NewEdge returns a weightedEdge that can be added to the Markov chain. func (g *AbsorbingMarkovChain) NewEdge(from, to graph.Node) graph.Edge { e := g.WeightedDirectedGraph.NewWeightedLine(from, to, math.NaN()).(multi.WeightedLine) return &weightedEdge{WeightedLine: e} } +// NewNode returns a Node that can be added to the Markov chain. func (g *AbsorbingMarkovChain) NewNode() graph.Node { return &Node{Node: g.WeightedDirectedGraph.NewNode()} } +// SetEdge adds a weighted edge to the Markov chain. func (g *AbsorbingMarkovChain) SetEdge(e graph.Edge) { g.WeightedDirectedGraph.SetWeightedLine(e.(*weightedEdge)) } +// weightedEdge is a DOT-aware multi.WeightedLine. By being a +// multi.WeightedLine, it allows for self-loops, which are important for +// absorbing Markov chains. +// TODO: this is a little confusing, maybe just have checks in the code that +// there's only one line in each edge? type weightedEdge struct { multi.WeightedLine } +// ReversedEdge returns a new weightedEdge with the same weight, but the +// direction reversed. It exists mainly to satisfy the graph.Edge interface. func (e *weightedEdge) ReversedEdge() graph.Edge { revLine := multi.WeightedLine{F: e.T, T: e.F, W: e.W} return &weightedEdge{WeightedLine: revLine} } +// SetAttribute enables storing the weight read from a DOT file. It errors if +// an attribute is read that can't be stored in a weightedEdge. func (e *weightedEdge) SetAttribute(attr encoding.Attribute) error { var err error @@ -162,21 +184,25 @@ func (e *weightedEdge) SetAttribute(attr encoding.Attribute) error { case "weight": e.W, err = strconv.ParseFloat(attr.Value, 64) default: - err = errors.New("unknown key" + attr.Key) + err = errors.New("unknown key:" + attr.Key) } return err } +// Node is a DOT-aware graph.Node. type Node struct { graph.Node dotID string } +// SetDOTID sets the node's DOT ID. It enables storing the node name read from +// a DOT file. func (n *Node) SetDOTID(id string) { n.dotID = id } +// DOTID returns the node's DOT ID. func (n *Node) DOTID() string { return n.dotID }