Skip to content

Commit

Permalink
Merge pull request #18 from cvxgrp/ms-enable_settings
Browse files Browse the repository at this point in the history
Enable verbose setting with OSQP
  • Loading branch information
maxschaller authored Apr 23, 2023
2 parents 3904c71 + 0574989 commit d2417a3
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 29 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,14 @@ cpg.generate_code(problem, code_dir='nonneg_LS', solver='SCS')
where the generated code is stored inside `nonneg_LS` and the `SCS` solver is used.
Next to the positional argument `problem`, all keyword arguments for the `generate_code()` method are summarized below.

| Argument | Meaning | Default |
| ------------- | ------------- | ------------- |
| `code_dir` | directory for code to be stored in | `'CPG_code'` |
| `solver` | canonical solver to generate code with | CVXPY default |
| `unroll` | unroll loops in canonicalization code | `False` |
| `prefix` | prefix for unique code symbols when dealing with multiple problems | `''`
| `wrapper` | compile Python wrapper for CVXPY interface | `True` |
| Argument | Meaning | Type | Default |
| ------------- | ------------- | ------------- | ------------- |
| `code_dir` | directory for code to be stored in | String | `'CPG_code'` |
| `solver` | canonical solver to generate code with | String | CVXPY default |
| `enable_settings`| enabled settings that are otherwise locked by embedded solver | List of Strings | `[]` |
| `unroll` | unroll loops in canonicalization code | Bool | `False` |
| `prefix` | prefix for unique code symbols when dealing with multiple problems | String | `''` |
| `wrapper` | compile Python wrapper for CVXPY interface | Bool | `True` |

You can find an overview of the code generation result in `nonneg_LS/README.html`.

Expand Down
86 changes: 71 additions & 15 deletions cvxpygen/cpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from cvxpy.reductions.solvers.conic_solvers.ecos_conif import ECOS


def generate_code(problem, code_dir='CPG_code', solver=None, unroll=False, prefix='', wrapper=True):
def generate_code(problem, code_dir='CPG_code', solver=None, enable_settings=[], unroll=False, prefix='', wrapper=True):
"""
Generate C code for CVXPY problem and (optionally) python wrapper
"""
Expand Down Expand Up @@ -439,6 +439,7 @@ def user_p_value(user_p_id):
canon_p_id_to_changes[p_id] = mapping[:, :-1].nnz > 0
canon_p_id_to_size[p_id] = mapping.shape[0]

# TODO: move this into a config file
if solver_name == 'OSQP':

# solver settings
Expand All @@ -448,6 +449,53 @@ def user_p_value(user_p_id):
'c_int']
settings_defaults = []

# extra settings
extra_settings_names = ['verbose', 'polish', 'polish_refine_iter', 'delta']
extra_settings_types = ['c_int', 'c_int', 'c_int', 'c_float']
extra_settings_defaults = [None] * len(extra_settings_names)

elif solver_name == 'SCS':

# solver settings
settings_names = ['normalize', 'scale', 'adaptive_scale', 'rho_x', 'max_iters', 'eps_abs', 'eps_rel',
'eps_infeas', 'alpha', 'time_limit_secs', 'verbose', 'warm_start', 'acceleration_lookback',
'acceleration_interval', 'write_data_filename', 'log_csv_filename']
settings_types = ['c_int', 'c_float', 'c_int', 'c_float', 'c_int', 'c_float', 'c_float', 'c_float', 'c_float',
'c_float', 'c_int', 'c_int', 'c_int', 'c_int', 'const char*', 'const char*']
settings_defaults = ['1', '0.1', '1', '1e-6', '1e5', '1e-4', '1e-4', '1e-7', '1.5', '0', '0', '0', '0', '1',
'SCS_NULL', 'SCS_NULL']

# extra settings
extra_settings_names = []
extra_settings_types = []
extra_settings_defaults = []

elif solver_name == 'ECOS':

# solver settings
settings_names = ['feastol', 'abstol', 'reltol', 'feastol_inacc', 'abstol_inacc', 'reltol_inacc', 'maxit']
settings_types = ['c_float', 'c_float', 'c_float', 'c_float', 'c_float', 'c_float', 'c_int']
settings_defaults = ['1e-8', '1e-8', '1e-8', '1e-4', '5e-5', '5e-5', '100']

