Repository: novonordisk-research/ProcessOptimizer

Issue:
Using sum_constraints with "later" dimensions breaks opt.ask()
Sum_equals constraints break if you use a set of dimensions that do not start from the first one and continue upwards. E.g. constraining dimensions [0, 1, 2] works, but constraining dimensions [1, 2, 3] does not. The error is caused by the step that checks whether candidate points in the constrained dimensions actually lie within the space.

To reproduce
```python
##% Import packages
from ProcessOptimizer import Optimizer
from ProcessOptimizer.space.constraints import SumEquals

# Step 1: This code works just fine
seed = 42

dimensions = [
    (200.0, 450.0),
    (50.0, 450.0),
    (0.0, 450.0),
    (10.0, 60.0),
]
# Build optimizer
opt = Optimizer(
    dimensions=dimensions,
    lhs=False,
    acq_func="EI",
    n_initial_points=5,
    random_state=seed,
)

# Create a constraint among the three factors with the 450 upper limit
constraints = [SumEquals(dimensions=[0, 1, 2], value=450)]
opt.set_constraints(constraints)

x = opt.ask(
    5,
    strategy="cl_min",
)

# ==============================
# Step 2: This code breaks
seed = 42

dimensions = [
    (10.0, 60.0), # <-- We have moved this dimension from last, to first
    (200.0, 450.0),
    (50.0, 450.0),
    (0.0, 450.0),
]
# Build optimizer
opt = Optimizer(
    dimensions=dimensions,
    lhs=False,
    acq_func="EI",
    n_initial_points=5,
    random_state=seed,
)

# Create the constraint, again just with the three factors with the 450 upper limit
constraints = [SumEquals(dimensions=[1, 2, 3], value=450)]
opt.set_constraints(constraints)

x = opt.ask(
    5,
    strategy="cl_min",
)
```
The code above throws an error in a later step, where the coordinates are transformed, with an error indicating that some points lie outside the defined space.

Discussion:
@dk-teknologisk-mon: I have already identified and corrected the problem, working on a pull request now.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6cc2a550..e26216cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,10 @@
 
 ### Bugfixes
 
--
+- Fixed a bug where sum_equals constraints would break if used with dimensions other than 
+  an increasing list from 0. E.g. constraining dimensions [0, 1, 2] would work, but 
+  constraining [1, 2, 3] would not. 
+  
 
 ## Version 1.0.1 (October 2024)
 
diff --git a/ProcessOptimizer/space/constraints.py b/ProcessOptimizer/space/constraints.py
index 64e57c46..c70cc714 100644
--- a/ProcessOptimizer/space/constraints.py
+++ b/ProcessOptimizer/space/constraints.py
@@ -252,9 +252,9 @@ def phi(d):
             sample_candidate = sample_candidate[0]
             # Check that the candidate is inside the original parameter space
             inspace = [
-                (sample_candidate[i] >= self.space.bounds[i][0]) and
-                (sample_candidate[i] <= self.space.bounds[i][1])
-                for i in range(d)
+                (sample_candidate[i] >= self.space.bounds[dim][0]) and
+                (sample_candidate[i] <= self.space.bounds[dim][1])
+                for i, dim in enumerate(self.sum_equals[0].dimensions)
             ]
             # Only accept the candidate if it is in our space
             if all(inspace):


Test patch:
diff --git a/ProcessOptimizer/tests/test_constraints.py b/ProcessOptimizer/tests/test_constraints.py
index 648b0978..5124d96f 100644
--- a/ProcessOptimizer/tests/test_constraints.py
+++ b/ProcessOptimizer/tests/test_constraints.py
@@ -234,6 +234,48 @@ def test_SumEquals():
     # Check that the other dimensions have correct type
     assert isinstance(samples[0][3], str)
     assert not isinstance(samples[0][2], str)
+    
+    # Test that the constraint works, irrespective of which dimensions we use
+    dimensions = [
+        (200.0, 450.0),
+        (50.0, 450.0),
+        (0.0, 450.0),
+        (10.0, 60.0),
+    ]
+    # Build optimizer
+    opt = Optimizer(
+        dimensions=dimensions,
+        lhs=False,
+        acq_func="EI",
+        n_initial_points=5,
+        random_state=42,
+    )
+    # Constrain the first three dimensions, and leave out the fourth
+    constraints = [SumEquals(dimensions=[0, 1, 2], value=450)]
+    opt.set_constraints(constraints)    
+    x1 = opt.ask(1)
+    # Change the order of the dimensions
+    dimensions = [
+        (10.0, 60.0),
+        (200.0, 450.0),
+        (50.0, 450.0),
+        (0.0, 450.0),        
+    ]
+    # Build optimizer
+    opt = Optimizer(
+        dimensions=dimensions,
+        lhs=False,
+        acq_func="EI",
+        n_initial_points=5,
+        random_state=42,
+    )
+    # Constrain the same dimensions, that are now second to fourth
+    constraints = [SumEquals(dimensions=[1, 2, 3], value=450)]
+    opt.set_constraints(constraints)    
+    x2 = opt.ask()
+    # The values suggested for the constrained dimensions should be the same 
+    # irrespective of their order in the dimension list
+    assert x1[:3] == x2[1:]
 
 @pytest.mark.fast_test
 def test_Conditional():