Skip to content

Commit 979e032

Browse files
committed
Merge branch 'release-0.4'
2 parents a5a58c9 + b50b02d commit 979e032

File tree

7 files changed

+162
-29
lines changed

7 files changed

+162
-29
lines changed

.travis.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: python
22
python:
33
- "2.7"
4-
- "3.4"
54
before_install:
65
- sudo apt-get update -qq
76
- sudo apt-get install -qq libgmp-dev libgmp10
@@ -14,5 +13,9 @@ before_install:
1413

1514
# Install Cython
1615
- pip install cython
16+
- pip install tox
1717

18-
script: ./setup.py test
18+
script: tox -v
19+
env:
20+
- TOXENV=py27
21+
- TOXENV=py34

README.rst

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ QSopt\_ex Python bindings
55
:alt: Build Status
66
:target: https://travis-ci.org/jonls/python-qsoptex
77

8+
.. image:: https://badge.fury.io/py/python-qsoptex.svg
9+
:alt: PyPI badge
10+
:target: http://badge.fury.io/py/python-qsoptex
11+
812
Usage
913
-----
1014

@@ -23,30 +27,62 @@ other ``numbers.Rational``) or anything that can be converted to
2327
.. code:: python
2428
2529
import qsoptex
30+
import logging
31+
32+
logging.basicConfig(level=logging.DEBUG)
2633
2734
p = qsoptex.ExactProblem()
2835
2936
p.add_variable(name='x', objective=2, lower=3.5, upper=17.5)
3037
p.add_variable(name='y', objective=-1, lower=None, upper=2)
31-
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'x': 1, 'y': 1}, rhs=0)
38+
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL,
39+
{'x': 1, 'y': 1}, rhs=0)
3240
p.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
3341
3442
p.set_param(qsoptex.Parameter.SIMPLEX_DISPLAY, 1)
3543
status = p.solve()
3644
if status == qsoptex.SolutionStatus.OPTIMAL:
37-
print 'Optimal solution'
38-
print p.get_objective_value()
39-
print p.get_value('x')
45+
print('Optimal solution')
46+
print(p.get_objective_value())
47+
print(p.get_value('x'))
4048
4149
The module is also able to load problems from external files:
4250

4351
.. code:: python
4452
4553
p = qsoptex.ExactProblem()
46-
p.read('netlib/cycle.mps', filetype='MPS') # 'LP' is also supported
54+
p.read('netlib/cycle.mps', filetype='MPS') # 'LP' is also supported
4755
p.set_param(qsoptex.Parameter.SIMPLEX_DISPLAY, 1)
4856
status = p.solve()
4957
58+
Known issues
59+
------------
60+
61+
When creating a problem with the QSopt\_ex library, the variables and
62+
constraints will be assigned a default name if no name is specified by the
63+
user. Variables will be named ``xN`` or ``x_N`` and constraints will be named
64+
``cN`` or ``c_N`` (where ``N`` is an integer). If the user later adds a named
65+
variable or constraint which uses a name that is already in use, the name of
66+
the new variable or constraint will be silently changed by the QSopt\_ex
67+
library. For example, the last line of the following code will remove the
68+
first constraint from the problem, not the second.
69+
70+
.. code:: python
71+
72+
p = qsoptex.ExactProblem()
73+
p.add_variable(name='x', objective=2, lower=3.5, upper=17.5)
74+
p.add_variable(name='y', objective=-1, lower=None, upper=2)
75+
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL,
76+
{'x': 1, 'y': 1}, rhs=0)
77+
p.add_linear_constraint(qsoptex.ConstraintSense.LESS,
78+
{'x': 1}, rhs=15, name='c1')
79+
# Deletes the first constraint, not the second
80+
p.delete_linear_constraint('c1')
81+
82+
This issue can be avoided by always assigning names to variables and
83+
constraints, or by avoiding using the same names as QSopt\_ex uses as default
84+
names.
85+
5086
Building
5187
--------
5288