# extra settings
extra_settings_names = []
extra_settings_types = []
extra_settings_defaults = []

# add extra settings
extra_settings_n2t = {n: t for n, t in zip(extra_settings_names, extra_settings_types)}
extra_settings_n2d = {n: d for n, d in zip(extra_settings_names, extra_settings_defaults)}

for s in (set(enable_settings)-set(settings_names)):
if s in extra_settings_names:
settings_names.append(s)
settings_types.append(extra_settings_n2t[s])
settings_defaults.append(extra_settings_n2d[s])
else:
warnings.warn('Cannot enable setting %s for solver %s' % (s, solver_name))

if solver_name == 'OSQP':

# OSQP codegen
osqp_obj = osqp.OSQP()
osqp_obj.setup(P=canon_p_csc['P'], q=canon_p['q'], A=canon_p_csc['A'], l=canon_p['l'], u=canon_p['u'])
Expand All @@ -465,16 +513,29 @@ def user_p_value(user_p_id):
os.path.join(solver_code_dir, 'LICENSE'))
shutil.copy(os.path.join(cvxpygen_directory, 'template', 'LICENSE'), code_dir)

elif solver_name == 'SCS':
# modify for extra settings
if 'verbose' in enable_settings:
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'),
[('message(STATUS "Disabling printing for embedded")', 'message(STATUS "Not disabling printing for embedded by user request")'),
('set(PRINTING OFF)', '')])
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'include', 'constants.h'),
[('# ifdef __cplusplus\n}', '# define VERBOSE (1)\n\n# ifdef __cplusplus\n}')])
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'include', 'types.h'),
[('} OSQPInfo;', ' c_int status_polish;\n} OSQPInfo;'),
('} OSQPSettings;', ' c_int polish;\n c_int verbose;\n} OSQPSettings;'),
('# ifndef EMBEDDED\n c_int nthreads; ///< number of threads active\n# endif // ifndef EMBEDDED', ' c_int nthreads;')])
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'include', 'osqp.h'),
[('# ifdef __cplusplus\n}', 'c_int osqp_update_verbose(OSQPWorkspace *work, c_int verbose_new);\n\n# ifdef __cplusplus\n}')])
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'src', 'osqp', 'util.c'),
[('// Print Settings', '/* Print Settings'),
('LINSYS_SOLVER_NAME[settings->linsys_solver]);', 'LINSYS_SOLVER_NAME[settings->linsys_solver]);*/')])
utils.replace_in_file(os.path.join(code_dir, 'c', 'solver_code', 'src', 'osqp', 'osqp.c'),
[('void osqp_set_default_settings(OSQPSettings *settings) {', 'void osqp_set_default_settings(OSQPSettings *settings) {\n settings->verbose = VERBOSE;'),
('c_int osqp_update_verbose', '#endif // EMBEDDED\n\nc_int osqp_update_verbose'),
('verbose = verbose_new;\n\n return 0;\n}\n\n#endif // EMBEDDED', 'verbose = verbose_new;\n\n return 0;\n}')])


# solver settings
settings_names = ['normalize', 'scale', 'adaptive_scale', 'rho_x', 'max_iters', 'eps_abs', 'eps_rel',
'eps_infeas', 'alpha', 'time_limit_secs', 'verbose', 'warm_start', 'acceleration_lookback',
'acceleration_interval', 'write_data_filename', 'log_csv_filename']
settings_types = ['c_int', 'c_float', 'c_int', 'c_float', 'c_int', 'c_float', 'c_float', 'c_float', 'c_float',
'c_float', 'c_int', 'c_int', 'c_int', 'c_int', 'const char*', 'const char*']
settings_defaults = ['1', '0.1', '1', '1e-6', '1e5', '1e-4', '1e-4', '1e-7', '1.5', '0', '0', '0', '0', '1',
'SCS_NULL', 'SCS_NULL']
elif solver_name == 'SCS':

# copy sources
if os.path.isdir(solver_code_dir):
Expand Down Expand Up @@ -528,11 +589,6 @@ def user_p_value(user_p_id):

elif solver_name == 'ECOS':

