Source code for sasoptpy.interface.solver.cas_mediator

# CAS (SAS Viya) interface for sasoptpy

import inspect

import pandas as pd
import numpy as np
import warnings

import sasoptpy
from sasoptpy.interface import Mediator

[docs]class CASMediator(Mediator): """ Handles the connection between sasoptpy and the SAS Viya (CAS) server Parameters ---------- caller : :class:`sasoptpy.Model` or :class:`sasoptpy.Workspace` Model or workspace that mediator belongs to cas_session : :class:`swat.cas.connection.CAS` CAS connection Notes ----- * CAS Mediator is used by :class:`sasoptpy.Model` and :class:`sasoptpy.Workspace` objects internally. """ def __init__(self, caller, cas_session): self.caller = caller self.session = cas_session
[docs] def solve(self, **kwargs): """ Solve action for :class:`Model` objects """ self.session.loadactionset(actionset='optimization') has_user_called_mps = kwargs.get('mps', kwargs.get('frame', False)) options = kwargs.get('options', dict()) mps_indicator = self.is_mps_format_needed( has_user_called_mps, options) if mps_indicator: return self.solve_with_mps(**kwargs) else: return self.solve_with_optmodel(**kwargs)
[docs] def submit(self, **kwargs): """ Submit action for custom input and :class:`sasoptpy.Workspace` objects """ self.session.loadactionset(actionset='optimization') return self.submit_optmodel_code(**kwargs)
[docs] def tune(self, **kwargs): """ Wrapper for the MILP tuner """ self.session.loadactionset(actionset='optimization') if not hasattr(self.session, 'optimization.tuner'): raise RuntimeError('Current CAS session version do not have tuner capability.') return self.tune_problem(**kwargs)
def is_mps_format_needed(self, mps_option, options): enforced = False mps_option = mps_option session = self.session caller = self.caller model = caller # If runOptmodel action is not available on server if not hasattr(session.optimization, 'runoptmodel'): mps_option = True enforced = True if 'decomp' in options: mps_option = True enforced = True if mps_option: switch = False # Sets and parameter belong to abstract models if model.get_sets() or model.get_parameters(): warnings.warn( 'INFO: Model {} has abstract elements, '.format( model.get_name()) + 'switching to OPTMODEL mode.', UserWarning) switch = True # MPS format cannot represent nonlinear problems elif not sasoptpy.is_linear(model): warnings.warn( 'INFO: Model {} includes nonlinear or abstract '.format( model.get_name()) + 'components, switching to OPTMODEL mode.', UserWarning) switch = True if switch and enforced and mps_option: raise RuntimeError('Problem requires runOptmodel action which ' 'is not available or appropriate') elif switch: mps_option = False return mps_option
[docs] def solve_with_mps(self, **kwargs): """ Submits the problem in MPS (DataFrame) format, supported by old versions Parameters ---------- kwargs : dict Keyword arguments for solver settings and options Returns ------- primal_solution : :class:`swat.dataframe.SASDataFrame` Solution of the model or None """ model = self.caller session = self.session verbose = kwargs.get('verbose', False) submit = kwargs.get('submit', True) options = kwargs.get('options', dict()) primalin = kwargs.get('primalin', False) name = kwargs.get('name', None) replace = kwargs.get('replace', True) drop = kwargs.get('drop', False) user_blocks = None print('NOTE: Converting model {} to DataFrame.'.format(model.get_name())) # Pre-upload argument parse # Find problem type and initial values ptype = 1 # LP for v in model.get_grouped_variables().values(): if v._type != sasoptpy.CONT: ptype = 2 break # Decomp check try: if options['decomp']['method'] == 'user': user_blocks = self.upload_user_blocks() options['decomp'] = {'blocks': user_blocks} except KeyError: pass # Initial value check for MIP if primalin: init_values = [] var_names = [] if ptype == 2: for v in model.loop_variables(): if v._init is not None: var_names.append(v.get_name()) init_values.append(v._init) if (len(init_values) > 0 and options.get('primalin', 1) is not None): primalinTable = pd.DataFrame( data={'_VAR_': var_names, '_VALUE_': init_values}) session.upload_frame( primalinTable, casout={ 'name': 'PRIMALINTABLE', 'replace': True}) options['primalin'] = 'PRIMALINTABLE' # Check if objective constant workaround is needed sfunc = session.solveLp if ptype == 1 else session.solveMilp has_arg = 'objconstant' in inspect.signature(sfunc).parameters if has_arg and 'objconstant' not in options: objconstant = model.get_objective()._linCoef['CONST']['val'] options['objconstant'] = objconstant # Upload the problem mps_table = self.upload_model(name, replace=replace, constant=not has_arg, verbose=verbose) if verbose: print(mps_table.to_string()) if not submit: return mps_table if ptype == 1: valid_opts = inspect.signature(session.solveLp).parameters lp_opts = {} for key, value in options.items(): if key in valid_opts: lp_opts[key] = value response = session.solveLp( data=mps_table.name, **lp_opts, primalOut={'caslib': 'CASUSER', 'name': 'primal', 'replace': True}, dualOut={'caslib': 'CASUSER', 'name': 'dual', 'replace': True}, objSense=model.get_objective().get_sense()) elif ptype == 2: valid_opts = inspect.signature(session.solveMilp).parameters milp_opts = {} for key, value in options.items(): if key in valid_opts: milp_opts[key] = value response = session.solveMilp( data=mps_table.name, **milp_opts, primalOut={'caslib': 'CASUSER', 'name': 'primal', 'replace': True}, dualOut={'caslib': 'CASUSER', 'name': 'dual', 'replace': True}, objSense=model.get_objective().get_sense()) model.response = response # Parse solution if(response.get_tables('status')[0] == 'OK'): model._primalSolution = session.CASTable( 'primal', caslib='CASUSER').to_frame() model._dualSolution = session.CASTable( 'dual', caslib='CASUSER').to_frame() # Bring solution to variables for _, row in model._primalSolution.iterrows(): if ('_SOL_' in model._primalSolution and row['_SOL_'] == 1)\ or '_SOL_' not in model._primalSolution: model.get_variable(row['_VAR_']).set_value(row['_VALUE_']) # Capturing dual values for LP problems if ptype == 1: model._primalSolution = model._primalSolution[ ['_VAR_', '_LBOUND_', '_UBOUND_', '_VALUE_', '_R_COST_']] model._primalSolution.columns = ['var', 'lb', 'ub', 'value', 'rc'] model._dualSolution = model._dualSolution[ ['_ROW_', '_ACTIVITY_', '_VALUE_']] model._dualSolution.columns = ['con', 'value', 'dual'] for row in model._primalSolution.itertuples(): model.get_variable(row.var)._dual = row.rc for row in model._dualSolution.itertuples(): model.get_constraint(row.con)._dual = row.dual elif ptype == 2: model._primalSolution = model._primalSolution[ ['_VAR_', '_LBOUND_', '_UBOUND_', '_VALUE_', '_SOL_']] model._primalSolution.columns = ['var', 'lb', 'ub', 'value', 'solution'] model._dualSolution = model._dualSolution[ ['_ROW_', '_ACTIVITY_', '_SOL_']] model._dualSolution.columns = ['con', 'value', 'solution'] # Drop tables if drop: session.table.droptable(table=mps_table.name) if user_blocks is not None: session.table.droptable(table=user_blocks) if primalin: session.table.droptable(table='PRIMALINTABLE') # Post-solve parse if(response.get_tables('status')[0] == 'OK'): # Print problem and solution summaries model._problemSummary = response.ProblemSummary[['Label1', 'cValue1']] model._solutionSummary = response.SolutionSummary[['Label1', 'cValue1']] model._problemSummary.set_index(['Label1'], inplace=True) model._problemSummary.columns = ['Value'] model._problemSummary.index.names = ['Label'] model._solutionSummary.set_index(['Label1'], inplace=True) model._solutionSummary.columns = ['Value'] model._solutionSummary.index.names = ['Label'] # Record status and time model._status = response.solutionStatus model._soltime = response.solutionTime if('OPTIMAL' in response.solutionStatus): model._objval = response.objective # Replace initial values with current values for v in model.loop_variables(): v._init = v._value return model._primalSolution else: warnings.warn('Solution message is not OPTIMAL: {}'.format(response.solutionStatus), UserWarning) model._objval = 0 return None else: raise RuntimeError('Solve came back with message: {}'.format( response.get_tables('status')[0]))
[docs] def solve_with_optmodel(self, **kwargs): """ Submits the problem in OPTMODEL format Parameters ---------- kwargs : dict Keyword arguments for solver settings and options Returns ------- primal_solution : :class:`swat.dataframe.SASDataFrame` Solution of the model or None """ model = self.caller session = self.session verbose = kwargs.get('verbose', False) submit = kwargs.get('submit', True) print('NOTE: Converting model {} to OPTMODEL.'.format( model.get_name())) options = kwargs.get('options', dict()) primalin = kwargs.get('primalin', False) optmodel_string = model.to_optmodel(header=False, options=options, ods=False, primalin=primalin, parse=True) if verbose: print(optmodel_string) if not submit: return optmodel_string print('NOTE: Submitting OPTMODEL code to CAS server.') response = session.runOptmodel( optmodel_string, outputTables={ 'names': {'solutionSummary': 'solutionSummary', 'problemSummary': 'problemSummary'} } ) model.response = response # Parse solution return self.parse_cas_solution()
[docs] def parse_cas_solution(self): """ Performs post-solve operations Returns ------- solution : :class:`swat.dataframe.SASDataFrame` Solution of the problem """ caller = self.caller session = self.session response = caller.response if response.status == 'Syntax Error': raise SyntaxError('An invalid symbol is generated, check object names') elif response.status == 'Semantic Error': raise RuntimeError('A semantic error has occured, check statements and object types') solution = session.CASTable('solution').to_frame() dual_solution = session.CASTable('dual').to_frame() caller._primalSolution = solution caller._dualSolution = dual_solution caller._problemSummary = self.parse_cas_table('problemSummary') caller._solutionSummary = self.parse_cas_table('solutionSummary') caller._status = response.solutionStatus caller._soltime = response.solutionTime self.set_variable_values(solution) self.set_constraint_values(dual_solution) self.set_model_objective_value() self.set_variable_init_values() return solution
def parse_table(self, table): session = self.session table = session.CASTable(table).to_frame() return table
[docs] def parse_cas_table(self, table): """ Converts requested :class:`swat.cas.table.CASTable` objects to :class:`swat.dataframe.SASDataFrame` """ session = self.session table = session.CASTable(table).to_frame() return sasoptpy.interface.parse_optmodel_table(table)
[docs] def set_variable_values(self, solution): """ Performs post-solve assignment of variable values Parameters ---------- solution : class:`swat.dataframe.SASDataFrame` Primal solution of the problem """ caller = self.caller solver = '' try: solver = caller.get_solution_summary().loc['Solver', 'Value'] except: pass for row in solution.itertuples(): caller.set_variable_value(row.var, row.value) if solver == 'LP': caller.set_dual_value(row.var, row.rc)
[docs] def set_constraint_values(self, solution): """ Performs post-solve assignment of constraint values Parameters ---------- solution : class:`swat.dataframe.SASDataFrame` Primal solution of the problem """ caller = self.caller solver = '' try: solver = caller.get_solution_summary().loc['Solver', 'Value'] except: pass if solver == 'LP': for row in solution.itertuples(): con = caller.get_constraint(row.con) if con is not None: con.set_dual(row.dual)
[docs] def set_model_objective_value(self): """ Performs post-solve assignment of objective values Parameters ---------- solution : class:`swat.dataframe.SASDataFrame` Primal solution of the problem """ caller = self.caller if sasoptpy.core.util.is_model(caller): if hasattr(caller.response, 'objective'): objval = caller.response.objective caller.set_objective_value(objval)
[docs] def set_variable_init_values(self): """ Performs post-solve assignment of variable initial values Parameters ---------- solution : class:`swat.dataframe.SASDataFrame` Primal solution of the problem """ caller = self.caller if sasoptpy.core.util.is_model(caller): for v in caller.loop_variables(): v.set_init(v.get_value())
[docs] def upload_user_blocks(self): """ Uploads user-defined decomposition blocks to the CAS server Returns ------- name : string CAS table name of the user-defined decomposition blocks Examples -------- >>> userblocks = m.upload_user_blocks() >>> m.solve(milp={'decomp': {'blocks': userblocks}}) """ sess = self.session model = self.caller blocks_dict = {} block_counter = 0 decomp_table = [] for c in model.loop_constraints(): if c._block is not None: if c._block not in blocks_dict: blocks_dict[c._block] = block_counter block_counter += 1 block_no = blocks_dict[c._block] decomp_table.append([c.get_name(), block_no]) frame_decomp_table = pd.DataFrame(decomp_table, columns=['_ROW_', '_BLOCK_']) response = sess.upload_frame(frame_decomp_table, casout={'name': 'BLOCKSTABLE', 'replace': True}) return(response.name)
[docs] def upload_model(self, name=None, replace=True, constant=False, verbose=False): """ Converts internal model to MPS table and upload to CAS session Parameters ---------- name : string, optional Desired name of the MPS table on the server replace : boolean, optional Option to replace the existing MPS table Returns ------- frame : :class:`swat.cas.table.CASTable` Reference to the uploaded CAS Table Notes ----- - This method returns None if the model session is not valid. - Name of the table is randomly assigned if name argument is None or not given. - This method should not be used if :func:`Model.solve` is going to be used. :func:`Model.solve` calls this method internally. """ model = self.caller df = model.to_mps(constant=constant) if verbose: print(df.to_string()) print('NOTE: Uploading the problem DataFrame to the server.') if name is not None: return self.session.upload_frame( data=df, casout={'name': name, 'replace': replace}) else: return self.session.upload_frame( data=df, casout={'replace': replace})
[docs] def submit_optmodel_code(self, **kwargs): """ Converts caller into OPTMODEL code and submits using optimization.runOptmodel action Parameters ---------- kwargs : Solver settings and options """ caller = self.caller session = self.session optmodel_code = sasoptpy.util.to_optmodel(caller, header=False, parse=True) verbose = kwargs.get('verbose', None) if verbose: print(sasoptpy.to_optmodel(caller)) response = session.runOptmodel( optmodel_code ) caller.response = response caller.parse_solve_responses() caller.parse_print_responses() caller.parse_create_data_responses(self) return self.parse_cas_workspace_response()
[docs] def parse_cas_workspace_response(self): """ Parses results of workspace submission """ caller = self.caller session = self.session response = caller.response if response.status == 'Syntax Error': raise SyntaxError('An invalid symbol is generated, check object names') elif response.status == 'Semantic Error': raise RuntimeError('A semantic error has occured, check statements and object types') solution = session.CASTable('solution').to_frame() dual_solution = session.CASTable('dual').to_frame() caller._primalSolution = solution caller._dualSolution = dual_solution self.set_workspace_variable_values(solution) #self.set_constraint_values(dual_solution) # self.set_model_objective_value() # self.set_variable_init_values() return solution
[docs] def set_workspace_variable_values(self, solution): """ Performs post-solve assignment of :class:`sasoptpy.Workspace` variable values """ caller = self.caller for row in solution.itertuples(): caller.set_variable_value(row.var, row.value)
[docs] def tune_problem(self, **kwargs): """ Calls optimization.tuner CAS action to finds out the ideal configuration """ model = self.caller session = self.session name = model.get_name() if not sasoptpy.is_linear(model): raise TypeError('Model {} is not linear'.format(model.get_name())) if not sasoptpy.util.has_integer_variables(model): raise TypeError('Model {} do not have integer or binary variables'.format(model.get_name())) self.upload_model(name=name) if kwargs.get('tunerParameters') is None: kwargs['tunerParameters'] = {'maxconfigs': 100} response = session.optimization.tuner( instances=[{'data': name}], **kwargs ) def replace_column_names(sasdf): colnames = sasdf.columns collabels = [] for i in colnames: if sasdf.colinfo[i].label is not None: collabels.append(sasdf.colinfo[i].label) else: collabels.append(i) sasdf.columns = collabels return sasdf performance = response.PerformanceInformation info = response.TunerInformation summary = response.TunerSummary results = response.TunerResults performance = replace_column_names(performance) info = replace_column_names(info) summary = replace_column_names(summary) results = replace_column_names(results) model._tunerResults = { 'Performance Information': performance, 'Tuner Information': info, 'Tuner Summary': summary, 'Tuner Results': results } return results