In [1] I was trying out different formulations of a large, sparse (but very easy) transportation model using different modeling tools and solvers. The conclusion was:
- Exploiting sparsity leads to the best performance
- GAMS/Cplex is about 10 times as fast as Python/PuLP/CBC
- GAMS/Cplex is about 1000 times as fast as R/OMPR/GLPK
Don't overinterpret these numbers: this is a single model, which may not be at all representable for your models. Let's see how CVXPY[2] and CVXR[3] are doing. To recap we want to solve a large (but easy) transportation model:
| Dense Transportation Model |
|---|
| \[\begin{align}\min&\sum_{i,j}\color{darkblue}c_{i,j}\cdot\color{darkred}x_{i,j}\\ & \sum_j \color{darkred}x_{i,j} \le \color{darkblue}a_i && \forall i \\& \sum_i \color{darkred}x_{i,j} \ge \color{darkblue}b_j && \forall j \\ & \color{darkred}x_{i,j} \ge 0 \end{align}\] |
The additional wrinkle is that only a small fraction of the links \(i \rightarrow j\) exist.
There are (at least) three ways to model this:
- Use a large cost coefficient for forbidden links.
- Fix \(\color{darkred}x_{i,j}=0\) for non-existing links.
- Exploit sparsity directly by only summing over existing links.
CVXPY/CVXR are matrix-based modeling tools. That gives some extra things to think about. A transportation model in matrix-notation can look like:
| Transportation Model in Matrix Notation |
|---|
| \[\begin{align}\min\>&{\bf tr}(\color{darkblue}C^T\color{darkred}X)\\&\color{darkred}X\color{darkblue}e \le \color{darkblue}a \\ & \color{darkred}X^t\color{darkblue}e \ge \color{darkblue}b \\ & \color{darkred}X \ge 0 \end{align}\] |
#-------------------------------------------------
# dense transportation model
# matrix formulation
#-------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X")
# two different e vectors
ei = np.ones(capacity.shape)
ej = np.ones(demand.shape)
prob = cp.Problem(
cp.Minimize(cp.trace(cost.T@X)),
[X.T@ei >= demand,
X@ej <= capacity,
X>=0])
prob.solve(verbose=True)
print("status:",prob.status)
print("objective:",prob.value)
Here @ indicates matrix multiplication. Arguably, a more readable model is using elementwise multiplication and summations:
Here cp.multiply() indicates elementwise multiplication.
#-------------------------------------------------
# dense transportation model
# sum formulation
#-------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X")
prob = cp.Problem(
cp.Minimize(cp.sum(cp.multiply(cost,X))),
[cp.sum(X,axis=0) >= demand,
cp.sum(X,axis=1) <= capacity,
X >= 0])
prob.solve(verbose=True)
print("status:",prob.status)
print("objective:",prob.value)
Here cp.multiply() indicates elementwise multiplication.
The same model in R, using cvxr can look like:
X <- Variable(rows=n,cols=m,name='X',nonneg=T)
prob <- Problem(Minimize(sum_entries(cost*X)),
constraints = list(
sum_entries(X,axis=1) >= dem,
sum_entries(X,axis=2) <= cap))
res <- solve(prob,verbose=T)
res$status
res$value
Solvers
I use the default solvers for this exercise, and default settings. For CVXPY the default solver is ECOS[4], and CVXR uses OSQP[5] by default. These are not standard Simplex-based solvers.
Formulation 1: Dense Transportation Model with Large Cost Coefficients.
Here we apply the costs:
largecost = cost + (1-link)*1.0e6
where link is a 0/1 constant matrix with values: 0 if the link does not exist, and 1 if the link exists.
CVXPY uses the conic algorithm ECOS for solving LPs. This solver cannot solve the problem using default settings:
98-4.660e+04-4.660e+04+5e+051e-047e-158e-022e+000.02326e-01122 | 00
99-4.655e+04-4.655e+04+4e+051e-049e-151e-012e+000.02727e-01122 | 00
100-4.654e+04-4.654e+04+4e+051e-041e-141e-012e+000.04077e-01121 | 00
Maximum number of iterations reached, recovering best iterate (88) and stopping.
RAN OUT OF ITERATIONS (reached feastol=1.2e-05, reltol=4.7e-02, abstol=1.1e+03).
Runtime: 13.731298 seconds.
We can add a larger iteration limit. This gives:
257+1.202e+04+1.202e+04+2e-013e-089e-174e-077e-070.98907e-02111 | 00
258+1.202e+04+1.202e+04+5e-031e-096e-171e-082e-080.97131e-04211 | 00
259+1.202e+04+1.202e+04+5e-051e-118e-171e-102e-100.98901e-04211 | 00
OPTIMAL (within feastol=1.4e-11, reltol=4.4e-09, abstol=5.3e-05).
Runtime: 34.450007 seconds.
-------------------------------------------------------------------------------
Summary
-------------------------------------------------------------------------------
(CVXPY) Aug 1012:44:50 PM: Problem status: optimal
(CVXPY) Aug 1012:44:50 PM: Optimal value: 1.202e+04
(CVXPY) Aug 1012:44:50 PM: Compilation took 5.605e+00 seconds
(CVXPY) Aug 1012:44:50 PM: Solver (including time spent in interface) took 3.446e+01 seconds
status: optimal
objective: 12015.21812055369
96002.5164e+055.37e-046.25e+001.01e-016.13e+01s
98002.5184e+055.37e-045.97e+001.01e-016.25e+01s
100002.5167e+055.37e-045.98e+001.01e-016.38e+01s
status: solved inaccurate
number of iterations: 10000
optimal objective: 251671.7459
run time: 6.38e+01s
optimal rho estimate: 1.53e-01
Warning messages:
1: In size(const) : NAs introduced by coercion to integer range
2: In size(const) : NAs introduced by coercion to integer range
optimal objective: 94120.8210we are actually not close to the optimal solution.
This is a disappointing result.
Formulation 2: Dense Transportation Model with Fixed Variables.
The second formulation is using:
x[i,j] = 0 if link[i,j] = 0
I don't really know how to do this in a vectorized way in CVXPY/CVXR. Very strange that this is not supported, as this is an operation that we see quite often. We could form a long series of scalar equalities. I chose to formulate:
x[i,j]*(1-link[i,j]) = 0
or in matrix notation: \[\color{darkred}X \circ (\color{darkred}e\cdot\color{darkred}e^T-\color{darkblue}{\mathit link}) = 0\] where \(\circ\) indicates the Hadamar or element-wise product. In Python notation this becomes: cp.multiply(1-link,X)==0.
When running this with CVXPY/ECOS we see success!
C:\tmp>py trnsport_cvx.py
CHECK: sum capacity = 10000.000000000016, sum demand = 10000.000000000002
===============================================================================
CVXPY
v1.2.1
===============================================================================
(CVXPY) Aug 1001:16:27 AM: Your problem has 250000 variables, 4 constraints, and 0 parameters.
(CVXPY) Aug 1001:16:27 AM: It is compliant with the following grammars: DCP, DQCP
(CVXPY) Aug 1001:16:27 AM: (If you need to solve this problem multiple times, but with different data, consider using parameters.)
(CVXPY) Aug 1001:16:27 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.
-------------------------------------------------------------------------------
Compilation
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:16:27 AM: Compiling problem (target solver=ECOS).
(CVXPY) Aug 1001:16:27 AM: Reduction chain: Dcp2Cone -> CvxAttr2Constr -> ConeMatrixStuffing -> ECOS
(CVXPY) Aug 1001:16:27 AM: Applying reduction Dcp2Cone
(CVXPY) Aug 1001:16:27 AM: Applying reduction CvxAttr2Constr
(CVXPY) Aug 1001:16:27 AM: Applying reduction ConeMatrixStuffing
(CVXPY) Aug 1001:16:27 AM: Applying reduction ECOS
(CVXPY) Aug 1001:16:27 AM: Finished problem compilation (took 4.100e-01 seconds).
-------------------------------------------------------------------------------
Numerical solver
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:16:27 AM: Invoking solver ECOS to obtain a solution.
ECOS 2.0.10 - (C) embotech GmbH, Zurich Switzerland, 2012-15. Web: www.embotech.com/ECOS
It pcost dcost gap pres dres k/t mu step sigma IR | BT
0+5.471e+04+5.471e+04+2e+065e-015e-011e+001e+01 --- --- 11 - | - -
1+3.188e+04+3.188e+04+1e+063e-012e-012e+005e+000.69913e-01111 | 00
2+2.670e+04+2.670e+04+1e+062e-012e-013e+004e+000.35116e-01110 | 00
3+1.558e+04+1.558e+04+4e+058e-026e-022e+002e+000.95183e-01111 | 00
4+1.381e+04+1.381e+04+2e+053e-022e-028e-017e-010.72072e-01110 | 00
5+1.318e+04+1.318e+04+1e+052e-022e-024e-014e-010.67655e-01110 | 00
6+1.261e+04+1.261e+04+5e+048e-037e-032e-012e-010.61741e-01111 | 00
7+1.235e+04+1.235e+04+3e+044e-033e-039e-021e-010.66613e-01110 | 00
8+1.218e+04+1.218e+04+1e+042e-031e-033e-025e-020.77333e-01111 | 00
9+1.211e+04+1.211e+04+6e+038e-047e-042e-022e-020.71993e-01111 | 00
10+1.206e+04+1.206e+04+3e+034e-044e-048e-031e-020.74493e-01111 | 00
11+1.204e+04+1.204e+04+1e+032e-042e-043e-035e-030.60629e-02111 | 00
12+1.202e+04+1.202e+04+5e+027e-057e-051e-032e-030.67821e-01111 | 00
13+1.202e+04+1.202e+04+2e+022e-052e-054e-046e-040.78781e-01111 | 00
14+1.202e+04+1.202e+04+9e+011e-051e-052e-044e-040.49282e-01211 | 00
15+1.202e+04+1.202e+04+3e+013e-063e-066e-051e-040.85732e-01211 | 00
16+1.202e+04+1.202e+04+2e+012e-062e-064e-056e-050.66354e-01312 | 00
17+1.202e+04+1.202e+04+2e+002e-072e-075e-067e-060.89472e-02412 | 00
18+1.202e+04+1.202e+04+2e-013e-082e-085e-078e-070.93434e-02623 | 00
19+1.202e+04+1.202e+04+2e-033e-103e-106e-099e-090.98901e-03111 | 00
20+1.202e+04+1.202e+04+3e-053e-123e-126e-111e-100.98901e-04200 | 00
OPTIMAL (within feastol=3.4e-12, reltol=2.2e-09, abstol=2.6e-05).
Runtime: 4.309761 seconds.
-------------------------------------------------------------------------------
Summary
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:16:31 AM: Problem status: optimal
(CVXPY) Aug 1001:16:31 AM: Optimal value: 1.202e+04
(CVXPY) Aug 1001:16:31 AM: Compilation took 4.100e-01 seconds
(CVXPY) Aug 1001:16:31 AM: Solver (including time spent in interface) took 4.322e+00 seconds
status: optimal
objective: 12015.21812552263
C:\tmp>
The first thing we notice is that counting is difficult: Your problem has 250000 variables, 4 constraints. We seem to count here individual variables and constraint blocks. I guarantee you there are more than 4 constraints. That sounds strange. I mean, why in the world would you do that? Choose the same units!
Now let's see if we also have some success with the CVXR/OSQP setup. Unfortunately, this still does not solve:
-----------------------------------------------------------------
OSQP v0.6.0 - Operator Splitting QP Solver
(c) Bartolomeo Stellato, Goran Banjac
University of Oxford - Stanford University 2019
-----------------------------------------------------------------
problem: variables n = 250000, constraints m = 501000
nnz(P) + nnz(A) = 950334
settings: linear system solver = qdldl,
eps_abs = 1.0e-05, eps_rel = 1.0e-05,
eps_prim_inf = 1.0e-04, eps_dual_inf = 1.0e-04,
rho = 1.00e-01 (adaptive),
sigma = 1.00e-06, alpha = 1.60, max_iter = 10000
check_termination: on (interval 25),
scaling: on, scaled_termination: off
warm start: on, polish: on, time_limit: off
iter objective pri res dua res rho time
1-5.4672e+054.33e+014.22e+011.00e-018.30e-01s
2001.2118e+045.96e-021.12e-011.00e-013.08e+00s
4001.2047e+041.95e-029.76e-021.00e-015.46e+00s
6001.2031e+047.57e-033.55e-021.00e-017.73e+00s
8001.2043e+044.77e-032.27e-021.00e-019.95e+00s
10001.2023e+041.63e-021.40e-021.87e-021.23e+01s
12001.2055e+047.39e-038.88e-031.87e-021.45e+01s
14001.2034e+046.94e-037.34e-031.87e-021.67e+01s
16001.2048e+043.92e-034.51e-031.87e-021.89e+01s
18001.2047e+043.34e-034.63e-031.87e-022.12e+01s
20001.2038e+043.35e-032.90e-031.87e-022.34e+01s
22001.2041e+042.34e-032.50e-031.87e-022.56e+01s
24001.2040e+042.32e-032.47e-031.87e-022.78e+01s
26001.2046e+041.75e-031.82e-031.87e-023.00e+01s
28001.2044e+041.86e-031.45e-031.87e-023.22e+01s
30001.2044e+041.39e-031.47e-031.87e-023.45e+01s
32001.2041e+041.33e-031.34e-031.87e-023.67e+01s
34001.2045e+041.02e-031.62e-031.87e-023.89e+01s
36001.2043e+049.00e-041.20e-031.87e-024.12e+01s
38001.2043e+041.04e-031.12e-031.87e-024.34e+01s
40001.2040e+041.32e-031.17e-031.87e-024.56e+01s
42001.2044e+047.46e-049.03e-041.87e-024.78e+01s
44001.2043e+046.78e-048.80e-041.87e-025.00e+01s
46001.2042e+046.31e-041.19e-031.87e-025.22e+01s
48001.2042e+047.80e-048.06e-041.87e-025.44e+01s
50001.2042e+046.57e-048.05e-041.87e-025.67e+01s
52001.2044e+046.17e-049.06e-041.87e-025.89e+01s
54001.2042e+046.12e-046.06e-041.87e-026.11e+01s
56001.2041e+046.23e-046.60e-041.87e-026.33e+01s
58001.2041e+043.80e-047.29e-041.87e-026.55e+01s
60001.2043e+045.39e-046.00e-041.87e-026.78e+01s
62001.2043e+044.63e-046.70e-041.87e-027.00e+01s
64001.2042e+044.04e-046.56e-041.87e-027.22e+01s
66001.2041e+045.45e-044.48e-041.87e-027.44e+01s
68001.2042e+046.47e-045.94e-041.87e-027.66e+01s
70001.2043e+043.81e-045.65e-041.87e-027.88e+01s
72001.2043e+047.66e-044.04e-041.87e-028.11e+01s
74001.2042e+042.75e-045.74e-041.87e-028.33e+01s
76001.2043e+045.49e-044.90e-041.87e-028.55e+01s
78001.2042e+043.88e-043.64e-041.87e-028.77e+01s
80001.2042e+043.03e-045.45e-041.87e-028.99e+01s
82001.2043e+043.03e-045.13e-041.87e-029.21e+01s
84001.2043e+043.23e-043.85e-041.87e-029.43e+01s
86001.2042e+043.14e-044.93e-041.87e-029.65e+01s
88001.2043e+042.98e-043.68e-041.87e-029.87e+01s
90001.2042e+043.10e-042.46e-041.87e-021.01e+02s
92001.2043e+042.24e-044.20e-041.87e-021.03e+02s
94001.2042e+042.69e-043.78e-041.87e-021.05e+02s
96001.2042e+042.80e-042.56e-041.87e-021.08e+02s
98001.2043e+042.09e-044.05e-041.87e-021.10e+02s
100001.2042e+042.78e-043.05e-041.87e-021.12e+02s
status: solved inaccurate
number of iterations: 10000
optimal objective: 12042.4795
run time: 1.12e+02s
optimal rho estimate: 9.00e-03
Warning messages:
1: In size(const) : NAs introduced by coercion to integer range
2: In size(const) : NAs introduced by coercion to integer range
3: In size(const) : NAs introduced by coercion to integer range
Formulation 3: exploit sparsity
This is a more difficult exercise. CVXPY and CVXR do not have a conditional generation facility. Or in other words, we cannot generate a sparse variable. In practical modeling, this is rather essential. So how can we generate only variables that are related to existing links? Well, we can do everything ourselves and basically do all the work that CVXPY/CVXR is supposed to do. We can also fudge things a bit. We still generate all variables, but we multiply each variable in each constraint by 0 if it should not exist. This is only a partial solution, as we still generate all variables. However, a variable with all zeros in its column should be removed by the solver. We hope that is happening, but we have little feedback that is actually happening.
In CVXPY I did:
#----------------------------------------------------
# model 3
# exploit sparsity
#----------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X")
prob = cp.Problem(
cp.Minimize(cp.sum(cp.multiply(cp.multiply(cost,plink),X))),
[cp.sum(cp.multiply(plink,X),axis=0) >= demand,
cp.sum(cp.multiply(plink,X),axis=1) <= capacity,
cp.multiply(plink,X)>=0]
)
prob.solve(verbose=True)
print("status:",prob.status)
print("objective:",prob.value)
This actually seems to help a bit:
C:\tmp>py trnsport_cvx.py
CHECK: sum capacity = 10000.000000000016, sum demand = 10000.000000000002
===============================================================================
CVXPY
v1.2.1
===============================================================================
(CVXPY) Aug 1001:57:25 AM: Your problem has 250000 variables, 3 constraints, and 0 parameters.
(CVXPY) Aug 1001:57:25 AM: It is compliant with the following grammars: DCP, DQCP
(CVXPY) Aug 1001:57:25 AM: (If you need to solve this problem multiple times, but with different data, consider using parameters.)
(CVXPY) Aug 1001:57:25 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.
-------------------------------------------------------------------------------
Compilation
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:57:25 AM: Compiling problem (target solver=ECOS).
(CVXPY) Aug 1001:57:25 AM: Reduction chain: Dcp2Cone -> CvxAttr2Constr -> ConeMatrixStuffing -> ECOS
(CVXPY) Aug 1001:57:25 AM: Applying reduction Dcp2Cone
(CVXPY) Aug 1001:57:25 AM: Applying reduction CvxAttr2Constr
(CVXPY) Aug 1001:57:25 AM: Applying reduction ConeMatrixStuffing
(CVXPY) Aug 1001:57:25 AM: Applying reduction ECOS
(CVXPY) Aug 1001:57:25 AM: Finished problem compilation (took 1.830e-01 seconds).
-------------------------------------------------------------------------------
Numerical solver
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:57:25 AM: Invoking solver ECOS to obtain a solution.
ECOS 2.0.10 - (C) embotech GmbH, Zurich Switzerland, 2012-15. Web: www.embotech.com/ECOS
It pcost dcost gap pres dres k/t mu step sigma IR | BT
0+5.471e+04+5.471e+04+2e+065e-013e-011e+001e+01 --- --- 11 - | - -
1+3.188e+04+3.188e+04+1e+063e-011e-012e+005e+000.69913e-01111 | 00
2+2.670e+04+2.670e+04+1e+062e-011e-013e+004e+000.35116e-01110 | 00
3+1.558e+04+1.558e+04+4e+058e-024e-022e+002e+000.95193e-01110 | 00
4+1.381e+04+1.381e+04+2e+053e-022e-028e-017e-010.72072e-01100 | 00
5+1.318e+04+1.318e+04+1e+052e-021e-024e-014e-010.67655e-01100 | 00
6+1.261e+04+1.261e+04+5e+048e-035e-032e-012e-010.61741e-01100 | 00
7+1.235e+04+1.235e+04+3e+044e-033e-039e-021e-010.66613e-01100 | 00
8+1.218e+04+1.218e+04+1e+042e-031e-033e-025e-020.77333e-01101 | 00
9+1.211e+04+1.211e+04+6e+038e-046e-042e-022e-020.71983e-01111 | 00
10+1.206e+04+1.206e+04+3e+034e-043e-048e-031e-020.74493e-01111 | 00
11+1.204e+04+1.204e+04+1e+032e-041e-043e-035e-030.60639e-02111 | 00
12+1.202e+04+1.202e+04+5e+027e-055e-051e-032e-030.67821e-01111 | 00
13+1.202e+04+1.202e+04+2e+022e-052e-054e-046e-040.78781e-01111 | 00
14+1.202e+04+1.202e+04+9e+011e-059e-062e-044e-040.49292e-01111 | 00
15+1.202e+04+1.202e+04+3e+013e-063e-066e-051e-040.85682e-01111 | 00
16+1.202e+04+1.202e+04+2e+012e-062e-064e-056e-050.66384e-01111 | 00
17+1.202e+04+1.202e+04+2e+002e-072e-075e-067e-060.89452e-02111 | 00
18+1.202e+04+1.202e+04+2e-013e-082e-085e-078e-070.93414e-02211 | 00
19+1.202e+04+1.202e+04+2e-033e-102e-106e-099e-090.98901e-03300 | 00
20+1.202e+04+1.202e+04+3e-053e-123e-126e-111e-100.98901e-04500 | 00
OPTIMAL (within feastol=3.4e-12, reltol=2.2e-09, abstol=2.6e-05).
Runtime: 2.398611 seconds.
-------------------------------------------------------------------------------
Summary
-------------------------------------------------------------------------------
(CVXPY) Aug 1001:57:27 AM: Problem status: optimal
(CVXPY) Aug 1001:57:27 AM: Optimal value: 1.202e+04
(CVXPY) Aug 1001:57:27 AM: Compilation took 1.830e-01 seconds
(CVXPY) Aug 1001:57:27 AM: Solver (including time spent in interface) took 2.408e+00 seconds
status: optimal
objective: 12015.218125523525
C:\tmp>
I also tried this with CVXR/OSQP, but the result was still a non-optimal solution: solved inaccurate. After increasing the iteration limit the status became solved. However, the objective value was not really close to the optimal objective.
Conclusion
The conclusion is not encouraging for OSQP. That solver has difficulties finding the optimal solution.
| CVXPY/ECOS | CVXR/OSQP | |||||
|---|---|---|---|---|---|---|
| Large Cost | Fixed Variables | Sparse Formulation | Large Cost | Fixed Variables | Sparse Formulation | |
| Variables | 250,000 | 250,000 | 250,000 | 250,000 | 250,000 | 250,000 |
| Constraints | na | na | na | na | na | na |
| Status | Optimal | Optimal | Optimal | solved | solved | solved |
| Objective | 12015.218 | 12015.218 | 12015.218 | 94120.8210 | 12042.4629 | 12042.4477 |
| Compilation Time | 5.6 | 0.4 | 0.1 | |||
| Solver Time | 34.5 | 4.4 | 2.3 | 69.3 | 169 | 93.8 |
| Iterations | 259 | 20 | 20 | 10775 | 14450 | 16100 |
CVXPY and CVXR do not allow variables with more than 2 dimensions. Also, it does not support sparse variables. ECOS delivers correct optimal solutions, but OSQP gives suboptimal solutions when solving this very large but very easy LP. Note that this is with default tolerances eps_rel and eps_abs. We can tighten these to get better solutions, but at a considerable computational cost. OSQP is a bit of a special solver based on a so-called first-order method (allowing relatively large problems to be approximated quickly using limited amounts of memory). It may not be the most suited as a default solver.
It is interesting to see that our haphazard sparse formulation works in the sense that it gives the correct solution, but in less time (compilation time and solver time).
The sparse formulation in CVXPY/ECOS is about 10 times as slow as GAMS/CPLEX [1].
References
- Transportation model with some non-existing links, https://yetanothermathprogrammingconsultant.blogspot.com/2022/07/transportation-model-with-some-non.html
- Welcome to CVXPY 1.2, https://www.cvxpy.org/
- Convex Optimization in R, https://cvxr.rbind.io/
- Alexander Domahidi, Eric Chu, Stephen Boyd, ECOS: An SOCP solver for embedded systems, Proceedings European Control Conference, pages 3071-3076, Zurich, July 2013.
- OSQP, https://osqp.org/
Appendix: Python code
import numpy as np
import pickle
import pandas as pd
import cvxpy as cp
#
# get the GAMS data
# stored in a pickle file
#
datafile = r"c:\tmp\assignment.pkl"
data = pickle.load(open(datafile,'rb'))
capacity = data['capacity']
demand = data['demand']
cost = data['cost']
plink = data['plink']
#
# we should have a balanced problem
#
print(f"CHECK: sum capacity = {sum(capacity)}, sum demand = {sum(demand)}")
#-------------------------------------------------
# problem 1: large cost
#-------------------------------------------------
largecost = cost+(1-plink)*1.0e6
#-------------------------------------------------
# dense transportation model
# matrix formulation
#-------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X")
# two different e vectors
ei = np.ones(capacity.shape)
ej = np.ones(demand.shape)
prob = cp.Problem(
cp.Minimize(cp.trace(largecost.T@X)),
[X.T@ei >= demand,
X@ej <= capacity,
X>=0])
prob.solve(verbose=True,max_iters=1000)
print("status:",prob.status)
print("objective:",prob.value)
#-------------------------------------------------
# dense transportation model
# sum formulation
#-------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X",nonneg=True)
prob = cp.Problem(
cp.Minimize(cp.sum(cp.multiply(largecost,X))),
[cp.sum(X,axis=0) >= demand,
cp.sum(X,axis=1) <= capacity])
prob.solve(verbose=True,max_iters=1000)
print("status:",prob.status)
print("objective:",prob.value)
#----------------------------------------------------
# model 2
# fix variables
#----------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X",nonneg=True)
prob = cp.Problem(
cp.Minimize(cp.sum(cp.multiply(cost,X))),
[cp.sum(X,axis=0) >= demand,
cp.sum(X,axis=1) <= capacity,
cp.multiply(1-plink,X)==0])
prob.solve(verbose=True)
print("status:",prob.status)
print("objective:",prob.value)
#----------------------------------------------------
# model 3
# exploit sparsity (somewhat)
#----------------------------------------------------
# X has same shape as our cost matrix
X = cp.Variable(cost.shape,"X")
prob = cp.Problem(
cp.Minimize(cp.sum(cp.multiply(cp.multiply(cost,plink),X))),
[cp.sum(cp.multiply(plink,X),axis=0) >= demand,
cp.sum(cp.multiply(plink,X),axis=1) <= capacity,
cp.multiply(plink,X)>=0]
)
prob.solve(verbose=True)
print("status:",prob.status)
print("objective:",prob.value)
Appendix: R code
#-----------------------------------------------------------
# libraries
#-----------------------------------------------------------
library(CVXR)
#-----------------------------------------------------------
# read GAMS data
# capacity, demand, c, plink
#-----------------------------------------------------------
load(file="c:/tmp/gamsdata.Rdata")
#-----------------------------------------------------------
# convert from dataframes to matrices/vectors
#-----------------------------------------------------------
convSup <- 1:nrow(capacity)
names(convSup) <- capacity$i
convDem <- 1:nrow(demand)
names(convDem) <- demand$j
cost<-matrix(data=0,nrow=length(convSup),ncol=length(convDem))
# loop is fast enough
for (r in1:nrow(c)) {
cost[convSup[c$i[r]],convDem[c$j[r]]] = c$value[r]
}
islink<-matrix(data=0,nrow=length(convSup),ncol=length(convDem))
# loop is fast enough
for (r in1:nrow(plink)) {
islink[convSup[plink$i[r]],convDem[plink$j[r]]] = plink$value[r]
}
cap <- capacity$value
dem <- demand$value
paste(sum(cap),sum(dem))
n <- length(cap)
m <- length(dem)
#-----------------------------------------------------------
# Model 1: large cost
#-----------------------------------------------------------
largecost = cost + (1-islink)*1e6
X <- Variable(rows=n,cols=m,name='X',nonneg=T)
prob <- Problem(Minimize(sum_entries(largecost*X)),
constraints = list(
sum_entries(X,axis=1) >= dem,
sum_entries(X,axis=2) <= cap))
res <- solve(prob,verbose=T,max_iter=1000000)
res$status
res$value
#-----------------------------------------------------------
# Model 2: Fixed Variables
#-----------------------------------------------------------
X <- Variable(rows=n,cols=m,name='X',nonneg=T)
prob <- Problem(Minimize(sum_entries(cost*X)),
constraints = list(
sum_entries(X,axis=1) >= dem,
sum_entries(X,axis=2) <= cap,
X*(1-islink)==0))
res <- solve(prob,verbose=T,max_iter=1000000)
res$status
res$value
#-----------------------------------------------------------
# Model 3: Exploit sparsity
#-----------------------------------------------------------
X <- Variable(rows=n,cols=m,name='X')
prob <- Problem(Minimize(sum_entries(cost*islink*X)),
constraints = list(
sum_entries(islink*X,axis=1) >= dem,
sum_entries(islink*X,axis=2) <= cap,
islink*X>=0))
res <- solve(prob,verbose=T,max_iter=1000000)
res$status
res$value