diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/README.md b/README.md index b0ac87f..4962bea 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,33 @@ This repository describes a file-format for mathematical optimization problems called _MathOptFormat_ with the file extension `.mof.json`. -It is heavily inspired by [MathOptInterface](https://github.com/JuliaOpt/MathOptInterface.jl). +MathOptFormat is rigidly defined by the [JSON schema](http://json-schema.org/) +available at +[`https://jump.dev/MathOptFormat/mof.0.4.schema.json`](https://jump.dev/MathOptFormat/mof.0.4.schema.json). + +It is intended for the schema to be self-documenting. Instead of modifying or +adding to this documentation, clarifying edits should be made to the +`description` field of the relevant part of the schema. + +A number of examples of optimization problems encoded using MathOptFormat are +provided in the [`/examples` directory](https://github.com/odow/MathOptFormat/tree/master/examples). + +A paper describing the motivation, design principles, and historical setting of +MathOptFormat is available at: + +Legat, B., Dowson, O., Garcia, J.D., Lubin, M. (2020). MathOptInterface: a data +structure for mathematical optimization problems. +[[preprint]](http://www.optimization-online.org/DB_HTML/2020/02/7609.html) +[[repository]](https://github.com/jump-dev/MathOptFormat) + +**We highly recommend you read that paper before reading further.** + +## Implementations + +- Julia + + - The [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) package + supports reading and writing MathOptFormat files. ## Standard form @@ -146,20 +172,6 @@ required keys at the top level: with a lower bound of `1`. See [List of supported sets](#list-of-supported-sets) for other sets supported by MathOptFormat. -### Other examples - -A number of examples of optimization problems encoded using MathOptFormat are -provided in the [`/examples` directory](https://github.com/odow/MathOptFormat/tree/master/examples). - -## The schema - -A [JSON schema](http://json-schema.org/) for the `.mof.json` file-format is -provided in the file [`mof.schema.json`](https://github.com/odow/MathOptFormat/blob/master/mof.schema.json). - -It is intended for the schema to be self-documenting. Instead of modifying or -adding to this documentation, clarifying edits should be made to the -`description` field of the relevant part of the schema. - ### List of supported functions The list of functions supported by MathOptFormat are contained in the @@ -178,7 +190,6 @@ Here is a summary of the functions defined by MathOptFormat. | `"ScalarQuadraticFunction"` | The function `0.5x'Qx + a'x + b`, where `a` is a sparse vector of `ScalarAffineTerm`s in `affine_terms`, `b` is the scalar `constant`, and `Q` is a symmetric matrix specified by a list of `ScalarQuadraticTerm`s in `quadratic_terms`. Duplicate indices in `affine_terms` and `quadratic` are accepted, and the corresponding coefficients are summed together. Mirrored indices in `quadratic_terms` (i.e., `(i,j)` and `(j, i)`) are considered duplicates; only one need to be specified. | {"head": "ScalarQuadraticFunction", "constant": 1.0, "affine_terms": [{"coefficient": 2.5, "variable": "x"}], "quadratic_terms": [{"coefficient": 2.0, "variable_1": "x", "variable_2": "y"}]} | | `"ScalarNonlinearFunction"` | An expression graph representing a scalar nonlinear function. | | - For more information on `"ScalarNonlinearFunction"` functions, see [Nonlinear functions](nonlinear-functions). @@ -397,10 +408,3 @@ In MathOptFormat, this expression graph can be encoded as follows: ] } ``` - -## Implementations - -- Julia - - - The [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) package - supports reading and writing MathOptFormat files. diff --git a/examples/nlp.json b/examples/nlp.mof.json similarity index 100% rename from examples/nlp.json rename to examples/nlp.mof.json diff --git a/examples/vector.json b/examples/vector.mof.json similarity index 100% rename from examples/vector.json rename to examples/vector.mof.json diff --git a/mof.schema.json b/mof.0.4.schema.json similarity index 99% rename from mof.schema.json rename to mof.0.4.schema.json index 096b352..1751ea8 100644 --- a/mof.schema.json +++ b/mof.0.4.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/schema#", - "$id": "https://github.com/odow/MathOptFormat/blob/master/mof.schema.json", + "$id": "https://jump.dev/MathOptFormat/mof.0.4.schema.json", "title": "The schema for MathOptFormat", "type": "object", "required": ["version", "variables", "objective", "constraints"], diff --git a/mof/.gitignore b/mof/.gitignore deleted file mode 100644 index 864263b..0000000 --- a/mof/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*.exe -*.md -*.bat -*staticSchema.go -mof diff --git a/mof/generate-static-schema.go b/mof/generate-static-schema.go deleted file mode 100644 index 850c1c1..0000000 --- a/mof/generate-static-schema.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build ignore - -package main - -import ( - "encoding/base64" - "io/ioutil" - "os" -) - -func main() { - filename := os.Args[1] - bytes, err := ioutil.ReadFile(filename) - if err != nil { - panic(err) - } - base64str := base64.StdEncoding.EncodeToString(bytes) - packageString := "// Code generated by go generate; DO NOT EDIT.\n" + - "package main\n" + - "\n" + - "var jsonSchema64 = \"" + base64str + "\"" - if err := ioutil.WriteFile("staticSchema.go", []byte(packageString), os.ModePerm); err != nil { - panic(err) - } -} diff --git a/mof/mof.go b/mof/mof.go deleted file mode 100644 index 94c9932..0000000 --- a/mof/mof.go +++ /dev/null @@ -1,203 +0,0 @@ -package main - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "github.com/qri-io/jsonschema" - "io/ioutil" - "os" - "strings" -) - -//go:generate go run generate-static-schema.go ../mof.schema.json - -func main() { - if len(os.Args) < 2 { - fmt.Println("Invalid arguments to `mof`. Here is the help:") - fmt.Println() - PrintHelp() - } else { - switch os.Args[1] { - case "validate": - if len(os.Args) != 3 { - fmt.Println("Invalid arguments to `mof validate`") - PrintHelp() - } - filename := os.Args[2] - if err := ValidateFile(filename); err != nil { - fmt.Printf("%s is not a valid MOF file\nThe error is:\n", filename) - fmt.Println(err) - } else { - fmt.Printf("Success! %s conforms to the MathOptFormat schema", filename) - } - case "summarize": - summary, err := SummarizeSchema() - if err != nil { - fmt.Println(err) - } else { - fmt.Println(summary) - } - case "help": - PrintHelp() - default: - fmt.Println("Invalid arguments to `mof`.") - PrintHelp() - } - } -} - -func PrintHelp() { - fmt.Println("mof [arg] [args...]") - fmt.Println() - fmt.Println("mof validate filename.json") - fmt.Println(" Validate the file `filename.json` using the MathOptFormat schema") - fmt.Println("mof summarize") - fmt.Println(" Print a summary of the functions and sets supported by MathOptFormat") -} - -func ValidateFile(filename string) error { - schemaBytes, err := base64.StdEncoding.DecodeString(jsonSchema64) - if err != nil { - fmt.Println("Unable to decode JSON schema") - return err - } - rs := &jsonschema.RootSchema{} - if err := json.Unmarshal(schemaBytes, rs); err != nil { - fmt.Println("Unable to unmarshall schema") - return err - } - modelData, err := ioutil.ReadFile(filename) - if err != nil { - fmt.Printf("Unable to read %s\n", filename) - return err - } - if errs, _ := rs.ValidateBytes(modelData); len(errs) > 0 { - fmt.Printf("Error validating file") - for _, err = range errs { - fmt.Println(err.Error()) - } - return errs[0] - } - return nil -} - -func SummarizeSchema() (string, error) { - schemaBytes, err := base64.StdEncoding.DecodeString(jsonSchema64) - if err != nil { - fmt.Println("Unable to decode JSON schema") - return "", err - } - var data map[string]interface{} - if err := json.Unmarshal(schemaBytes, &data); err != nil { - fmt.Println("Unable to unmarshall schema") - return "", err - } - operators, leaves := summarizeNonlinear(data) - summary := "## Sets\n\n" + - "### Scalar Sets\n\n" + - summarize(data, "scalar_sets") + "\n" + - "### Vector Sets\n\n" + - summarize(data, "vector_sets") + "\n" + - "## Functions\n\n" + - "### Scalar Functions\n\n" + - summarize(data, "scalar_functions") + "\n" + - "### Vector Functions\n\n" + - summarize(data, "vector_functions") + "\n" + - "### Nonlinear functions\n\n" + - "#### Leaf nodes\n\n" + - leaves + "\n" + - "#### Operators\n\n" + - operators - return summary, nil -} - -type Object struct { - Head string - Description string - Example string -} - -func oneOfToObject(data map[string]interface{}) []Object { - val, ok := data["description"] - var description string - if ok { - description = strings.ReplaceAll(val.(string), "|", "\\|") - } else { - description = "" - } - vals, ok := data["examples"] - var example string - if !ok { - example = "" - } else { - example = vals.([]interface{})[0].(string) - } - properties := data["properties"].(map[string]interface{}) - head := properties["head"].(map[string]interface{}) - if val, ok := head["const"]; ok { - return []Object{Object{ - Head: val.(string), Description: description, Example: example}} - } else if val, ok := head["enum"]; ok { - objects := []Object{} - for _, name := range val.([]interface{}) { - objects = append(objects, Object{ - Head: name.(string), Description: description, Example: example}) - } - return objects - } - return []Object{} -} - -func summarize(data map[string]interface{}, key string) string { - summary := "| Name | Description | Example |\n" + - "| ---- | ----------- | ------- |\n" - definitions := data["definitions"].(map[string]interface{}) - keyData := definitions[key].(map[string]interface{}) - for _, oneOf := range keyData["oneOf"].([]interface{}) { - for _, o := range oneOfToObject(oneOf.(map[string]interface{})) { - summary = summary + fmt.Sprintf( - "| `\"%s\"` | %s | %s |\n", o.Head, o.Description, o.Example) - } - } - return summary -} - -func summarizeNonlinear(data map[string]interface{}) (string, string) { - definitions := data["definitions"].(map[string]interface{}) - nonlinearTerm := definitions["NonlinearTerm"].(map[string]interface{}) - operators := "| Name | Arity |\n" + - "| ---- | ----- |\n" - leaves := "| Name | Description | Example |\n" + - "| ---- | ----------- | ------- |\n" - for _, term := range nonlinearTerm["oneOf"].([]interface{}) { - oneOf := term.(map[string]interface{}) - objects := oneOfToObject(oneOf) - switch oneOf["description"] { - case "Unary operators": - for _, f := range objects { - operators = operators + fmt.Sprintf( - "| `\"%s\"` | Unary |\n", f.Head) - } - case "Binary operators": - for _, f := range objects { - operators = operators + fmt.Sprintf( - "| `\"%s\"` | Binary |\n", f.Head) - } - case "N-ary operators": - for _, f := range objects { - operators = operators + fmt.Sprintf( - "| `\"%s\"` | N-ary |\n", f.Head) - } - default: - if len(objects) == 1 { - leaves = leaves + fmt.Sprintf( - "| `\"%s\"` | %s | %s |\n", - objects[0].Head, objects[0].Description, objects[0].Example) - } else { - fmt.Printf("Unsupported object: %s\n", oneOf) - } - } - } - return operators, leaves -} diff --git a/mof/mof_test.go b/mof/mof_test.go deleted file mode 100644 index ae55f9e..0000000 --- a/mof/mof_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "io/ioutil" - "testing" -) - -func TestExamples(t *testing.T) { - files, err := ioutil.ReadDir("../examples") - if err != nil { - t.Errorf("%s", err) - } - for _, file := range files { - if err := ValidateFile("../examples/" + file.Name()); err != nil { - t.Errorf("%s failed to validate", file.Name()) - } - } -} - -func TestSummary(t *testing.T) { - summary, err := SummarizeSchema() - if err != nil || len(summary) < 100 { - t.Errorf("Failed to summarize schema: %s", err) - } -} - -func TestHelp(t *testing.T) { - PrintHelp() -} diff --git a/python/mof.py b/python/mof.py new file mode 100644 index 0000000..a1df6c2 --- /dev/null +++ b/python/mof.py @@ -0,0 +1,90 @@ +import json +import jsonschema +import os + +SCHEMA_FILENAME = '../mof.0.4.schema.json' + +def validate(filename): + with open(filename, 'r') as io: + instance = json.load(io) + with open(SCHEMA_FILENAME, 'r') as io: + schema = json.load(io) + jsonschema.validate(instance = instance, schema = schema) + +def summarize_schema(): + with open(SCHEMA_FILENAME, 'r') as io: + schema = json.load(io) + summary = "## Sets\n" + summary += "\n### Scalar Sets\n\n" + summarize(schema, "scalar_sets") + summary += "\n### Vector Sets\n\n" + summarize(schema, "vector_sets") + summary += "\n## Functions\n" + summary += "\n### Scalar Functions\n\n" + summarize(schema, "scalar_functions") + summary += "\n### Vector Functions\n\n" + summarize(schema, "vector_functions") + operators, leaves = summarize_nonlinear(schema) + summary += "\n### Nonlinear\n" + summary += "\n#### Leaf nodes\n\n" + leaves + summary += '\n#### Operators\n\n' + operators + return summary + +def oneOf_to_object(item): + head = item["properties"]["head"] + ret = [] + if "const" in head: + description = item.get("description", "").replace("|", "\\|") + example = item.get("examples", [""]) + assert(len(example) == 1) + ret.append({ + 'name': head["const"], + 'description': description, + 'example': example[0], + }) + else: + for k in head["enum"]: + ret.append({ + 'name': k, + 'description': "", + 'example': "", + }) + return ret + +def summarize(schema, key): + summary = \ + "| Name | Description | Example |\n" + \ + "| ---- | ----------- | ------- |\n" + for item in schema["definitions"][key]["oneOf"]: + for obj in oneOf_to_object(item): + summary += "| `\"%s\"` | %s | %s |\n" % \ + (obj['name'], obj['description'], obj['example']) + return summary + +def summarize_nonlinear(schema): + operators = "| Name | Arity |\n| ---- | ----- |\n" + leaves = "| Name | Description | Example |\n| ---- | ----------- | ------- |\n" + description_map = { + "Unary operators": 'Unary', + "Binary operators": 'Binary', + "N-ary operators": 'N-ary', + } + for item in schema["definitions"]["NonlinearTerm"]["oneOf"]: + desc = description_map.get(item["description"], None) + if desc == None: + obj = oneOf_to_object(item)[0] + leaves += "| `\"%s\"` | %s | %s |\n" % \ + (obj['name'], obj['description'], obj['example']) + else: + for obj in oneOf_to_object(item): + operators += "| `\"%s\"` | Unary |\n" % obj['name'] + return operators, leaves + +### +### Validate all the files in the examples directory. +### + +for filename in os.listdir('../examples'): + validate(os.path.join('../examples', filename)) + +### +### Summarize the schema for the README table. +### + +print(summarize_schema())