diff --git a/cmd/internal/markov/absorbing.go b/cmd/internal/markov/absorbing.go new file mode 100644 index 0000000..26f0794 --- /dev/null +++ b/cmd/internal/markov/absorbing.go @@ -0,0 +1,107 @@ +/* +Copyright © 2025 filifa + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package markov + +import ( + "errors" + "math" + "strconv" + + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/encoding" + "gonum.org/v1/gonum/graph/simple" +) + +type AbsorbingMarkovChain struct { + *simple.WeightedDirectedGraph +} + +func NewAbsorbingMarkovChain() *AbsorbingMarkovChain { + return &AbsorbingMarkovChain{WeightedDirectedGraph: simple.NewWeightedDirectedGraph(math.NaN(), math.NaN())} +} + +func (g *AbsorbingMarkovChain) IsValid() bool { + for nodes := g.Nodes(); nodes.Next(); { + u := nodes.Node().(*node) + + if g.From(u.ID()).Len() == 0 { + continue + } + + if g.outWeightSum(u) != 1 { + return false + } + } + + return true +} + +func (g *AbsorbingMarkovChain) outWeightSum(u *node) float64 { + sum := 0.0 + for nodes := g.From(u.ID()); nodes.Next(); { + v := nodes.Node() + e := g.WeightedEdge(u.ID(), v.ID()) + if e != nil { + sum += e.(*weightedEdge).W + } + } + + return sum +} + +func (g *AbsorbingMarkovChain) NewEdge(from, to graph.Node) graph.Edge { + e := g.WeightedDirectedGraph.NewWeightedEdge(from, to, math.NaN()).(simple.WeightedEdge) + return &weightedEdge{WeightedEdge: e} +} + +func (g *AbsorbingMarkovChain) NewNode() graph.Node { + return &node{Node: g.WeightedDirectedGraph.NewNode()} +} + +func (g *AbsorbingMarkovChain) SetEdge(e graph.Edge) { + g.WeightedDirectedGraph.SetWeightedEdge(e.(*weightedEdge)) +} + +type weightedEdge struct { + simple.WeightedEdge +} + +func (e *weightedEdge) SetAttribute(attr encoding.Attribute) error { + var err error + + switch attr.Key { + case "weight": + e.W, err = strconv.ParseFloat(attr.Value, 64) + default: + err = errors.New("unknown key" + attr.Key) + } + + return err +} + +type node struct { + graph.Node + dotID string +} + +func (n *node) SetDOTID(id string) { + n.dotID = id +} + +func (n *node) DOTID() string { + return n.dotID +} diff --git a/cmd/root.go b/cmd/root.go index fe7b277..cb4765f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,10 +19,13 @@ package cmd import ( "os" + "scm.dairydemon.net/filifa/dptdist/cmd/internal/markov" + "github.com/spf13/cobra" + "gonum.org/v1/gonum/graph/encoding/dot" ) - +var file string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -36,7 +39,24 @@ This application is a tool to generate the needed files to quickly create a Cobra application.`, // Uncomment the following line if your bare application // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, + Run: parse, +} + +func parse(cmd *cobra.Command, args []string) { + data, err := os.ReadFile(file) + if err != nil { + panic(err) + } + + graph := markov.NewAbsorbingMarkovChain() + err = dot.Unmarshal(data, graph) + if err != nil { + panic(err) + } + + if !graph.IsValid() { + panic("not an absorbing Markov chain!") + } } // Execute adds all child commands to the root command and sets flags appropriately. @@ -58,6 +78,7 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + + rootCmd.Flags().StringVarP(&file, "file", "f", "", "dot file with absorbing Markov chain") + rootCmd.MarkFlagRequired("file") } - - diff --git a/go.mod b/go.mod index 10328c6..6a3f50f 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + gonum.org/v1/gonum v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index ffae55e..1813065 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,7 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=