diff --git a/symbolic/constant_vector.go b/symbolic/constant_vector.go index a2d36f1..63414f2 100644 --- a/symbolic/constant_vector.go +++ b/symbolic/constant_vector.go @@ -307,6 +307,15 @@ func (kv KVector) Comparison(rightIn interface{}, sense ConstrSense) Constraint } switch rhsConverted := rightIn.(type) { + case float64: + var rhsAsVector mat.VecDense + onesVector := OnesVector(kv.Len()) + rhsAsVector.ScaleVec(rhsConverted, &onesVector) + return kv.Comparison(rhsAsVector, sense) + case int: + return kv.Comparison(float64(rhsConverted), sense) + case K: + return kv.Comparison(float64(rhsConverted), sense) case mat.VecDense: // Use KVector's Comparison method return kv.Comparison(VecDenseToKVector(rhsConverted), sense) @@ -612,6 +621,11 @@ func (kv KVector) ToPolynomialVector() PolynomialVector { return kv.ToMonomialVector().ToPolynomialVector() } +/* +ToKMatrix +Description: +*/ + /* Degree Description: diff --git a/symbolic/constr_sense.go b/symbolic/constr_sense.go index e34ee70..75d4bc0 100644 --- a/symbolic/constr_sense.go +++ b/symbolic/constr_sense.go @@ -10,8 +10,8 @@ type ConstrSense byte // Different constraint senses conforming to Gurobi's encoding. const ( SenseEqual ConstrSense = '=' - SenseLessThanEqual = '<' - SenseGreaterThanEqual = '>' + SenseLessThanEqual ConstrSense = '<' + SenseGreaterThanEqual ConstrSense = '>' ) func (cs ConstrSense) String() string { diff --git a/symbolic/constraint.go b/symbolic/constraint.go index 2caa3c2..e8a7e7f 100644 --- a/symbolic/constraint.go +++ b/symbolic/constraint.go @@ -1,5 +1,7 @@ package symbolic +import "fmt" + /* constraint.go Description: @@ -27,6 +29,10 @@ type Constraint interface { // AsSimplifiedConstraint // Simplifies the constraint by moving all variables to the left hand side and the constants to the right. AsSimplifiedConstraint() Constraint + + // // IsPositivityConstraint + // // Returns true if the constraint defines a non-negativity constraint of the form x >= 0 + // IsNonnegativityConstraint() bool } func IsConstraint(c interface{}) bool { @@ -83,3 +89,44 @@ func VariablesInThisConstraint(c Constraint) []Variable { return vars } + +/* +CompileConstraintsIntoScalarConstraints +Description: + + This method analyzes all constraints in an OptimizationProblem and converts them all + into scalar constraints. +*/ +func CompileConstraintsIntoScalarConstraints(constraints []Constraint) []ScalarConstraint { + // Setup + var out []ScalarConstraint + + // Iterate through all constraints + for _, constraint := range constraints { + // Switch statement based on the type of the constraint + switch concreteConstraint := constraint.(type) { + case ScalarConstraint: + out = append(out, concreteConstraint) + case VectorConstraint: + for ii := 0; ii < concreteConstraint.Len(); ii++ { + out = append(out, concreteConstraint.AtVec(ii)) + } + case MatrixConstraint: + dims := concreteConstraint.Dims() + for rowIdx := 0; rowIdx < dims[0]; rowIdx++ { + for colIdx := 0; colIdx < dims[1]; colIdx++ { + out = append(out, concreteConstraint.At(rowIdx, colIdx)) + } + } + default: + panic( + fmt.Errorf( + "The received constraint type (%T) is not supported by ExtractScalarConstraints!", + constraint, + ), + ) + } + } + + return out +} diff --git a/symbolic/monomial.go b/symbolic/monomial.go index ac1440b..07b7eb1 100644 --- a/symbolic/monomial.go +++ b/symbolic/monomial.go @@ -201,6 +201,8 @@ func (m Monomial) Multiply(e interface{}) Expression { switch right := e.(type) { case float64: return m.Multiply(K(right)) + case int: + return m.Multiply(K(float64(right))) case K: rightAsFloat64 := float64(right) monomialOut := m @@ -339,6 +341,8 @@ func (m Monomial) Comparison(rhsIn interface{}, sense ConstrSense) Constraint { switch right := rhsIn.(type) { case float64: return m.Comparison(K(right), sense) + case int: + return m.Comparison(K(float64(right)), sense) case K: return ScalarConstraint{m, right, sense} case Variable: diff --git a/symbolic/polynomial.go b/symbolic/polynomial.go index 9bd2538..9e2b3a7 100644 --- a/symbolic/polynomial.go +++ b/symbolic/polynomial.go @@ -466,6 +466,8 @@ func (p Polynomial) Comparison(rightIn interface{}, sense ConstrSense) Constrain switch right := rightIn.(type) { case float64: return p.Comparison(K(right), sense) + case int: + return p.Comparison(float64(right), sense) case K: return ScalarConstraint{p, right, sense} case Variable: diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index 70f860e..6177ea1 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -285,7 +285,7 @@ func (sc ScalarConstraint) String() string { } /* -Simplify +AsSimplifiedConstraint Description: Simplifies the constraint by moving all variables to the left hand side and the constants to the right. @@ -445,3 +445,63 @@ func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { return false } + +/* +IsNonnegativityConstraint +Description: + + Checks to see if the constraint is of the form: + - x >= 0, or + - 0 <= x +*/ +func (sc ScalarConstraint) IsNonnegativityConstraint() bool { + // Setup + err := sc.Check() + if err != nil { + panic(err) + } + + simplified := sc.AsSimplifiedConstraint().(ScalarConstraint) + + // Check to see if constraint contains more than 1 variable + if len(simplified.Variables()) != 1 { + return false + } + + // Otherwise, the sense is SenseGreaterThanEqual, and this is a non-negativity + // constraint if: + // - LHS is Variable-Like + // - RHS is Zero + + lhsIsVariableLike := false + + // LHS Is Variable Like if: + // - It is a variable + // - It is a monomial with a positive coefficient + simplifiedAsPL, tf := simplified.LeftHandSide.(PolynomialLikeScalar) + if !tf { + return false // If lhs is not polynomial like, then return false. + } + + lhsIsVariableLike = simplifiedAsPL.Degree() == 1 + + if !lhsIsVariableLike { + return false // If lhs is still not variable like, then return false. + } + + // Check to see if rhs is zero + rhsAsK := simplified.RightHandSide.(K) + rhsIsZero := float64(rhsAsK) == 0 + + if !rhsIsZero { + return false + } + + // Finally, the constraint is non-negativie if: + // - LHS has positive coefficient AND sense is GreaterThanEqual + // - LHS has negative coefficient AND sense is LessThanEqual + coeffs := simplified.LeftHandSide.LinearCoeff() + + return (coeffs.AtVec(0) > 0 && simplified.Sense == SenseGreaterThanEqual) || + (coeffs.AtVec(0) < 0 && simplified.Sense == SenseLessThanEqual) +} diff --git a/symbolic/variable.go b/symbolic/variable.go index 9aad657..5188659 100644 --- a/symbolic/variable.go +++ b/symbolic/variable.go @@ -232,6 +232,9 @@ func (v Variable) Comparison(rhsIn interface{}, sense ConstrSense) Constraint { case float64: // Use version of comparison for K return v.Comparison(K(rhs), sense) + case int: + // Use version of comparison for K + return v.Comparison(K(float64(rhs)), sense) case K: // Create a new constraint return ScalarConstraint{v, rhs, sense} diff --git a/testing/symbolic/constant_vector_test.go b/testing/symbolic/constant_vector_test.go index 9a77da8..96a5cf8 100644 --- a/testing/symbolic/constant_vector_test.go +++ b/testing/symbolic/constant_vector_test.go @@ -720,6 +720,154 @@ func TestConstantVector_Comparison1(t *testing.T) { kv1.Comparison(input, symbolic.SenseEqual) } +/* +TestConstantVector_Comparison2 +Description: + + Verifies that the Comparison method produces a proper + constraint when the left-hand side is a KVector and + the right-hand side is a float64. + The resulting compmarison should have a right hand + side which is a KVector. +*/ +func TestConstantVector_Comparison2(t *testing.T) { + // Constants + kv1 := symbolic.VecDenseToKVector(symbolic.OnesVector(3)) + var input float64 = 3.14 + + // Test + constraint := kv1.Comparison(input, symbolic.SenseEqual) + + // Verify that the left hand side is of type KVector + if _, tf := constraint.Left().(symbolic.KVector); !tf { + t.Errorf( + "Expected constraint.LeftHandSide to be of type KVector; received %v", + constraint.Left(), + ) + } + + // Verify that the right hand side is of type KVector + if _, tf := constraint.Right().(symbolic.KVector); !tf { + t.Errorf( + "Expected constraint.RightHandSide to be of type KVector; received %v", + constraint.Right(), + ) + } + + // Verify that the sense of the constraint is Equal + if constraint.ConstrSense() != symbolic.SenseEqual { + t.Errorf( + "Expected constraint.Sense to be Equal; received %v", + constraint.ConstrSense(), + ) + } +} + +/* +TestConstantVector_Comparison3 +Description: + + Verifies that the Comparison method produces a proper + constraint when the left-hand side is a KVector and + the right-hand side is a int. + The resulting compmarison should have a right hand + side which is a KVector. +*/ +func TestConstantVector_Comparison3(t *testing.T) { + // Constants + N := 3 + kv1 := symbolic.VecDenseToKVector(symbolic.OnesVector(N)) + var input int = 3 + + // Test + constraint := kv1.Comparison(input, symbolic.SenseEqual) + + // Verify that the left hand side is of type KVector + if _, tf := constraint.Left().(symbolic.KVector); !tf { + t.Errorf( + "Expected constraint.LeftHandSide to be of type KVector; received %v", + constraint.Left(), + ) + } + + // Verify that the right hand side is of type KVector + kv, tf := constraint.Right().(symbolic.KVector) + if !tf { + t.Errorf( + "Expected constraint.RightHandSide to be of type KVector; received %v", + constraint.Right(), + ) + } + + // Verify that the length of the right hand side is N + if kv.Len() != N { + t.Errorf( + "Expected constraint.RightHandSide to have length %d; received %d", + N, kv.Len(), + ) + } + + // Verify that the sense of the constraint is Equal + if constraint.ConstrSense() != symbolic.SenseEqual { + t.Errorf( + "Expected constraint.Sense to be Equal; received %v", + constraint.ConstrSense(), + ) + } +} + +/* +TestConstantVector_Comparison4 +Description: + + Verifies that the Comparison method produces a proper + constraint when the left-hand side is a KVector and + the right-hand side is a symbolic.K. + The resulting compmarison should have a right hand + side which is a KVector. +*/ +func TestConstantVector_Comparison4(t *testing.T) { + // Constants + N := 3 + kv1 := symbolic.VecDenseToKVector(symbolic.OnesVector(N)) + var input symbolic.K = symbolic.K(3.0) + + // Test + constraint := kv1.Comparison(input, symbolic.SenseEqual) + + // Verify that the left hand side is of type KVector + if _, tf := constraint.Left().(symbolic.KVector); !tf { + t.Errorf( + "Expected constraint.LeftHandSide to be of type KVector; received %v", + constraint.Left(), + ) + } + + // Verify that the right hand side is of type KVector and has length N + kv, tf := constraint.Right().(symbolic.KVector) + if !tf { + t.Errorf( + "Expected constraint.RightHandSide to be of type KVector; received %v", + constraint.Right(), + ) + } + + if kv.Len() != N { + t.Errorf( + "Expected constraint.RightHandSide to have length %d; received %d", + N, kv.Len(), + ) + } + + // Verify that the sense of the constraint is Equal + if constraint.ConstrSense() != symbolic.SenseEqual { + t.Errorf( + "Expected constraint.Sense to be Equal; received %v", + constraint.ConstrSense(), + ) + } +} + /* TestConstantVector_Multiply1 Description: diff --git a/testing/symbolic/constraint_test.go b/testing/symbolic/constraint_test.go index 359d0db..3e94bcd 100644 --- a/testing/symbolic/constraint_test.go +++ b/testing/symbolic/constraint_test.go @@ -216,3 +216,71 @@ func TestConstraint_VariablesInThisConstraint1(t *testing.T) { ) } } + +/* +TestConstraint_CompileConstraintsIntoScalarConstraints1 +Description: + + This test verifies that a single vector constraint + of length 3 is transformed into 3 constraints. +*/ +func TestConstraint_CompileConstraintsIntoScalarConstraints1(t *testing.T) { + // Setup + N := 3 + x := symbolic.NewVariableVector(N) + + // Create Vector Inequality Constraint + vectorConstraint := x.LessEq(symbolic.OnesVector(N)) + + // Decompose + constraints := symbolic.CompileConstraintsIntoScalarConstraints( + []symbolic.Constraint{vectorConstraint}, + ) + if len(constraints) != N { + t.Errorf( + "Expected to find %v ScalarConstraints; discovered %v", + N, + len(constraints), + ) + } + +} + +/* +TestConstraint_CompileConstraintsIntoScalarConstraints2 +Description: + + This test verifies that compiling a slice of: + - one ScalarConstraint + - one matrix constraint of size 4x4, and + - one vector constraint of length 2 + gets compiled into 19 = 1 + 16 + 2. +*/ +func TestConstraint_CompileConstraintsIntoScalarConstraints2(t *testing.T) { + // Setup + N := 4 + x := symbolic.NewVariableMatrix(N, N) + M := 2 + y := symbolic.NewVariableVector(M) + z := symbolic.NewVariable() + + // Create Matrix Inequality Constraint + matrixConstraint := x.LessEq(symbolic.OnesMatrix(N, N)) + + // Create Vector Inequality Constraint + vectorConstraint := y.LessEq(symbolic.OnesVector(M)) + + // Decompose + inputConstraints := []symbolic.Constraint{matrixConstraint, vectorConstraint, z.LessEq(-3.0)} + constraints := symbolic.CompileConstraintsIntoScalarConstraints( + inputConstraints, + ) + if len(constraints) != N*N+M+1 { + t.Errorf( + "Expected to find %v ScalarConstraints; discovered %v", + N*N+M+1, + len(constraints), + ) + } + +} diff --git a/testing/symbolic/scalar_constraint_test.go b/testing/symbolic/scalar_constraint_test.go index 9b5238d..6321d33 100644 --- a/testing/symbolic/scalar_constraint_test.go +++ b/testing/symbolic/scalar_constraint_test.go @@ -2000,3 +2000,264 @@ func TestScalarConstraint_AsSimplifiedConstraint1(t *testing.T) { ) } } + +/* +TestScalarConstraint_IsNonnegativityConstraint1 +Description: + + Tests the IsNonnegativityConstraint() method of a scalar constraint. + This test verifies that the method correctly identifies a non-negativity + constraint of the form: x >= 0. +*/ +func TestScalarConstraint_IsNonnegativityConstraint1(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := x.GreaterEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as a non-negativity constraint + if !sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be true; received false", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint2 +Description: + + This test verifies that the new method panics if the + input ScalarConstraint is not well-defined. +*/ +func TestScalarConstraint_IsNonnegativityConstraint2(t *testing.T) { + // Setup + x := symbolic.NewVariable() + + // Create a malformed monomial + m1 := symbolic.Monomial{ + Coefficient: 2, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x}, + } + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: symbolic.K(0), + Sense: symbolic.SenseGreaterThanEqual, + } + + // Verify that the IsNonnegativityConstraint() method + // panics + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected IsNonnegativityConstraint() to panic; received no panic", + ) + } + + err, ok := r.(error) + if !ok { + t.Errorf( + "Expected IsNonnegativityConstraint() to panic with an error; received %v", + r, + ) + } + + if err.Error() != m1.Check().Error() { + t.Errorf( + "Expected IsNonnegativityConstraint() to panic with error %v; received %v", + m1.Check().Error(), + err.Error(), + ) + } + }() + + sc.IsNonnegativityConstraint() + t.Errorf("Expected IsNonnegativityConstraint() to panic; received no panic") +} + +/* +TestScalarConstraint_IsNonnegativityConstraint3 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly + returns false when the scalar constraint contains more than one variable. +*/ +func TestScalarConstraint_IsNonnegativityConstraint3(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + + // Create constraint + constr := x.Plus(y).GreaterEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as NOT a non-negativity constraint + if sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be false; received true", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint4 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly identifies + that a non-positivity constraint (i.e., x <= 0) is NOT a non-negativity constraint. +*/ +func TestScalarConstraint_IsNonnegativityConstraint4(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := x.LessEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as NOT a non-negativity constraint + if sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be false; received true", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint5 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly identifies + a constraint of the form 2 * x >= 0 IS ALSO A non-negativity constraint. +*/ +func TestScalarConstraint_IsNonnegativityConstraint5(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := symbolic.K(2).Multiply(x).GreaterEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as a non-negativity constraint + if !sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be true; received false", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint6 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly + identifies that a constraint of the form x >= 3 is NOT a non-negativity constraint. +*/ +func TestScalarConstraint_IsNonnegativityConstraint6(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := x.GreaterEq(3) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as NOT a non-negativity constraint + if sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be false; received true", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint7 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly identifies + that a constraint of the form - x <= 0 IS a non-negativity constraint. +*/ +func TestScalarConstraint_IsNonnegativityConstraint7(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := symbolic.K(-1).Multiply(x).LessEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as a non-negativity constraint + if !sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be true; received false", + ) + } +} + +/* +TestScalarConstraint_IsNonnegativityConstraint8 +Description: + + This test verifies that the IsNonnegativityConstraint() method correctly identifies + that a constraint of the form x^2 >= 0 IS NOT a non-negativity constraint. +*/ +func TestScalarConstraint_IsNonnegativityConstraint8(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + constr := x.Power(2).GreaterEq(0) + sc, ok := constr.(symbolic.ScalarConstraint) + if !ok { + t.Errorf( + "Expected constr to be a symbolic.ScalarConstraint; received %T", + constr, + ) + } + + // Verify that the constraint is identified as NOT a non-negativity constraint + if sc.IsNonnegativityConstraint() { + t.Errorf( + "Expected sc.IsNonnegativityConstraint() to be false; received true", + ) + } +}