diff --git a/cvxpygen/cpg.py b/cvxpygen/cpg.py index f65faae..f288b62 100644 --- a/cvxpygen/cpg.py +++ b/cvxpygen/cpg.py @@ -18,6 +18,8 @@ import warnings from cvxpygen import utils +from cvxpygen.utils import write_file, read_write_file, write_example_def, write_module_prot, write_module_def, \ + write_canon_cmake, write_method, replace_cmake_data, replace_setup_data, replace_html_data from cvxpygen.mappings import Configuration, PrimalVariableInfo, DualVariableInfo, ConstraintInfo, \ ParameterCanon, ParameterInfo from cvxpygen.solvers import get_interface_class @@ -30,13 +32,13 @@ from cvxpy.expressions.variable import upper_tri_to_full -def generate_code(problem, code_dir='CPG_code', solver=None, solver_opts=None, enable_settings=[], unroll=False, prefix='', wrapper=True): +def generate_code(problem, code_dir='CPG_code', solver=None, solver_opts=None, + enable_settings=[], unroll=False, prefix='', wrapper=True): """ - Generate C code for CVXPY problem and (optionally) python wrapper + Generate C code to solve a CVXPY problem """ - sys.stdout.write('Generating code with CVXPYgen ...\n') - + create_folder_structure(code_dir) # problem data @@ -45,41 +47,31 @@ def generate_code(problem, code_dir='CPG_code', solver=None, solver_opts=None, e gp=False, enforce_dpp=True, verbose=False, - solver_opts=solver_opts, + solver_opts=solver_opts ) param_prob = data['param_prob'] - solver_name = solving_chain.solver.name() interface_class, cvxpy_interface_class = get_interface_class(solver_name) # configuration configuration = get_configuration(code_dir, solver, unroll, prefix) - # for cone problems, check if all cones are supported + # cone problems check if hasattr(param_prob, 'cone_dims'): cone_dims = param_prob.cone_dims interface_class.check_unsupported_cones(cone_dims) - # checks in sparsity handle_sparsity(param_prob) - # dimensions and information specific to solver solver_interface = interface_class(data, param_prob, enable_settings) # noqa - - # variable information variable_info = get_variable_info(problem, inverse_data) - - # dual variable information dual_variable_info = get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class) - - # user parameters parameter_info = get_parameter_info(param_prob) - constraint_info = get_constraint_info(solver_interface) - adjacency, parameter_canon, canon_p_ids = process_canonical_parameters(constraint_info, param_prob, - parameter_info, solver_interface, - solver_opts, problem, cvxpy_interface_class) + adjacency, parameter_canon, canon_p_ids = process_canonical_parameters( + constraint_info, param_prob, parameter_info, solver_interface, solver_opts, problem, cvxpy_interface_class + ) cvxpygen_directory = os.path.dirname(os.path.realpath(__file__)) solver_code_dir = os.path.join(code_dir, 'c', 'solver_code') @@ -87,83 +79,87 @@ def generate_code(problem, code_dir='CPG_code', solver=None, solver_opts=None, e parameter_canon.user_p_name_to_canon_outdated = { user_p_name: [canon_p_ids[j] for j in np.nonzero(adjacency[:, i])[0]] - for i, user_p_name in enumerate(parameter_info.names)} + for i, user_p_name in enumerate(parameter_info.names) + } - write_c_code(problem, configuration, variable_info, dual_variable_info, parameter_info, - parameter_canon, solver_interface) + write_c_code(problem, configuration, variable_info, dual_variable_info, + parameter_info, parameter_canon, solver_interface) sys.stdout.write('CVXPYgen finished generating code.\n') - + if wrapper: compile_python_module(code_dir) -def get_quad_obj(problem, solver_type, solver_opts, solver_class): +def get_quad_obj(problem, solver_type, solver_opts, solver_class) -> bool: + if solver_type == 'quadratic': return True - if solver_opts is None: - use_quad_obj = True - else: - use_quad_obj = solver_opts.get('use_quad_obj', True) - return use_quad_obj and solver_class().supports_quad_obj() and \ - problem.objective.expr.has_quadratic_term() + + use_quad_obj = solver_opts.get('use_quad_obj', True) if solver_opts else True + return use_quad_obj and solver_class().supports_quad_obj() and problem.objective.expr.has_quadratic_term() -def process_canonical_parameters(constraint_info, param_prob, parameter_info, solver_interface, solver_opts, problem, cvxpy_interface_class): + + +def process_canonical_parameters( + constraint_info, param_prob, parameter_info, + solver_interface, solver_opts, problem, cvxpy_interface_class): + parameter_canon = ParameterCanon() - parameter_canon.quad_obj = get_quad_obj(problem, solver_interface.solver_type, solver_opts, cvxpy_interface_class) + parameter_canon.quad_obj = get_quad_obj( + problem, solver_interface.solver_type, solver_opts, cvxpy_interface_class + ) if not parameter_canon.quad_obj: canon_p_ids = [p_id for p_id in solver_interface.canon_p_ids if p_id != 'P'] else: canon_p_ids = solver_interface.canon_p_ids - adjacency = np.zeros(shape=(len(canon_p_ids), parameter_info.num), dtype=bool) - # compute affine mapping for each canonical parameter + adjacency = np.zeros((len(canon_p_ids), parameter_info.num), dtype=bool) + for i, p_id in enumerate(canon_p_ids): - affine_map = solver_interface.get_affine_map(p_id, param_prob, constraint_info) - if affine_map is not None: - + if affine_map: if p_id in solver_interface.canon_p_ids_constr_vec: affine_map = update_to_dense_mapping(affine_map, param_prob) - if p_id == 'd': parameter_canon.nonzero_d = affine_map.mapping.nnz > 0 adjacency = update_adjacency_matrix(adjacency, i, parameter_info, affine_map.mapping) - - # take sign into account - affine_map.mapping = sparse.csc_matrix(affine_map.mapping.toarray() * affine_map.sign) # be able to use broadcasting - - # take sparsity into account + affine_map.mapping = sparse.csc_matrix(affine_map.mapping.toarray() * affine_map.sign) affine_map.mapping = affine_map.mapping[:, parameter_info.sparsity_mask] - - # compute default values of canonical parameters affine_map, parameter_canon = set_default_values(affine_map, p_id, parameter_canon, parameter_info, solver_interface) - + parameter_canon.p_id_to_mapping[p_id] = affine_map.mapping.tocsr() parameter_canon.p_id_to_changes[p_id] = affine_map.mapping[:, :-1].nnz > 0 parameter_canon.p_id_to_size[p_id] = affine_map.mapping.shape[0] - else: - parameter_canon.p_id_to_mapping[p_id] = None parameter_canon.p_id_to_changes[p_id] = False parameter_canon.p_id_to_size[p_id] = 0 + + parameter_canon.is_maximization = isinstance(problem.objective, Maximize) - parameter_canon.is_maximization = type(problem.objective) == Maximize return adjacency, parameter_canon, canon_p_ids + def update_to_dense_mapping(affine_map, param_prob): + + # Extract the sparse matrix and prepare a zero-initialized dense matrix mapping_to_sparse = param_prob.reduced_A.reduced_mat[affine_map.mapping_rows] - mapping_to_dense = sparse.lil_matrix( - np.zeros((affine_map.shape[0], mapping_to_sparse.shape[1]))) - for i_data in range(mapping_to_sparse.shape[0]): - mapping_to_dense[affine_map.indices[i_data], :] = mapping_to_sparse[i_data, :] + dense_shape = (affine_map.shape[0], mapping_to_sparse.shape[1]) + mapping_to_dense = sparse.lil_matrix(np.zeros(dense_shape)) + + # Update dense mapping with data from sparse mapping + for i_data, sparse_row in enumerate(mapping_to_sparse): + mapping_to_dense[affine_map.indices[i_data], :] = sparse_row + + # Convert to Compressed Sparse Column format and update mapping affine_map.mapping = sparse.csc_matrix(mapping_to_dense) + return affine_map @@ -243,6 +239,7 @@ def get_variable_info(problem, inverse_data) -> PrimalVariableInfo: def get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class) -> DualVariableInfo: + # get chain of constraint id maps for 'CvxAttr2Constr' and 'Canonicalization' objects dual_id_maps = [] if solver_interface.solver_type == 'quadratic': @@ -254,12 +251,14 @@ def get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class if inverse_data[-3]: dual_id_maps.append(inverse_data[-3][2]) dual_id_maps.append(inverse_data[-2].cons_id_map) + # recurse chain of constraint ids to get ordered list of constraint ids dual_ids = [] for dual_id in dual_id_maps[0].keys(): for dual_id_map in dual_id_maps[1:]: dual_id = dual_id_map[dual_id] dual_ids.append(dual_id) + # get canonical constraint information if solver_interface.solver_type == 'quadratic': con_canon = inverse_data[-2].constraints # same order as in canonical dual vector @@ -274,6 +273,7 @@ def get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class else: d_vectors = solver_interface.dual_var_names * len(d_canon_offsets) d_canon_offsets_dict = {c.id: off for c, off in zip(con_canon, d_canon_offsets)} + # select for user-defined constraints d_offsets = [d_canon_offsets_dict[i] for i in dual_ids] d_sizes = [con_canon_dict[i].size for i in dual_ids] @@ -286,6 +286,7 @@ def get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class d_name_to_vec = {n: v for n, v in zip(d_names, d_vectors)} d_name_to_offset = {n: o for n, o in zip(d_names, d_offsets)} d_name_to_size = {n: s for n, s in zip(d_names, d_sizes)} + # initialize values to zero d_name_to_init = dict() for name, shape in d_name_to_shape.items(): @@ -300,84 +301,102 @@ def get_dual_variable_info(inverse_data, solver_interface, cvxpy_interface_class def get_constraint_info(solver_interface) -> ConstraintInfo: + n_data_constr = len(solver_interface.indices_constr) - n_data_constr_vec = solver_interface.indptr_constr[-1] - solver_interface.indptr_constr[-2] + n_data_constr_vec = (solver_interface.indptr_constr[-1] + - solver_interface.indptr_constr[-2]) n_data_constr_mat = n_data_constr - n_data_constr_vec - mapping_rows_eq = np.nonzero(solver_interface.indices_constr < solver_interface.n_eq)[0] - mapping_rows_ineq = np.nonzero(solver_interface.indices_constr >= solver_interface.n_eq)[0] + # Obtain rows related to equalities and inequalities + mapping_rows_eq = np.nonzero(solver_interface.indices_constr + < solver_interface.n_eq)[0] + mapping_rows_ineq = np.nonzero(solver_interface.indices_constr + >= solver_interface.n_eq)[0] - return ConstraintInfo(n_data_constr, n_data_constr_mat, mapping_rows_eq, mapping_rows_ineq) + return ConstraintInfo(n_data_constr, n_data_constr_mat, + mapping_rows_eq, mapping_rows_ineq) def update_adjacency_matrix(adjacency, i, parameter_info, mapping) -> np.ndarray: - # compute adjacency matrix + + # Iterate through parameters and update adjacency if there are non-zero entries in mapping for j in range(parameter_info.num): column_slice = slice(parameter_info.id_to_col[parameter_info.ids[j]], parameter_info.id_to_col[parameter_info.ids[j + 1]]) - if mapping[:, column_slice].nnz > 0: - adjacency[i, j] = True + # Update adjacency matrix if there are non-zero entries in the mapped slice + adjacency[i, j] = mapping[:, column_slice].nnz > 0 + return adjacency -def write_c_code(problem: cp.Problem, configuration: dict, variable_info: dict, dual_variable_info: dict, - parameter_info: dict, parameter_canon: dict, solver_interface: dict) -> None: - # 'workspace' prototypes - with open(os.path.join(configuration.code_dir, 'c', 'include', 'cpg_workspace.h'), 'w') as f: - utils.write_workspace_prot(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface) - # 'workspace' definitions - with open(os.path.join(configuration.code_dir, 'c', 'src', 'cpg_workspace.c'), 'w') as f: - utils.write_workspace_def(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface) - # 'solve' prototypes - with open(os.path.join(configuration.code_dir, 'c', 'include', 'cpg_solve.h'), 'w') as f: - utils.write_solve_prot(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface) - # 'solve' definitions - with open(os.path.join(configuration.code_dir, 'c', 'src', 'cpg_solve.c'), 'w') as f: - utils.write_solve_def(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface) - # 'example' definitions - with open(os.path.join(configuration.code_dir, 'c', 'src', 'cpg_example.c'), 'w') as f: - utils.write_example_def(f, configuration, variable_info, dual_variable_info, parameter_info) - # adapt top-level CMakeLists.txt - with open(os.path.join(configuration.code_dir, 'c', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() - cmake_data = utils.replace_cmake_data(cmake_data, configuration) - with open(os.path.join(configuration.code_dir, 'c', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data) - # adapt solver CMakeLists.txt - with open(os.path.join(configuration.code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'a') as f: - utils.write_canon_cmake(f, configuration, solver_interface) - # binding module prototypes - with open(os.path.join(configuration.code_dir, 'cpp', 'include', 'cpg_module.hpp'), 'w') as f: - utils.write_module_prot(f, configuration, parameter_info, variable_info, dual_variable_info, solver_interface) - # binding module definition - with open(os.path.join(configuration.code_dir, 'cpp', 'src', 'cpg_module.cpp'), 'w') as f: - utils.write_module_def(f, configuration, variable_info, dual_variable_info, parameter_info, solver_interface) - # adapt setup.py - with open(os.path.join(configuration.code_dir, 'setup.py'), 'r') as f: - setup_data = f.read() - setup_data = utils.replace_setup_data(setup_data) - with open(os.path.join(configuration.code_dir, 'setup.py'), 'w') as f: - f.write(setup_data) - # custom CVXPY solve method - with open(os.path.join(configuration.code_dir, 'cpg_solver.py'), 'w') as f: - utils.write_method(f, configuration, variable_info, dual_variable_info, parameter_info, solver_interface) - # serialize problem formulation - with open(os.path.join(configuration.code_dir, 'problem.pickle'), 'wb') as f: - pickle.dump(cp.Problem(problem.objective, problem.constraints), f) - # html documentation file - with open(os.path.join(configuration.code_dir, 'README.html'), 'r') as f: - html_data = f.read() - html_data = utils.replace_html_data(html_data, configuration, variable_info, dual_variable_info, parameter_info, solver_interface) - with open(os.path.join(configuration.code_dir, 'README.html'), 'w') as f: - f.write(html_data) +def write_c_code(problem: cp.Problem, configuration: Configuration, variable_info: DualVariableInfo, + dual_variable_info: DualVariableInfo, parameter_info: ParameterInfo, + parameter_canon: ParameterCanon, solver_interface) -> None: + # Simplified directory and file access + c_dir = os.path.join(configuration.code_dir, 'c') + cpp_dir = os.path.join(configuration.code_dir, 'cpp') + include_dir = os.path.join(c_dir, 'include') + src_dir = os.path.join(c_dir, 'src') + solver_code_dir = os.path.join(c_dir, 'solver_code') + + # write files + for name in ['workspace', 'solve']: + write_file(os.path.join(include_dir, f'cpg_{name}.h'), 'w', + getattr(utils, f'write_{name}_prot'), + configuration, variable_info, dual_variable_info, + parameter_info, parameter_canon, solver_interface) + + write_file(os.path.join(src_dir, f'cpg_{name}.c'), 'w', + getattr(utils, f'write_{name}_def'), + configuration, variable_info, dual_variable_info, + parameter_info, parameter_canon, solver_interface) + + write_file(os.path.join(src_dir, 'cpg_example.c'), 'w', + write_example_def, + configuration, variable_info, dual_variable_info, parameter_info) + + write_file(os.path.join(cpp_dir, 'include', 'cpg_module.hpp'), 'w', + write_module_prot, + configuration, parameter_info, variable_info, + dual_variable_info, solver_interface) + + write_file(os.path.join(cpp_dir, 'src', 'cpg_module.cpp'), 'w', + write_module_def, + configuration, variable_info, dual_variable_info, + parameter_info, solver_interface) + + write_file(os.path.join(solver_code_dir, 'CMakeLists.txt'), 'a', + write_canon_cmake, + configuration, solver_interface) + + write_file(os.path.join(configuration.code_dir, 'cpg_solver.py'), 'w', + write_method, + configuration, variable_info, dual_variable_info, + parameter_info, solver_interface) + + write_file(os.path.join(configuration.code_dir, 'problem.pickle'), 'wb', + lambda x, y: pickle.dump(y, x), + cp.Problem(problem.objective, problem.constraints)) + + # replace file contents + read_write_file(os.path.join(c_dir, 'CMakeLists.txt'), + replace_cmake_data, + configuration) + + read_write_file(os.path.join(configuration.code_dir, 'setup.py'), + replace_setup_data) + + read_write_file(os.path.join(configuration.code_dir, 'README.html'), + replace_html_data, + configuration, variable_info, dual_variable_info, + parameter_info, solver_interface) + def adjust_prefix(prefix): - if prefix != '': - if not prefix[0].isalpha(): - prefix = '_' + prefix - prefix = prefix + '_' - return prefix + if prefix and not prefix[0].isalpha(): + prefix = '_' + prefix + return prefix + '_' if prefix else prefix def get_configuration(code_dir, solver_name, unroll, prefix) -> Configuration: @@ -453,32 +472,36 @@ def user_p_value(user_p_id): def handle_sparsity(p_prob: cp.Problem) -> None: - for p in p_prob.parameters: - if p.attributes['sparsity'] is not None: - if p.size == 1: - warnings.warn(f'Ignoring sparsity pattern for scalar parameter {p.name()}!') - p.attributes['sparsity'] = None - elif max(p.shape) == p.size: - warnings.warn(f'Ignoring sparsity pattern for vector parameter {p.name()}!') - p.attributes['sparsity'] = None + for param in p_prob.parameters: + sparsity = param.attributes['sparsity'] + + # Check and warn about inappropriate sparsity for scalar and vector + if sparsity is not None: + if param.size == 1 or max(param.shape) == param.size: + param_type = 'scalar' if param.size == 1 else 'vector' + warnings.warn(f'Ignoring sparsity pattern for {param_type} parameter {param.name()}!') + param.attributes['sparsity'] = None else: - for coord in p.attributes['sparsity']: - if coord[0] < 0 or coord[1] < 0 or coord[0] >= p.shape[0] or coord[1] >= \ - p.shape[1]: - warnings.warn(f'Invalid sparsity pattern for parameter {p.name()} - out of range! ' - 'Ignoring sparsity pattern.') - p.attributes['sparsity'] = None + invalid_sparsity = False + for coord in sparsity: + if coord[0] < 0 or coord[1] < 0 or coord[0] >= param.shape[0] or coord[1] >= param.shape[1]: + warnings.warn(f'Invalid sparsity pattern for parameter {param.name()} - out of range! Ignoring sparsity pattern.') + param.attributes['sparsity'] = None + invalid_sparsity = True break - p.attributes['sparsity'] = list(set(p.attributes['sparsity'])) - elif p.attributes['diag']: - p.attributes['sparsity'] = [(i, i) for i in range(p.shape[0])] - if p.attributes['sparsity'] is not None and p.value is not None: - for i in range(p.shape[0]): - for j in range(p.shape[1]): - if (i, j) not in p.attributes['sparsity'] and p.value[i, j] != 0: - warnings.warn( - f'Ignoring nonzero value outside of sparsity pattern for parameter {p.name()}!') - p.value[i, j] = 0 + if not invalid_sparsity: + param.attributes['sparsity'] = list(set(param.attributes['sparsity'])) + elif param.attributes['diag']: + param.attributes['sparsity'] = [(i, i) for i in range(param.shape[0])] + + # Zero out non-sparse values + if param.attributes['sparsity'] is not None and param.value is not None: + for i in range(param.shape[0]): + for j in range(param.shape[1]): + if (i, j) not in param.attributes['sparsity'] and param.value[i, j] != 0: + warnings.warn(f'Ignoring nonzero value outside of sparsity pattern for parameter {param.name()}!') + param.value[i, j] = 0 + def compile_python_module(code_dir: str): @@ -493,18 +516,21 @@ def compile_python_module(code_dir: str): def create_folder_structure(code_dir: str): cvxpygen_directory = os.path.dirname(os.path.realpath(__file__)) - # create code directory and copy template files - if os.path.isdir(code_dir): - shutil.rmtree(code_dir) + # Re-create code directory + shutil.rmtree(code_dir, ignore_errors=True) os.mkdir(code_dir) - os.mkdir(os.path.join(code_dir, 'c')) - for d in ['src', 'include', 'build']: - os.mkdir(os.path.join(code_dir, 'c', d)) - os.mkdir(os.path.join(code_dir, 'cpp')) - for d in ['src', 'include']: - os.mkdir(os.path.join(code_dir, 'cpp', d)) + + # Create directory structures + os.makedirs(os.path.join(code_dir, 'c', 'src')) + os.makedirs(os.path.join(code_dir, 'c', 'include')) + os.makedirs(os.path.join(code_dir, 'c', 'build')) + os.makedirs(os.path.join(code_dir, 'cpp', 'src')) + os.makedirs(os.path.join(code_dir, 'cpp', 'include')) + + # Copy template files shutil.copy(os.path.join(cvxpygen_directory, 'template', 'CMakeLists.txt'), os.path.join(code_dir, 'c')) for file in ['setup.py', 'README.html', '__init__.py']: shutil.copy(os.path.join(cvxpygen_directory, 'template', file), code_dir) + return cvxpygen_directory diff --git a/cvxpygen/solvers.py b/cvxpygen/solvers.py index 2040992..ff5e2ca 100644 --- a/cvxpygen/solvers.py +++ b/cvxpygen/solvers.py @@ -8,7 +8,8 @@ import numpy as np import scipy as sp -from cvxpygen.utils import replace_in_file, write_struct_prot, write_struct_def, write_vec_prot, write_vec_def +from cvxpygen.utils import read_write_file, write_struct_prot, write_struct_def, \ + write_vec_prot, write_vec_def, multiple_replace from cvxpygen.mappings import PrimalVariableInfo, DualVariableInfo, ConstraintInfo, AffineMap, \ ParameterCanon, WorkspacePointerInfo, UpdatePendingLogic, ParameterUpdateLogic @@ -275,23 +276,29 @@ def __init__(self, data, p_prob, enable_settings): indices_constr, indptr_constr, shape_constr, canon_constants, enable_settings) def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, - parameter_canon: ParameterCanon) -> None: + parameter_canon: ParameterCanon) -> None: import osqp + from sys import platform # OSQP codegen osqp_obj = osqp.OSQP() osqp_obj.setup(P=parameter_canon.p_csc['P'], q=parameter_canon.p['q'], - A=parameter_canon.p_csc['A'], l=parameter_canon.p['l'], - u=parameter_canon.p['u']) - if system() == 'Windows': - cmake_generator = 'MinGW Makefiles' - elif system() == 'Linux' or system() == 'Darwin': - cmake_generator = 'Unix Makefiles' - else: - raise ValueError(f'Unsupported OS {system()}.') + A=parameter_canon.p_csc['A'], l=parameter_canon.p['l'], + u=parameter_canon.p['u']) + + cmake_generators = { + 'win32': 'MinGW Makefiles', + 'linux': 'Unix Makefiles', + 'darwin': 'Unix Makefiles' + } + + try: + cmake_generator = cmake_generators[platform] + except KeyError: + raise ValueError(f'Unsupported OS {platform}.') osqp_obj.codegen(os.path.join(code_dir, 'c', 'solver_code'), project_type=cmake_generator, - parameters='matrices', force_rewrite=True) + parameters='matrices', force_rewrite=True) # copy license files shutil.copyfile(os.path.join(cvxpygen_directory, 'solvers', 'osqp-python', 'LICENSE'), @@ -300,24 +307,39 @@ def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, # modify for extra settings if 'verbose' in self.enable_settings: - 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)', '')]) - 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}')]) - 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;')]) - 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}')]) - 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]);*/')]) - 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}')]) + replacements_by_file = { + 'CMakeLists.txt': [ + ('message(STATUS "Disabling printing for embedded")', 'message(STATUS "Not disabling printing for embedded by user request")'), + ('set(PRINTING OFF)', '') + ], + os.path.join('include', 'constants.h'): [ + ('# ifdef __cplusplus\n}', '# define VERBOSE (1)\n\n# ifdef __cplusplus\n}') + ], + os.path.join('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;') + ], + os.path.join('include', 'osqp.h'): [ + ('# ifdef __cplusplus\n}', 'c_int osqp_update_verbose(OSQPWorkspace *work, c_int verbose_new);\n\n# ifdef __cplusplus\n}') + ], + os.path.join('src', 'osqp', 'util.c'): [ + ('// Print Settings', '/* Print Settings'), + ('LINSYS_SOLVER_NAME[settings->linsys_solver]);', 'LINSYS_SOLVER_NAME[settings->linsys_solver]);*/') + ], + os.path.join('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_code_dir = os.path.join(code_dir, 'c', 'solver_code') + for filename, replacements in replacements_by_file.items(): + filepath = os.path.join(solver_code_dir, filename) + read_write_file(filepath, lambda x: multiple_replace(x, replacements)) + + def get_affine_map(self, p_id, param_prob, constraint_info: ConstraintInfo) -> AffineMap: affine_map = AffineMap() @@ -462,7 +484,6 @@ def __init__(self, data, p_prob, enable_settings): n_ineq = 0 indices_obj, indptr_obj, shape_obj = self.get_problem_data_index(p_prob.reduced_P) - indices_constr, indptr_constr, shape_constr = self.get_problem_data_index(p_prob.reduced_A) canon_constants = {'n': n_var, 'm': n_eq, 'z': p_prob.cone_dims.zero, @@ -481,7 +502,7 @@ def check_unsupported_cones(cone_dims: "ConeDims") -> None: 'is not supported yet.') def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, - parameter_canon: ParameterCanon) -> None: + parameter_canon: ParameterCanon) -> None: # copy sources if os.path.isdir(solver_code_dir): @@ -498,41 +519,32 @@ def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, shutil.copy(os.path.join(cvxpygen_directory, 'template', 'LICENSE'), code_dir) # disable BLAS and LAPACK - with open(os.path.join(code_dir, 'c', 'solver_code', 'scs.mk'), 'r') as f: - scs_mk_data = f.read() - scs_mk_data = scs_mk_data.replace('USE_LAPACK = 1', 'USE_LAPACK = 0') - with open(os.path.join(code_dir, 'c', 'solver_code', 'scs.mk'), 'w') as f: - f.write(scs_mk_data) + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'scs.mk'), + lambda x: x.replace('USE_LAPACK = 1', 'USE_LAPACK = 0')) # modify CMakeLists.txt - with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() - cmake_data = cmake_data.replace(' include/', ' ${CMAKE_CURRENT_SOURCE_DIR}/include/') - cmake_data = cmake_data.replace(' src/', ' ${CMAKE_CURRENT_SOURCE_DIR}/src/') - cmake_data = cmake_data.replace(' ${LINSYS}/', ' ${CMAKE_CURRENT_SOURCE_DIR}/${LINSYS}/') - with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data) + cmake_replacements = [ + (' include/', ' ${CMAKE_CURRENT_SOURCE_DIR}/include/'), + (' src/', ' ${CMAKE_CURRENT_SOURCE_DIR}/src/'), + (' ${LINSYS}/', ' ${CMAKE_CURRENT_SOURCE_DIR}/${LINSYS}/') + ] + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), + lambda x: multiple_replace(x, cmake_replacements)) # adjust top-level CMakeLists.txt - with open(os.path.join(code_dir, 'c', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() - indent = ' ' * 6 sdir = '${CMAKE_CURRENT_SOURCE_DIR}/solver_code/' - cmake_data = cmake_data.replace(sdir + 'include', - sdir + 'include\n' + - indent + sdir + 'linsys') - with open(os.path.join(code_dir, 'c', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data) + indent = ' ' * 6 + read_write_file(os.path.join(code_dir, 'c', 'CMakeLists.txt'), + lambda x: x.replace(sdir + 'include', + sdir + 'include\n' + indent + sdir + 'linsys')) # adjust setup.py - with open(os.path.join(code_dir, 'setup.py'), 'r') as f: - setup_text = f.read() indent = ' ' * 30 - setup_text = setup_text.replace("os.path.join('c', 'solver_code', 'include'),", - "os.path.join('c', 'solver_code', 'include'),\n" + - indent + "os.path.join('c', 'solver_code', 'linsys'),") - with open(os.path.join(code_dir, 'setup.py'), 'w') as f: - f.write(setup_text) + read_write_file(os.path.join(code_dir, 'setup.py'), + lambda x: x.replace("os.path.join('c', 'solver_code', 'include'),", + "os.path.join('c', 'solver_code', 'include'),\n" + + indent + "os.path.join('c', 'solver_code', 'linsys'),")) + def declare_workspace(self, f, prefix, parameter_canon) -> None: matrices = ['P', 'A'] if parameter_canon.quad_obj else ['A'] @@ -685,7 +697,6 @@ def __init__(self, data, p_prob, enable_settings): n_ineq = data['G'].shape[0] indices_obj, indptr_obj, shape_obj = self.get_problem_data_index(p_prob.reduced_P) - indices_constr, indptr_constr, shape_constr = self.get_problem_data_index(p_prob.reduced_A) canon_constants = {'n': n_var, 'm': n_ineq, 'p': n_eq, @@ -739,7 +750,7 @@ def ret_dual_func_exists(dual_variable_info: DualVariableInfo) -> bool: return True def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, - parameter_canon: ParameterCanon) -> None: + parameter_canon: ParameterCanon) -> None: # copy sources if os.path.isdir(solver_code_dir): @@ -749,54 +760,51 @@ def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, for dtc in dirs_to_copy: shutil.copytree(os.path.join(cvxpygen_directory, 'solvers', 'ecos', dtc), os.path.join(solver_code_dir, dtc)) - shutil.copyfile(os.path.join(cvxpygen_directory, 'solvers', 'ecos', 'CMakeLists.txt'), - os.path.join(solver_code_dir, 'CMakeLists.txt')) - shutil.copyfile(os.path.join(cvxpygen_directory, 'solvers', 'ecos', 'COPYING'), - os.path.join(solver_code_dir, 'COPYING')) + + files_to_copy = ['CMakeLists.txt', 'COPYING'] + for fl in files_to_copy: + shutil.copyfile(os.path.join(cvxpygen_directory, 'solvers', 'ecos', fl), + os.path.join(solver_code_dir, fl)) + shutil.copyfile(os.path.join(cvxpygen_directory, 'solvers', 'ecos', 'COPYING'), os.path.join(code_dir, 'COPYING')) # adjust print level - with open(os.path.join(code_dir, 'c', 'solver_code', 'include', 'glblopts.h'), 'r') as f: - glbl_opts_data = f.read() - glbl_opts_data = glbl_opts_data.replace('#define PRINTLEVEL (2)', '#define PRINTLEVEL (0)') - with open(os.path.join(code_dir, 'c', 'solver_code', 'include', 'glblopts.h'), 'w') as f: - f.write(glbl_opts_data) + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'include', 'glblopts.h'), + lambda x: x.replace('#define PRINTLEVEL (2)', '#define PRINTLEVEL (0)')) # adjust top-level CMakeLists.txt - with open(os.path.join(code_dir, 'c', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() indent = ' ' * 6 sdir = '${CMAKE_CURRENT_SOURCE_DIR}/solver_code/' - cmake_data = cmake_data.replace(sdir + 'include', - sdir + 'include\n' + - indent + sdir + 'external/SuiteSparse_config\n' + - indent + sdir + 'external/amd/include\n' + - indent + sdir + 'external/ldl/include') - with open(os.path.join(code_dir, 'c', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data) + cmake_replacements = [ + (sdir + 'include', + sdir + 'include\n' + + indent + sdir + 'external/SuiteSparse_config\n' + + indent + sdir + 'external/amd/include\n' + + indent + sdir + 'external/ldl/include') + ] + read_write_file(os.path.join(code_dir, 'c', 'CMakeLists.txt'), + lambda x: multiple_replace(x, cmake_replacements)) # remove library target from ECOS CMakeLists.txt with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'r') as f: - lines = f.readlines() + lines = [line for line in f if '# ECOS library' not in line] with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'w') as f: - for line in lines: - if '# ECOS library' in line: - break - f.write(line) + f.writelines(lines) # adjust setup.py - with open(os.path.join(code_dir, 'setup.py'), 'r') as f: - setup_text = f.read() indent = ' ' * 30 - setup_text = setup_text.replace("os.path.join('c', 'solver_code', 'include'),", - "os.path.join('c', 'solver_code', 'include'),\n" + - indent + "os.path.join('c', 'solver_code', 'external', 'SuiteSparse_config'),\n" + - indent + "os.path.join('c', 'solver_code', 'external', 'amd', 'include'),\n" + - indent + "os.path.join('c', 'solver_code', 'external', 'ldl', 'include'),") - setup_text = setup_text.replace("license='Apache 2.0'", "license='GPL 3.0'") - with open(os.path.join(code_dir, 'setup.py'), 'w') as f: - f.write(setup_text) + setup_replacements = [ + ("os.path.join('c', 'solver_code', 'include'),", + "os.path.join('c', 'solver_code', 'include'),\n" + + indent + "os.path.join('c', 'solver_code', 'external', 'SuiteSparse_config'),\n" + + indent + "os.path.join('c', 'solver_code', 'external', 'amd', 'include'),\n" + + indent + "os.path.join('c', 'solver_code', 'external', 'ldl', 'include'),"), + ("license='Apache 2.0'", "license='GPL 3.0'") + ] + read_write_file(os.path.join(code_dir, 'setup.py'), + lambda x: multiple_replace(x, setup_replacements)) + def declare_workspace(self, f, prefix, parameter_canon) -> None: if self.canon_constants['n_cones'] > 0: @@ -825,8 +833,7 @@ class ClarabelInterface(SolverInterface): # header and source files header_files = [''] - cmake_headers = [] - cmake_sources = [] + cmake_headers, cmake_sources = [], [] # preconditioning of problem data happening in-memory inmemory_preconditioning = True @@ -944,8 +951,8 @@ def ret_dual_func_exists(dual_variable_info: DualVariableInfo) -> bool: return True def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, - parameter_canon: ParameterCanon) -> None: - + parameter_canon: ParameterCanon) -> None: + # copy sources if os.path.isdir(solver_code_dir): shutil.rmtree(solver_code_dir) @@ -963,35 +970,29 @@ def generate_code(self, code_dir, solver_code_dir, cvxpygen_directory, # adjust top-level CMakeLists.txt with open(os.path.join(code_dir, 'c', 'CMakeLists.txt'), 'a') as f: f.write('\ntarget_link_libraries(cpg_example PRIVATE libclarabel_c_static)\n') - f.write('\ntarget_link_libraries(cpg PRIVATE libclarabel_c_static)\n') + f.write('target_link_libraries(cpg PRIVATE libclarabel_c_static)\n') # remove examples target from Clarabel.cpp/CMakeLists.txt - with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() - with open(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data.replace('add_subdirectory(examples)', '# add_subdirectory(examples)')) + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'CMakeLists.txt'), + lambda x: x.replace('add_subdirectory(examples)', '# add_subdirectory(examples)')) # adjust paths in Clarabel.cpp/rust_wrapper/CMakeLists.txt - with open(os.path.join(code_dir, 'c', 'solver_code', 'rust_wrapper', 'CMakeLists.txt'), 'r') as f: - cmake_data = f.read() - cmake_data = cmake_data.replace('${CMAKE_SOURCE_DIR}/', '${CMAKE_SOURCE_DIR}/solver_code/') - cmake_data = cmake_data.replace('/libclarabel_c.lib', '/clarabel_c.lib') # until fixed on Clarabel side - with open(os.path.join(code_dir, 'c', 'solver_code', 'rust_wrapper', 'CMakeLists.txt'), 'w') as f: - f.write(cmake_data) + replacements = [ + ('${CMAKE_SOURCE_DIR}/', '${CMAKE_SOURCE_DIR}/solver_code/'), + ('/libclarabel_c.lib', '/clarabel_c.lib') # until fixed on Clarabel side + ] + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'rust_wrapper', 'CMakeLists.txt'), + lambda x: multiple_replace(x, replacements)) # adjust Clarabel - with open(os.path.join(code_dir, 'c', 'solver_code', 'include', 'Clarabel'), 'r') as f: - clarabel_text = f.read() - with open(os.path.join(code_dir, 'c', 'solver_code', 'include', 'Clarabel'), 'w') as f: - f.write(clarabel_text.replace('cpp/', 'c/')) + read_write_file(os.path.join(code_dir, 'c', 'solver_code', 'include', 'Clarabel'), + lambda x: x.replace('cpp/', 'c/')) # adjust setup.py - with open(os.path.join(code_dir, 'setup.py'), 'r') as f: - setup_text = f.read() - setup_text = setup_text.replace("extra_objects=[cpg_lib])", - "extra_objects=[cpg_lib, os.path.join(cpg_dir, 'solver_code', 'rust_wrapper', 'target', 'debug', 'libclarabel_c.a')])") - with open(os.path.join(code_dir, 'setup.py'), 'w') as f: - f.write(setup_text) + read_write_file(os.path.join(code_dir, 'setup.py'), + lambda x: x.replace("extra_objects=[cpg_lib])", + "extra_objects=[cpg_lib, os.path.join(cpg_dir, 'solver_code', 'rust_wrapper', 'target', 'debug', 'libclarabel_c.a')])")) + def declare_workspace(self, f, prefix, parameter_canon) -> None: f.write('\n// Clarabel workspace\n') diff --git a/cvxpygen/utils.py b/cvxpygen/utils.py index d8bc767..bcff55b 100644 --- a/cvxpygen/utils.py +++ b/cvxpygen/utils.py @@ -15,6 +15,28 @@ from datetime import datetime +def write_file(path, mode, function, *args): + """Write data to a file using a specific utility function.""" + with open(path, mode) as file: + function(file, *args) + + +def read_write_file(path, function, *args): + """Read data from a file, process it, and write back.""" + with open(path, 'r') as file: + data = file.read() + data = function(data, *args) + with open(path, 'w') as file: + file.write(data) + + +def multiple_replace(text, replacements): + """Perform multiple replacements (list of 2-tuples) on text""" + for old, new in replacements: + text = text.replace(old, new) + return text + + def write_vec_def(f, vec, name, typ): """ Write vector to file @@ -305,6 +327,12 @@ def extend_functions_if_false(pus, functions_if_false): return extended_functions_if_false +def remove_function(functions, function_to_remove): + if function_to_remove in functions: + functions.remove(function_to_remove) + return functions + + def analyze_pus(pus, p_id_to_changes): ''' Analyze parameter update structure (pus) to return set of canonical update functions @@ -325,8 +353,8 @@ def analyze_pus(pus, p_id_to_changes): if operator in ['&&', '&', 'and', 'AND']: skip = False for p in up_logic.parameters_outdated: - if not p_id_to_changes[p]: - functions_called.remove(function) + if not p_id_to_changes.get(p, False): + functions_called = remove_function(functions_called, function) skip = True if skip: continue @@ -336,11 +364,11 @@ def analyze_pus(pus, p_id_to_changes): if p_id_to_changes.get(p, False): skip = False if skip: - functions_called.remove(function) + functions_called = remove_function(functions_called, function) continue elif operator is None: if up_logic.extra_condition_operator is None and len(up_logic.parameters_outdated) == 1 and not p_id_to_changes[function]: - functions_called.remove(function) + functions_called = remove_function(functions_called, function) continue else: raise ValueError(f'Operator "{operator}" not implemented.')