@@ -73,4 +109,3 @@ For example, if GnuMP is installed in the ``/opt/local`` prefix
73109
74110
$ GMP_INCLUDE_DIR=/opt/local/include GMP_LIBRARY_DIR=/opt/local/lib \
75111
./setup.py install
76-

cqsoptex.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ cdef extern from 'qsopt_ex/qsopt_mpq.h' nogil:
7676
int mpq_QSget_rowcount(mpq_QSdata* problem)
7777

7878
int mpq_QSget_column_index(mpq_QSdata* problem, const char* name, int* index)
79+
int mpq_QSget_row_index(mpq_QSdata* problem, const char* name, int* index)
7980

8081
int mpq_QSchange_objsense(mpq_QSdata* problem, int sense)
8182
int mpq_QSchange_objcoef(mpq_QSdata* problem, int index, mpq_t value)
8283

8384
int mpq_QSnew_col(mpq_QSdata* problem, const mpq_t objective, const mpq_t lower, const mpq_t upper, const char* name)
8485
int mpq_QSadd_row(mpq_QSdata* problem, int count, int* indices, const mpq_t* values, const mpq_t* rhs, char sense, const char* name)
86+
int mpq_QSdelete_col(mpq_QSdata* problem, int index)
87+
int mpq_QSdelete_row(mpq_QSdata* problem, int index)
8588

8689
mpq_QSdata* mpq_QSread_prob(const char* filepath, const char* filetype)
8790
int mpq_QSwrite_prob(mpq_QSdata* problem, const char* filepath, const char* filetype)

qsoptex.pyx

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,8 @@ cdef class ExactProblem:
221221
self._c_sol_nvars = 0
222222
self._c_sol_x = NULL
223223

224-
def _index_maybe_string(self, variable):
225-
"""Get the column index from a variable that is either name (string)
226-
or index"""
224+
def _col_index_maybe_string(self, variable):
225+
"""Get the column index from either name (string) or index"""
227226

228227
cdef int r, colindex
229228

@@ -238,6 +237,22 @@ cdef class ExactProblem:
238237
else:
239238
return int(variable)
240239

240+
def _row_index_maybe_string(self, constraint):
241+
"""Get the row index from either name (string) or index"""
242+
243+
cdef int r, rowindex
244+
245+
if isinstance(constraint, string_types):
246+
constraint = _chars(constraint)
247+
r = cqsoptex.mpq_QSget_row_index(
248+
self._c_qsdata, constraint, &rowindex)
249+
if r != 0:
250+
raise ExactProblemError(
251+
'An error occured in QSget_row_index()')
252+
return rowindex
253+
else:
254+
return int(constraint)
255+
241256
def get_variable_count(self):
242257
"""Get number of variables defined in the problem"""
243258
return cqsoptex.mpq_QSget_colcount(self._c_qsdata)
@@ -307,6 +322,20 @@ cdef class ExactProblem:
307322
cgmp.mpq_clear(lower_q)
308323
cgmp.mpq_clear(upper_q)
309324

325+
def delete_variable(self, variable):
326+
"""Delete a linear constraint
327+
328+
The constraint can be specified as an integer index or as a named
329+
constraint.
330+
"""
331+
cdef index = self._col_index_maybe_string(variable)
332+
if index < 0:
333+
raise IndexError('Invalid variable index')
334+
335+
cdef r = cqsoptex.mpq_QSdelete_col(self._c_qsdata, index)
336+
if r != 0:
337+
raise ExactProblemError('An error occured in QSdelete_col()')
338+
310339
def add_linear_constraint(self, sense, values=None, rhs=0, name=None):
311340
"""Add linear constraint to problem"""
312341
cdef cgmp.mpq_t rhs_q
@@ -363,7 +392,7 @@ cdef class ExactProblem:
363392
variable, value = pair
364393

365394
# Get variable index and value
366-
indices[i] = self._index_maybe_string(variable)
395+
indices[i] = self._col_index_maybe_string(variable)
367396
mpq_set_pynumeric(values_q[i], value)
368397

