From 006a5d48abf67e2dc9e1bfa768c08db8613629aa Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Thu, 29 Feb 2024 15:56:23 -0500 Subject: [PATCH 1/5] Added a Check() method for the optimization problem --- problem/optimization_problem.go | 33 +++++++++ testing/problem/optimization_problem_test.go | 74 ++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/problem/optimization_problem.go b/problem/optimization_problem.go index 7b0d480..83033b4 100644 --- a/problem/optimization_problem.go +++ b/problem/optimization_problem.go @@ -268,3 +268,36 @@ func From(inputModel optim.Model) (*OptimizationProblem, error) { return newOptimProblem, nil } + +/* +Check +Description: + + Checks that the OptimizationProblem is valid. +*/ +func (op *OptimizationProblem) Check() error { + // Check Objective + err := op.Objective.Check() + if err != nil { + return fmt.Errorf("the objective is not valid: %v", err) + } + + // Check Variables + for _, variable := range op.Variables { + err = variable.Check() + if err != nil { + return fmt.Errorf("the variable is not valid: %v", err) + } + } + + // Check Constraints + for _, constraint := range op.Constraints { + err = constraint.Check() + if err != nil { + return fmt.Errorf("the constraint is not valid: %v", err) + } + } + + // All Checks Passed! + return nil +} diff --git a/testing/problem/optimization_problem_test.go b/testing/problem/optimization_problem_test.go index 71ddd45..8815261 100644 --- a/testing/problem/optimization_problem_test.go +++ b/testing/problem/optimization_problem_test.go @@ -945,3 +945,77 @@ func TestOptimizationProblem_From10(t *testing.T) { } } } + +/* +TestOptimizationProblem_Check1 +Description: + + Tests the Check function with a simple problem + that has one variable, one constraint and an objective + that is not well-defined. +*/ +func TestOptimizationProblem_Check1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_Check1") + v1 := p1.AddVariable() + c1 := v1.LessEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create bad objective + p1.Objective = *problem.NewObjective( + symbolic.Variable{}, problem.SenseMaximize, + ) + + // Algorithm + err := p1.Check() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + if !strings.Contains( + err.Error(), + p1.Objective.Check().Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + +/* +TestOptimizationProblem_Check2 +Description: + + Tests the Check function with a simple problem + that has one variable, one well-defined objective + and a set of constraints containing one bad constraint. +*/ +func TestOptimizationProblem_Check2(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_Check2") + v1 := p1.AddVariable() + c1 := symbolic.ScalarConstraint{ + LeftHandSide: v1, + RightHandSide: symbolic.Variable{}, + Sense: symbolic.SenseLessThanEqual, + } + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + v1, problem.SenseMaximize, + ) + + // Algorithm + err := p1.Check() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + if !strings.Contains( + err.Error(), + c1.Check().Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} From 12fbd43c4b73526e74d6fdee5591c51f2b30dbf6 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 16 Dec 2024 09:15:05 -0400 Subject: [PATCH 2/5] Added in dependency on newer SymbolicMath.go --- go.mod | 2 +- go.sum | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 76a33a2..d3ca0a0 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,4 @@ go 1.21 require gonum.org/v1/gonum v0.14.0 -require github.com/MatProGo-dev/SymbolicMath.go v0.1.4 +require github.com/MatProGo-dev/SymbolicMath.go v0.1.8 diff --git a/go.sum b/go.sum index 51762e4..3dc744e 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,5 @@ -github.com/MatProGo-dev/SymbolicMath.go v0.0.0-20240104201035-f9a42f642121 h1:nnJVXcnTvAdkfR4kZRBw4Ot+KoL8S3PijlLTmKOooco= -github.com/MatProGo-dev/SymbolicMath.go v0.0.0-20240104201035-f9a42f642121/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.1.0 h1:FUwLQZzZhtgGj6WKyuwQE74P9UYhgbNr5eD5f8zzuu8= -github.com/MatProGo-dev/SymbolicMath.go v0.1.0/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.1.1 h1:YigpL7w5D8qLu0xzDAayMXePBh9LK0/w3ykvuoVZwXQ= -github.com/MatProGo-dev/SymbolicMath.go v0.1.1/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.1.2 h1:9tYbiHWm0doXOSipd02pxtENqBU8WhmHTrDXetPW+ms= -github.com/MatProGo-dev/SymbolicMath.go v0.1.2/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.1.3 h1:IeofFqvZ/jAO6LywlZR/UMl5t/KOyMAvvctVgTzvgHI= -github.com/MatProGo-dev/SymbolicMath.go v0.1.3/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.1.4 h1:6DaDRYoANmKMkZL0uSUSx7Ibx8Hc9S6AX+7L0hIy1A4= -github.com/MatProGo-dev/SymbolicMath.go v0.1.4/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= +github.com/MatProGo-dev/SymbolicMath.go v0.1.8 h1:lpe+6cK/2fg29WwxOykm4hKvfJeqvFUBGturC9qh5ug= +github.com/MatProGo-dev/SymbolicMath.go v0.1.8/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= From c29d02eef5f6a70605f04303491bb134ca39bae3 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 16 Dec 2024 09:15:28 -0400 Subject: [PATCH 3/5] Introducing the IsLinear methods for OptimizationProblem and Objective --- problem/objective.go | 10 ++ problem/optimization_problem.go | 27 ++++++ testing/problem/optimization_problem_test.go | 97 +++++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/problem/objective.go b/problem/objective.go index 584e789..3c72804 100644 --- a/problem/objective.go +++ b/problem/objective.go @@ -14,3 +14,13 @@ type Objective struct { func NewObjective(e symbolic.Expression, sense ObjSense) *Objective { return &Objective{e, sense} } + +/* +IsLinear +Description: + + This method returns true if the objective is linear, false otherwise. +*/ +func (o *Objective) IsLinear() bool { + return symbolic.IsLinear(o.Expression) +} diff --git a/problem/optimization_problem.go b/problem/optimization_problem.go index 83033b4..25ef7fa 100644 --- a/problem/optimization_problem.go +++ b/problem/optimization_problem.go @@ -2,6 +2,7 @@ package problem import ( "fmt" + "github.com/MatProGo-dev/MatProInterface.go/optim" "github.com/MatProGo-dev/SymbolicMath.go/symbolic" ) @@ -301,3 +302,29 @@ func (op *OptimizationProblem) Check() error { // All Checks Passed! return nil } + +/* +IsLinear +Description: + + Checks if the optimization problem is linear. + Per the definition of a linear optimization problem, the problem is linear if and only if: + 1. The objective function is linear (i.e., a constant or an affine combination of variables). + 2. All constraints are linear (i.e., an affine combination of variables in an inequality or equality). +*/ +func (op *OptimizationProblem) IsLinear() bool { + // Check Objective + if !op.Objective.IsLinear() { + return false + } + + // Check Constraints + for _, constraint := range op.Constraints { + if !constraint.IsLinear() { + return false + } + } + + // All Checks Passed! + return true +} diff --git a/testing/problem/optimization_problem_test.go b/testing/problem/optimization_problem_test.go index 8815261..1089e6b 100644 --- a/testing/problem/optimization_problem_test.go +++ b/testing/problem/optimization_problem_test.go @@ -9,12 +9,13 @@ Description: import ( "fmt" + "strings" + "testing" + "github.com/MatProGo-dev/MatProInterface.go/optim" "github.com/MatProGo-dev/MatProInterface.go/problem" "github.com/MatProGo-dev/SymbolicMath.go/symbolic" "gonum.org/v1/gonum/mat" - "strings" - "testing" ) /* @@ -1019,3 +1020,95 @@ func TestOptimizationProblem_Check2(t *testing.T) { } } } + +/* +TestOptimizationProblem_IsLinear1 +Description: + + Tests the IsLinear function with a simple problem + that has a constant objective and a single, linear constraint. +*/ +func TestOptimizationProblem_IsLinear1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_IsLinear1") + v1 := p1.AddVariable() + c1 := v1.LessEq(1.0) + k1 := symbolic.K(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + k1, problem.SenseFind, + ) + + // Algorithm + isLinear := p1.IsLinear() + if !isLinear { + t.Errorf("expected the problem to be linear; received non-linear") + } +} + +/* +TestOptimizationProblem_IsLinear2 +Description: + + Tests the IsLinear function with a simple problem + that has a linear objective containing 3 variables and two lienar constraints, + each containing one variable. +*/ +func TestOptimizationProblem_IsLinear2(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_IsLinear2") + vv1 := p1.AddVariableVector(3) + c1 := vv1.AtVec(0).LessEq(1.0) + c2 := vv1.AtVec(1).LessEq(1.0) + + // Add constraints + p1.Constraints = append(p1.Constraints, c1) + p1.Constraints = append(p1.Constraints, c2) + + // Create good objective + p1.Objective = *problem.NewObjective( + vv1.Transpose().Multiply(symbolic.OnesVector(3)), + problem.SenseMinimize, + ) + + // Algorithm + isLinear := p1.IsLinear() + if !isLinear { + t.Errorf("expected the problem to be linear; received non-linear") + } +} + +/* +TestOptimizationProblem_IsLinear3 +Description: + + Tests the IsLinear function with a simple problem + that has a quadratic objective containing 3 variables and two lienar constraints, + each containing one variable. +*/ +func TestOptimizationProblem_IsLinear3(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_IsLinear3") + vv1 := p1.AddVariableVector(3) + c1 := vv1.AtVec(0).LessEq(1.0) + c2 := vv1.AtVec(1).LessEq(1.0) + + // Add constraints + p1.Constraints = append(p1.Constraints, c1) + p1.Constraints = append(p1.Constraints, c2) + + // Create good objective + p1.Objective = *problem.NewObjective( + vv1.Transpose().Multiply(vv1), + problem.SenseMaximize, + ) + + // Algorithm + isLinear := p1.IsLinear() + if isLinear { + t.Errorf("expected the problem to be non-linear; received linear") + } +} From 7541505419262a98facb9062468777b7802bd177 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 16 Dec 2024 09:27:45 -0400 Subject: [PATCH 4/5] Added some tests for ScalarConstraint objects in the optim package --- testing/optim/scalar_constraint_test.go | 136 +++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/testing/optim/scalar_constraint_test.go b/testing/optim/scalar_constraint_test.go index 8021684..527b19e 100644 --- a/testing/optim/scalar_constraint_test.go +++ b/testing/optim/scalar_constraint_test.go @@ -7,9 +7,10 @@ Description: */ import ( + "testing" + "github.com/MatProGo-dev/MatProInterface.go/optim" "gonum.org/v1/gonum/mat" - "testing" ) func TestScalarConstraint_ScalarConstraint1(t *testing.T) { @@ -64,7 +65,7 @@ func TestScalarConstraint_IsLinear1(t *testing.T) { } /* -TestScalarConstraint_IsLinear1 +TestScalarConstraint_IsLinear2 Description: Detects whether a simple inequality between @@ -99,6 +100,43 @@ func TestScalarConstraint_IsLinear2(t *testing.T) { } +/* +TestScalarConstraint_IsLinear3 +Description: + + Detects whether a simple inequality between + a variable and a constant is a quadratic expression. + The function should identify that this is NOT a linear expression. +*/ +func TestScalarConstraint_IsLinear3(t *testing.T) { + // Constants + m := optim.NewModel("scalar-constraint-test1") + v1 := m.AddVariable() + sqe2 := optim.ScalarQuadraticExpression{ + L: optim.OnesVector(1), + Q: *mat.NewDense(1, 1, []float64{3.14}), + X: optim.VarVector{Elements: []optim.Variable{v1}}, + } + + k1 := optim.K(2.8) + + // Algorithm + sc1 := optim.ScalarConstraint{ + LeftHandSide: k1, + RightHandSide: sqe2, + Sense: optim.SenseEqual, + } + + tf, err := sc1.IsLinear() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if tf { + t.Errorf("sc1 is not linear, but function claims it is!") + } + +} + /* TestScalarConstraint_Simplify1 Description: @@ -138,3 +176,97 @@ func TestScalarConstraint_Simplify1(t *testing.T) { t.Errorf("Remainder on LHS was not contained properly") } } + +/* +TestScalarConstraint_Simplify2 +Description: + + Attempts to simplify the constraint between + a scalar linear epression and a variable. + The resulting constraint should have a zero remainder on the right hand side + and the left hand side should contain a variable vector with 1 more element than it started with. +*/ +func TestScalarConstraint_Simplify2(t *testing.T) { + // Constants + m := optim.NewModel("scalar-constraint-test2") + vv1 := m.AddVariableVector(3) + sle2 := optim.ScalarLinearExpr{ + L: optim.OnesVector(vv1.Len()), + X: vv1, + C: 2.0, + } + v3 := m.AddVariable() + + // Create sles + sc1 := optim.ScalarConstraint{ + LeftHandSide: sle2, + RightHandSide: v3, + Sense: optim.SenseEqual, + } + + // Attempt to simplify + sc2, err := sc1.Simplify() + if err != nil { + t.Errorf("unexpected error during simplify(): %v", err) + } + + if float64(sc2.RightHandSide.(optim.K)) != 0.0 { + t.Errorf("Remainder on LHS was not contained properly") + } + + if sc2.LeftHandSide.(optim.ScalarLinearExpr).X.Len() != 4 { + t.Errorf("Variable vector did not increase in size") + } +} + +/* +TestScalarConstraint_Simplify3 +Description: + + Attempts to simplify the constraint between + a scalar linear epression and a scalar quadratic expression. +*/ +func TestScalarConstraint_Simplify3(t *testing.T) { + // Constants + m := optim.NewModel("scalar-constraint-test3") + vv1 := m.AddVariableVector(3) + sle2 := optim.ScalarLinearExpr{ + L: optim.OnesVector(vv1.Len()), + X: vv1, + C: 2.0, + } + sqe3 := optim.ScalarQuadraticExpression{ + L: optim.OnesVector(3), + Q: *mat.NewDense( + 3, 3, + []float64{ + 3.14, 0.0, 0.0, + 0.0, 3.14, 0.0, + 0.0, 0.0, 3.14, + }, + ), + X: vv1, + C: 1.5, + } + + // Create sles + sc1 := optim.ScalarConstraint{ + LeftHandSide: sle2, + RightHandSide: sqe3, + Sense: optim.SenseLessThanEqual, + } + + // Attempt to simplify + sc2, err := sc1.Simplify() + if err != nil { + t.Errorf("unexpected error during simplify(): %v", err) + } + + if float64(sc2.RightHandSide.(optim.K)) != sqe3.C { + t.Errorf( + "Remainder on RHS was not contained properly; expected %v, received %v", + sqe3.C, + sc2.RightHandSide.(optim.K), + ) + } +} From 70497181b9dd0d0d19056dcb10e161ba441dd49c Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 16 Dec 2024 09:44:02 -0400 Subject: [PATCH 5/5] Made sure that Check() is called before doing much else in IsLinear() and added a case for catching undefined objectives in Check() + added tests for all of the above --- mpiErrors/no_objective_defined.go | 7 ++ problem/optimization_problem.go | 12 +++ testing/problem/optimization_problem_test.go | 101 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 mpiErrors/no_objective_defined.go diff --git a/mpiErrors/no_objective_defined.go b/mpiErrors/no_objective_defined.go new file mode 100644 index 0000000..67561b4 --- /dev/null +++ b/mpiErrors/no_objective_defined.go @@ -0,0 +1,7 @@ +package mpiErrors + +type NoObjectiveDefinedError struct{} + +func (e NoObjectiveDefinedError) Error() string { + return "No objective defined for the optimization problem; please define one with SetObjective()" +} diff --git a/problem/optimization_problem.go b/problem/optimization_problem.go index 25ef7fa..33eb1e6 100644 --- a/problem/optimization_problem.go +++ b/problem/optimization_problem.go @@ -3,6 +3,7 @@ package problem import ( "fmt" + "github.com/MatProGo-dev/MatProInterface.go/mpiErrors" "github.com/MatProGo-dev/MatProInterface.go/optim" "github.com/MatProGo-dev/SymbolicMath.go/symbolic" ) @@ -278,6 +279,10 @@ Description: */ func (op *OptimizationProblem) Check() error { // Check Objective + if op.Objective == (Objective{}) { + return mpiErrors.NoObjectiveDefinedError{} + } + err := op.Objective.Check() if err != nil { return fmt.Errorf("the objective is not valid: %v", err) @@ -313,6 +318,13 @@ Description: 2. All constraints are linear (i.e., an affine combination of variables in an inequality or equality). */ func (op *OptimizationProblem) IsLinear() bool { + // Input Processing + // Verify that the problem is well-formed + err := op.Check() + if err != nil { + panic(fmt.Errorf("the optimization problem is not well-formed: %v", err)) + } + // Check Objective if !op.Objective.IsLinear() { return false diff --git a/testing/problem/optimization_problem_test.go b/testing/problem/optimization_problem_test.go index 1089e6b..bd0fede 100644 --- a/testing/problem/optimization_problem_test.go +++ b/testing/problem/optimization_problem_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/MatProGo-dev/MatProInterface.go/mpiErrors" "github.com/MatProGo-dev/MatProInterface.go/optim" "github.com/MatProGo-dev/MatProInterface.go/problem" "github.com/MatProGo-dev/SymbolicMath.go/symbolic" @@ -1021,6 +1022,74 @@ func TestOptimizationProblem_Check2(t *testing.T) { } } +/* +TestOptimizationProblem_Check3 +Description: + + Tests the Check function with a simple problem + that has one variable and no objective defined. + The mpiErrors.NoObjectiveDefinedError should be created. +*/ +func TestOptimizationProblem_Check3(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_Check3") + v1 := p1.AddVariable() + + // Add variable to problem + p1.Variables = append(p1.Variables, v1) + + // Algorithm + err := p1.Check() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + expectedError := mpiErrors.NoObjectiveDefinedError{} + if err.Error() != expectedError.Error() { + t.Errorf("unexpected error: %v", err) + } + } +} + +/* +TestOptimizationProblem_Check4 +Description: + + Tests the Check function with a simple problem + that has: + - objective defined + - two variables (one is NOT well-defined) + - and no constraints defined. + The result should throw an error relating to the bad variable. +*/ +func TestOptimizationProblem_Check4(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_Check4") + v1 := p1.AddVariable() + v2 := symbolic.Variable{} + + // Add variables to problem + // p1.Variables = append(p1.Variables, v1) // Already added + p1.Variables = append(p1.Variables, v2) + + // Create good objective + p1.Objective = *problem.NewObjective( + v1, problem.SenseMaximize, + ) + + // Algorithm + err := p1.Check() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + if !strings.Contains( + err.Error(), + v2.Check().Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + /* TestOptimizationProblem_IsLinear1 Description: @@ -1112,3 +1181,35 @@ func TestOptimizationProblem_IsLinear3(t *testing.T) { t.Errorf("expected the problem to be non-linear; received linear") } } + +/* +TestOptimizationProblem_IsLinear4 +Description: + + Tests the IsLinear function with a simple problem + that has a constant objective and a single, quadratic constraint. + The problem should be non-linear. +*/ +func TestOptimizationProblem_IsLinear4(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_IsLinear4") + vv1 := p1.AddVariableVector(3) + + // Add constraints + p1.Constraints = append( + p1.Constraints, + vv1.Transpose().Multiply(vv1).Plus(vv1).LessEq(1.0), + ) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + isLinear := p1.IsLinear() + if isLinear { + t.Errorf("expected the problem to be non-linear; received linear") + } +}