(Update) Column-wise CVX-based approach
Idea
It should be noted that
- If
x_i=1
is guaranteed for position i
, then excluding the i
-th column from A
(number of columns will be reduced by 1
) has no valid solutions like (x_1, x_2, ..., x_{i-1}, x_{i+1}, ...)
to meet the linear sum constraint b
.
- If
x_i=0
is guaranteed for position i
, then excluding the i
-th column from A
(number of columns will be reduced by 1
) has no valid solutions like (x_1, x_2, ..., x_{i-1}, x_{i+1}, ...)
to meet the linear sum constraint b - A_{:,i}
, where A_{:,i}
denotes the i
-th column of A
.
Based on the observation from above, you only need to check if there exists a solution to make the equality hold. If no solution can be found when excluding i
-th column, then x_i
is either "definite 1" or "definite 0". This would be much faster than my older brute-force approach, while the only speed constraint comes from the CVX solver working on each column.
Code Example in R
Since I am more proficient (sorry for my poor Python
skills) with writing CVX
code in R, so here is just an example how it could be implemented
library(CVXR)
f <- function(A, b, tol = 1e-6) {
res0s <- res1s <- vector(length = ncol(A))
vars <- Variable(ncol(A) - 1, boolean = TRUE)
for (i in 1:ncol(A)) {
x <- vars
# guaranteed to be zeros
obj0 <- Minimize(norm2(A[, -i] %*% x - (b - A[, i])))
prob0 <- Problem(obj0)
res0s[i] <- abs(solve(prob0)$value) > tol
# guaranteed to be ones
obj1 <- Minimize(norm2(A[, -i] %*% x - b))
prob1 <- Problem(obj1)
res1s[i] <- abs(solve(prob1)$value) > tol
}
idx0s <- which(res0s)
idx1s <- which(res1s)
if (length(interaction(idx0s, idx1s)) > 0) {
print("no solution")
} else {
list(
idx_guaranteed_0 = idx0s,
idx_guaranteed_1 = idx1s
)
}
}
and the output looks like
> (A <- matrix(+(runif(90) > 0.5), 6))
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,] 1 1 0 1 0 0 1 1 0 0 0 0 1 0
[2,] 0 1 1 0 0 0 1 1 1 0 1 0 0 1
[3,] 0 1 0 1 0 1 0 1 1 0 0 1 0 1
[4,] 1 1 1 1 0 0 1 1 0 0 0 0 0 0
[5,] 1 0 0 0 0 0 0 1 1 1 0 1 1 1
[6,] 0 0 1 1 1 1 1 0 0 1 1 0 1 0
[,15]
[1,] 0
[2,] 1
[3,] 0
[4,] 1
[5,] 0
[6,] 0
> (b <- rbind(1, 2, 4, 2, 2, 3))
[,1]
[1,] 1
[2,] 2
[3,] 4
[4,] 2
[5,] 2
[6,] 3
> f(A, b)
$idx_guaranteed_0
[1] 1 2 7 8 10 11 13
$idx_guaranteed_1
[1] 4 6 12
(Naive) Brute Force ... Inefficient!!!
Here is a brute-force approach (with time complexity O(2^n)
) to enumerate all possible solutions, by turning integers into their binary representations (you could replace np.unit8
by other longer dtype
if you need more bit width)
m, n = A_eq.shape
v = np.unpackbits(np.array(np.arange(2**n),dtype=np.uint8)[None,:],axis = 0)[-n:,:]
s = v[:,np.sum(A_eq@v==b_eq[:,None],axis = 0)==m]
p0 = np.argwhere(np.logical_and.reduce(s == 0, axis = 1)).flatten()
p1 = np.argwhere(np.logical_and.reduce(s == 1, axis = 1)).flatten()
{'guarantee0':p0, 'guarantee1':p1}
Example
Given A_eq
and b_eq
like below
A_eq = np.array(
[
[1, 1, 1, 0, 1, 0],
[1, 0, 0, 1, 1, 1],
[1, 1, 0, 1, 0, 1],
]
)
b_eq = np.array([3, 2, 3])
the indices of guaranteed 0
s and 1
s are obtained as
{'guarantee0': array([4]), 'guarantee1': array([0, 1, 2])}