diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5de00a8..60f5a81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -93,10 +93,10 @@ jobs: go get ./... - name: Run examples 1 run: | - xvfb-run --auto-servernum --server-num=1 go run . -s 120 -d _examples/base -x params.Termination.MaxTicks=365 - go run . -r 5 -d _examples/base -e experiment.json -x params.Termination.MaxTicks=365 - xvfb-run --auto-servernum --server-num=1 go run . -s 120 -d _examples/patches -x params.Termination.MaxTicks=365 - xvfb-run --auto-servernum --server-num=1 go run . -s 120 -d _examples/weather_file -x params.Termination.MaxTicks=730 - xvfb-run --auto-servernum --server-num=1 go run . -s 120 -d _examples/weather_builtin -x params.Termination.MaxTicks=730 - xvfb-run --auto-servernum --server-num=1 go run . -s 120 -d _examples/systems --systems systems.json -x params.Termination.MaxTicks=730 - go run . -d _examples/systems --systems systems.json -e experiment.json -r 10 + xvfb-run --auto-servernum --server-num=1 go run . --tps 120 -d _examples/base -x params.Termination.MaxTicks=365 + go run . -r 5 -d _examples/base -e -x params.Termination.MaxTicks=365 + xvfb-run --auto-servernum --server-num=1 go run . --tps 120 -d _examples/patches -o -x params.Termination.MaxTicks=365 + xvfb-run --auto-servernum --server-num=1 go run . --tps 120 -d _examples/weather_file -o -x params.Termination.MaxTicks=730 + xvfb-run --auto-servernum --server-num=1 go run . --tps 120 -d _examples/weather_builtin -o -x params.Termination.MaxTicks=730 + xvfb-run --auto-servernum --server-num=1 go run . --tps 120 -d _examples/systems -o -s -x params.Termination.MaxTicks=730 + go run . -d _examples/systems -o -s -e -r 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3d047..86ae524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Adds support for seasonal and "scripted" patch dynamics (#23) - Adds support for weather/foraging period files (#24) - Adds support for adding custom resources/parameters as well as systems (#25) +- Rework of the command line interface for a simpler syntax when using default file names (#29) ### Other diff --git a/README.md b/README.md index 8f2023f..02ea319 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,16 @@ Get CLI help like this: beecs-cli -h ``` -A single, slowed down run of the base example, with live plots: +A single simulation with live plots, at 30 ticks per second: ``` -beecs-cli -s 30 -d _examples/base +beecs-cli -d _examples/base --observers --tps 30 ``` Run the full base example with parameter variation and 10 runs per parameter set: ``` -beecs-cli -r 10 -d _examples/base -e experiment.json +beecs-cli -d _examples/base --experiment -r 10 ``` Print all default parameters in the tool's input format: @@ -76,7 +76,7 @@ The default is file `parameters.json` in the working directory. Here is an examp For any kind of output, an **observers file** is required. It specifies which observers for visualizations or file output should be used. -The default is file `parameters.json` in the working directory. Here is an example: +Here is an example: ```json { @@ -90,6 +90,8 @@ The default is file `parameters.json` in the working directory. Here is an examp } ``` +Observers must be enabled using the `-o` flag. The default is file `observers.json` in the working directory. + These files are sufficient for single simulations with visual of file output. With a further **experiment file**, parameters can be systematically varied in various ways. @@ -114,6 +116,8 @@ Here is an example: ] ``` +Experiments must be enabled using the `-e` flag. The default is file `experiments.json` in the working directory. + > Note: The prefix `params.` is required to unambiguously identify the type of the parameter group to modify. See also the [examples](https://github.com/mlange-42/beecs-cli/tree/main/_examples) for the format of the required JSON files. diff --git a/cli/cli.go b/cli/cli.go index a2030d0..ddfcfee 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -18,6 +18,7 @@ import ( "github.com/mlange-42/beecs/experiment" baseparams "github.com/mlange-42/beecs/params" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/exp/rand" ) @@ -25,6 +26,7 @@ const ( _PARAMETERS = "parameters.json" _OBSERVERS = "observers.json" _EXPERIMENT = "experiment.json" + _SYSTEMS = "systems.json" ) func Run() { @@ -40,16 +42,17 @@ func rootCommand() *cobra.Command { var dir string var outDir string var paramFiles []string - var expFile string - var obsFile string - var sysFile string + var expFile []string + var obsFile []string + var sysFile []string var speed float64 var threads int var runs int var overwrite []string var seed int - root := &cobra.Command{ + var root cobra.Command + root = cobra.Command{ Use: "beecs-cli", Short: "beecs-cli provides a command line interface for the beecs model.", Long: `beecs-cli provides a command line interface for the beecs model.`, @@ -64,6 +67,11 @@ func rootCommand() *cobra.Command { os.Exit(0) } + flagUsed := map[string]bool{} + root.Flags().Visit(func(f *pflag.Flag) { + flagUsed[f.Name] = true + }) + rand.Seed(uint64(time.Now().UTC().Nanosecond())) if outDir == "" { @@ -90,8 +98,11 @@ func rootCommand() *cobra.Command { var exp experiment.Experiment var err error - if expFile != "" { - exp, err = util.ExperimentFromFile(path.Join(dir, expFile)) + if flagUsed["experiment"] { + if len(expFile) > 1 { + return fmt.Errorf("only one (optional) experiment file can be used") + } + exp, err = util.ExperimentFromFile(path.Join(dir, expFile[0])) if err != nil { return err } @@ -103,15 +114,21 @@ func rootCommand() *cobra.Command { } var observers util.ObserversDef - if obsFile != "" { - observers, err = util.ObserversDefFromFile(path.Join(dir, obsFile)) + if flagUsed["observers"] { + if len(obsFile) > 1 { + return fmt.Errorf("only one (optional) observers file can be used") + } + observers, err = util.ObserversDefFromFile(path.Join(dir, obsFile[0])) if err != nil { return err } } var systems []model.System - if sysFile != "" { - systems, err = util.SystemsFromFile(path.Join(dir, sysFile)) + if flagUsed["systems"] { + if len(sysFile) > 1 { + return fmt.Errorf("only one (optional) systems file can be used") + } + systems, err = util.SystemsFromFile(path.Join(dir, sysFile[0])) if err != nil { return err } @@ -147,11 +164,21 @@ func rootCommand() *cobra.Command { root.Flags().StringVarP(&dir, "directory", "d", ".", "Working directory") root.Flags().StringVarP(&outDir, "output", "", "", "Output directory if different from working directory") - root.Flags().StringSliceVarP(¶mFiles, "parameters", "p", []string{_PARAMETERS}, "Parameter files, processed in the given order") - root.Flags().StringVarP(&expFile, "experiment", "e", "", "Experiment file for parameter variation") - root.Flags().StringVarP(&obsFile, "observers", "o", _OBSERVERS, "Observers file for adding observers") - root.Flags().StringVarP(&sysFile, "systems", "", "", "Systems file for using custom systems or changing the scheduling") - root.Flags().Float64VarP(&speed, "speed", "s", 0, "Speed limit in ticks per second. Default: 0 (unlimited)") + root.Flags().StringSliceVarP(¶mFiles, "parameters", "p", []string{_PARAMETERS}, "Parameter files, processed in the given order\n") + + root.Flags().StringSliceVarP(&expFile, "experiment", "e", []string{_EXPERIMENT}, + "Run experiment. Optionally one experiment file for parameter variation\n") + root.Flag("experiment").NoOptDefVal = _EXPERIMENT + + root.Flags().StringSliceVarP(&obsFile, "observers", "o", []string{_OBSERVERS}, + "Run with observers. Optionally one observers file for adding observers\n") + root.Flag("observers").NoOptDefVal = _OBSERVERS + + root.Flags().StringSliceVarP(&sysFile, "systems", "s", []string{_SYSTEMS}, + "Run with custom systems. Optionally one systems file for using custom systems or changing the scheduling\n") + root.Flag("systems").NoOptDefVal = _SYSTEMS + + root.Flags().Float64VarP(&speed, "tps", "", 0, "Speed limit in ticks per second. Default: 0 (unlimited)") root.Flags().IntVarP(&threads, "threads", "t", runtime.NumCPU(), "Number of threads") root.Flags().IntVarP(&runs, "runs", "r", 1, "Runs per parameter set") root.Flags().IntVarP(&seed, "seed", "", 0, "Super random seed for seed generation. Default: 0 (unseeded)") @@ -160,7 +187,7 @@ func rootCommand() *cobra.Command { root.AddCommand(initCommand()) root.AddCommand(parametersCommand()) - return root + return &root } func parametersCommand() *cobra.Command { diff --git a/go.mod b/go.mod index 8d76c89..5c43694 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/mlange-42/arche-pixel v0.9.0 github.com/mlange-42/beecs v0.1.1-0.20240520193329-cfbd62899b75 github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa ) @@ -31,7 +32,6 @@ require ( github.com/mazznoer/csscolorparser v0.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect golang.org/x/image v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect gonum.org/v1/gonum v0.15.0 // indirect