Forum Discussion

FICO's avatar
FICO
Community Manager
5 years ago

Bug report: addVariable() has quadratic time complexity on QCQP's

Hi,

I'm using the Python API of Xpress and I noticed that addVariable() has quadratic time complexity when the problem is a QCQP. This code here demonstrates the issue:

import timeit
 
def create_qcqp(size):
    import xpress as xp
    p = xp.problem()
    for i in range(size):
        v = xp.var(name=f"v{i}")
        w = xp.var(name=f"w{i}")
        p.addVariable(v, w)
        p.addConstraint(v ** 2 + w ** 2 <= 0)  # make problem a QCQP
 
print(f"{'NUM_VARS':>10}: {'TIME':>10}")
for size in [2000 * (2 ** i) for i in range(10)]:
    runtime = timeit.timeit(lambda: create_qcqp(size), number=1)
    print(f"{size:>10,}: {runtime:>10.3f}")

The output is:

  NUM_VARS:       TIME
     2,000:      0.056
     4,000:      0.103
     8,000:      0.285
    16,000:      0.844
    32,000:      3.634
    64,000:     29.386
   128,000:    164.903

Clearly, the run-time grows quadratically (or maybe even cubic) with the number of variable. I suppose this is a bug, since I expect the time to grow linearly, since I'm only adding independent variables which are not linked by any constraints.

The problem exists for QCQP only, for the following LP

import timeit
 
def create_lp(size):
    import xpress as xp
    p = xp.problem()
    for i in range(size):
        v = xp.var(name=f"v{i}")
        w = xp.var(name=f"w{i}")
        p.addVariable(v, w)
        p.addConstraint(v + w <= 0)
 
print(f"{'NUM_VARS':>10}: {'TIME':>10}")
for size in [2000 * (2 ** i) for i in range(10)]:
    runtime = timeit.timeit(lambda: create_lp(size), number=1)
    print(f"{size:>10,}: {runtime:>10.3f}")

the output is

  NUM_VARS:       TIME
     2,000:      0.045
     4,000:      0.073
     8,000:      0.109
    16,000:      0.215
    32,000:      0.438
    64,000:      0.896
   128,000:      1.801
   256,000:      3.620
   512,000:      7.302
 1,024,000:     15.110

which is fine as the runtime grows linear with the problem size.

A line-by-line profiler shows that addVariable is the problem:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           @profile
     4                                           def func():
     5         1          1.0      1.0      0.0      size = 64000
     6                                           
     7         1        230.0    230.0      0.0      p = xp.problem()
     8     64001      49543.0      0.8      0.2      for i in range(size):
     9     64000     216261.0      3.4      0.7          v = xp.var(name=f"v{i}")
    10     64000      83085.0      1.3      0.3          w = xp.var(name=f"w{i}")
    11     64000   27572217.0    430.8     94.0          p.addVariable(v, w)
    12     64000    1409492.0     22.0      4.8          p.addConstraint(v ** 2 + w ** 2 <= 0)
 

3 Replies

  • FICO's avatar
    FICO
    Community Manager

    Thanks Michaja. It is a performance issue that is due to the extra overhead we need in order to link variables with quadratic constraints---it is indeed within the getVariable() call that most of the time is spent. We will fix this issue in a future release, but please note that for efficiency it would be far better to aggregate both the generation of variables and the insertion of constraints, as in the following code:

    def create_qcqp(size):
        import xpress as xp
        p = xp.problem()
     
        x = [[xp.var(name=f"v{i}"), xp.var(name=f"w{i}")] for i in range(size)]
        p.addVariable(x)
        p.addConstraint(x[i][0]**2 + x[i][1]**2 <= 0 for i in range(size))

    These are the times I get using your script with the modified function above (on a decent laptop):​

        2,000:     0.020
        4,000:     0.039
        8,000:     0.077
       16,000:     0.152
       32,000:     0.301
       64,000:     0.619
      128,000:     1.271
      256,000:     2.700
      512,000:     5.643
     1,024,000:    11.469
  • FICO's avatar
    FICO
    Community Manager

    Thanks for the reply!

    Manually aggregating variables and constraints is very cumbersome, if the problem is complicated and distributed over dozens of source files (which it is in my case). Great if the addVariable function can be fixed to have constant-time overhead, so that aggregation is not that critical.

    For the time being I will wrap the Xpress problem class and do the aggregation in the wrapper, so that I don't have to change all my existing code.

  • FICO's avatar
    FICO
    Community Manager

    Hi, it's understandable that some modifications can't be made in bulk, so your workaround is for now ideal.