Source code for sasoptpy.interface.solver.sas_mediator

# SAS MVA interface for sasoptpy

import sasoptpy
from sasoptpy.libs import np
from sasoptpy.interface import Mediator
from saspy import SASsession
from sasoptpy.interface.util import wrap_long_lines, replace_long_names

import warnings

[docs]class SASMediator(Mediator): """ Handles the connection between sasoptpy and SAS instance Parameters ---------- caller : :class:`sasoptpy.Model` or :class:`sasoptpy.Workspace` Model or workspace that mediator belongs to sas_session : :class:`saspy.SASsession` SAS session object Notes ----- * SASMediator is used by :class:`sasoptpy.Model` and :class:`sasoptpy.Workspace` objects internally. """ def __init__(self, caller, sas_session): self.caller = caller self.session = sas_session self.conversion = dict()
[docs] def solve(self, **kwargs): """ Solve action for :class:`Model` objects """ mps_indicator = kwargs.get('mps', kwargs.get('frame', False)) user_options = kwargs.get('options', dict()) mps_indicator = self.is_mps_format_needed(mps_indicator, user_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 """ return self.submit_optmodel_code(**kwargs)
def is_mps_format_needed(self, mps_option, options): enforced = False session = self.session caller = self.caller model = caller if 'decomp' in options: mps_option = True enforced = True if mps_option: switch = False if model.get_sets() or model.get_parameters(): warnings.warn( 'INFO: Model {} has abstract objects, '.format( model.get_name()) + 'switching to OPTMODEL mode.', UserWarning) switch = True elif not sasoptpy.is_linear(model): warnings.warn( 'INFO: Model {} include nonlinear components, '.format( model.get_name()) + 'switching to OPTMODEL mode.', UserWarning) switch = True if switch and mps_option and enforced: raise RuntimeError('Cannot run either in OPTMODEL or MPS mode.') 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:`pandas.DataFrame` Solution of the model or None """ session = self.session model = self.caller verbose = kwargs.get('verbose', False) submit = kwargs.get('submit', True) #name = kwargs.get('name', None) name = sasoptpy.util.get_next_name() # Get the MPS data df = model.to_mps(constant=True) if verbose: print(df.to_string()) if not submit: return df # Upload MPS table with new arguments try: session.df2sd(df=df, table=name, keep_outer_quotes=True) except TypeError: # If user is using an old version of saspy, apply the hack session.df2sd(df=df, table=name) session.submit(""" data {}; set {}; field3=tranwrd(field3, "'MARKER'", "MARKER"); field3=tranwrd(field3, "MARKER", "'MARKER'"); field5=tranwrd(field5, "'INTORG'", "INTORG"); field5=tranwrd(field5, "INTORG", "'INTORG'"); field5=tranwrd(field5, "'INTEND'", "INTEND"); field5=tranwrd(field5, "INTEND", "'INTEND'"); run; """.format(name, name)) # 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 if ptype == 1: c = session.submit(""" ods output SolutionSummary=SOL_SUMMARY ProblemSummary=PROB_SUMMARY; proc optlp data = {} primalout = solution dualout = dual; run; """.format(name)) else: c = session.submit(""" ods output SolutionSummary=SOL_SUMMARY ProblemSummary=PROB_SUMMARY; proc optmilp data = {} primalout = solution dualout = dual; run; """.format(name)) logs = c['LOG'] for line in logs.split('\n'): if not line[0:1].isdigit(): print(line) return self.parse_sas_mps_solution()
[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:`pandas.DataFrame` 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=True, options=options, ods=False, primalin=primalin, parse=True) self.conversion = dict() # Check if any object has a long name limit_names = kwargs.get('limit_names', False) if limit_names: optmodel_string, conversion = replace_long_names(optmodel_string) self.conversion.update(conversion) wrap_lines = kwargs.get('wrap_lines', False) if wrap_lines: max_length = kwargs.get('max_line_length', 30000) optmodel_string = wrap_long_lines(optmodel_string, max_length) if verbose: print(optmodel_string) if not submit: return optmodel_string print('NOTE: Submitting OPTMODEL code to SAS instance.') optmodel_string = 'ods output SolutionSummary=SOL_SUMMARY ProblemSummary=PROB_SUMMARY;\n' + \ optmodel_string response = session.submit(optmodel_string) model.response = response # Print output for line in response['LOG'].split('\n'): first_word = line[0:1] if not first_word.isdigit(): print(line) if 'WARNING 524' in line: raise RuntimeError( r'Some object names are truncated, ' r'try submitting with limit_names=True parameter') elif 'The submitted line exceeds maximum line length' in line: raise RuntimeError( r'Some lines exceed maximum line length, ' r'try submitting with wrap_lines=True parameter') # Parse solution return self.parse_sas_solution()
[docs] def parse_sas_mps_solution(self): """ Parses MPS solution after `solve` and returns solution """ caller = self.caller session = self.session response = caller.response caller._problemSummary = self.parse_sas_table('PROB_SUMMARY') caller._solutionSummary = self.parse_sas_table('SOL_SUMMARY') solver = caller._solutionSummary.loc['Solver', 'Value'] # Parse solution solution_df = session.sd2df('solution', libref='WORK') primalsoln = solution_df[['_VAR_', '_VALUE_', '_LBOUND_', '_UBOUND_']].copy() primalsoln.columns = ['var', 'value', 'lb', 'ub'] if solver == 'LP': primalsoln['rc'] = solution_df['_R_COST_'] caller._primalSolution = primalsoln dual_df = session.sd2df('dual', libref='WORK') dualsoln = dual_df[['_ROW_', '_ACTIVITY_']].copy() dualsoln.columns = ['con', 'value'] if solver == 'LP': dualsoln['dual'] = dual_df['_VALUE_'] caller._dualSolution = dualsoln caller._status = caller._solutionSummary.loc['Solution Status'].Value caller._soltime = float( caller._solutionSummary.loc['Solution Time'].Value) self.perform_postsolve_operations() return caller._primalSolution
[docs] def parse_sas_solution(self): """ Performs post-solve operations Returns ------- solution : :class:`pandas.DataFrame` Solution of the problem """ caller = self.caller session = self.session # Parse solution caller._primalSolution = session.sd2df('SOLUTION', libref='WORK') self.convert_to_original(caller._primalSolution) caller._dualSolution = session.sd2df('DUAL', libref='WORK') self.convert_to_original(caller._dualSolution) caller._problemSummary = self.parse_sas_table('PROB_SUMMARY') caller._solutionSummary = self.parse_sas_table('SOL_SUMMARY') caller._status = caller._solutionSummary.loc['Solution Status'].Value caller._soltime = float(caller._solutionSummary.loc['Solution Time'].Value) self.perform_postsolve_operations() return caller._primalSolution
def parse_table(self, table): session = self.session return session.sd2df(table)
[docs] def parse_sas_table(self, table_name): """ Converts requested table name into :class:`pandas.DataFrame` """ session = self.session parsed_df = session.sd2df(table_name)[['Label1', 'cValue1']] parsed_df.replace(np.nan, '', inplace=True) parsed_df.columns = ['Label', 'Value'] parsed_df = parsed_df.set_index(['Label']) return parsed_df
[docs] def convert_to_original(self, table): """ Converts variable names to their original format if a placeholder gets used """ if len(self.conversion) == 0: return name_from = [] name_to = [] for i in self.conversion: name_from.append(r'\b' + i + r'\b') name_to.append(self.conversion[i]) table.replace(name_from, name_to, inplace=True, regex=True)
[docs] def perform_postsolve_operations(self): """ Performs post-solve operations for proper output display """ caller = self.caller response = caller.response solution = caller._primalSolution dual = caller._dualSolution # Variable values solver = caller.get_solution_summary().loc['Solver', 'Value'] for row in solution.itertuples(): caller.set_variable_value(row.var, row.value) if solver == 'LP': caller.set_dual_value(row.var, row.rc) # Constraint values (dual) only for LP solver = caller.get_solution_summary().loc['Solver', 'Value'] if solver == 'LP': for row in dual.itertuples(): con = caller.get_constraint(row.con) if con is not None: con.set_dual(row.dual) # Objective value if sasoptpy.core.util.is_model(caller): if 'Objective Value' in caller._solutionSummary.index: objval = caller._solutionSummary.loc['Objective Value'].Value objval = float(objval) caller.set_objective_value(objval) # Variable init values if sasoptpy.core.util.is_model(caller): for v in caller.loop_variables(): v.set_init(v.get_value())
[docs] def submit_optmodel_code(self, **kwargs): """ Submits given :class:`sasoptpy.Workspace` object in OPTMODEL format Parameters ---------- kwargs : Solver settings and options """ caller = self.caller session = self.session optmodel_code = sasoptpy.util.to_optmodel(caller, header=True, parse=True, ods=True) verbose = kwargs.get('verbose', None) # Check if any object has a long name limit_names = kwargs.get('limit_names', False) if limit_names: optmodel_code, conversion = replace_long_names(optmodel_code) self.conversion = conversion wrap_lines = kwargs.get('wrap_lines', False) if wrap_lines: max_length = kwargs.get('max_line_length', 30000) optmodel_code = wrap_long_lines(optmodel_code, max_length) if verbose: print(optmodel_code) response = session.submit(optmodel_code) caller.response = response # Print output for line in response['LOG'].split('\n'): first_word = line[0:1] if not first_word.isdigit(): print(line) if session.SYSERR() != 0: raise RuntimeError('SAS submission failed with following error: {}'. format(session.SYSERRORTEXT())) # caller.parse_solve_responses() # caller.parse_print_responses() # list of tables: session.list_tables('WORK') # soln: session.sd2df('SOLUTION', libref='WORK') # dual: session.sd2df('DUAL', libref='WORK') # Error msg: session.SYSERRORTEXT() # Error status: session.SYSERR() caller.parse_create_data_responses(self) return self.parse_sas_workspace_response()
[docs] def parse_sas_workspace_response(self): """ Parses results of workspace submission """ caller = self.caller session = self.session response = caller.response solution = session.sd2df('SOLUTION', libref='WORK') self.convert_to_original(solution) dual_solution = session.sd2df('DUAL', libref='WORK') self.convert_to_original(dual_solution) 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)