From ef35c12947ae2313d067e9f253c107135a10aabf Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Sun, 20 Jul 2025 00:52:20 -0400 Subject: [PATCH 01/10] Introduced a few new methods for constraints --- symbolic/constraint.go | 47 +++ symbolic/matrix_constraint.go | 76 ++++ symbolic/scalar_constraint.go | 161 ++++++++ symbolic/variable.go | 8 + symbolic/vector_constraint.go | 83 ++++ testing/symbolic/matrix_constraint_test.go | 52 +++ testing/symbolic/scalar_constraint_test.go | 446 +++++++++++++++++++++ 7 files changed, 873 insertions(+) diff --git a/symbolic/constraint.go b/symbolic/constraint.go index e375fd1..2caa3c2 100644 --- a/symbolic/constraint.go +++ b/symbolic/constraint.go @@ -15,6 +15,18 @@ type Constraint interface { IsLinear() bool Substitute(vIn Variable, seIn ScalarExpression) Constraint SubstituteAccordingTo(subMap map[Variable]Expression) Constraint + + // Variables + // Returns a slice of all the variables in the constraint. + Variables() []Variable + + // ImpliesThisIsAlsoSatisfied + // Returns true if this constraint implies that the other constraint is also satisfied. + ImpliesThisIsAlsoSatisfied(other Constraint) bool + + // AsSimplifiedConstraint + // Simplifies the constraint by moving all variables to the left hand side and the constants to the right. + AsSimplifiedConstraint() Constraint } func IsConstraint(c interface{}) bool { @@ -36,3 +48,38 @@ func IsConstraint(c interface{}) bool { // Return false, if the constraint is not a scalar or vector constraint. return false } + +/* +Variables +Description: + + Returns a slice of all the variables in the constraint. +*/ +func VariablesInThisConstraint(c Constraint) []Variable { + // Setup + varsMap := make(map[Variable]bool) + + // Input check + err := c.Check() + if err != nil { + panic(err) + } + + // Get variables from the left hand side + for _, v := range c.Left().Variables() { + varsMap[v] = true + } + + // Get variables from the right hand side + for _, v := range c.Right().Variables() { + varsMap[v] = true + } + + // Convert the map to a slice + vars := make([]Variable, 0, len(varsMap)) + for v := range varsMap { + vars = append(vars, v) + } + + return vars +} diff --git a/symbolic/matrix_constraint.go b/symbolic/matrix_constraint.go index 0715ef9..ffe18f4 100644 --- a/symbolic/matrix_constraint.go +++ b/symbolic/matrix_constraint.go @@ -191,3 +191,79 @@ func (mc MatrixConstraint) SubstituteAccordingTo(subMap map[Variable]Expression) return MatrixConstraint{newLHS, newRHS, mc.Sense} } + +/* +AsSimplifiedConstraint +Description: + + Simplifies the constraint by moving all variables to the left hand side and the constants to the right. +*/ +func (mc MatrixConstraint) AsSimplifiedConstraint() Constraint { + // Create Left Hand side of all of the expressions + var newLHS Expression = mc.LeftHandSide.Minus(mc.LeftHandSide.Constant()) + newLHS = newLHS.Minus( + mc.RightHandSide.Minus(mc.RightHandSide.Constant()), + ) + + // Create Right Hand Side of only constants + var newRHS Expression = DenseToKMatrix(mc.RightHandSide.Constant()).Minus( + mc.LeftHandSide.Constant(), + ) + + // Return new constraint + return MatrixConstraint{ + LeftHandSide: newLHS.(MatrixExpression), + RightHandSide: newRHS.(MatrixExpression), + Sense: mc.Sense, + } +} + +/* +Variables +Description: + + Returns a slice of all the variables in the constraint. +*/ +func (mc MatrixConstraint) Variables() []Variable { + return VariablesInThisConstraint(mc) +} + +/* +ImpliesThisIsAlsoSatisfied +Description: + + Returns true if this constraint implies that the other constraint is also satisfied. +*/ +func (mc MatrixConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { + // Input Processing + err := mc.Check() + if err != nil { + panic(err) + } + + err = other.Check() + if err != nil { + panic(err) + } + + // Implication Avenues + switch otherC := other.(type) { + case ScalarConstraint: + // If the other constraint is a scalar constraint, + // then it can only be implied if: + // - one of the elements of the matrix constraint implies the scalar constraint. + for i := 0; i < mc.Dims()[0]; i++ { + for j := 0; j < mc.Dims()[1]; j++ { + if mc.At(i, j).ImpliesThisIsAlsoSatisfied(otherC) { + return true + } + } + } + default: + // Other types of constraints are not currently supported. + return false + } + + // If no avenues for implication were found, return false. + return false +} diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index 18cffbb..545fa9f 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -1,6 +1,8 @@ package symbolic import ( + "fmt" + "github.com/MatProGo-dev/SymbolicMath.go/smErrors" "gonum.org/v1/gonum/mat" ) @@ -281,3 +283,162 @@ func (sc ScalarConstraint) String() string { // Create the string representation return sc.LeftHandSide.String() + " " + sc.Sense.String() + " " + sc.RightHandSide.String() } + +/* +Simplify +Description: + + Simplifies the constraint by moving all variables to the left hand side and the constants to the right. +*/ +func (sc ScalarConstraint) AsSimplifiedConstraint() Constraint { + return sc.Simplify() +} + +func (sc ScalarConstraint) Variables() []Variable { + return VariablesInThisConstraint(sc) +} + +func (sc ScalarConstraint) ScaleBy(factor float64) Constraint { + // Check that the constraint is well formed. + err := sc.Check() + if err != nil { + panic(err) + } + + // Scale the left hand side + newLHS := sc.LeftHandSide.Multiply(factor).(ScalarExpression) + + // Scale the right hand side + newRHS := sc.RightHandSide.Multiply(factor).(ScalarExpression) + + // If the factor is negative, then flip the sense of the constraint + newSense := sc.Sense + if factor < 0 { + if sc.Sense == SenseLessThanEqual { + newSense = SenseGreaterThanEqual + } else if sc.Sense == SenseGreaterThanEqual { + newSense = SenseLessThanEqual + } + } + + // Return the new constraint + return ScalarConstraint{ + LeftHandSide: newLHS, + RightHandSide: newRHS, + Sense: newSense, + } +} + +/* +ImpliesThisIsAlsoSatisfied +Description: + + Returns true if this constraint implies that the other constraint is also satisfied. +*/ +func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { + // Check that the constraint is well formed. + err := sc.Check() + if err != nil { + panic(err) + } + + // Check that the other constraint is well formed. + err = other.Check() + if err != nil { + panic(err) + } + + // Simplify both constraints + sc = sc.Simplify() + other = other.AsSimplifiedConstraint().(ScalarConstraint) + + switch otherC := other.(type) { + case ScalarConstraint: + // Continue + // Naive implication check: + // 1. Both constraints contain only 1 variable AND it is the same variable. Then, simply check the bounds. + containsOneVar := len(sc.Variables()) == 1 && len(otherC.Variables()) == 1 + scAndOtherShareSameVar := len(UnionOfVariables(sc.Variables(), otherC.Variables())) == 1 + + if containsOneVar && scAndOtherShareSameVar { + // Get the coefficient of the single variable + scCoeffVector := sc.LeftHandSide.LinearCoeff(sc.Variables()) + scCoeff := scCoeffVector.AtVec(0) + otherCCoeffVector := otherC.RightHandSide.LinearCoeff(otherC.Variables()) + otherCCoeff := otherCCoeffVector.AtVec(0) + + // If the coefficient of scCoeff is < 0, + // then flip the signs of both sides of the constraint + if scCoeff < 0 { + sc = sc.ScaleBy(-1).(ScalarConstraint) + } + + if otherCCoeff < 0 { + otherC = otherC.ScaleBy(-1).(ScalarConstraint) + } + + // The implication holds if all of the following are true: + // 1. The sense of sc and otherC are either the same (or one is equality) + // 2. The bounds of the constraint with the LessThanEqual or GreaterThanEqual sense are within the bounds of the other constraint. + sensesAreCompatible := sc.Sense == otherC.Sense || + sc.Sense == SenseEqual || + otherC.Sense == SenseEqual + + if !sensesAreCompatible { + return false + } + + switch sc.Sense { + case SenseLessThanEqual: + // Check the senses of otherC + switch otherC.Sense { + case SenseLessThanEqual: + // Both are <= + // Then the implication holds if the upper bound of sc is <= the upper bound of otherC + return sc.RightHandSide.Constant() <= otherC.RightHandSide.Constant() + default: + // sc is <= and otherC is either >= or == + // Then the implication holds if the upper bound of sc is <= the lower bound of otherC + return false + } + case SenseGreaterThanEqual: + // Check the senses of otherC + switch otherC.Sense { + case SenseGreaterThanEqual: + // Both are >= + // Then the implication holds if the lower bound of sc is >= the lower bound of otherC + return sc.RightHandSide.Constant() >= otherC.RightHandSide.Constant() + default: + // sc is >= and otherC is either <= or == + // Then the implication holds if the lower bound of sc is >= the upper bound of otherC + return false + } + case SenseEqual: + // Check the senses of otherC + switch otherC.Sense { + case SenseEqual: + // Both are == + // Then the implication holds if the bounds are equal + return sc.RightHandSide.Constant() == otherC.RightHandSide.Constant() + case SenseLessThanEqual: + // sc is == and otherC is <= + // Then the implication holds if the bound of sc is <= the upper bound of otherC + return sc.RightHandSide.Constant() <= otherC.RightHandSide.Constant() + case SenseGreaterThanEqual: + // sc is == and otherC is >= + // Then the implication holds if the bound of sc is >= the lower bound of otherC + return sc.RightHandSide.Constant() >= otherC.RightHandSide.Constant() + } + default: + panic("unreachable code reached in ScalarConstraint.ImpliesThisIsAlsoSatisfied") + } + } + default: + // Other types of constraints are not currently supported. + panic( + fmt.Errorf("implication checking between ScalarConstraint and %T is not currently supported", other), + ) + } + + return false +} diff --git a/symbolic/variable.go b/symbolic/variable.go index 5c888f0..f287348 100644 --- a/symbolic/variable.go +++ b/symbolic/variable.go @@ -650,3 +650,11 @@ func (v Variable) At(ii, jj int) ScalarExpression { // Algorithm return v } + +func UnionOfVariables(varSlices ...[]Variable) []Variable { + var allVars []Variable + for _, varSlice := range varSlices { + allVars = append(allVars, varSlice...) + } + return UniqueVars(allVars) +} diff --git a/symbolic/vector_constraint.go b/symbolic/vector_constraint.go index 1bd8bea..4f8deee 100644 --- a/symbolic/vector_constraint.go +++ b/symbolic/vector_constraint.go @@ -1,6 +1,8 @@ package symbolic import ( + "fmt" + "github.com/MatProGo-dev/SymbolicMath.go/smErrors" "gonum.org/v1/gonum/mat" ) @@ -316,3 +318,84 @@ func (vc VectorConstraint) Len() int { // Return the length of the vector constraint. return vc.LeftHandSide.Len() } + +/* +AsSimplifiedConstraint +Description: + + Simplifies the constraint by moving all variables to the left hand side and the constants to the right. +*/ +func (vc VectorConstraint) AsSimplifiedConstraint() Constraint { + // Input Checking + err := vc.Check() + if err != nil { + panic(err) + } + + // Create Left Hand side of all of the expressions + var newLHS Expression = vc.LeftHandSide.Minus(vc.LeftHandSide.Constant()) + newLHS = newLHS.Minus( + vc.RightHandSide.Minus(vc.RightHandSide.Constant()), + ) + + // Create Right Hand Side of only constants + var newRHS Expression = VecDenseToKVector(vc.RightHandSide.Constant()).Minus( + vc.LeftHandSide.Constant(), + ) + + // Return new constraint + return VectorConstraint{ + LeftHandSide: newLHS.(VectorExpression), + RightHandSide: newRHS.(VectorExpression), + Sense: vc.Sense, + } +} + +/* +Variables +Description: + + Returns a slice of all the variables in the constraint. +*/ +func (vc VectorConstraint) Variables() []Variable { + return VariablesInThisConstraint(vc) +} + +/* +ImpliesThisIsAlsoSatisfied +Description: + + Returns true if this constraint implies that the other constraint is also satisfied. +*/ +func (vc VectorConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { + // Input Processing + err := vc.Check() + if err != nil { + panic(err) + } + + // Check the other constraint + err = other.Check() + if err != nil { + panic(err) + } + + switch otherC := other.(type) { + case ScalarConstraint: + // Continue + // Naive implication check: + // 1. One of the scalar constraints produces the correct implications. + for i := 0; i < vc.Len(); i++ { + if vc.AtVec(i).ImpliesThisIsAlsoSatisfied(otherC) { + return true + } + } + default: + // Other types of constraints are not currently supported. + panic( + fmt.Errorf("implication checking between VectorConstraint and %T is not currently supported", other), + ) + } + + return false +} diff --git a/testing/symbolic/matrix_constraint_test.go b/testing/symbolic/matrix_constraint_test.go index b939c26..3ac6598 100644 --- a/testing/symbolic/matrix_constraint_test.go +++ b/testing/symbolic/matrix_constraint_test.go @@ -641,3 +641,55 @@ func TestMatrixConstraint_SubstituteAccordingTo2(t *testing.T) { ) } } + +/* +TestMatrixConstraint_AsSimplifiedConstraint1 +Description: + + This test verifies that AsSimplifiedConstraint() properly panics + whenever a malformed MatrixConstraint is used to call it. +*/ +func TestMatrixConstraint_AsSimplifiedConstraint1(t *testing.T) { + // Create constraint + left := symbolic.MonomialMatrix{} + right := symbolic.DenseToKMatrix(symbolic.ZerosMatrix(3, 4)) + mc := symbolic.MatrixConstraint{left, right, symbolic.SenseLessThanEqual} + + expectedError := left.Check() + + // Test + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected mc.SubstituteAccordingTo() to panic; did not panic", + ) + } + + // Check that the error is the expected error + err, ok := r.(error) + if !ok { + t.Errorf( + "Expected mc.SubstituteAccordingTo() to panic with type error; received %T", + r, + ) + } + + if err.Error() != expectedError.Error() { + t.Errorf( + "Expected mc.SubstituteAccordingTo() to panic with error \"%v\"; received \"%v\"", + expectedError, + err, + ) + } + + }() + + // Call AsSimplifiedConstraint() + mc.AsSimplifiedConstraint() + + t.Errorf( + "Expected mc.AsSimplifiedConstraint() to panic; did not panic", + ) + +} diff --git a/testing/symbolic/scalar_constraint_test.go b/testing/symbolic/scalar_constraint_test.go index 7343aca..ecc3ad5 100644 --- a/testing/symbolic/scalar_constraint_test.go +++ b/testing/symbolic/scalar_constraint_test.go @@ -1257,3 +1257,449 @@ func TestScalarConstraint_String1(t *testing.T) { ) } } + +/* +TestScalarConstraint_ScaleBy1 +Description: + + This tests that the ScalarConstraint.ScaleBy() method properly panics + when the left hand side is not a valid monomial. +*/ +func TestScalarConstraint_ScaleBy1(t *testing.T) { + // Constants + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{}, + } + v2 := symbolic.NewVariable() + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: v2, + Sense: symbolic.SenseLessThanEqual, + } + + // Create the panic handling function + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected sc.ScaleBy() to panic; received nil", + ) + } + + rAsError := r.(error) + expectedError := m1.Check() + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected sc.ScaleBy() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + sc.ScaleBy(2) + + t.Errorf( + "Expected sc.ScaleBy() to panic; received nil", + ) +} + +/* +TestScalarConstraint_ScaleBy2 +Description: + + This tests that the ScalarConstraint.ScaleBy() method properly + returns a new ScalarConstraint when the left hand side is a valid monomial. + In this case, we scale by a negative number, which should lead to a new + constraint with the opposite sense. +*/ +func TestScalarConstraint_ScaleBy2(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x, y}, + } + c2 := symbolic.K(3.14) + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: c2, + Sense: symbolic.SenseLessThanEqual, + } + + // Scale + newSc := sc.ScaleBy(-2) + + // Verify that the new left hand side is a monomial + if _, ok := newSc.Left().(symbolic.Monomial); !ok { + t.Errorf( + "Expected newSc.LeftHandSide to be a symbolic.Monomial; received %T", + newSc.Left(), + ) + } + + // Verify that the new right hand side is a constant 6.28 + m2, ok := newSc.Right().(symbolic.K) + if !ok { + t.Errorf( + "Expected newSc.RightHandSide to be a symbolic.K; received %T", + newSc.Right(), + ) + } + + // Verify that the new right hand side is a constant 6.28 + if float64(m2) != -6.28 { + t.Errorf( + "Expected newSc.RightHandSide to be 6.28; received %v", + m2, + ) + } + + // Verify that the new constraint has the opposite sense + if newSc.ConstrSense() != symbolic.SenseGreaterThanEqual { + t.Errorf( + "Expected newSc.Sense to be different from %v; received %v", + sc.Sense, + newSc.ConstrSense(), + ) + } +} + +/* +TestScalarConstraint_ScaleBy3 +Description: + + This tests that the ScalarConstraint.ScaleBy() method properly + returns a new ScalarConstraint when the left hand side is a valid monomial. + In this case, we scale by a positive number, which should lead to a new + constraint with the same sense. +*/ +func TestScalarConstraint_ScaleBy3(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x, y}, + } + c2 := symbolic.K(3.14) + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: c2, + Sense: symbolic.SenseLessThanEqual, + } + + // Scale + newSc := sc.ScaleBy(2) + + // Verify that the new left hand side is a monomial + if _, ok := newSc.Left().(symbolic.Monomial); !ok { + t.Errorf( + "Expected newSc.LeftHandSide to be a symbolic.Monomial; received %T", + newSc.Left(), + ) + } + + // Verify that the new right hand side is a constant 6.28 + m2, ok := newSc.Right().(symbolic.K) + if !ok { + t.Errorf( + "Expected newSc.RightHandSide to be a symbolic.K; received %T", + newSc.Right(), + ) + } + + // Verify that the new right hand side is a constant 6.28 + if float64(m2) != 6.28 { + t.Errorf( + "Expected newSc.RightHandSide to be 6.28; received %v", + m2, + ) + } + + // Verify that the new constraint has the same sense + if newSc.ConstrSense() != sc.Sense { + t.Errorf( + "Expected newSc.Sense to be %v; received %v", + sc.Sense, + newSc.ConstrSense(), + ) + } +} + +/* +TestScalarConstraint_Variables1 +Description: + + Tests the Variables() method of a scalar constraint. This test verifies + that the method properly returns a slice of all variables in the constraint. + We will create a constraint with 2 variables (one in a monomial on the left + and a constant on the right). +*/ +func TestScalarConstraint_Variables1(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x, y}, + } + c2 := symbolic.K(3.14) + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: c2, + Sense: symbolic.SenseLessThanEqual, + } + + // Verify variables + vars := sc.Variables() + if len(vars) != 2 { + t.Errorf( + "Expected 2 variables; received %d", + len(vars), + ) + } +} + +/* +TestScalarConstraint_Variables2 +Description: + + Tests the Variables() method of a scalar constraint. This test verifies + that the method properly panics when the left hand side is not a valid monomial. +*/ +func TestScalarConstraint_Variables2(t *testing.T) { + // Constants + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{}, + } + v2 := symbolic.NewVariable() + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: v2, + Sense: symbolic.SenseLessThanEqual, + } + + // Create the panic handling function + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected sc.Variables() to panic; received nil", + ) + } + + rAsError := r.(error) + expectedError := m1.Check() + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected sc.Variables() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + sc.Variables() + + t.Errorf( + "Expected sc.Variables() to panic; received nil", + ) +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied1 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly panics when the given + scalar constraint is not valid (in this case, the left hand side is not + a valid monomial). +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied1(t *testing.T) { + // Constants + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{}, + } + v2 := symbolic.NewVariable() + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: v2, + Sense: symbolic.SenseLessThanEqual, + } + + // Create a second constraint + sc2 := v2.GreaterEq(symbolic.K(1)) + + // Create the panic handling function + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) + } + + rAsError := r.(error) + expectedError := m1.Check() + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + sc.ImpliesThisIsAlsoSatisfied(sc2) + + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied2 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly panics when the constraint + given as an argument is not valid (in this case, the left hand side is not + a valid monomial). +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied2(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x, y}, + } + c2 := symbolic.K(3.14) + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: c2, + Sense: symbolic.SenseLessThanEqual, + } + + // Create a second constraint + m2 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{}, + } + v2 := symbolic.NewVariable() + sc2 := symbolic.ScalarConstraint{ + LeftHandSide: m2, + RightHandSide: v2, + Sense: symbolic.SenseGreaterThanEqual, + } + + // Create the panic handling function + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) + } + + rAsError := r.(error) + expectedError := m2.Check() + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + sc.ImpliesThisIsAlsoSatisfied(sc2) + + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied3 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly returns false when the + constraint does not imply the other constraint. In this case, we have + x + y <= 3 and y >= 1. The first constraint does NOT imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + c2 := symbolic.K(3.0) + + // Create constraint + sc := x.Plus(y).LessEq(c2) + + // Create a second constraint + sc2 := y.GreaterEq(symbolic.K(1)) + + // Verify that the first constraint does NOT imply the second + if sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be false; received true", + ) + } +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied4 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly returns true when the + constraint DOES imply the other constraint AND they are both single-variable + constraints. In this case, we have + x <= 3 and x <= 4. The first constraint DOES imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied4(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.LessEq(3.0) + + // Create a second constraint + sc2 := x.LessEq(4.0) + + // Verify that the first constraint DOES imply the second + if !sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be true; received false", + ) + } +} From 057f4fe1a8ac0ff74106f0b05d8e0f8e58beb7a0 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Sun, 20 Jul 2025 01:01:24 -0400 Subject: [PATCH 02/10] Added a few more tests to increase coverage of the implication method for ScalarConstraint --- testing/symbolic/scalar_constraint_test.go | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/testing/symbolic/scalar_constraint_test.go b/testing/symbolic/scalar_constraint_test.go index ecc3ad5..3b841f4 100644 --- a/testing/symbolic/scalar_constraint_test.go +++ b/testing/symbolic/scalar_constraint_test.go @@ -1703,3 +1703,87 @@ func TestScalarConstraint_ImpliesThisIsAlsoSatisfied4(t *testing.T) { ) } } + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied5 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly returns true when the + constraint DOES imply the other constraint AND they are both single-variable + constraints. In this case, we have + x >= 4 and x >= 3. The first constraint DOES imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied5(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.GreaterEq(4.0) + + // Create a second constraint + sc2 := x.GreaterEq(3.0) + + // Verify that the first constraint DOES imply the second + if !sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be true; received false", + ) + } +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied6 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly returns true when the + constraint DOES imply the other constraint AND they are both single-variable + constraints. In this case, we have + x = 3 and x <= 4. The first constraint DOES imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied6(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.Eq(3.0) + + // Create a second constraint + sc2 := x.LessEq(4.0) + + // Verify that the first constraint DOES imply the second + if !sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be true; received false", + ) + } +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied7 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly returns true when the + constraint DOES imply the other constraint AND they are both single-variable + constraints. In this case, we have + x = 3 and x >= 2. The first constraint DOES imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied7(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.Eq(3.0) + + // Create a second constraint + sc2 := x.GreaterEq(2.0) + + // Verify that the first constraint DOES imply the second + if !sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be true; received false", + ) + } +} From aacd0d84ac430b73892defab0e7aec7bb04042fb Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Sun, 20 Jul 2025 01:03:32 -0400 Subject: [PATCH 03/10] Adding a test for VariablesInThisConstraint to increase coverage --- testing/symbolic/constraint_test.go | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/testing/symbolic/constraint_test.go b/testing/symbolic/constraint_test.go index 2fd82ea..359d0db 100644 --- a/testing/symbolic/constraint_test.go +++ b/testing/symbolic/constraint_test.go @@ -7,8 +7,9 @@ Description: */ import ( - "github.com/MatProGo-dev/SymbolicMath.go/symbolic" "testing" + + "github.com/MatProGo-dev/SymbolicMath.go/symbolic" ) /* @@ -174,3 +175,44 @@ func TestConstraint_IsConstraint7(t *testing.T) { ) } } + +/* +TestConstraint_VariablesInThisConstraint1 +Description: + + Verifies that the VariablesInThisConstraint function works as expected. + We verify that the method properly returns 5 unique variables when + there are 3 variables on the left hand side and 3 variables on the right hand side, + but one of the variables is shared between the two sides. +*/ +func TestConstraint_VariablesInThisConstraint1(t *testing.T) { + // Constants + x1 := symbolic.NewVariable() + x2 := symbolic.NewVariable() + x3 := symbolic.NewVariable() + x4 := symbolic.NewVariable() + x5 := symbolic.NewVariable() + + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1, 2, 1}, + VariableFactors: []symbolic.Variable{x1, x2, x3}, + } + + m2 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{3, 1, 1}, + VariableFactors: []symbolic.Variable{x3, x4, x5}, + } + + sc := symbolic.ScalarConstraint{m1, m2, symbolic.SenseEqual} + + // Test + vars := symbolic.VariablesInThisConstraint(sc) + if len(vars) != 5 { + t.Errorf( + "Expected VariablesInThisConstraint to return 5 unique variables; received %d", + len(vars), + ) + } +} From 9dc9f8daf31d2c8816c57e0546566165a95b86b5 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Sun, 20 Jul 2025 01:13:29 -0400 Subject: [PATCH 04/10] Added more tests for coverage of new functions --- symbolic/scalar_constraint.go | 4 +- testing/symbolic/matrix_constraint_test.go | 180 +++++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index 545fa9f..0790497 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -350,11 +350,11 @@ func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { // Simplify both constraints sc = sc.Simplify() - other = other.AsSimplifiedConstraint().(ScalarConstraint) switch otherC := other.(type) { case ScalarConstraint: - // Continue + otherC = otherC.Simplify() + // Naive implication check: // 1. Both constraints contain only 1 variable AND it is the same variable. Then, simply check the bounds. containsOneVar := len(sc.Variables()) == 1 && len(otherC.Variables()) == 1 diff --git a/testing/symbolic/matrix_constraint_test.go b/testing/symbolic/matrix_constraint_test.go index 3ac6598..687a65c 100644 --- a/testing/symbolic/matrix_constraint_test.go +++ b/testing/symbolic/matrix_constraint_test.go @@ -693,3 +693,183 @@ func TestMatrixConstraint_AsSimplifiedConstraint1(t *testing.T) { ) } + +/* +TestMatrixConstraint_Variables1 +Description: + + Verifies that the Variables() method works as expected. + We verify that the method properly returns 9 unique variables when + we create a constraint between a 3x3 matrix of variables and a + 3x3 matrix of constants. +*/ +func TestMatrixConstraint_Variables1(t *testing.T) { + // Constants + vm1 := symbolic.NewVariableMatrix(3, 3) + km2 := symbolic.DenseToKMatrix(symbolic.OnesMatrix(3, 3)) + + mConstr := symbolic.MatrixConstraint{vm1, km2, symbolic.SenseLessThanEqual} + + // Test + vars := mConstr.Variables() + + if len(vars) != 9 { + t.Errorf( + "Expected len(vars) to be 9; received %v", + len(vars), + ) + } + // Check that each variable is unique + varsMap := make(map[symbolic.Variable]bool) + for _, v := range vars { + if _, ok := varsMap[v]; ok { + t.Errorf( + "Expected all variables to be unique; received duplicate variable %v", + v, + ) + } + varsMap[v] = true + } +} + +/* +TestMatrixConstraint_ImpliesThisIsAlsoSatisfied1 +Description: + + Verifies that the ImpliesThisIsAlsoSatisfied method works as expected + when the input constraint is a scalar constraint that matches one of + the constraints in the original matrix constraint (i.e., the scalar constraint + is just the (0,1)-th constraint in the original matrix constraint). +*/ +func TestMatrixConstraint_ImpliesThisIsAlsoSatisfied1(t *testing.T) { + // Constants + vm1 := symbolic.NewVariableMatrix(3, 3) + km2 := symbolic.DenseToKMatrix(symbolic.OnesMatrix(3, 3)) + + mConstr := symbolic.MatrixConstraint{vm1, km2, symbolic.SenseLessThanEqual} + + // Extract the (0,1)-th constraint from mConstr + scalarConstraint := mConstr.At(0, 1) + + // Test + if !mConstr.ImpliesThisIsAlsoSatisfied(scalarConstraint) { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(scalarConstraint) to be true; received false", + ) + } +} + +/* +TestMatrixConstraint_ImpliesThisIsAlsoSatisfied2 +Description: + + Verifies that the ImpliesThisIsAlsoSatisfied method correctly panics + when the input matrix constraint is malformed. +*/ +func TestMatrixConstraint_ImpliesThisIsAlsoSatisfied2(t *testing.T) { + // Constants + vm1 := symbolic.NewVariableMatrix(3, 2) + km2 := symbolic.DenseToKMatrix(symbolic.OnesMatrix(3, 3)) + + // Create matrix constraint + mConstr := symbolic.MatrixConstraint{vm1, km2, symbolic.SenseLessThanEqual} + + // Create normal matrix constraint to use as input + left := symbolic.DenseToKMatrix(symbolic.Identity(3)) + right := symbolic.DenseToKMatrix(symbolic.ZerosMatrix(3, 3)) + normalMC := symbolic.MatrixConstraint{left, right, symbolic.SenseLessThanEqual} + + expectedError := mConstr.Check() + + // Test + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(normalMC) to panic; did not panic", + ) + } + + // Check that the error is the expected error + err, ok := r.(error) + if !ok { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(normalMC) to panic with type error; received %T", + r, + ) + } + if err.Error() != expectedError.Error() { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(normalMC) to panic with error \"%v\"; received \"%v\"", + expectedError, + err, + ) + } + + }() + + // Call ImpliesThisIsAlsoSatisfied + mConstr.ImpliesThisIsAlsoSatisfied(normalMC) + + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(normalMC) to panic; did not panic", + ) +} + +/* +TestConstraint_ImpliesThisIsAlsoSatisfied3 +Description: + + Verifies that the ImpliesThisIsAlsoSatisfied method correctly panics + when the receiver is well-defined but the input constraint is not well-formed. +*/ +func TestConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { + // Constants + vm1 := symbolic.NewVariableMatrix(3, 3) + km2 := symbolic.DenseToKMatrix(symbolic.OnesMatrix(3, 3)) + + // Create matrix constraint + mConstr := symbolic.MatrixConstraint{vm1, km2, symbolic.SenseLessThanEqual} + + // Create malformed scalar constraint to use as input + scLeft := symbolic.Monomial{ + Exponents: []int{1}, + } + scRight := symbolic.K(5) + sc := symbolic.ScalarConstraint{scLeft, scRight, symbolic.SenseEqual} + expectedError := sc.Check() + + // Test + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(sc) to panic; did not panic", + ) + } + + // Check that the error is the expected error + err, ok := r.(error) + if !ok { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(sc) to panic with type error; received %T", + r, + ) + } + if err.Error() != expectedError.Error() { + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(sc) to panic with error \"%v\"; received \"%v\"", + expectedError, + err, + ) + } + + }() + + // Call ImpliesThisIsAlsoSatisfied + mConstr.ImpliesThisIsAlsoSatisfied(sc) + + t.Errorf( + "Expected mConstr.ImpliesThisIsAlsoSatisfied(sc) to panic; did not panic", + ) +} From 2f7d0ad0268d4668c9265c589afd2d92369f0eb9 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 21 Jul 2025 22:58:09 -0400 Subject: [PATCH 05/10] Added more tests to increase coverage of the VectorConstraint methods --- testing/symbolic/vector_constraint_test.go | 319 +++++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/testing/symbolic/vector_constraint_test.go b/testing/symbolic/vector_constraint_test.go index 2fee4d9..fb9be39 100644 --- a/testing/symbolic/vector_constraint_test.go +++ b/testing/symbolic/vector_constraint_test.go @@ -1040,3 +1040,322 @@ func TestVectorConstraint_SubstituteAccordingTo2(t *testing.T) { ) } } + +/* +TestVectorConstraint_AsSimplifiedConstraint1 +Description: + + This function tests that the AsSimplifiedConstraint method properly + panics if the input vector constraint is not well-defined. + (In this case, the left and right hand sides have different lengths.) +*/ +func TestVectorConstraint_AsSimplifiedConstraint1(t *testing.T) { + // Setup + N := 7 + left := symbolic.VecDenseToKVector(symbolic.OnesVector(N)) + right := symbolic.NewVariableVector(N + 1) + + // Create the vector constraint + vc := symbolic.VectorConstraint{left, right, symbolic.SenseLessThanEqual} + + // Create the function for handling the panic + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to panic; received nil", + ) + } + + rAsError := r.(error) + expectedError := smErrors.VectorDimensionError{ + Operation: fmt.Sprintf("Comparison (%v)", vc.Sense), + Arg1: vc.LeftHandSide, + Arg2: vc.RightHandSide, + } + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + // Test + vc.AsSimplifiedConstraint() + + // Raise an error if the test did not panic + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to panic; received nil", + ) + +} + +/* +TestVectorConstraint_AsSimplifiedConstraint2 +Description: + + This function tests that the AsSimplifiedConstraint method properly + returns a simplified vector constraint when the original vector constraint + is well-defined. In this case, the original vector constraint will have + a left hand side that is a vector of monomials and a right hand side + that is a vector of polynomials. + After simplification, the left hand side should be a vector of polynomials + and the right hand side should be a vector of constants. +*/ +func TestVectorConstraint_AsSimplifiedConstraint2(t *testing.T) { + // Setup + N := 7 + x := symbolic.NewVariableVector(N) + left := x.ToMonomialVector() + right := x.Multiply(2.0).Plus(symbolic.VecDenseToKVector(symbolic.OnesVector(N))).(symbolic.PolynomialVector) + + // Create the vector constraint + vc := symbolic.VectorConstraint{left, right, symbolic.SenseLessThanEqual} + + // Test + simplifiedVC := vc.AsSimplifiedConstraint().(symbolic.VectorConstraint) + + // Check the left hand side + // it should now be a vector of polynomials + if _, ok := simplifiedVC.LeftHandSide.(symbolic.PolynomialVector); !ok { + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to return a vector constraint with a polynomial vector on the left hand side; received %v", + simplifiedVC.LeftHandSide, + ) + } + + // Check the right hand side + if _, ok := simplifiedVC.RightHandSide.(symbolic.KVector); !ok { + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to return a vector constraint with a constant vector on the right hand side; received %v", + simplifiedVC.RightHandSide, + ) + } + + // Check the sense + if simplifiedVC.Sense != vc.Sense { + t.Errorf( + "Expected vc.AsSimplifiedConstraint() to return a vector constraint with the same sense; received %v", + simplifiedVC.Sense, + ) + } +} + +/* +TestVectorConstraint_Variables1 +Description: + + This function tests that the Variables method properly + returns the correct set of variables in a well-defined vector constraint. + In this case, the left hand side will be a vector of N variables + and the right hand side will be a vector of N different variables. + The result should be a set containing all 2N variables. +*/ +func TestVectorConstraint_Variables1(t *testing.T) { + // Setup + N := 7 + x := symbolic.NewVariableVector(N) + y := symbolic.NewVariableVector(N) + left := x + right := y + + // Create the vector constraint + vc := symbolic.VectorConstraint{left, right, symbolic.SenseLessThanEqual} + + // Test + vars := vc.Variables() + + // Check the length of the variable set + if len(vars) != 2*N { + t.Errorf( + "Expected vc.Variables() to return a set with %v variables; received %v", + 2*N, + len(vars), + ) + } + + // Check that all variables are present in the set + for i := 0; i < N; i++ { + idx, err := symbolic.FindInSlice(x[i], vars) + if err != nil { + t.Errorf( + "Received an error when searching for variable %v in the set: %v", + x[i], + err) + } + if idx == -1 { + t.Errorf( + "Expected vc.Variables() to return a set containing variable %v; it was not found in the set", + x[i], + ) + } + // Check for y[i] + idx, err = symbolic.FindInSlice(y[i], vars) + if err != nil { + t.Errorf( + "Received an error when searching for variable %v in the set: %v", + y[i], + err) + } + + if idx == -1 { + t.Errorf( + "Expected vc.Variables() to return a set containing variable %v; it was not found in the set", + y[i], + ) + } + } +} + +/* +TestVectorConstraint_ImpliesThisIsAlsoSatisfied1 +Description: + + This function tests that the ImpliesThisIsAlsoSatisfied method properly + panics if the receiver vector constraint is not well-defined. + (In this case, the left and right hand sides have different lengths.) +*/ +func TestVectorConstraint_ImpliesThisIsAlsoSatisfied1(t *testing.T) { + // Setup + N := 7 + x := symbolic.NewVariableVector(N) + y := symbolic.NewVariableVector(N) + left := x + right := symbolic.NewVariableVector(N - 1) + + // Create the vector constraint + vc := symbolic.VectorConstraint{left, right, symbolic.SenseLessThanEqual} + + // Create the second vector constraint + vc2 := symbolic.VectorConstraint{x, y, symbolic.SenseLessThanEqual} + + // Test + expectedError := vc.Check() + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) + } + + rAsError := r.(error) + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + vc.ImpliesThisIsAlsoSatisfied(vc2) +} + +/* +TestVectorConstraint_ImpliesThisIsAlsoSatisfied2 +Description: + + This function tests that the ImpliesThisIsAlsoSatisfied method properly + panics if: + - The receiver vector constraint is well-defined, but + - The input vector constraint is not well-defined. + (In this case, the left and right hand sides of the input vector constraint + have different lengths.) +*/ +func TestVectorConstraint_ImpliesThisIsAlsoSatisfied2(t *testing.T) { + // Setup + N := 7 + x := symbolic.NewVariableVector(N) + y := symbolic.NewVariableVector(N) + left := x + right := y + + // Create the vector constraint + vc := symbolic.VectorConstraint{left, right, symbolic.SenseLessThanEqual} + + // Create the second vector constraint + vc2 := symbolic.VectorConstraint{x, symbolic.NewVariableVector(N - 1), symbolic.SenseLessThanEqual} + + // Test + expectedError := vc2.Check() + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to panic; received nil", + ) + } + + rAsError := r.(error) + if rAsError.Error() != expectedError.Error() { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to panic with error \"%v\"; received \"%v\"", + expectedError.Error(), + rAsError.Error(), + ) + } + }() + + vc.ImpliesThisIsAlsoSatisfied(vc2) +} + +/* +TestVectorConstraint_ImpliesThisIsAlsoSatisfied3 +Description: + + This function tests that the ImpliesThisIsAlsoSatisfied method properly + returns false if the receiver and input vector constraints are well-defined, + but the input constraint is not implied by the receiver constraint. + In this case, the receiver constraint will be: + [1, 0; + 0, 1] * x <= [1; 1] + and the input constraint will be: + [1, 0; + 0, 1] * x <= [2; 2] + + TODO: This constraint should return true, but the implementation + does not currently handle this case. Let's fix this in a future update. +*/ +func TestVectorConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { + // Setup + N := 2 + x := symbolic.NewVariableVector(N) + left := x + right := mat.NewVecDense(N, []float64{1, 1}) + + // Create the vector constraint + vc := symbolic.VectorConstraint{ + left, + symbolic.VecDenseToKVector(*right), + symbolic.SenseLessThanEqual, + } + + // Create the second vector constraint + right2 := mat.NewVecDense(N, []float64{2, 2}) + vc2 := symbolic.VectorConstraint{ + left, + symbolic.VecDenseToKVector(*right2), + symbolic.SenseLessThanEqual, + } + + // Test + defer func() { + r := recover() + if r == nil { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to panic; received \"%v\"", + r, + ) + } + }() + + result := vc.ImpliesThisIsAlsoSatisfied(vc2) + if result { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to return false; received true", + ) + } +} From 692485960d00874674970c7b7a882acff23cc971 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Wed, 23 Jul 2025 22:14:43 -0400 Subject: [PATCH 06/10] Fixed noticeable bug in ImpliesThisIsAlsoSatisfied --- symbolic/scalar_constraint.go | 2 +- symbolic/variable.go | 4 + testing/symbolic/scalar_constraint_test.go | 151 +++++++++++++++++++++ testing/symbolic/vector_constraint_test.go | 12 +- 4 files changed, 162 insertions(+), 7 deletions(-) diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index 0790497..011e7b1 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -364,7 +364,7 @@ func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { // Get the coefficient of the single variable scCoeffVector := sc.LeftHandSide.LinearCoeff(sc.Variables()) scCoeff := scCoeffVector.AtVec(0) - otherCCoeffVector := otherC.RightHandSide.LinearCoeff(otherC.Variables()) + otherCCoeffVector := otherC.LeftHandSide.LinearCoeff(otherC.Variables()) otherCCoeff := otherCCoeffVector.AtVec(0) // If the coefficient of scCoeff is < 0, diff --git a/symbolic/variable.go b/symbolic/variable.go index f287348..9aad657 100644 --- a/symbolic/variable.go +++ b/symbolic/variable.go @@ -89,6 +89,8 @@ func (v Variable) Plus(rightIn interface{}) Expression { switch right := rightIn.(type) { case float64: return v.Plus(K(right)) + case int: + return v.Plus(K(float64(right))) case K: return Polynomial{ Monomials: []Monomial{ @@ -316,6 +318,8 @@ func (v Variable) Multiply(rightIn interface{}) Expression { switch right := rightIn.(type) { case float64: return v.Multiply(K(right)) + case int: + return v.Multiply(K(float64(right))) case K: // Create a new monomial monomialOut := Monomial{ diff --git a/testing/symbolic/scalar_constraint_test.go b/testing/symbolic/scalar_constraint_test.go index 3b841f4..5375c7e 100644 --- a/testing/symbolic/scalar_constraint_test.go +++ b/testing/symbolic/scalar_constraint_test.go @@ -1438,6 +1438,44 @@ func TestScalarConstraint_ScaleBy3(t *testing.T) { } } +/* +TestScalarConstraint_ScaleBy4 +Description: + + This tests that the ScalarConstraint.ScaleBy() method properly + flips the sign of a constraint with the SenseGreaterThanEqual sense + when the scale factor is negative. +*/ +func TestScalarConstraint_ScaleBy4(t *testing.T) { + // Constants + x := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{x}, + } + c2 := symbolic.K(3.14) + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: c2, + Sense: symbolic.SenseGreaterThanEqual, + } + + // Scale + newSc := sc.ScaleBy(-2) + + // Verify that the new constraint has the opposite sense + if newSc.ConstrSense() != symbolic.SenseLessThanEqual { + t.Errorf( + "Expected newSc.Sense to be different from %v; received %v", + sc.Sense, + newSc.ConstrSense(), + ) + } +} + /* TestScalarConstraint_Variables1 Description: @@ -1787,3 +1825,116 @@ func TestScalarConstraint_ImpliesThisIsAlsoSatisfied7(t *testing.T) { ) } } + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied8 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly handles the case when both + constraints contain negative coefficients on the same variable. + In this case, we have: + -2 x <= 4 and + -2 x <= 5 + as the input constraints. The first constraint DOES imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied8(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.Multiply(-2).LessEq(4.0) + + // Create a second constraint + sc2 := x.Multiply(-2).LessEq(5.0) + + // Verify that the first constraint DOES imply the second + if !sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be true; received false", + ) + } +} + +/* +TestScalarConstraint_AsSimplifiedConstraint1 +Description: + + This function tests the AsSimplifiedConstraint() method of a scalar constraint. + We will create a scalar constraint with a monomial on the left hand side + and a polynomial on the right hand side. The expected output is a new + constraint with a polynomial on the left hand side and a constant on the + right hand side. +*/ +func TestScalarConstraint_AsSimplifiedConstraint1(t *testing.T) { + // Constants + x := symbolic.NewVariable() + y := symbolic.NewVariable() + m1 := symbolic.Monomial{ + Coefficient: 2, + Exponents: []int{1, 1}, + VariableFactors: []symbolic.Variable{x, y}, + } + p2 := symbolic.Polynomial{ + Monomials: []symbolic.Monomial{ + { + Coefficient: 1, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{x}, + }, + { + Coefficient: 3, + Exponents: []int{1}, + VariableFactors: []symbolic.Variable{y}, + }, + { + Coefficient: 4, + Exponents: []int{}, + VariableFactors: []symbolic.Variable{}, + }, + }, + } + + // Create constraint + sc := symbolic.ScalarConstraint{ + LeftHandSide: m1, + RightHandSide: p2, + Sense: symbolic.SenseLessThanEqual, + } + + // Get simplified constraint + simplifiedSc := sc.AsSimplifiedConstraint() + + // Verify that the left hand side is a polynomial + if _, ok := simplifiedSc.Left().(symbolic.Polynomial); !ok { + t.Errorf( + "Expected simplifiedSc.LeftHandSide to be a symbolic.Polynomial; received %T", + simplifiedSc.Left(), + ) + } + + // Verify that the right hand side is a constant + k, ok := simplifiedSc.Right().(symbolic.K) + if !ok { + t.Errorf( + "Expected simplifiedSc.RightHandSide to be a symbolic.K; received %T", + simplifiedSc.Right(), + ) + } + + if float64(k) != 4 { + t.Errorf( + "Expected simplifiedSc.RightHandSide to be 4; received %v", + k, + ) + } + + // Verify that the sense is the same + if simplifiedSc.ConstrSense() != sc.ConstrSense() { + t.Errorf( + "Expected simplifiedSc.Sense to be %v; received %v", + sc.ConstrSense(), + simplifiedSc.ConstrSense(), + ) + } +} diff --git a/testing/symbolic/vector_constraint_test.go b/testing/symbolic/vector_constraint_test.go index fb9be39..33fbd09 100644 --- a/testing/symbolic/vector_constraint_test.go +++ b/testing/symbolic/vector_constraint_test.go @@ -1328,17 +1328,17 @@ func TestVectorConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { // Create the vector constraint vc := symbolic.VectorConstraint{ - left, - symbolic.VecDenseToKVector(*right), - symbolic.SenseLessThanEqual, + LeftHandSide: left, + RightHandSide: symbolic.VecDenseToKVector(*right), + Sense: symbolic.SenseLessThanEqual, } // Create the second vector constraint right2 := mat.NewVecDense(N, []float64{2, 2}) vc2 := symbolic.VectorConstraint{ - left, - symbolic.VecDenseToKVector(*right2), - symbolic.SenseLessThanEqual, + LeftHandSide: left, + RightHandSide: symbolic.VecDenseToKVector(*right2), + Sense: symbolic.SenseLessThanEqual, } // Test From c76d90ae9dcbf8b7e4431f1b373442c903212387 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Wed, 23 Jul 2025 22:17:04 -0400 Subject: [PATCH 07/10] Added test for ImpliesThisIsAlsoSatisfied --- testing/symbolic/scalar_constraint_test.go | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/testing/symbolic/scalar_constraint_test.go b/testing/symbolic/scalar_constraint_test.go index 5375c7e..9b5238d 100644 --- a/testing/symbolic/scalar_constraint_test.go +++ b/testing/symbolic/scalar_constraint_test.go @@ -1856,6 +1856,68 @@ func TestScalarConstraint_ImpliesThisIsAlsoSatisfied8(t *testing.T) { } } +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied9 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly handles the case when both + constraints are SenseGreaterThanEqual and have positive coefficients + on the same variable, but they do NOT imply each other. + In this case, we have: + 2 x >= 4 and + 2 x >= 5 + as the input constraints. The first constraint does NOT imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied9(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.Multiply(2).GreaterEq(4.0) + + // Create a second constraint + sc2 := x.Multiply(2).GreaterEq(5.0) + + // Verify that the first constraint does NOT imply the second + if sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be false; received true", + ) + } +} + +/* +TestScalarConstraint_ImpliesThisIsAlsoSatisfied10 +Description: + + Tests the ImpliesThisIsAlsoSatisfied() method of a scalar constraint. + This test verifies that the method properly handles the case when both + cosntraints are SenseEqual and have positive coefficients + on the same variable, but they do NOT imply each other. + In this case, we have: + 2 x = 4 and + 2 x = 5 + as the input constraints. The first constraint does NOT imply the second. +*/ +func TestScalarConstraint_ImpliesThisIsAlsoSatisfied10(t *testing.T) { + // Constants + x := symbolic.NewVariable() + + // Create constraint + sc := x.Multiply(2).Eq(4.0) + + // Create a second constraint + sc2 := x.Multiply(2).Eq(5.0) + + // Verify that the first constraint does NOT imply the second + if sc.ImpliesThisIsAlsoSatisfied(sc2) { + t.Errorf( + "Expected sc.ImpliesThisIsAlsoSatisfied(sc2) to be false; received true", + ) + } +} + /* TestScalarConstraint_AsSimplifiedConstraint1 Description: From 3de3a30a228a0f3d130cd416d326e37c8c382119 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Wed, 23 Jul 2025 22:23:42 -0400 Subject: [PATCH 08/10] Testing naive implication check in Implies This Is Also Satisfied for VectorConstraint --- testing/symbolic/vector_constraint_test.go | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/testing/symbolic/vector_constraint_test.go b/testing/symbolic/vector_constraint_test.go index 33fbd09..6b5c6f5 100644 --- a/testing/symbolic/vector_constraint_test.go +++ b/testing/symbolic/vector_constraint_test.go @@ -1359,3 +1359,50 @@ func TestVectorConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { ) } } + +/* +TestVectorConstraint_ImpliesThisIsAlsoSatisfied4 +Description: + + This function tests that the ImpliesThisIsAlsoSatisfied method properly + returns true if: + - the receiver vector constraints are well-defined, + - the input is a well-defined scalar constraint, and + - the input constraint is implied by one of the receiver constraints. + In this case, the receiver constraint will be: + [1, 0; + 0, 1] * x <= [1; 1] + and the input constraint will be: + [1, 0] * x <= 1 +*/ +func TestVectorConstraint_ImpliesThisIsAlsoSatisfied4(t *testing.T) { + // Setup + N := 2 + x := symbolic.NewVariableVector(N) + left := x + right := mat.NewVecDense(N, []float64{1, 1}) + + // Create the vector constraint + vc := symbolic.VectorConstraint{ + LeftHandSide: left, + RightHandSide: symbolic.VecDenseToKVector(*right), + Sense: symbolic.SenseLessThanEqual, + } + + // Create the scalar constraint + left2 := x.AtVec(0) + right2 := symbolic.K(1) + sc := symbolic.ScalarConstraint{ + LeftHandSide: left2, + RightHandSide: right2, + Sense: symbolic.SenseLessThanEqual, + } + + // Test + result := vc.ImpliesThisIsAlsoSatisfied(sc) + if !result { + t.Errorf( + "Expected vc.ImpliesThisIsAlsoSatisfied() to return true; received false", + ) + } +} From 87e29a821b4c58d576bf22210bd6996274feb847 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Wed, 23 Jul 2025 22:27:24 -0400 Subject: [PATCH 09/10] Added todos for other constraint types --- symbolic/matrix_constraint.go | 10 +++++++++- symbolic/scalar_constraint.go | 6 ++++++ symbolic/vector_constraint.go | 6 ++++++ testing/symbolic/vector_constraint_test.go | 14 ++------------ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/symbolic/matrix_constraint.go b/symbolic/matrix_constraint.go index ffe18f4..6cbda57 100644 --- a/symbolic/matrix_constraint.go +++ b/symbolic/matrix_constraint.go @@ -259,9 +259,17 @@ func (mc MatrixConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { } } } + case VectorConstraint: + // TODO: Implement more advanced implication checks. + return false + case MatrixConstraint: + // TODO: Implement more advanced implication checks. + return false default: // Other types of constraints are not currently supported. - return false + panic( + fmt.Errorf("implication checking between MatrixConstraint and %T is not currently supported", other), + ) } // If no avenues for implication were found, return false. diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index 011e7b1..b01d77c 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -433,6 +433,12 @@ func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { panic("unreachable code reached in ScalarConstraint.ImpliesThisIsAlsoSatisfied") } } + case VectorConstraint: + // TODO: Implement more advanced implication checks. + return false + case MatrixConstraint: + // TODO: Implement more advanced implication checks. + return false default: // Other types of constraints are not currently supported. panic( diff --git a/symbolic/vector_constraint.go b/symbolic/vector_constraint.go index 4f8deee..cb883fd 100644 --- a/symbolic/vector_constraint.go +++ b/symbolic/vector_constraint.go @@ -390,6 +390,12 @@ func (vc VectorConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { return true } } + case VectorConstraint: + // TODO: Implement more advanced implication checks. + return false + case MatrixConstraint: + // TODO: Implement more advanced implication checks. + return false default: // Other types of constraints are not currently supported. panic( diff --git a/testing/symbolic/vector_constraint_test.go b/testing/symbolic/vector_constraint_test.go index 6b5c6f5..d86e9d5 100644 --- a/testing/symbolic/vector_constraint_test.go +++ b/testing/symbolic/vector_constraint_test.go @@ -1342,20 +1342,10 @@ func TestVectorConstraint_ImpliesThisIsAlsoSatisfied3(t *testing.T) { } // Test - defer func() { - r := recover() - if r == nil { - t.Errorf( - "Expected vc.ImpliesThisIsAlsoSatisfied() to panic; received \"%v\"", - r, - ) - } - }() - - result := vc.ImpliesThisIsAlsoSatisfied(vc2) + result := vc.ImpliesThisIsAlsoSatisfied(vc2) // TODO: Fix this test to return true if result { t.Errorf( - "Expected vc.ImpliesThisIsAlsoSatisfied() to return false; received true", + "Expected vc.ImpliesThisIsAlsoSatisfied() to return true; received false", ) } } From 09452e8d60a957da826828c839e22f8a32fb22fc Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Wed, 23 Jul 2025 22:29:39 -0400 Subject: [PATCH 10/10] Condensing some of the TODO sections --- symbolic/matrix_constraint.go | 5 +---- symbolic/scalar_constraint.go | 5 +---- symbolic/vector_constraint.go | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/symbolic/matrix_constraint.go b/symbolic/matrix_constraint.go index 6cbda57..c906382 100644 --- a/symbolic/matrix_constraint.go +++ b/symbolic/matrix_constraint.go @@ -259,10 +259,7 @@ func (mc MatrixConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { } } } - case VectorConstraint: - // TODO: Implement more advanced implication checks. - return false - case MatrixConstraint: + case VectorConstraint, MatrixConstraint: // TODO: Implement more advanced implication checks. return false default: diff --git a/symbolic/scalar_constraint.go b/symbolic/scalar_constraint.go index b01d77c..70f860e 100644 --- a/symbolic/scalar_constraint.go +++ b/symbolic/scalar_constraint.go @@ -433,10 +433,7 @@ func (sc ScalarConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { panic("unreachable code reached in ScalarConstraint.ImpliesThisIsAlsoSatisfied") } } - case VectorConstraint: - // TODO: Implement more advanced implication checks. - return false - case MatrixConstraint: + case VectorConstraint, MatrixConstraint: // TODO: Implement more advanced implication checks. return false default: diff --git a/symbolic/vector_constraint.go b/symbolic/vector_constraint.go index cb883fd..57911ef 100644 --- a/symbolic/vector_constraint.go +++ b/symbolic/vector_constraint.go @@ -390,10 +390,7 @@ func (vc VectorConstraint) ImpliesThisIsAlsoSatisfied(other Constraint) bool { return true } } - case VectorConstraint: - // TODO: Implement more advanced implication checks. - return false - case MatrixConstraint: + case VectorConstraint, MatrixConstraint: // TODO: Implement more advanced implication checks. return false default: