Search code examples
least-squaresscipy-optimize

minimum-difference constrained sparse least squares problem (called from python)


I'm struggling a bit finding a fast algorithm that's suitable.

I just want to minimize:

norm2(x-s)

st G.x <= h

x >= 0

sum(x) = R

G is sparse and contains only 1s (and zeros obviously).

In the case of iterative algorithms, it would be nice to get the interim solutions to show to the user.

The context is that s is a vector of current results, and the user is saying "well the sum of these few entries (entries indicated by a few 1.0's in a row in G) should be less than this value (a row in h). So we have to remove quantities from the entries the user specified (indicated by 1.0 entries in G) in a least-squares optimal way, but since we have a global constraint on the total (R) the values removed need to be allocated in a least-squares optimal way amongst the other entries. The entries can't go negative.

All the algorithms I'm looking at are much more general, and as a result are much more complex. Also, they seem quite slow. I don't see this as a complex problem, although mixes of equality and inequality constraints always seem to make things more complex.

This has to be called from Python, so I'm looking at Python libraries like qpsolvers and scipy.optimize. But I suppose Java or C++ libraries could be used and called from Python, which might be good since multithreading is better in Java and C++.

Any thoughts on what library/package/approach to use to best solve this problem?

The size of the problem is about 150,000 rows in s, and a few dozen rows in G.

Thanks!


Solution

  • Your problem is a linear least squares:

    minimize_x   norm2(x-s)
    such that    G x <= h
                 x >= 0
                 1^T x = R
    

    Thus it fits the bill of the solve_ls function in qpsolvers.

    Here is an instance of how I imagine your problem matrices would look like, given what you specified. Since it is sparse we should use SciPy CSC matrices, and regular NumPy arrays for vectors:

    import numpy as np
    import scipy.sparse as spa
    
    n = 150_000
    
    # minimize 1/2 || x - s ||^2
    R = spa.eye(n, format="csc")
    s = np.array(range(n), dtype=float)
    
    # such that G * x <= h
    G = spa.diags(
        diagonals=[
            [1.0 if i % 2 == 0 else 0.0 for i in range(n)],
            [1.0 if i % 3 == 0 else 0.0 for i in range(n - 1)],
            [1.0 if i % 5 == 0 else 0.0 for i in range(n - 1)],
        ],
        offsets=[0, 1, -1],
    )
    a_dozen_rows = np.linspace(0, n - 1, 12, dtype=int)
    G = G[a_dozen_rows]
    h = np.ones(12)
    
    # such that sum(x) == 42
    A = spa.csc_matrix(np.ones((1, n)))
    b = np.array([42.0]).reshape((1,))
    
    # such that x >= 0
    lb = np.zeros(n)
    

    Next, we can solve this problem with:

    from qpsolvers import solve_ls
    
    x = solve_ls(R, s, G, h, A, b, lb, solver="osqp", verbose=True)
    

    Here I picked CVXOPT but there are other open-source solvers you can install such as ProxQP, OSQP or SCS. You can install a set of open-source solvers by: pip install qpsolvers[open_source_solvers]. After some solvers are installed, you can list those for sparse matrices by:

    print(qpsolvers.sparse_solvers)
    

    Finally, here is some code to check that the solution returned by the solver satisfies our constraints:

    tol = 1e-6  # tolerance for checks
    print(f"- Objective: {0.5 * (x - s).dot(x - s):.1f}")
    print(f"- G * x <= h: {(G.dot(x) <= h + tol).all()}")
    print(f"- x >= 0: {(x + tol >= 0.0).all()}")
    print(f"- sum(x) = {x.sum():.1f}")
    

    I just tried it with OSQP (adding the eps_rel=1e-5 keyword argument when calling solve_ls, otherwise the returned solution would be less accurate than the tol = 1e-6 tolerance) and it found a solution is 737 milliseconds on my (rather old) CPU with:

    - Objective: 562494373088866.8
    - G * x <= h: True
    - x >= 0: True
    - sum(x) = 42.0
    

    Hoping this helps. Happy solving!