From 21824c678363ac5535ce3869b07ff52b38d59cf9 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 16 Mar 2019 16:50:41 -0300 Subject: [PATCH] [MOF] Begin formal JSON schema (#47) * [MOF] Begin formal JSON schema * More updates to the schema * Finish Base MOI schema * A few minor fixes * Add nonlinear schema * Update schema * Add JSONSchema to validate models * Remove Manifest file * Add manifest to .gitignore * Add minimum values to some integer fields * Split functions into scalar and vector types * Re-order top-level properties of schema * Update field names after #49 * Add top-level `name` field and minor comment fixes * Tidy schema * Add (validated) example files * Add more examples * Fix coefficients * Add descriptions to schema * Add VariablePrimalStart attribute --- .gitignore | 1 + Project.toml | 15 + src/MOF/MOF.jl | 23 + src/MOF/schema/examples/biobjective.mof.json | 32 ++ src/MOF/schema/examples/milp.mof.json | 58 ++ src/MOF/schema/examples/nlp.json | 57 ++ src/MOF/schema/examples/quadratic.mof.json | 35 ++ src/MOF/schema/examples/vector.json | 45 ++ src/MOF/schema/mof.schema.json | 527 +++++++++++++++++++ test/MOF/MOF.jl | 8 + test/MOF/nonlinear.jl | 2 + 11 files changed, 803 insertions(+) create mode 100644 Project.toml create mode 100644 src/MOF/schema/examples/biobjective.mof.json create mode 100644 src/MOF/schema/examples/milp.mof.json create mode 100644 src/MOF/schema/examples/nlp.json create mode 100644 src/MOF/schema/examples/quadratic.mof.json create mode 100644 src/MOF/schema/examples/vector.json create mode 100644 src/MOF/schema/mof.schema.json diff --git a/.gitignore b/.gitignore index 8c960ec..3f02ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.jl.cov *.jl.*.cov *.jl.mem +Manifest.toml diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..9850886 --- /dev/null +++ b/Project.toml @@ -0,0 +1,15 @@ +name = "MathOptFormat" +uuid = "f4570300-c277-12e8-125c-4912f86ce65d" +authors = ["Oscar Dowson = 1, x ∈ [0, 1], y ∈ {0, 1}}", + "version": 0, + "variables": [{ + "name": "x", + "primal_start": 0.0 + }, { + "name": "y", + "primal_start": 1.0 + }], + "objectives": [{ + "sense": "min", + "function": { + "head": "SingleVariable", + "variable": "x" + } + }], + "constraints": [{ + "name": "x + y >= 1", + "function": { + "head": "ScalarAffineFunction", + "terms": [{ + "coefficient": 1, + "variable": "x" + }, + { + "coefficient": 1, + "variable": "y" + } + ], + "constant": 0 + }, + "set": { + "head": "GreaterThan", + "lower": 1 + } + }, { + "name": "x ∈ [0, 1]", + "function": { + "head": "SingleVariable", + "variable": "x" + }, + "set": { + "head": "Interval", + "lower": 0, + "upper": 1 + } + }, { + "name": "y ∈ {0, 1}", + "function": { + "head": "SingleVariable", + "variable": "y" + }, + "set": { + "head": "ZeroOne" + } + }] +} \ No newline at end of file diff --git a/src/MOF/schema/examples/nlp.json b/src/MOF/schema/examples/nlp.json new file mode 100644 index 0000000..7008c44 --- /dev/null +++ b/src/MOF/schema/examples/nlp.json @@ -0,0 +1,57 @@ +{ + "description": "The problem: min{2x + sin(x)^2 + y}.", + "version": 0, + "variables": [{ + "name": "x" + }, { + "name": "y" + }], + "objectives": [{ + "sense": "min", + "function": { + "head": "Nonlinear", + "root": { + "head": "node", + "index": 4 + }, + "node_list": [{ + "head": "*", + "args": [{ + "head": "real", + "value": 2 + }, { + "head": "variable", + "name": "x" + }] + }, { + "head": "sin", + "args": [{ + "head": "variable", + "name": "x" + }] + }, { + "head": "^", + "args": [{ + "head": "node", + "index": 2 + }, { + "head": "real", + "value": 2 + }] + }, { + "head": "+", + "args": [{ + "head": "node", + "index": 1 + }, { + "head": "node", + "index": 3 + }, { + "head": "variable", + "name": "y" + }] + }] + } + }], + "constraints": [] +} \ No newline at end of file diff --git a/src/MOF/schema/examples/quadratic.mof.json b/src/MOF/schema/examples/quadratic.mof.json new file mode 100644 index 0000000..122652d --- /dev/null +++ b/src/MOF/schema/examples/quadratic.mof.json @@ -0,0 +1,35 @@ +{ + "description": "The problem: min{x^2 + x * y + y^2}", + "version": 0, + "variables": [{ + "name": "x" + }, { + "name": "y" + }], + "objectives": [{ + "sense": "min", + "function": { + "description": "Note that the format is `a'x + 0.5 x' Q x + c`.", + "head": "ScalarQuadraticFunction", + "affine_terms": [], + "quadratic_terms": [{ + "coefficient": 2, + "variable_1": "x", + "variable_2": "x" + }, + { + "coefficient": 1, + "variable_1": "x", + "variable_2": "y" + }, + { + "coefficient": 2, + "variable_1": "y", + "variable_2": "y" + } + ], + "constant": 0 + } + }], + "constraints": [] +} \ No newline at end of file diff --git a/src/MOF/schema/examples/vector.json b/src/MOF/schema/examples/vector.json new file mode 100644 index 0000000..87cec76 --- /dev/null +++ b/src/MOF/schema/examples/vector.json @@ -0,0 +1,45 @@ +{ + "description": "The problem: min{0 | [1 2; 3 4][x, y] + [5, 6] ∈ R+.", + "version": 0, + "variables": [{ + "name": "x" + }, { + "name": "y" + }], + "objectives": [], + "constraints": [{ + "function": { + "head": "VectorAffineFunction", + "terms": [{ + "output_index": 1, + "scalar_term": { + "coefficient": 1, + "variable": "x" + } + }, { + "output_index": 1, + "scalar_term": { + "coefficient": 2, + "variable": "y" + } + }, { + "output_index": 2, + "scalar_term": { + "coefficient": 3, + "variable": "x" + } + }, { + "output_index": 2, + "scalar_term": { + "coefficient": 4, + "variable": "y" + } + }], + "constants": [5, 6] + }, + "set": { + "head": "Nonnegatives", + "dimension": 2 + } + }] +} \ No newline at end of file diff --git a/src/MOF/schema/mof.schema.json b/src/MOF/schema/mof.schema.json new file mode 100644 index 0000000..dbd8fea --- /dev/null +++ b/src/MOF/schema/mof.schema.json @@ -0,0 +1,527 @@ +{ + "$schema": "https://json-schema.org/schema#", + "$id": "https://github.com/odow/MathOptFormat.jl/tree/master/src/MOF/mof.schema.json", + "title": "The schema for MathOptFormat", + "type": "object", + "required": ["version", "variables", "objectives", "constraints"], + "properties": { + "version": { + "description": "The version of MathOptFormat that this schema validates against.", + "const": 0 + }, + "name": { + "description": "The name of the model.", + "type": "string" + }, + "author": { + "description": "The author of the model for citation purposes.", + "type": "string" + }, + "description": { + "description": "A human-readable description of the model.", + "type": "string" + }, + "variables": { + "description": "An array of variables in the model. Each must have a unique name.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "primal_start": { + "description": "An initial value that the optimizer may use to warm-start the solution process.", + "type": "number" + } + } + }, + "uniqueItems": true + }, + "objectives": { + "description": "An array of objectives in the model.", + "type": "array", + "items": { + "type": "object", + "required": ["sense", "function"], + "properties": { + "sense": { + "enum": ["min", "max"] + }, + "function": { + "$ref": "#/definitions/scalar_functions" + } + } + } + }, + "constraints": { + "description": "An array of constraints in the model. Scalar-valued functions can only be paired with scalar-sets, and the same applies for vector-valued functions and sets.", + "type": "array", + "items": { + "type": "object", + "required": ["function", "set"], + "properties": { + "name": { + "type": "string" + } + }, + "oneOf": [{ + "description": "A scalar-valued constraint.", + "properties": { + "function": { + "$ref": "#/definitions/scalar_functions" + }, + "set": { + "$ref": "#/definitions/scalar_sets" + } + } + }, { + "description": "A vector-valued constraint.", + "properties": { + "function": { + "$ref": "#/definitions/vector_functions" + }, + "set": { + "$ref": "#/definitions/vector_sets" + } + } + }] + }, + "uniqueItems": true + } + }, + "definitions": { + "ScalarAffineTerm": { + "description": "A helper object that represents `coefficent * variable`.", + "type": "object", + "required": ["coefficient", "variable"], + "properties": { + "coefficient": { + "type": "number" + }, + "variable": { + "type": "string" + } + } + }, + "ScalarQuadraticTerm": { + "description": "A helper object that represents `coefficent * variable_1 * variable_2`.", + "type": "object", + "required": ["coefficient", "variable_1", "variable_2"], + "properties": { + "coefficient": { + "type": "number" + }, + "variable_1": { + "type": "string" + }, + "variable_2": { + "type": "string" + } + } + }, + "VectorAffineTerm": { + "description": "A helper object that represents a `ScalarAffineTerm` in row `output_index`.", + "type": "object", + "required": ["output_index", "scalar_term"], + "properties": { + "output_index": { + "type": "integer", + "minimum": 1 + }, + "scalar_term": { + "$ref": "#/definitions/ScalarAffineTerm" + } + } + }, + "VectorQuadraticTerm": { + "description": "A helper object that represents a `ScalarQuadraticTerm` in row `output_index`.", + "type": "object", + "required": ["output_index", "scalar_term"], + "properties": { + "output_index": { + "type": "integer", + "minimum": 1 + }, + "scalar_term": { + "$ref": "#/definitions/ScalarQuadraticTerm" + } + } + }, + "NonlinearTerm": { + "description": "A node in an expresion graph representing a nonlinear function.", + "type": "object", + "required": ["head"], + "oneOf": [{ + "description": "Unary operators.", + "required": ["args"], + "properties": { + "head": { + "enum": [ + "log", "log10", "exp", "sqrt", "floor", "ceil", + "abs", "cos", "sin", "tan", "acos", "asin", "atan", + "cosh", "sinh", "tanh", "acosh", "asinh", "atanh" + ] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/definitions/NonlinearTerm" + }, + "minItems": 1, + "maxItems": 1 + } + } + }, { + "description": "Binary operators.", + "required": ["args"], + "properties": { + "head": { + "enum": ["/", "^"] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/definitions/NonlinearTerm" + }, + "minItems": 2, + "maxItems": 2 + } + } + }, { + "description": "N-ary operators.", + "required": ["args"], + "properties": { + "head": { + "enum": ["+", "-", "*", "min", "max"] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/definitions/NonlinearTerm" + }, + "minItems": 1 + } + } + }, { + "description": "A real-valued numeric constant.", + "required": ["value"], + "properties": { + "head": { + "const": "real" + }, + "value": { + "type": "number" + } + } + }, { + "description": "A complex-valued numeric constant.", + "required": ["real", "imag"], + "properties": { + "head": { + "const": "complex" + }, + "real": { + "type": "number" + }, + "imag": { + "type": "number" + } + } + }, { + "description": "A reference to an optimization variable", + "required": ["name"], + "properties": { + "head": { + "const": "variable" + }, + "name": { + "type": "string" + } + } + }, { + "description": "A pointer to a (1-indexed) element in the `node_list` field in a nonlinear function", + "required": ["index"], + "properties": { + "head": { + "const": "node" + }, + "index": { + "type": "integer", + "minimum": 1 + } + } + }] + }, + "scalar_functions": { + "description": "A schema for the scalar-valued functions defined by MathOptFormat.See http://www.juliaopt.org/MathOptInterface.jl/v0.8/apireference/#Functions-and-function-modifications-1 for a list of the functions and their meanings.", + "type": "object", + "required": ["head"], + "oneOf": [{ + "description": "The scalar variable `variable`.", + "required": ["variable"], + "properties": { + "head": { + "const": "SingleVariable" + }, + "variable": { + "type": "string" + } + } + }, { + "description": "The function `a'x + b`, where `a` is a sparse vector specified by a list of `ScalarAffineTerm`s in `terms` and `b` is the scalar in `constant`. Duplicate variables in `terms` are accepted, and the corresponding coefficients are summed together.", + "required": ["constant", "terms"], + "properties": { + "head": { + "const": "ScalarAffineFunction" + }, + "constant": { + "type": "number" + }, + "terms": { + "type": "array", + "items": { + "$ref": "#/definitions/ScalarAffineTerm" + } + } + } + }, { + "description": "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.", + "required": ["constant", "affine_terms", "quadratic_terms"], + "properties": { + "head": { + "const": "ScalarQuadraticFunction" + }, + "constant": { + "type": "number" + }, + "affine_terms": { + "type": "array", + "items": { + "$ref": "#/definitions/ScalarAffineTerm" + } + }, + "quadratic_terms": { + "type": "array", + "items": { + "$ref": "#/definitions/ScalarQuadraticTerm" + } + } + } + }, { + "description": "An expression graph representing a scalar function.", + "required": ["root", "node_list"], + "properties": { + "head": { + "const": "Nonlinear" + }, + "root": { + "$ref": "#/definitions/NonlinearTerm" + }, + "node_list": { + "type": "array", + "items": { + "$ref": "#/definitions/NonlinearTerm" + } + } + } + }] + }, + "vector_functions": { + "description": "A schema for the vector-valued functions defined by MathOptFormat.See http://www.juliaopt.org/MathOptInterface.jl/v0.8/apireference/#Functions-and-function-modifications-1 for a list of the functions and their meanings.", + "type": "object", + "required": ["head"], + "oneOf": [{ + "description": "An ordered list of variables.", + "required": ["variables"], + "properties": { + "head": { + "const": "VectorOfVariables" + }, + "variables": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, { + "description": "The function `Ax + b`, where `A` is a sparse matrix specified by a list of `VectorAffineTerm`s in `terms` and `b` is a dense vector specified by `constants`.", + "required": ["constants", "terms"], + "properties": { + "head": { + "const": "VectorAffineFunction" + }, + "constants": { + "type": "array", + "items": { + "type": "number" + } + }, + "terms": { + "type": "array", + "items": { + "$ref": "#/definitions/VectorAffineTerm" + } + } + } + }, { + "description": "The vector-valued quadratic function `q(x) + Ax + b`, where `q(x)` is specified by a list of `VectorQuadraticTerm`s in `quadratic_terms`, `A` is a sparse matrix specified by a list of `VectorAffineTerm`s in `affine_terms` and `b` is a dense vector specified by `constants`.", + "required": ["constants", "affine_terms", "quadratic_terms"], + "properties": { + "head": { + "const": "VectorQuadraticFunction" + }, + "constants": { + "type": "array", + "items": { + "type": "number" + } + }, + "affine_terms": { + "type": "array", + "items": { + "$ref": "#/definitions/VectorAffineTerm" + } + }, + "quadratc_terms": { + "type": "array", + "items": { + "$ref": "#/definitions/VectorQuadraticTerm" + } + } + } + }] + }, + "scalar_sets": { + "description": "A schema for the scalar-valued sets defined by MathOptFormat. See http: //www.juliaopt.org/MathOptInterface.jl/v0.8/apireference/#Sets-1 for a list of the sets and their meanings.", + "type": "object", + "required": ["head"], + "oneOf": [{ + "description": "The set `(-inf, upper]`.", + "required": ["upper"], + "properties": { + "head": { + "const": "LessThan" + }, + "upper": { + "type": "number" + } + } + }, { + "description": "The set `[lower, upper)`.", + "required": ["lower"], + "properties": { + "head": { + "const": "GreaterThan" + }, + "lower": { + "type": "number" + } + } + }, { + "description": "The set `{value}`.", + "required": ["value"], + "properties": { + "head": { + "const": "EqualTo" + }, + "value": { + "type": "number" + } + } + }, { + "description": "Interval: the set `[lower, upper]`\nSemiinteger: the set `{0} ∪ {lower, lower + 1, ..., upper}`\nSemicontinuous: the set `{0} ∪ [lower, upper]`.", + "required": ["lower", "upper"], + "properties": { + "head": { + "enum": ["Interval", "Semiinteger", "Semicontinuous"] + }, + "lower": { + "type": "number" + }, + "upper": { + "type": "number" + } + } + }, { + "description": "ZeroOne: the set `{0, 1}`\nInteger: the set `ℤ`.", + "properties": { + "head": { + "enum": ["ZeroOne", "Integer"] + } + } + }] + }, + "vector_sets": { + "description": "A schema for the vector-valued sets defined by MathOptFormat. See http: //www.juliaopt.org/MathOptInterface.jl/v0.8/apireference/#Sets-1 for a list of the sets and their meanings.", + "type": "object", + "required": ["head"], + "oneOf": [{ + "description": "ExponentialCone: the cone `[x, y, z] ∈ {R³: y * exp(x / y) ≤ z, y ≥ 0}`\nDualExponentialCone: the cone `[u, v, w] ∈ {R³: -u * exp(v / u) ≤ exp(1) * w, u < 0}`.", + "properties": { + "head": { + "enum": ["ExponentialCone", "DualExponentialCone"] + } + } + }, { + "required": ["weights"], + "properties": { + "head": { + "enum": ["SOS1", "SOS2"] + }, + "weights": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, { + "description": "Zeros: the set `{0}^{dimension}`\nReals: the set `R^{dimension}`\nNonpositives: the set `R-^{dimension}`\nNonnegatives: the set `R+^{dimension}`\nSecondOrderCone: the cone `[t, x] ∈ {R^{dimension} : t ≥ ||x||₂`\nRotatedSecondOrderCone: the cone `[t, u, x] ∈ {R^{dimension} : 2tu ≥ (||x||₂)²; t, u ≥ 0}\nGeometricMeanCone: the cone `[t, x] ∈ {R^{dimension}: t ≤ (Πxᵢ)^{1 / (dimension-1)}}`.", + "required": ["dimension"], + "properties": { + "head": { + "enum": [ + "Zeros", "Reals", "Nonpositives", "Nonnegatives", + "SecondOrderCone", "RotatedSecondOrderCone", + "GeometricMeanCone" + ] + }, + "dimension": { + "type": "integer", + "minimum": 1 + } + } + }, { + "required": ["side_dimension"], + "properties": { + "head": { + "enum": [ + "RootDetConeTriangle", "RootDetConeSquare", + "LogDetConeTriangle", "LogDetConeSquare", + "PositiveSemidefiniteConeTriangle", + "PositiveSemidefiniteConeSquare" + ] + }, + "side_dimension": { + "type": "integer", + "minimum": 1 + } + } + }, { + "description": "PowerCone: the cone `[x, y, z] ∈ {R³: x^{exponent} y^{1-exponent} ≥ |z|; x, y ≥ 0}`\nDualPowerCone: the cone `[u, v, w] ∈ {R³: (u / exponent)^{exponent} (v / (1-exponent))^{1-exponent} ≥ |w|; u, v ≥ 0}`.", + "required": ["exponent"], + "properties": { + "head": { + "enum": ["PowerCone", "DualPowerCone"] + }, + "exponent": { + "type": "number" + } + } + }] + } + } +} \ No newline at end of file diff --git a/test/MOF/MOF.jl b/test/MOF/MOF.jl index f5b84b9..8355c7c 100644 --- a/test/MOF/MOF.jl +++ b/test/MOF/MOF.jl @@ -1,5 +1,12 @@ const MOF = MathOptFormat.MOF +@testset "Validate schema examples" begin + examples_path = joinpath(dirname(MOF.SCHEMA_PATH), "examples") + @testset "$example" for example in readdir(examples_path) + @test MOF.validate(joinpath(examples_path, example)) === nothing + end +end + const TEST_MOF_FILE = "test.mof.json" @test sprint(show, MOF.Model()) == "A MathOptFormat Model" @@ -16,6 +23,7 @@ function test_model_equality(model_string, variables, constraints) model_2 = MOF.Model() MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, variables, constraints) + MOF.validate(TEST_MOF_FILE) end @testset "read_from_file" begin diff --git a/test/MOF/nonlinear.jl b/test/MOF/nonlinear.jl index fffdf46..dc994a7 100644 --- a/test/MOF/nonlinear.jl +++ b/test/MOF/nonlinear.jl @@ -44,6 +44,7 @@ end MOI.write_to_file(model, TEST_MOF_FILE) @test replace(read(TEST_MOF_FILE, String), '\r' => "") == replace(read(joinpath(@__DIR__, "nlp.mof.json"), String), '\r' => "") + MOF.validate(TEST_MOF_FILE) end @testset "Error handling" begin node_list = MOF.Object[] @@ -126,5 +127,6 @@ end @test foo2.expr == :(2 * $x + sin($x)^2 - $y) @test MOI.get(model, MOI.ConstraintSet(), con) == MOI.get(model2, MOI.ConstraintSet(), con2) + MOF.validate(TEST_MOF_FILE) end end