369398
# Set right-hand side
@@ -390,6 +419,20 @@ cdef class ExactProblem:
390419
cgmp.mpq_clear(values_q[i])
391420
stdlib.free(values_q)
392421

422+
def delete_linear_constraint(self, constraint):
423+
"""Delete a linear constraint
424+
425+
The constraint can be specified as an integer index or as a named
426+
constraint.
427+
"""
428+
cdef index = self._row_index_maybe_string(constraint)
429+
if index < 0:
430+
raise IndexError('Invalid constraint index')
431+
432+
cdef r = cqsoptex.mpq_QSdelete_row(self._c_qsdata, index)
433+
if r != 0:
434+
raise ExactProblemError('An error occured in QSdelete_row()')
435+
393436
def set_objective_sense(self, sense):
394437
"""Set objective sense (i.e. minimize or maximize)"""
395438

@@ -409,7 +452,7 @@ cdef class ExactProblem:
409452
cgmp.mpq_init(value_q)
410453
try:
411454
for variable, value in values:
412-
index = self._index_maybe_string(variable)
455+
index = self._col_index_maybe_string(variable)
413456
mpq_set_pynumeric(value_q, value)
414457

415458
r = cqsoptex.mpq_QSchange_objcoef(
@@ -476,7 +519,7 @@ cdef class ExactProblem:
476519
def get_value(self, variable):
477520
"""Get value of variable"""
478521

479-
cdef int index = self._index_maybe_string(variable)
522+
cdef int index = self._col_index_maybe_string(variable)
480523
if index < 0 or index >= self._c_sol_nvars:
481524
raise IndexError('Invalid variable index')
482525
return pyrational_from_mpq(self._c_sol_x[index])

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050

5151
setup(
5252
name='python-qsoptex',
53-
version='0.3',
53+
version='0.4',
5454
license='GPLv3+',
5555
url='https://github.com/jonls/python-qsoptex',
5656
author='Jon Lund Steffensen',

test_qsoptex.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
#!/usr/bin/env python
22

33
import unittest
4-
import numbers, fractions
4+
import numbers
5+
import fractions
56

67
import qsoptex
78

9+
from six import b, u
10+
11+
812
class TestVariableType(unittest.TestCase):
913
def test_variable_str(self):
10-
'''Test that a variable of bytes type works'''
14+
"""Test that a variable of bytes type works"""
1115
self._problem = qsoptex.ExactProblem()
12-
self._problem.add_variable(name=b'x')
16+
self._problem.add_variable(name=b('x'))
1317

1418
def test_variable_unicode(self):
15-
'''Test that a variable of unicode type works'''
19+
"""Test that a variable of unicode type works"""
1620
self._problem = qsoptex.ExactProblem()
17-
self._problem.add_variable(name=u'x')
21+
self._problem.add_variable(name=u('x'))
22+
1823

1924
class TestSmallExactProblem(unittest.TestCase):
2025
def setUp(self):
2126
self._problem = qsoptex.ExactProblem()
22-
self._problem.add_variable(name='x', objective=2, lower=3.5, upper=17.5)
23-
self._problem.add_variable(name='y', objective=-1, lower=None, upper=2)
24-
self._problem.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'x': 1, 'y': 1}, rhs=0)
27+
self._problem.add_variable(
28+
name='x', objective=2, lower=3.5, upper=17.5)
29+
self._problem.add_variable(
30+
name='y', objective=-1, lower=None, upper=2)
31+
self._problem.add_linear_constraint(
32+
qsoptex.ConstraintSense.EQUAL, {'x': 1, 'y': 1}, rhs=0, name='c1')
2533
self._problem.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
2634

2735
def test_reaches_status_optimal(self):
@@ -30,12 +38,14 @@ def test_reaches_status_optimal(self):
3038

