diff --git a/.github/workflows/coverage1.yaml b/.github/workflows/coverage1.yaml index 0daccbf..1fe9a34 100644 --- a/.github/workflows/coverage1.yaml +++ b/.github/workflows/coverage1.yaml @@ -1,20 +1,17 @@ -name: Go # The name of the workflow that will appear on Github +name: Testing # The name of the workflow that will appear on Github on: - push: - branches: [ main , kr-feature-problem1 ] pull_request: - branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: - build: + compute-coverage: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: [1.19] + go: [1.23] permissions: # Give the default GITHUB_TOKEN write permission to commit and push the # added or changed files to the repository. @@ -40,7 +37,8 @@ jobs: go tool cover -func coverage.out -o coverage2.out - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 + with: + files: ./coverage2.out env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.out diff --git a/causeOfProblemNonlinearity/causes.go b/causeOfProblemNonlinearity/causes.go new file mode 100644 index 0000000..7a5181a --- /dev/null +++ b/causeOfProblemNonlinearity/causes.go @@ -0,0 +1,9 @@ +package causeOfProblemNonlinearity + +type Cause string + +const ( + Objective Cause = "Objective" + Constraint = "Constraint" + NotWellDefined = "NotWellDefined" +) diff --git a/go.mod b/go.mod index 97bf669..32ca132 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,9 @@ module github.com/MatProGo-dev/MatProInterface.go -go 1.21 +go 1.23.0 -require gonum.org/v1/gonum v0.14.0 +toolchain go1.23.9 -require github.com/MatProGo-dev/SymbolicMath.go v0.2.1 +require gonum.org/v1/gonum v0.16.0 + +require github.com/MatProGo-dev/SymbolicMath.go v0.2.2 diff --git a/go.sum b/go.sum index f4742d8..d445b9f 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,4 @@ -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= -github.com/MatProGo-dev/SymbolicMath.go v0.1.9 h1:NMqvS9Bt2DWWLGxd+j3Qta4Ckq/x74gpMM7bt32om5g= -github.com/MatProGo-dev/SymbolicMath.go v0.1.9/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.2.0 h1:W8IREsZGeIuPAKHgJDyeCr3vLJjMr6H/O9RNFAjOVp8= -github.com/MatProGo-dev/SymbolicMath.go v0.2.0/go.mod h1:gKbGR/6sYWi2koMUEDIPWBPi6jQPELKle0ijIM+eaHU= -github.com/MatProGo-dev/SymbolicMath.go v0.2.1 h1:3qcLYNx3+9Ud/saS4QWxJzmDUbVvYZIWt9aX2bQ9iOk= -github.com/MatProGo-dev/SymbolicMath.go v0.2.1/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= -gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= +github.com/MatProGo-dev/SymbolicMath.go v0.2.2 h1:U9nLLgtslRXbweCsgW9uSw8AvoGMgjq2luQtXIuN3eA= +github.com/MatProGo-dev/SymbolicMath.go v0.2.2/go.mod h1:tW8thj4pkaTV9lFNU3OCKmwQ3mZ2Eim6S4JpHRDfRvU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= diff --git a/mpiErrors/no_equality_constraints_found.go b/mpiErrors/no_equality_constraints_found.go new file mode 100644 index 0000000..5845199 --- /dev/null +++ b/mpiErrors/no_equality_constraints_found.go @@ -0,0 +1,7 @@ +package mpiErrors + +type NoEqualityConstraintsFoundError struct{} + +func (e NoEqualityConstraintsFoundError) Error() string { + return "No equality constraints found in the optimization problem; please define some by adding them with \"testProblem1.Constraints = append(testProblem1.Constraints, newConstraint)\"" +} diff --git a/mpiErrors/no_inequality_constraints_found.go b/mpiErrors/no_inequality_constraints_found.go new file mode 100644 index 0000000..bf37b38 --- /dev/null +++ b/mpiErrors/no_inequality_constraints_found.go @@ -0,0 +1,7 @@ +package mpiErrors + +type NoInequalityConstraintsFoundError struct{} + +func (e NoInequalityConstraintsFoundError) Error() string { + return "No inequality constraints found in the optimization problem; please define some by adding them with \"testProblem1.Constraints = append(testProblem1.Constraints, newConstraint)\"" +} diff --git a/mpiErrors/not_linear.go b/mpiErrors/not_linear.go new file mode 100644 index 0000000..4317b17 --- /dev/null +++ b/mpiErrors/not_linear.go @@ -0,0 +1,29 @@ +package mpiErrors + +import ( + "fmt" + + "github.com/MatProGo-dev/MatProInterface.go/causeOfProblemNonlinearity" +) + +type ProblemNotLinearError struct { + ProblemName string + Cause causeOfProblemNonlinearity.Cause + ConstraintIndex int // Index of the constraint that is not linear, if applicable +} + +func (e ProblemNotLinearError) Error() string { + preamble := "The problem " + e.ProblemName + " is not linear" + switch e.Cause { + case causeOfProblemNonlinearity.Objective: + preamble += "; the objective is not linear" + case causeOfProblemNonlinearity.Constraint: + preamble += fmt.Sprintf("; constraint #%v is not linear", e.ConstraintIndex) + case causeOfProblemNonlinearity.NotWellDefined: + preamble += "; the problem is not well defined" + default: + preamble += "; the cause of the problem is not recognized (create an issue on GitHub if you think this is a bug!)" + } + // Return the error message + return preamble +} diff --git a/problem/examples.go b/problem/examples.go new file mode 100644 index 0000000..44ac535 --- /dev/null +++ b/problem/examples.go @@ -0,0 +1,62 @@ +package problem + +import ( + getKMatrix "github.com/MatProGo-dev/SymbolicMath.go/get/KMatrix" + getKVector "github.com/MatProGo-dev/SymbolicMath.go/get/KVector" + "github.com/MatProGo-dev/SymbolicMath.go/symbolic" +) + +/* +GetExampleProblem3 +Description: + + Returns the LP from this youtube video: + https://www.youtube.com/watch?v=QAR8zthQypc&t=483s + It should look like this: + Maximize 4 x1 + 3 x2 + 5 x3 + Subject to + x1 + 2 x2 + 2 x3 <= 4 + 3 x1 + 4 x3 <= 6 + 2 x1 + x2 + 4 x3 <= 8 + x1 >= 0 + x2 >= 0 + x3 >= 0 +*/ +func GetExampleProblem3() *OptimizationProblem { + // Setup + out := NewProblem("TestProblem3") + + // Create variables + x := out.AddVariableVectorClassic( + 3, + 0.0, + symbolic.Infinity.Constant(), + symbolic.Continuous, + ) + + // Create Basic Objective + c := getKVector.From([]float64{4.0, 3.0, 5.0}) + out.SetObjective( + c.Transpose().Multiply(x), + SenseMaximize, + ) + + // Create Constraints (using one big matrix) + A := getKMatrix.From([][]float64{ + {1.0, 2.0, 2.0}, + {3.0, 0.0, 4.0}, + {2.0, 1.0, 4.0}, + }) + b := getKVector.From([]float64{4.0, 6.0, 8.0}) + out.Constraints = append(out.Constraints, A.Multiply(x).LessEq(b)) + + // TODO(kwesi): Figure out how to add non-negativity constraints + // // Add non-negativity constraints + // for _, varII := range x { + // out.Constraints = append( + // out.Constraints, + // varII.GreaterEq(0.0), + // ) + // } + return out +} diff --git a/problem/optimization_problem.go b/problem/optimization_problem.go index a2618ca..8df14e3 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/causeOfProblemNonlinearity" "github.com/MatProGo-dev/MatProInterface.go/mpiErrors" "github.com/MatProGo-dev/MatProInterface.go/optim" getKVector "github.com/MatProGo-dev/SymbolicMath.go/get/KVector" @@ -320,25 +321,28 @@ 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() + // Run the check method + err := op.CheckIfLinear() if err != nil { - panic(fmt.Errorf("the optimization problem is not well-formed: %v", err)) - } + // If the check method returns a NotWellDefinedError, then the problem is not well defined + // and we should panic. + notWellDefinedError := mpiErrors.ProblemNotLinearError{ + ProblemName: op.Name, + Cause: causeOfProblemNonlinearity.NotWellDefined, + ConstraintIndex: -1, + } + if err.Error() == notWellDefinedError.Error() { + panic( + fmt.Errorf( + "the optimization problem is not well defined; %v", + op.Check(), + )) + } - // Check Objective - if !op.Objective.IsLinear() { + // If the check returns any other error, then the problem is not linear return false } - // Check Constraints - for _, constraint := range op.Constraints { - if !constraint.IsLinear() { - return false - } - } - // All Checks Passed! return true } @@ -353,7 +357,7 @@ Description: Where A is the matrix of coefficients, x is the vector of variables, and b is the vector of constants. We return A and b. */ -func (op *OptimizationProblem) LinearInequalityConstraintMatrices() (symbolic.KMatrix, symbolic.KVector) { +func (op *OptimizationProblem) LinearInequalityConstraintMatrices() (symbolic.KMatrix, symbolic.KVector, error) { // Setup // Collect the Variables of this Problem @@ -377,9 +381,18 @@ func (op *OptimizationProblem) LinearInequalityConstraintMatrices() (symbolic.KM scalar_constraints = append(scalar_constraints, c) case symbolic.VectorConstraint: vector_constraints = append(vector_constraints, c) + default: + return nil, nil, fmt.Errorf( + "the constraint is not a scalar or vector constraint: %T. Please create a GitHub Issue to address this!", c, + ) } } + // Check if there are no inequality constraints + if len(scalar_constraints) == 0 && len(vector_constraints) == 0 { + return nil, nil, mpiErrors.NoInequalityConstraintsFoundError{} + } + // Create the matrix and vector elements from the scalar constraints A_components_scalar := make([]mat.VecDense, len(scalar_constraints)) b_components_scalar := make([]float64, len(scalar_constraints)) @@ -437,7 +450,7 @@ func (op *OptimizationProblem) LinearInequalityConstraintMatrices() (symbolic.KM } } - return AOut.(symbolic.KMatrix), bOut.(symbolic.KVector) + return AOut.(symbolic.KMatrix), bOut.(symbolic.KVector), nil } /* @@ -450,7 +463,7 @@ Description: Where C is the matrix of coefficients, x is the vector of variables, and d is the vector of constants. We return C and d. */ -func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMatrix, symbolic.KVector) { +func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMatrix, symbolic.KVector, error) { // Setup // Collect the Variables of this Problem @@ -477,6 +490,11 @@ func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMat } } + // Check if there are no equality constraints + if len(scalar_constraints) == 0 && len(vector_constraints) == 0 { + return nil, nil, mpiErrors.NoEqualityConstraintsFoundError{} + } + // Create the matrix and vector elements from the scalar constraints C_components_scalar := make([]mat.VecDense, len(scalar_constraints)) d_components_scalar := make([]float64, len(scalar_constraints)) @@ -506,7 +524,8 @@ func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMat dOut = getKVector.From(d_components_scalar) } vector_constraint_matrices_exist := len(C_components_vector) > 0 - + // fmt.Printf("vector_constraint_matrices_exist: %v\n", vector_constraint_matrices_exist) + // fmt.Printf("len(C_components_vector): %v\n", len(C_components_vector)) if vector_constraint_matrices_exist { // Create the matrix, if it doesn't already exist if !scalar_constraint_matrices_exist { @@ -533,5 +552,308 @@ func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMat ) } } - return COut.(symbolic.KMatrix), dOut.(symbolic.KVector) + + // Extract the KMatrix and KVector from the symbolic expressions + COut2, ok := COut.(symbolic.KMatrix) + if !ok { + return nil, nil, fmt.Errorf("the output C is not a KMatrix: %T", COut) + } + + dOut2, ok := dOut.(symbolic.KVector) + if !ok { + return nil, nil, fmt.Errorf("the output d is not a KVector: %T", dOut) + } + + // Return the KMatrix and KVector + return COut2, dOut2, nil +} + +/* +ToProblemWithAllPositiveVariables +Description: + + Transforms the given optimization problem into a new optimization problem + that only contains positive variables. + In math, this means that we will create two new variables (x_+ and x_-) for each + original variable (x), one for the positive part and one for the negative part. + Then, we replace every instance of the original variable with the difference + of the two new variables (x = x_+ - x_-). +*/ +func (op *OptimizationProblem) ToProblemWithAllPositiveVariables() (*OptimizationProblem, error) { + // Setup + newProblem := NewProblem(op.Name + " (All Positive Variables)") + + // For each variable, let's create two new variables + // and set the original variable to be the difference of the two + mapFromOriginalVariablesToNewExpressions := make(map[symbolic.Variable]symbolic.Expression) + for ii := 0; ii < len(op.Variables); ii++ { + // Setup + xII := op.Variables[ii] + + // Create the two new variables + newProblem.AddVariableClassic(0.0, symbolic.Infinity.Constant(), symbolic.Continuous) + nVariables := len(newProblem.Variables) + newProblem.Variables[nVariables-1].Name = xII.Name + " (+)" + variablePositivePart := newProblem.Variables[nVariables-1] + + newProblem.AddVariableClassic(0.0, symbolic.Infinity.Constant(), symbolic.Continuous) + nVariables = len(newProblem.Variables) + newProblem.Variables[nVariables-1].Name = xII.Name + " (-)" + variableNegativePart := newProblem.Variables[nVariables-1] + + // Set the original variable to be the difference of the two new variables + mapFromOriginalVariablesToNewExpressions[xII] = + variablePositivePart.Minus(variableNegativePart) + } + + // Now, let's create the new constraints by replacing the variables in the + // original constraints with the new expressions + for _, constraint := range op.Constraints { + // Add the new constraint to the problem + newProblem.Constraints = append( + newProblem.Constraints, + constraint.SubstituteAccordingTo(mapFromOriginalVariablesToNewExpressions), + ) + } + + // Now, let's create the new objective function by substituting the variables + // according to the map we created above + newObjectiveExpression := op.Objective.Expression.SubstituteAccordingTo( + mapFromOriginalVariablesToNewExpressions, + ) + newProblem.SetObjective( + newObjectiveExpression, + op.Objective.Sense, + ) + + return newProblem, nil +} + +/* +ToLPStandardForm1 +Description: + + Transforms the given linear program (represented in an OptimizationProblem object) + into a standard form (i.e., only linear equality constraints and a linear objective function). + + sense c^T * x + subject to + A * x = b + x >= 0 + + Where A is a matrix of coefficients, b is a vector of constants, and c is the vector of coefficients + for the objective function. This method also returns the slack variables (i.e., the variables that + are added to the problem to convert the inequalities into equalities). +*/ +func (problemIn *OptimizationProblem) ToLPStandardForm1() (*OptimizationProblem, []symbolic.Variable, error) { + // Input Processing + err := problemIn.Check() + if err != nil { + return nil, nil, fmt.Errorf("the optimization problem is not well-formed: %v", err) + } + + // Check if the problem is linear + if !problemIn.IsLinear() { + return nil, nil, problemIn.CheckIfLinear() + } + + // Setup + problemWithAllPositiveVariables, err := problemIn.ToProblemWithAllPositiveVariables() + if err != nil { + return nil, nil, err + } + + // Create a new problem + problemInStandardForm := NewProblem( + problemIn.Name + " (In Standard Form)", + ) + + // Copy over each of the + + // Add all variables to the new problem + mapFromInToNewVariables := make(map[symbolic.Variable]symbolic.Expression) + for _, varII := range problemWithAllPositiveVariables.Variables { + problemInStandardForm.AddVariable() + nVariables := len(problemInStandardForm.Variables) + mapFromInToNewVariables[varII] = problemInStandardForm.Variables[nVariables-1] + } + + // Add all constraints to the new problem + slackVariables := []symbolic.Variable{} + for _, constraint := range problemWithAllPositiveVariables.Constraints { + // Create a new expression by substituting the variables according + // to the map we created above + oldLHS := constraint.Left() + newLHS := oldLHS.SubstituteAccordingTo(mapFromInToNewVariables) + + oldRHS := constraint.Right() + newRHS := oldRHS.SubstituteAccordingTo(mapFromInToNewVariables) + + switch constraint.ConstrSense() { + case symbolic.SenseEqual: + // No need to do anything + case symbolic.SenseGreaterThanEqual: + switch concreteConstraint := constraint.(type) { + case symbolic.ScalarConstraint: + // Add a new SCALAR slack variable to the right hand side + problemInStandardForm.AddVariableClassic(0.0, symbolic.Infinity.Constant(), symbolic.Continuous) + nVariables := len(problemInStandardForm.Variables) + problemInStandardForm.Variables[nVariables-1].Name = problemInStandardForm.Variables[nVariables-1].Name + " (slack)" + slackVariables = append( + slackVariables, + problemInStandardForm.Variables[nVariables-1], + ) + + newRHS = newRHS.Plus(problemInStandardForm.Variables[nVariables-1]) + case symbolic.VectorConstraint: + // Add a new VECTOR slack variable to the right hand side + // TODO(Kwesi): Revisit this when we have a proper Len() method for constraints. + dims := concreteConstraint.Dims() + nRows := dims[0] + problemInStandardForm.AddVariableVectorClassic( + nRows, + 0.0, + symbolic.Infinity.Constant(), + symbolic.Continuous, + ) + nVariables := len(problemInStandardForm.Variables) + for jj := nRows - 1; jj >= 0; jj-- { + problemInStandardForm.Variables[nVariables-1-jj].Name = problemInStandardForm.Variables[nVariables-1-jj].Name + " (slack)" + slackVariables = append( + slackVariables, + problemInStandardForm.Variables[nVariables-1-jj], + ) + } + + // Add the slack variable to the right hand side + newRHS = newRHS.Plus( + symbolic.VariableVector(problemInStandardForm.Variables[nVariables-1-nRows : nVariables-1]), + ) + default: + return nil, nil, fmt.Errorf( + "Unexpected constraint type: %T for \"ToStandardFormWithSlackVariables\" with %v sense", + constraint, + constraint.ConstrSense(), + ) + + } + case symbolic.SenseLessThanEqual: + // Use a switch statement to handle different dimensions of the constraint + switch concreteConstraint := constraint.(type) { + case symbolic.ScalarConstraint: + // Add a new SCALAR slack variable to the left hand side + problemInStandardForm.AddVariableClassic(0.0, symbolic.Infinity.Constant(), symbolic.Continuous) + nVariables := len(problemInStandardForm.Variables) + problemInStandardForm.Variables[nVariables-1].Name = problemInStandardForm.Variables[nVariables-1].Name + " (slack)" + slackVariables = append( + slackVariables, + problemInStandardForm.Variables[nVariables-1], + ) + newLHS = newLHS.Plus(problemInStandardForm.Variables[nVariables-1]) + case symbolic.VectorConstraint: + // Add a new VECTOR slack variable to the left hand side + // TODO(Kwesi): Revisit this when we have a proper Len() method for constraints. + dims := concreteConstraint.Dims() + nRows := dims[0] + problemInStandardForm.AddVariableVectorClassic( + nRows, + 0.0, + symbolic.Infinity.Constant(), + symbolic.Continuous, + ) + nVariables := len(problemInStandardForm.Variables) + for jj := nRows - 1; jj >= 0; jj-- { + problemInStandardForm.Variables[nVariables-1-jj].Name = problemInStandardForm.Variables[nVariables-1-jj].Name + " (slack)" + slackVariables = append( + slackVariables, + problemInStandardForm.Variables[nVariables-1-jj], + ) + // fmt.Printf("Slack variable %d: %v\n", jj, problemInStandardForm.Variables[nVariables-1-jj]) + } + // Add the slack variable to the left hand side + newLHS = newLHS.Plus( + symbolic.VariableVector(problemInStandardForm.Variables[nVariables-1-nRows : nVariables-1]), + ) + default: + return nil, nil, fmt.Errorf( + "Unexpected constraint type %T for \"ToStandardFormWithSlackVariables\" with %v sense", + constraint, + constraint.ConstrSense(), + ) + } + default: + return nil, nil, fmt.Errorf( + "Unknown constraint sense: " + constraint.ConstrSense().String(), + ) + } + + newConstraint := newLHS.Comparison( + newRHS, + symbolic.SenseEqual, + ) + + // Add the new constraint to the problem + problemInStandardForm.Constraints = append( + problemInStandardForm.Constraints, + newConstraint, + ) + } + + // Now, let's create the new objective function by substituting the variables + // according to the map we created above + newObjectiveExpression := problemWithAllPositiveVariables.Objective.Expression.SubstituteAccordingTo( + mapFromInToNewVariables, + ) + problemInStandardForm.SetObjective( + newObjectiveExpression, + problemWithAllPositiveVariables.Objective.Sense, + ) + + // fmt.Printf("The slack variables are: %v\n", slackVariables) + + // Return the new problem and the slack variables + return problemInStandardForm, slackVariables, nil +} + +/* +CheckIfLinear +Description: + + Checks the current optimization problem to see if it is linear. + Returns an error if the problem is not linear. +*/ +func (op *OptimizationProblem) CheckIfLinear() error { + // Input Processing + // Verify that the problem is well-formed + err := op.Check() + if err != nil { + return mpiErrors.ProblemNotLinearError{ + ProblemName: op.Name, + Cause: causeOfProblemNonlinearity.NotWellDefined, + ConstraintIndex: -1, + } + } + + // Check Objective + if !op.Objective.IsLinear() { + return mpiErrors.ProblemNotLinearError{ + ProblemName: op.Name, + Cause: causeOfProblemNonlinearity.Objective, + ConstraintIndex: -2, + } + } + + // Check Constraints + for ii, constraint := range op.Constraints { + if !constraint.IsLinear() { + return mpiErrors.ProblemNotLinearError{ + ProblemName: op.Name, + Cause: causeOfProblemNonlinearity.Constraint, + ConstraintIndex: ii, + } + } + } + + // All Checks Passed! + return nil } diff --git a/testing/problem/optimization_problem_test.go b/testing/problem/optimization_problem_test.go index e1cc61c..4a14486 100644 --- a/testing/problem/optimization_problem_test.go +++ b/testing/problem/optimization_problem_test.go @@ -12,9 +12,12 @@ import ( "strings" "testing" + "github.com/MatProGo-dev/MatProInterface.go/causeOfProblemNonlinearity" "github.com/MatProGo-dev/MatProInterface.go/mpiErrors" "github.com/MatProGo-dev/MatProInterface.go/optim" "github.com/MatProGo-dev/MatProInterface.go/problem" + getKMatrix "github.com/MatProGo-dev/SymbolicMath.go/get/KMatrix" + getKVector "github.com/MatProGo-dev/SymbolicMath.go/get/KVector" "github.com/MatProGo-dev/SymbolicMath.go/symbolic" "gonum.org/v1/gonum/mat" ) @@ -1241,7 +1244,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices1(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 1 { @@ -1290,7 +1296,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices2(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 2 { @@ -1337,7 +1346,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices3(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 3 { @@ -1386,7 +1398,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices4(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 6 { @@ -1438,7 +1453,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices5(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 1 { @@ -1489,7 +1507,10 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices6(t *testing.T) { ) // Algorithm - A, b := p1.LinearInequalityConstraintMatrices() + A, b, err := p1.LinearInequalityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 4 { @@ -1510,6 +1531,42 @@ func TestOptimizationProblem_LinearInequalityConstraintMatrices6(t *testing.T) { } } +/* +TestOptimizationProblem_LinearInequalityConstraintMatrices7 +Description: + + Tests that the LinearInequalityConstraintMatrices function + properly produces an error when the problem has NO inequality constraints. +*/ +func TestOptimizationProblem_LinearInequalityConstraintMatrices7(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_LinearInequalityConstraintMatrices7") + v1 := p1.AddVariable() + v2 := p1.AddVariable() + + // Create good objective + p1.Objective = *problem.NewObjective( + v1.Plus(v2.Multiply(0.5)), + problem.SenseMaximize, + ) + + // Create a linear equality constraint + p1.Constraints = append(p1.Constraints, v1.Plus(v2).Eq(1.0)) + + // Algorithm + _, _, err := p1.LinearInequalityConstraintMatrices() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + if !strings.Contains( + err.Error(), + mpiErrors.NoInequalityConstraintsFoundError{}.Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + /* TestOptimizationProblem_LinearEqualityConstraintMatrices1 Description: @@ -1537,7 +1594,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices1(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 1 { @@ -1586,7 +1646,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices2(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 2 { @@ -1633,7 +1696,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices3(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 3 { @@ -1682,7 +1748,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices4(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 6 { @@ -1734,7 +1803,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices5(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 1 { @@ -1785,7 +1857,10 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices6(t *testing.T) { ) // Algorithm - A, b := p1.LinearEqualityConstraintMatrices() + A, b, err := p1.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } // Check that the number of rows is as expected. if A.Dims()[0] != 4 { @@ -1805,3 +1880,585 @@ func TestOptimizationProblem_LinearEqualityConstraintMatrices6(t *testing.T) { 4, len(b)) } } + +/* +TestOptimizationProblem_LinearEqualityConstraintMatrices7 +Description: + + Tests the LinearEqualityConstraintMatrices function with a problem + that led to panics in the field. + The problem is Problem3 from our examples file. + The problem will have: + - a linear objective + - 3 variables, + - and a single linear VECTORE equality constraint. +*/ +func TestOptimizationProblem_LinearEqualityConstraintMatrices7(t *testing.T) { + // Constants + p1 := problem.GetExampleProblem3() + + // Transform p1 into the standard form + p1Standard, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Attempt to Call LinearEqualityConstraintMatrices + A, b, err := p1Standard.LinearEqualityConstraintMatrices() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of rows is as expected. + if A.Dims()[0] != 3 { + t.Errorf("expected the number of rows to be %v; received %v", + 3, A.Dims()[0]) + } + + // Check that the number of columns is as expected. + nVariables1 := len(p1.Variables) + nInequalityConstraints1 := p1.Constraints[0].Left().Dims()[0] + if A.Dims()[1] != 2*nVariables1+nInequalityConstraints1 { + t.Errorf("expected the number of columns to be %v; received %v", + 2*nVariables1+nInequalityConstraints1, A.Dims()[1]) + } + + // Check that the number of elements in b is as expected. + if len(b) != 3 { + t.Errorf("expected the number of elements in b to be %v; received %v", + 3, len(b)) + } +} + +/* +TestOptimizationProblem_LinearEqualityConstraintMatrices8 +Description: + + Tests the LinearEqualityConstraintMatrices function properly produces + an NoEqualityConstraintsFoundError when the problem has NO equality constraints. +*/ +func TestOptimizationProblem_LinearEqualityConstraintMatrices8(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_LinearEqualityConstraintMatrices8") + v1 := p1.AddVariable() + v2 := p1.AddVariable() + + // Create good objective + p1.Objective = *problem.NewObjective( + v1.Plus(v2.Multiply(0.5)), + problem.SenseMaximize, + ) + + // Create a linear inequality constraint + p1.Constraints = append(p1.Constraints, v1.Plus(v2).LessEq(1.0)) + + // Algorithm + _, _, err := p1.LinearEqualityConstraintMatrices() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + if !strings.Contains( + err.Error(), + mpiErrors.NoEqualityConstraintsFoundError{}.Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + +/* +TestOptimizationProblem_ToProblemWithAllPositiveVariables1 +Description: + + Tests the ToProblemWithAllPositiveVariables function with a simple problem + that has: + - a constant objective + - 2 variables, + - and a single linear inequality constraint. + The result should be a problem with 4 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToProblemWithAllPositiveVariables1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToProblemWithAllPositiveVariables1") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.LessEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, err := p1.ToProblemWithAllPositiveVariables() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + if len(p2.Variables) != 4 { + t.Errorf("expected the number of variables to be %v; received %v", + 4, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + 1, len(p2.Constraints)) + } + + // Verify that the new constraint contains two variables in the left hand side + if len(p2.Constraints[0].Left().Variables()) != 2 { + t.Errorf("expected the number of variables in the left hand side to be %v; received %v", + 2, len(p2.Constraints[0].Left().Variables())) + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_1 +Description: + + Tests the ToLPStandardForm function with a simple problem + that contains: + - a constant objective + - 1 variable, + - and a single linear inequality constraint (SenseGreaterThanEqual). + The result should be a problem with 2 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_1") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.GreaterEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + expectedNumVariables := 0 + expectedNumVariables += 2 * len(p1.Variables) // original variables (positive and negative halfs) + expectedNumVariables += len(p1.Constraints) // slack variables + if len(p2.Variables) != expectedNumVariables { + t.Errorf("expected the number of variables to be %v; received %v", + 2, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + 1, len(p2.Constraints)) + } + + // Verify that all constraints are equality constraints + for _, c := range p2.Constraints { + if c.ConstrSense() != symbolic.SenseEqual { + t.Errorf("expected the constraint to be an equality constraint; received %v", + c.ConstrSense()) + } + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_2 +Description: + + Tests the ToLPStandardForm function with a simple problem + that contains: + - a constant objective + - 3 variables, + - and a single vector linear inequality constraint (SenseGreaterThanEqual) of 5 dimensions. + The result should be a problem with 3*2+5 = 11 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_2(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_2") + vv1 := p1.AddVariableVector(3) + A2 := getKMatrix.From([][]float64{ + {1.0, 2.0, 3.0}, + {4.0, 5.0, 6.0}, + {7.0, 8.0, 9.0}, + {10.0, 11.0, 12.0}, + {13.0, 14.0, 15.0}, + }) + c1 := A2.Multiply(vv1).GreaterEq(symbolic.OnesVector(5)) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + expectedNumVariables := 0 + expectedNumVariables += 2 * len(p1.Variables) // original variables (positive and negative halfs) + p1FirstConstraint := p1.Constraints[0] + p1FirstConstraintAsVC, ok := p1FirstConstraint.(symbolic.VectorConstraint) + if !ok { + t.Errorf("expected the first constraint to be a vector constraint; received %T", p1FirstConstraint) + } + expectedNumVariables += p1FirstConstraintAsVC.Dims()[0] // slack variables + if len(p2.Variables) != expectedNumVariables { + t.Errorf("expected the number of variables to be %v; received %v", + expectedNumVariables, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + 5, len(p2.Constraints)) + } + + // Verify that all constraints are equality constraints + for _, c := range p2.Constraints { + if c.ConstrSense() != symbolic.SenseEqual { + t.Errorf("expected the constraint to be an equality constraint; received %v", + c.ConstrSense()) + } + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_3 +Description: + + Tests the ToLPStandardForm function with a simple problem + that contains: + - a constant objective + - 3 variables, + - and a single vector linear inequality constraint (SenseLessThanEqual) of 5 dimensions. + The result should be a problem with 3*2+5 = 11 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_3(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_3") + vv1 := p1.AddVariableVector(3) + A2 := getKMatrix.From([][]float64{ + {1.0, 2.0, 3.0}, + {4.0, 5.0, 6.0}, + {7.0, 8.0, 9.0}, + {10.0, 11.0, 12.0}, + {13.0, 14.0, 15.0}, + }) + c1 := A2.Multiply(vv1).LessEq(symbolic.OnesVector(5)) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + expectedNumVariables := 0 + expectedNumVariables += 2 * len(p1.Variables) // original variables (positive and negative halfs) + p1FirstConstraint := p1.Constraints[0] + p1FirstConstraintAsVC, ok := p1FirstConstraint.(symbolic.VectorConstraint) + if !ok { + t.Errorf("expected the first constraint to be a vector constraint; received %T", p1FirstConstraint) + } + expectedNumVariables += p1FirstConstraintAsVC.Dims()[0] // slack variables + if len(p2.Variables) != expectedNumVariables { + t.Errorf("expected the number of variables to be %v; received %v", + expectedNumVariables, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + expectedNumVariables, len(p2.Constraints)) + } + + // Verify that all constraints are equality constraints + for _, c := range p2.Constraints { + if c.ConstrSense() != symbolic.SenseEqual { + t.Errorf( + "expected the constraint to be an equality constraint; received %v", + c.ConstrSense()) + } + } + +} + +/* +TestOptimizationProblem_ToLPStandardForm1_4 +Description: + + This test verifies that the ToLPStandardForm function throws an error + when called on a problem that is not linear. + In this case, we will define a problem with a quadratic objective function. + The problem will have: + - a quadratic objective + - 2 variables, + - and a single linear inequality constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_4(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_4") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.LessEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create a quadratic objective + p1.Objective = *problem.NewObjective( + v1.Multiply(v1), + problem.SenseMaximize, + ) + + // Algorithm + _, _, err := p1.ToLPStandardForm1() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + expectedError := mpiErrors.ProblemNotLinearError{ + ProblemName: p1.Name, + Cause: causeOfProblemNonlinearity.Objective, + ConstraintIndex: -2, + } + if !strings.Contains( + err.Error(), + expectedError.Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_5 +Description: + + This test verifies that the ToLPStandardForm function throws an error + when called on a problem that is not linear. + In this case, we will define a problem with a quadratic constraint. + The problem will have: + - a constant objective + - 2 variables, + - and a single quadratic inequality constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_5(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_5") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.Multiply(v1).LessEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + _, _, err := p1.ToLPStandardForm1() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + expectedError := mpiErrors.ProblemNotLinearError{ + ProblemName: p1.Name, + Cause: causeOfProblemNonlinearity.Constraint, + ConstraintIndex: 0, + } + if !strings.Contains( + err.Error(), + expectedError.Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_6 +Description: + + This test verifies that the ToLPStandardForm function properly handles + a simple problem with a single, scalar linear inequality constraint. + The problem will have: + - a constant objective + - 2 variables, + - and a single scalar linear inequality constraint (SenseLessThanEqual). + The result should be a problem with 2*2+1 = 5 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_6(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_6") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.LessEq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + expectedNumVariables := 0 + expectedNumVariables += 2 * len(p1.Variables) // original variables (positive and negative halfs) + expectedNumVariables += len(p1.Constraints) // slack variables + if len(p2.Variables) != expectedNumVariables { + t.Errorf("expected the number of variables to be %v; received %v", + expectedNumVariables, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + expectedNumVariables, len(p2.Constraints)) + } + + // Verify that all constraints are equality constraints + for _, c := range p2.Constraints { + if c.ConstrSense() != symbolic.SenseEqual { + t.Errorf("expected the constraint to be an equality constraint; received %v", + c.ConstrSense()) + } + } +} + +/* +TestOptimizationProblem_ToLPStandardForm1_7 +Description: + + This test verifies that the ToLPStandardForm function properly handles + a simple problem with a single, scalar equality constraint. + The problem will have: + - a constant objective + - 2 variables, + - and a single scalar linear equality constraint (SenseEqual). + The result should be a problem with 2*2 = 4 variables and 1 constraint. +*/ +func TestOptimizationProblem_ToLPStandardForm1_7(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_ToLPStandardForm1_7") + v1 := p1.AddVariable() + p1.AddVariable() + c1 := v1.Eq(1.0) + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + p2, _, err := p1.ToLPStandardForm1() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Check that the number of variables is as expected. + expectedNumVariables := 0 + expectedNumVariables += 2 * len(p1.Variables) // original variables (positive and negative halfs) + if len(p2.Variables) != expectedNumVariables { + t.Errorf("expected the number of variables to be %v; received %v", + expectedNumVariables, len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p2.Constraints) != 1 { + t.Errorf("expected the number of constraints to be %v; received %v", + expectedNumVariables, len(p2.Constraints)) + } + + // Verify that all constraints are equality constraints + for _, c := range p2.Constraints { + if c.ConstrSense() != symbolic.SenseEqual { + t.Errorf("expected the constraint to be an equality constraint; received %v", + c.ConstrSense()) + } + } +} + +/* +TestOptimizationProblem_CheckIfLinear1 +Description: + + This test verifies that the CheckIfLinear function properly identifies + a NOT well-defined problem is not linear. + The problem will have a vector constraint with mismatched dimensions. +*/ +func TestOptimizationProblem_CheckIfLinear1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_CheckIfLinear1") + vv1 := p1.AddVariableVector(3) + A2 := getKMatrix.From([][]float64{ + {1.0, 2.0, 3.0}, + {4.0, 5.0, 6.0}, + }) + c1 := symbolic.VectorConstraint{ + LeftHandSide: A2.Multiply(vv1).(symbolic.VectorExpression), + RightHandSide: getKVector.From(symbolic.OnesVector(5)), + Sense: symbolic.SenseLessThanEqual, + } + + p1.Constraints = append(p1.Constraints, c1) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Algorithm + err := p1.CheckIfLinear() + if err == nil { + t.Errorf("expected an error; received nil") + } else { + expectedError := mpiErrors.ProblemNotLinearError{ + ProblemName: p1.Name, + Cause: causeOfProblemNonlinearity.NotWellDefined, + ConstraintIndex: -1, + } + if !strings.Contains( + err.Error(), + expectedError.Error(), + ) { + t.Errorf("unexpected error: %v", err) + } + } +}