# solver settings
settings_names = ['feastol', 'abstol', 'reltol', 'feastol_inacc', 'abstol_inacc', 'reltol_inacc', 'maxit']
settings_types = ['c_float', 'c_float', 'c_float', 'c_float', 'c_float', 'c_float', 'c_int']
settings_defaults = ['1e-8', '1e-8', '1e-8', '1e-4', '5e-5', '5e-5', '100']

# copy sources
if os.path.isdir(solver_code_dir):
shutil.rmtree(solver_code_dir)
Expand Down
13 changes: 13 additions & 0 deletions cvxpygen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ def write_description(f, file_type, content):
f.write('%s\n\n' % comment_str[::-1])


def replace_in_file(filepath, replacements):
"""
Replace strings in file
"""

with open(filepath, 'r') as f:
t = f.read()
for old, new in replacements:
t = t.replace(old, new)
with open(filepath, 'w') as f:
f.write(t)


def replace_inf(v):
"""
Replace infinity by large number
Expand Down
29 changes: 29 additions & 0 deletions tests/test_E2E_QP.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
import glob
import os
import io
import importlib
import itertools
import pickle
Expand Down Expand Up @@ -222,3 +223,31 @@ def test(name, solver, style, seed):
assert np.linalg.norm(dual_cg - dual_py, 2) / dual_py_norm < 0.1
else:
assert np.linalg.norm(dual_cg, 2) < 1e-3


def test_OSQP_verbose():

prob = actuator_problem()
prob = assign_data(prob, 'actuator', 0)

cpg.generate_code(prob, code_dir='test_actuator_OSQP_verbose', solver='OSQP', unroll=False, prefix='actuator_OSQP_verbose', enable_settings=['verbose'])
assert len(glob.glob(os.path.join('test_actuator_OSQP_verbose', 'cpg_module.*'))) > 0

with open('test_actuator_OSQP_verbose/problem.pickle', 'rb') as f:
prob = pickle.load(f)

module = importlib.import_module('test_actuator_OSQP_verbose.cpg_solver')
prob.register_solve('CPG', module.cpg_solve)

prob = assign_data(prob, 'actuaor', 0)

verbose_output = io.StringIO()
sys.stdout = verbose_output

_ = utils_test.check(prob, 'OSQP', 'actuator', get_primal_vec, verbose=False)
assert 'optimal objective' not in verbose_output.getvalue()

_ = utils_test.check(prob, 'OSQP', 'actuator', get_primal_vec, verbose=True)
assert 'optimal objective' in verbose_output.getvalue()

sys.stdout = sys.__stdout__
14 changes: 7 additions & 7 deletions tests/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ def get_dual_vec(prob):
return np.concatenate(dual_values)


def check(prob, solver, name, func_get_primal_vec):
def check(prob, solver, name, func_get_primal_vec, **extra_settings):

if solver == 'OSQP':
val_py = prob.solve(solver='OSQP', eps_abs=1e-3, eps_rel=1e-3, max_iter=4000, polish=False,
adaptive_rho_interval=int(1e6), warm_start=False)
adaptive_rho_interval=int(1e6), warm_start=False, **extra_settings)
elif solver == 'SCS':
val_py = prob.solve(solver='SCS', warm_start=False, verbose=False)
val_py = prob.solve(solver='SCS', warm_start=False, verbose=False, **extra_settings)
else:
val_py = prob.solve(solver='ECOS')
val_py = prob.solve(solver='ECOS', **extra_settings)
prim_py = func_get_primal_vec(prob, name)
dual_py = get_dual_vec(prob)
if solver == 'OSQP':
val_cg = prob.solve(method='CPG', warm_start=False)
val_cg = prob.solve(method='CPG', warm_start=False, **extra_settings)
elif solver == 'SCS':
val_cg = prob.solve(method='CPG', warm_start=False, verbose=False)
val_cg = prob.solve(method='CPG', warm_start=False, verbose=False, **extra_settings)
else:
val_cg = prob.solve(method='CPG')
val_cg = prob.solve(method='CPG', **extra_settings)
prim_cg = func_get_primal_vec(prob, name)
dual_cg = get_dual_vec(prob)
prim_py_norm = np.linalg.norm(prim_py, 2)
Expand Down

0 comments on commit d2417a3

Please sign in to comment.