3139
def test_reaches_status_infeasible(self):
3240
self._problem.add_variable(name='z', lower=200, upper=None)
33-
self._problem.add_linear_constraint(qsoptex.ConstraintSense.GREATER, {'y': 1, 'z': -1 }, rhs=0)
41+
self._problem.add_linear_constraint(
42+
qsoptex.ConstraintSense.GREATER, {'y': 1, 'z': -1}, rhs=0)
3443
status = self._problem.solve()
3544
self.assertEqual(status, qsoptex.SolutionStatus.INFEASIBLE)
3645

3746
def test_reaches_status_unbounded(self):
38-
self._problem.add_variable(name='z', lower=200, upper=None, objective=1)
47+
self._problem.add_variable(
48+
name='z', lower=200, upper=None, objective=1)
3949
status = self._problem.solve()
4050
self.assertEqual(status, qsoptex.SolutionStatus.UNBOUNDED)
4151

@@ -74,12 +84,12 @@ def test_solution_get_all_values_are_correct(self):
7484
def test_solution_value_index_negative(self):
7585
self._problem.solve()
7686
with self.assertRaises(IndexError):
77-
x = self._problem.get_value(-1)
87+
self._problem.get_value(-1)
7888

7989
def test_solution_value_index_too_high(self):
8090
self._problem.solve()
8191
with self.assertRaises(IndexError):
82-
x = self._problem.get_value(10)
92+
self._problem.get_value(10)
8393

8494
def test_get_status_method(self):
8595
s = self._problem.solve()
@@ -96,10 +106,42 @@ def test_rerun_solve(self):
96106

97107
# Modified problem
98108
self._problem.add_variable(name='z', objective=1, lower=0, upper=200)
99-
self._problem.add_linear_constraint(qsoptex.ConstraintSense.LESS, {'z': 1, 'x': -10 }, rhs=-50)
109+
self._problem.add_linear_constraint(
110+
qsoptex.ConstraintSense.LESS, {'z': 1, 'x': -10}, rhs=-50)
100111
self._problem.solve()
101112

102-
self.assertEqual(self._problem.get_objective_value(), fractions.Fraction('355/2'))
113+
self.assertEqual(
114+
self._problem.get_objective_value(), fractions.Fraction('355/2'))
115+
116+
def test_delete_variable(self):
117+
self._problem.add_variable(name='z', objective=1, lower=0, upper=1)
118+
status = self._problem.solve()
119+
self.assertEqual(status, qsoptex.SolutionStatus.OPTIMAL)
120+
obj = self._problem.get_objective_value()
121+
self.assertEqual(obj, fractions.Fraction('107/2'))
122+
123+
self._problem.delete_variable('z')
124+
125+
# Solve problem again without z
126+
status = self._problem.solve()
127+
self.assertEqual(status, qsoptex.SolutionStatus.OPTIMAL)
128+
obj = self._problem.get_objective_value()
129+
self.assertEqual(obj, fractions.Fraction('105/2'))
130+
131+
def test_delete_constraint(self):
132+
self._problem.add_linear_constraint(
133+
qsoptex.ConstraintSense.LESS, {'x': 1}, rhs=15, name='constr2')
134+
status = self._problem.solve()
135+
self.assertEqual(status, qsoptex.SolutionStatus.OPTIMAL)
136+
self.assertEqual(self._problem.get_value('x'), 15)
137+
138+
self._problem.delete_linear_constraint('constr2')
139+
140+
status = self._problem.solve()
141+
self.assertEqual(status, qsoptex.SolutionStatus.OPTIMAL)
142+
self.assertEqual(
143+
self._problem.get_value('x'), fractions.Fraction('35/2'))
144+
103145

104146
if __name__ == '__main__':
105147
unittest.main()

tox.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[tox]
2+
envlist = py27, py34
3+
4+
[testenv]
5+
deps = cython
6+
passenv = CPATH LIBRARY_PATH
7+
commands = ./test_qsoptex.py -v

0 commit comments

Comments
 (0)