From 323083af1841575bdf93c8e564fcdda5443e0051 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Tue, 5 Aug 2025 15:40:39 -0400 Subject: [PATCH 1/2] Added a new method for simplifying constraints in a given optimization problem --- go.mod | 2 +- go.sum | 2 + problem/optimization_problem.go | 96 ++++++++++- problem/utils.go | 17 ++ testing/problem/optimization_problem_test.go | 158 +++++++++++++++++++ 5 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 problem/utils.go diff --git a/go.mod b/go.mod index 32ca132..e6400c7 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,4 @@ toolchain go1.23.9 require gonum.org/v1/gonum v0.16.0 -require github.com/MatProGo-dev/SymbolicMath.go v0.2.2 +require github.com/MatProGo-dev/SymbolicMath.go v0.2.3 diff --git a/go.sum b/go.sum index d445b9f..3e67137 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/MatProGo-dev/SymbolicMath.go v0.2.2 h1:U9nLLgtslRXbweCsgW9uSw8AvoGMgjq2luQtXIuN3eA= github.com/MatProGo-dev/SymbolicMath.go v0.2.2/go.mod h1:tW8thj4pkaTV9lFNU3OCKmwQ3mZ2Eim6S4JpHRDfRvU= +github.com/MatProGo-dev/SymbolicMath.go v0.2.3 h1:ffkUVU1oKzw2jj6fEu4BKW2YEYOWq55fwD7FOP9cY6k= +github.com/MatProGo-dev/SymbolicMath.go v0.2.3/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/problem/optimization_problem.go b/problem/optimization_problem.go index 8c9fb76..1fe1589 100644 --- a/problem/optimization_problem.go +++ b/problem/optimization_problem.go @@ -568,6 +568,26 @@ func (op *OptimizationProblem) LinearEqualityConstraintMatrices() (symbolic.KMat return COut2, dOut2, nil } +// func (op *OptimizationProblem) Simplify() OptimizationProblem { +// // Create a new optimization problem +// newProblem := NewProblem(op.Name + " (Simplified)") + +// // Add all variables to the new problem +// for _, variable := range op.Variables { +// newProblem.Variables = append(newProblem.Variables, variable) +// } + +// // Add all constraints to the new problem +// for _, constraint := range op.Constraints { +// newProblem.Constraints = append(newProblem.Constraints, constraint) +// } + +// // Set the objective of the new problem +// newProblem.Objective = op.Objective + +// return newProblem +// } + /* ToProblemWithAllPositiveVariables Description: @@ -668,8 +688,6 @@ func (problemIn *OptimizationProblem) ToLPStandardForm1() (*OptimizationProblem, 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 { @@ -857,3 +875,77 @@ func (op *OptimizationProblem) CheckIfLinear() error { // All Checks Passed! return nil } + +/* +CopyVariable +Description: + + Creates a deep copy of the given variable within + the optimization problem. +*/ +func (op *OptimizationProblem) CopyVariable(variable symbolic.Variable) symbolic.Variable { + // Setup + newVariable := variable + newVariable.ID = uint64(len(op.Variables)) // Assign a new ID + newVariable.Name = fmt.Sprintf("%s (copy)", variable.Name) + + // Add the new variable to the problem + op.Variables = append(op.Variables, newVariable) + return newVariable +} + +/* +Copy +Description: + + Returns a deep copy of the optimization problem. +*/ +func (op *OptimizationProblem) Copy() *OptimizationProblem { + // Setup + newProblem := NewProblem(op.Name) + + // Copy Variables + replacementMap := make(map[symbolic.Variable]symbolic.Expression) + for _, variable := range op.Variables { + newVariable := newProblem.CopyVariable(variable) + replacementMap[variable] = newVariable + } + + // Copy Constraints + for _, constraint := range op.Constraints { + newConstraint := constraint.SubstituteAccordingTo(replacementMap) + newProblem.Constraints = append(newProblem.Constraints, newConstraint) + } + + // Copy Objective + newProblem.Objective = op.Objective + + // Return the new problem + return newProblem +} + +/* +SimplifyConstraints +Description: + + This method simplifies the constraints of the optimization problem by removing redundant constraints. +*/ +func (op *OptimizationProblem) SimplifyConstraints() { + // Setup + newConstraints := make([]symbolic.Constraint, 0) + + // Iterate through all constraints and check if they are redundant + for _, constraint := range op.Constraints { + // Determine if the newConstraints imply that + // the current constraint is also satisfied. + if ConstraintIsRedundantGivenOthers(constraint, newConstraints) { + continue + } + + // Otherwise, add the constraint to the newConstraints + newConstraints = append(newConstraints, constraint) + } + + // Set the new constraints + op.Constraints = newConstraints +} diff --git a/problem/utils.go b/problem/utils.go new file mode 100644 index 0000000..291f025 --- /dev/null +++ b/problem/utils.go @@ -0,0 +1,17 @@ +package problem + +import "github.com/MatProGo-dev/SymbolicMath.go/symbolic" + +func ConstraintIsRedundantGivenOthers( + constraint symbolic.Constraint, + constraints []symbolic.Constraint, +) bool { + // Check if the expression can be derived from the constraints + for _, c := range constraints { + if c.ImpliesThisIsAlsoSatisfied(constraint) { + return true + } + } + + return false +} diff --git a/testing/problem/optimization_problem_test.go b/testing/problem/optimization_problem_test.go index 43a70f9..427fc41 100644 --- a/testing/problem/optimization_problem_test.go +++ b/testing/problem/optimization_problem_test.go @@ -2624,3 +2624,161 @@ func TestOptimizationProblem_CheckIfLinear1(t *testing.T) { } } } + +/* +TestOptimizationProblem_SimplifyConstraints1 +Description: + + This test verifies that the SimplifyConstraints function properly simplifies + a problem with redundant constraints. + The problem will have: + - a constant objective + - 1 variable1, + - and 2 linear inequality constraints: + x1 <= 1 + x1 <= 2 + The second constraint is redundant and should be removed. + The result should be a problem with 1 variable and 1 constraint. +*/ +func TestOptimizationProblem_SimplifyConstraints1(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_SimplifyConstraints1") + v1 := p1.AddVariable() + c1 := v1.LessEq(1.0) + c2 := v1.LessEq(2.0) + + p1.Constraints = append(p1.Constraints, c1) + p1.Constraints = append(p1.Constraints, c2) + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Create a copy of p1 to compare against after simplification + p2 := p1.Copy() + + // Algorithm + p1.SimplifyConstraints() + + // Check that the number of variables is as expected. + if len(p2.Variables) != len(p1.Variables) { + t.Errorf("expected the number of variables to be %v; received %v", + len(p1.Variables), len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p1.Constraints) != 1 { + t.Errorf("expected the new number of constraints to be %v; received %v", + 1, len(p1.Constraints)) + } + + // Check that the remaining constraint is the expected one. + p1FirstConstraint := p1.Constraints[0] + L1 := p1FirstConstraint.Left().(symbolic.ScalarExpression).LinearCoeff(p1.Variables) + p2FirstConstraint := p2.Constraints[0] + L2 := p2FirstConstraint.Left().(symbolic.ScalarExpression).LinearCoeff(p2.Variables) + + if L1.AtVec(0) != L2.AtVec(0) { + t.Errorf("expected the remaining constraint's coefficient to be %v; received %v", + L1, L2) + } +} + +/* +TestOptimizationProblem_SimplifyConstraints2 +Description: + + This test verifies that the SimplifyConstraints function properly handles + a problem with no constraints. + The problem will have: + - a constant objective + - 1 variable, + - and no constraints. + The result should be a problem with 1 variable and no constraints. +*/ +func TestOptimizationProblem_SimplifyConstraints2(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_SimplifyConstraints2") + + // Create good objective + p1.Objective = *problem.NewObjective( + symbolic.K(3.14), + problem.SenseMaximize, + ) + + // Create a copy of p1 to compare against after simplification + p2 := p1.Copy() + + // Algorithm + p1.SimplifyConstraints() + + // Check that the number of variables is as expected. + if len(p2.Variables) != len(p1.Variables) { + t.Errorf("expected the number of variables to be %v; received %v", + len(p1.Variables), len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p1.Constraints) != 0 { + t.Errorf("expected the new number of constraints to be %v; received %v", + 0, len(p1.Constraints)) + } +} + +/* +TestOptimizationProblem_SimplifyConstraints3 +Description: + + This test verifies that the SimplifyConstraints function properly handles + a problem with a single constraint that is not redundant. + The problem will have: + - a constant objective + - 1 variable, + - and a single linear inequality constraint. + The result should be a problem with 1 variable and 1 constraint. +*/ +func TestOptimizationProblem_SimplifyConstraints3(t *testing.T) { + // Constants + p1 := problem.NewProblem("TestOptimizationProblem_SimplifyConstraints3") + v1 := 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, + ) + + // Create a copy of p1 to compare against after simplification + p2 := p1.Copy() + + // Algorithm + p1.SimplifyConstraints() + + // Check that the number of variables is as expected. + if len(p2.Variables) != len(p1.Variables) { + t.Errorf("expected the number of variables to be %v; received %v", + len(p1.Variables), len(p2.Variables)) + } + + // Check that the number of constraints is as expected. + if len(p1.Constraints) != 1 { + t.Errorf("expected the new number of constraints to be %v; received %v", + 1, len(p1.Constraints)) + } + + // Check that the remaining constraint is the expected one. + p1FirstConstraint := p1.Constraints[0] + L1 := p1FirstConstraint.Left().(symbolic.ScalarExpression).LinearCoeff(p1.Variables) + p2FirstConstraint := p2.Constraints[0] + L2 := p2FirstConstraint.Left().(symbolic.ScalarExpression).LinearCoeff(p2.Variables) + + if L1.AtVec(0) != L2.AtVec(0) { + t.Errorf("expected the remaining constraint's coefficient to be %v; received %v", + L1, L2) + } +} From 6ed74abeaa39f02964d0366f95916af36f302918 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Tue, 5 Aug 2025 15:47:59 -0400 Subject: [PATCH 2/2] Addressed some PR review suggestions from copilot --- problem/objective.go | 11 +++++++++++ problem/optimization_problem.go | 12 ++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/problem/objective.go b/problem/objective.go index 3c72804..dfddd41 100644 --- a/problem/objective.go +++ b/problem/objective.go @@ -24,3 +24,14 @@ Description: func (o *Objective) IsLinear() bool { return symbolic.IsLinear(o.Expression) } + +/* +SubstituteAccordingTo +Description: + + Substitutes the variables in the objective according to the replacement map. +*/ +func (o *Objective) SubstituteAccordingTo(replacementMap map[symbolic.Variable]symbolic.Expression) *Objective { + newExpression := o.Expression.SubstituteAccordingTo(replacementMap) + return &Objective{newExpression, o.Sense} +} diff --git a/problem/optimization_problem.go b/problem/optimization_problem.go index 1fe1589..c69d505 100644 --- a/problem/optimization_problem.go +++ b/problem/optimization_problem.go @@ -886,9 +886,17 @@ Description: func (op *OptimizationProblem) CopyVariable(variable symbolic.Variable) symbolic.Variable { // Setup newVariable := variable - newVariable.ID = uint64(len(op.Variables)) // Assign a new ID newVariable.Name = fmt.Sprintf("%s (copy)", variable.Name) + // Assign a new, unique ID to the variable + maxID := uint64(0) + for _, v := range op.Variables { + if v.ID >= maxID { + maxID = v.ID + 1 + } + } + newVariable.ID = maxID + // Add the new variable to the problem op.Variables = append(op.Variables, newVariable) return newVariable @@ -918,7 +926,7 @@ func (op *OptimizationProblem) Copy() *OptimizationProblem { } // Copy Objective - newProblem.Objective = op.Objective + newProblem.Objective = *op.Objective.SubstituteAccordingTo(replacementMap) // Return the new problem return newProblem