diff --git a/README.md b/README.md index 43322e3..289fa4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # ChronoZinc Small wrapper for `mzn-fzn` to benchmark MiniZinc models + +# TODO +- Remove all usage of `viper` from parsing and runtime packages +- Provide separate the `run` and `parse` commands diff --git a/defaults.go b/defaults.go deleted file mode 100644 index eb33b17..0000000 --- a/defaults.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import "github.com/spf13/viper" - -func setDefaults() { - - viper.SetDefault("Processes", 1) - viper.SetDefault("RawDir", "raw") - viper.SetDefault("Solvers", map[string]interface{}{}) - -} diff --git a/main.go b/main.go index d94710a..80fb1fe 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,11 @@ package main import ( "fmt" "os" + "path/filepath" + "github.com/jjdekker/chronozinc/parsing" + "github.com/jjdekker/chronozinc/runtime" + "github.com/jjdekker/chronozinc/settings" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -18,11 +22,26 @@ var rootCmd = &cobra.Command{ a sub-set of declared solvers and report on the statistics gathered. The data gathered is saved in every stage.`, Run: func(cmd *cobra.Command, args []string) { - file, err := os.Open(path) - if err != nil { - return nil, err + for _, file := range args { + switch filepath.Ext(file) { + case ".mzn": + viper.Set("models", append(viper.GetStringSlice("models"), file)) + case ".dzn": + viper.Set("data", append(viper.GetStringSlice("data"), file)) + default: + viper.SetConfigFile(args[0]) + fmt.Println("Using config file:", args[0]) + err := viper.MergeInConfig() + if err != nil { + panic(err) + } + } } - viper.MergeConfig(file) + + solvers := settings.SolversFromViper() + instances := settings.InstancesFromViper() + runtime.RunAll(solvers, instances) + parsing.ParseAll(solvers, instances) }, } @@ -32,9 +51,9 @@ func initConfig() { viper.SetConfigFile(cfgFile) } - setDefaults() + settings.SetViperDefaults() - viper.SetConfigName("config.czn") // name of config file (without extension) + viper.SetConfigName("settings") // name of config file (without extension) viper.AddConfigPath("$HOME/.config/chronozinc") // add home directory as first search path viper.AddConfigPath("/etc/chronozinc") // adds global machine configuration viper.SetEnvPrefix("czn") // set environment prefix @@ -43,8 +62,6 @@ func initConfig() { // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { fmt.Println("Using config file:", viper.ConfigFileUsed()) - } else { - fmt.Println("No config file found; using ENV and defaults") } } diff --git a/parsing/extract.go b/parsing/extract.go new file mode 100644 index 0000000..c05e593 --- /dev/null +++ b/parsing/extract.go @@ -0,0 +1,35 @@ +package parsing + +import "regexp" + +func Extract(file []byte, reg *regexp.Regexp) string { + submatches := reg.FindSubmatch(file) + if submatches == nil { + return "" + } + + names := reg.SubexpNames() + for i, name := range names { + if name == "result" { + return string(submatches[i]) + } + } + + return "" +} + +func ExtractLast(file []byte, reg *regexp.Regexp) string { + submatches := reg.FindAllSubmatch(file, -1) + if submatches == nil { + return "" + } + + names := reg.SubexpNames() + for i, name := range names { + if name == "result" { + return string(submatches[len(submatches)-1][i]) + } + } + + return "" +} diff --git a/parsing/match.go b/parsing/match.go new file mode 100644 index 0000000..ddb51a6 --- /dev/null +++ b/parsing/match.go @@ -0,0 +1,16 @@ +package parsing + +import ( + "fmt" + "regexp" +) + +func Match(file []byte, dict map[string]*regexp.Regexp) string { + for key, reg := range dict { + fmt.Printf("test %s", key) + if reg.Match(file) { + return key + } + } + return "" +} diff --git a/parsing/parser.go b/parsing/parser.go new file mode 100644 index 0000000..9f42bd9 --- /dev/null +++ b/parsing/parser.go @@ -0,0 +1,73 @@ +package parsing + +import ( + "encoding/csv" + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/jjdekker/chronozinc/settings" + "github.com/spf13/viper" +) + +func ParseAll(solvers []settings.Solver, instances []settings.Instance) { + params := viper.GetStringSlice("parameters") + if len(params) > 0 { + f, err := os.Create(viper.GetString("output")) + if err != nil { + log.Panicf("Unable to create file %s", viper.GetString("output")) + } + defer f.Close() + w := csv.NewWriter(f) + defer w.Flush() + + headers := append(persistantHeaders(), params...) + w.Write(headers) + for i := range solvers { + for j := range instances { + record := make([]string, 0, len(headers)) + record = append(record, + []string{solvers[i].Name, instances[j].Model}...) + if instances[j].Data != "" { + record = append(record, instances[j].Data) + } + + for k := range params { + record = append(record, + ParseParameter(&solvers[i], &instances[j], params[k])) + } + + w.Write(record) + } + } + } +} + +func persistantHeaders() []string { + headers := []string{"solver", "model"} + if len(viper.GetStringSlice("data")) > 0 { + headers = append(headers, "data") + } + return headers +} + +func ParseParameter(solver *settings.Solver, instance *settings.Instance, + parameter string) string { + fmt.Println(solver.Matchers) + var res string + if f, err := ioutil.ReadFile(instance.OutPath(solver.Name)); err != nil { + log.Printf("Unable to open file %s", instance.OutPath(solver.Name)) + } else { + switch { + case (solver.Extractors[parameter] != nil): + res = Extract(f, solver.Extractors[parameter]) + case (solver.LastExtractors[parameter] != nil): + res = ExtractLast(f, solver.LastExtractors[parameter]) + case (solver.Matchers[parameter] != nil): + fmt.Println("do you see me?") + res = Match(f, solver.Matchers[parameter]) + } + } + return res +} diff --git a/runtime/run.go b/runtime/run.go new file mode 100644 index 0000000..e4bec9e --- /dev/null +++ b/runtime/run.go @@ -0,0 +1,75 @@ +package runtime + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/jjdekker/chronozinc/settings" + "github.com/spf13/viper" +) + +// RunAll runs every instance on every solver +func RunAll(solvers []settings.Solver, instances []settings.Instance) { + work := make(chan func()) + wait := govern(work) + + for i := range solvers { + for j := range instances { + work <- func() { RunInstance(&solvers[i], &instances[j]) } + } + } + close(work) + + wait.Wait() +} + +func govern(work <-chan func()) *sync.WaitGroup { + var wg sync.WaitGroup + procs := viper.GetInt("processes") + + wg.Add(procs) + for i := 0; i < procs; i++ { + go func() { + for f := range work { + f() + } + wg.Done() + }() + } + + return &wg +} + +// RunInstance runs an instance on a Solver using mzn-fzn +func RunInstance(solver *settings.Solver, instance *settings.Instance) { + args := []string{ + "--solver", solver.Binary, + "--flatzinc-flag", solver.Flags, + instance.Model, + } + if solver.Globals != "" { + args = append(args, "--globals-dir", solver.Globals) + } + if instance.Data != "" { + args = append(args, "--data", instance.Data) + } + args = append(args, strings.Split(viper.GetString("flags"), " ")...) + proc := exec.Command(viper.GetString("mznfzn"), args...) + + if out, err := proc.CombinedOutput(); err != nil { + log.Printf("Instance %s ended with error %s", instance, err) + } else { + path := instance.OutPath(solver.Name) + os.MkdirAll(filepath.Dir(path), os.ModePerm) + err := ioutil.WriteFile(path, out, 0644) + if err != nil { + log.Printf("Saving results for instance %s ended with error %s", + instance, err) + } + } +} diff --git a/settings/defaults.go b/settings/defaults.go new file mode 100644 index 0000000..4a26125 --- /dev/null +++ b/settings/defaults.go @@ -0,0 +1,16 @@ +package settings + +import "github.com/spf13/viper" + +// SetViperDefaults set the default settings for the viper handler +func SetViperDefaults() { + + viper.SetDefault("processes", 1) + viper.SetDefault("mznfzn", "mzn-fzn") + viper.SetDefault("raw_dir", "raw") + viper.SetDefault("flags", "--verbose --statistics") + viper.SetDefault("output", "benchmark.csv") + viper.SetDefault("comma", ",") + viper.SetDefault("crlf", false) + +} diff --git a/settings/instance.go b/settings/instance.go new file mode 100644 index 0000000..64443cd --- /dev/null +++ b/settings/instance.go @@ -0,0 +1,45 @@ +package settings + +import ( + "path/filepath" + "strings" + + "github.com/spf13/viper" +) + +// Instance contains the information to run one particular instance of a model +type Instance struct { + Model string + Data string +} + +// InstancesFromViper extracts all instance information from Viper +func InstancesFromViper() []Instance { + var instances []Instance + + data := viper.GetStringSlice("data") + for _, model := range viper.GetStringSlice("models") { + if len(data) > 0 { + for _, dat := range data { + instances = append(instances, Instance{Model: model, Data: dat}) + } + } else { + instances = append(instances, Instance{Model: model}) + } + } + + return instances +} + +// OutPath returns the intended output location of the instance given the solver +func (i *Instance) OutPath(solver string) string { + path := viper.GetString("raw_dir") + path = filepath.Join(path, solver) + cleanModel := strings.TrimSuffix(i.Model, filepath.Ext(i.Model)) + path = filepath.Join(path, cleanModel) + if i.Data != "" { + cleanData := strings.TrimSuffix(i.Data, filepath.Ext(i.Data)) + path = filepath.Join(path, cleanData) + } + return path + ".dat" +} diff --git a/settings/solver.go b/settings/solver.go new file mode 100644 index 0000000..0db2836 --- /dev/null +++ b/settings/solver.go @@ -0,0 +1,83 @@ +package settings + +import ( + "fmt" + "log" + "regexp" + + "github.com/spf13/viper" +) + +// Solver contains all information regarding a FlatZinc solver and its output +type Solver struct { + Name string // Solver name + Binary string // Binary location + Globals string // Globals directory + Flags string // FZN solver flags + + Extractors map[string]*regexp.Regexp + LastExtractors map[string]*regexp.Regexp + Matchers map[string]map[string]*regexp.Regexp +} + +// SolversFromViper extracts all solver information from Viper +func SolversFromViper() []Solver { + var solvers []Solver + + for key := range viper.GetStringMap("solvers") { + options := viper.GetStringMapString("solvers." + key) + + solver := Solver{ + Name: key, + Flags: options["flags"], + Globals: options["globals"], + } + if bin, exists := options["binary"]; exists { + solver.Binary = bin + } else { + solver.Binary = solver.Name + } + + solver.Extractors = make(map[string]*regexp.Regexp) + for i, str := range viper.GetStringMapString("solvers." + key + ".extractors") { + reg, err := regexp.Compile(str) + if err != nil { + log.Panicf("Error compiling extractor `%s:%s`: %s", + key, i, err) + } else { + solver.Extractors[i] = reg + } + } + + solver.LastExtractors = make(map[string]*regexp.Regexp) + for i, str := range viper.GetStringMapString("solvers." + key + ".last_extractors") { + reg, err := regexp.Compile(str) + if err != nil { + log.Panicf("Error compiling extractor `%s:%s`: %s", + key, i, err) + } else { + solver.LastExtractors[i] = reg + } + } + + solver.Matchers = make(map[string]map[string]*regexp.Regexp) + for i := range viper.GetStringMap("solvers." + key + ".matchers") { + matcher := make(map[string]*regexp.Regexp) + for j, str := range viper.GetStringMapString("solvers." + key + ".matchers." + i) { + reg, err := regexp.Compile(str) + if err != nil { + log.Panicf("Error compiling match case `%s:%s:`: %s", + key, i, err) + } else { + matcher[j] = reg + } + } + fmt.Println(matcher) + solver.Matchers[i] = matcher + } + + solvers = append(solvers, solver) + } + + return solvers +}