Workspaces¶
One of the most powerful features of SAS Optimization and PROC OPTMODEL is the ability to combine several optimization models in a single call. You can read a common data set once, or parallelize solve steps for similar subproblems by using this ability.
The newly introduced Workspace
provides this ability in a familiar syntax. Compared to Model
objects,
a Workspace
can consist of several models and can use server-side data and OPTMODEL statements in a
more detailed way.
You can create several models in the same workspace, and you can solve problems sequentially and concurrently.
All the statements are sent to the server after Workspace.submit()
is called.
Creating a workspace¶
A Workspace
should be called by using the with
Python keyword as follows:
>>> with so.Workspace('my_workspace') as w:
>>> ...
Adding components¶
Unlike Model
objects, whose components are added explicitly, objects that are defined inside a Workspace
are
added automatically.
For example, adding a new variable is performed as follows:
In [1]: with so.Workspace(name='my_workspace') as w:
...: x = so.Variable(name='x', vartype=so.integer)
...:
You can display contents of a workspace by using the Workspace.to_optmodel()
method:
In [2]: print(w.to_optmodel())
proc optmodel;
var x integer;
quit;
In the following example, data are loaded into the server and a problem is solved by using a workspace:
Create CAS session:
In [3]: import os In [4]: hostname = os.getenv('CASHOST') In [5]: port = os.getenv('CASPORT') In [6]: from swat import CAS In [7]: cas_conn = CAS(hostname, port) In [8]: import sasoptpy as so In [9]: import pandas as pd
Upload data:
In [10]: def send_data(): ....: data = [ ....: ['clock', 8, 4, 3], ....: ['mug', 10, 6, 5], ....: ['headphone', 15, 7, 2], ....: ['book', 20, 12, 10], ....: ['pen', 1, 1, 15] ....: ] ....: df = pd.DataFrame(data, columns=['item', 'value', 'weight', 'limit']) ....: cas_conn.upload_frame(df, casout={'name': 'mydata', 'replace': True}) ....: send_data() ....: NOTE: Cloud Analytic Services made the uploaded file available as table MYDATA in caslib CASUSER(casuser). NOTE: The table MYDATA has been created in caslib CASUSER(casuser) from binary data uploaded to Cloud Analytic Services.
Create workspace and model:
In [11]: from sasoptpy.actions import read_data, solve In [12]: def create_workspace(): ....: with so.Workspace('my_knapsack', session=cas_conn) as w: ....: items = so.Set(name='ITEMS', settype=so.string) ....: value = so.ParameterGroup(items, name='value') ....: weight = so.ParameterGroup(items, name='weight') ....: limit = so.ParameterGroup(items, name='limit') ....: total_weight = so.Parameter(name='total_weight', value=55) ....: read_data( ....: table='mydata', index={'target': items, 'key': ['item']}, ....: columns=[value, weight, limit] ....: ) ....: get = so.VariableGroup(items, name='get', vartype=so.integer, lb=0) ....: limit_con = so.ConstraintGroup((get[i] <= limit[i] for i in items), ....: name='limit_con') ....: weight_con = so.Constraint( ....: so.expr_sum(weight[i] * get[i] for i in items) <= total_weight, ....: name='weight_con') ....: total_value = so.Objective( ....: so.expr_sum(value[i] * get[i] for i in items), name='total_value', ....: sense=so.maximize) ....: solve() ....: return w ....: In [13]: my_workspace = create_workspace()
Print content:
In [14]: print(so.to_optmodel(my_workspace)) proc optmodel; set <str> ITEMS; num value {ITEMS}; num weight {ITEMS}; num limit {ITEMS}; num total_weight = 55; read data mydata into ITEMS=[item] value weight limit; var get {{ITEMS}} integer >= 0; con limit_con {o72 in ITEMS} : get[o72] - limit[o72] <= 0; con weight_con : total_weight - (sum {i in ITEMS} (weight[i] * get[i])) >= 0; max total_value = sum {i in ITEMS} (value[i] * get[i]); solve; quit;
Submit:
In [15]: my_workspace.submit() NOTE: Added action set 'optimization'. NOTE: There were 5 rows read from table 'MYDATA' in caslib 'CASUSER(casuser)'. NOTE: Problem generation will use 8 threads. NOTE: The problem has 5 variables (0 free, 0 fixed). NOTE: The problem has 0 binary and 5 integer variables. NOTE: The problem has 6 linear constraints (5 LE, 0 EQ, 1 GE, 0 range). NOTE: The problem has 10 linear constraint coefficients. NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range). NOTE: The OPTMODEL presolver is disabled for linear problems. NOTE: The initial MILP heuristics are applied. NOTE: The MILP presolver value AUTOMATIC is applied. NOTE: The MILP presolver removed 0 variables and 5 constraints. NOTE: The MILP presolver removed 5 constraint coefficients. NOTE: The MILP presolver modified 0 constraint coefficients. NOTE: The presolved problem has 5 variables, 1 constraints, and 5 constraint coefficients. NOTE: The MILP solver is called. NOTE: The parallel Branch and Cut algorithm is used. NOTE: The Branch and Cut algorithm is using up to 8 threads. Node Active Sols BestInteger BestBound Gap Time 0 1 4 99.0000000 199.0000000 50.25% 0 0 1 4 99.0000000 102.3333333 3.26% 0 0 0 4 99.0000000 99.0000000 0.00% 0 NOTE: Optimal. NOTE: Objective = 99. NOTE: The output table 'SOLUTION' in caslib 'CASUSER(casuser)' has 5 rows and 6 columns. NOTE: The output table 'DUAL' in caslib 'CASUSER(casuser)' has 6 rows and 4 columns. Out[15]: Selected Rows from Table SOLUTION i var value lb ub rc 0 1.0 get[book] 2.0 -0.0 1.797693e+308 NaN 1 2.0 get[clock] 3.0 -0.0 1.797693e+308 NaN 2 3.0 get[headphone] 2.0 -0.0 1.797693e+308 NaN 3 4.0 get[mug] -0.0 -0.0 1.797693e+308 NaN 4 5.0 get[pen] 5.0 -0.0 1.797693e+308 NaN
Abstract actions¶
As shown in the previous example, a Workspace
can contain statements such as actions.read_data()
and actions.solve()
.
These statements are called “Abstract Statements” and are fully supported inside Workspace
objects.
These actions are performed on the server at runtime.
A list of abstract actions is available in the API section.
Adding abstract actions¶
You can import abstract actions through sasoptpy.actions
as follows:
>>> from sasoptpy.actions import read_data, create_data
These abstract actions are performed on the server side by generating equivalent OPTMODEL code at execution.
Retrieving results¶
In order to solve a problem, you need to use the actions.solve()
function explicitly.
Because Workspace
objects allow several models and solve statements to be included,
each of these solve statements is retrieved
separately. You can return the solution after each solve by using the actions.print()
function or
by using the actions.create_data()
function to create table.
In the following example, a parameter is changed and the same problem is solved twice:
Create workspace and components:
In [16]: from sasoptpy.actions import read_data, solve, print_item In [17]: def create_multi_solve_workspace(): ....: with so.Workspace('my_knapsack', session=cas_conn) as w: ....: items = so.Set(name='ITEMS', settype=so.string) ....: value = so.ParameterGroup(items, name='value') ....: weight = so.ParameterGroup(items, name='weight') ....: limit = so.ParameterGroup(items, name='limit') ....: total_weight = so.Parameter(name='total_weight', init=55) ....: read_data(table='mydata', index={'target': items, 'key': ['item']}, columns=[value, weight, limit]) ....: get = so.VariableGroup(items, name='get', vartype=so.integer, lb=0) ....: limit_con = so.ConstraintGroup((get[i] <= limit[i] for i in items), name='limit_con') ....: weight_con = so.Constraint( ....: so.expr_sum(weight[i] * get[i] for i in items) <= total_weight, name='weight_con') ....: total_value = so.Objective(so.expr_sum(value[i] * get[i] for i in items), name='total_value', sense=so.MAX) ....: s1 = solve() ....: p1 = print_item(get) ....: total_weight.set_value(40) ....: s2 = solve() ....: p2 = print_item(get) ....: return w, s1, p1, s2, p2 ....: In [18]: (my_workspace, solve1, print1, solve2, print2) = create_multi_solve_workspace()
Submit to the server:
In [19]: my_workspace.submit() NOTE: Added action set 'optimization'. NOTE: There were 5 rows read from table 'MYDATA' in caslib 'CASUSER(casuser)'. NOTE: Problem generation will use 8 threads. NOTE: The problem has 5 variables (0 free, 0 fixed). NOTE: The problem has 0 binary and 5 integer variables. NOTE: The problem has 6 linear constraints (5 LE, 0 EQ, 1 GE, 0 range). NOTE: The problem has 10 linear constraint coefficients. NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range). NOTE: The OPTMODEL presolver is disabled for linear problems. NOTE: The initial MILP heuristics are applied. NOTE: The MILP presolver value AUTOMATIC is applied. NOTE: The MILP presolver removed 0 variables and 5 constraints. NOTE: The MILP presolver removed 5 constraint coefficients. NOTE: The MILP presolver modified 0 constraint coefficients. NOTE: The presolved problem has 5 variables, 1 constraints, and 5 constraint coefficients. NOTE: The MILP solver is called. NOTE: The parallel Branch and Cut algorithm is used. NOTE: The Branch and Cut algorithm is using up to 8 threads. Node Active Sols BestInteger BestBound Gap Time 0 1 4 99.0000000 199.0000000 50.25% 0 0 1 4 99.0000000 102.3333333 3.26% 0 0 0 4 99.0000000 99.0000000 0.00% 0 NOTE: Optimal. NOTE: Objective = 99. NOTE: Problem generation will use 8 threads. NOTE: The problem has 5 variables (0 free, 0 fixed). NOTE: The problem has 0 binary and 5 integer variables. NOTE: The problem has 6 linear constraints (5 LE, 0 EQ, 1 GE, 0 range). NOTE: The problem has 10 linear constraint coefficients. NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range). NOTE: The OPTMODEL presolver is disabled for linear problems. NOTE: The initial MILP heuristics are applied. NOTE: The MILP presolver value AUTOMATIC is applied. NOTE: The MILP presolver removed 0 variables and 5 constraints. NOTE: The MILP presolver removed 5 constraint coefficients. NOTE: The MILP presolver modified 0 constraint coefficients. NOTE: The presolved problem has 5 variables, 1 constraints, and 5 constraint coefficients. NOTE: The MILP solver is called. NOTE: The parallel Branch and Cut algorithm is used. NOTE: The Branch and Cut algorithm is using up to 8 threads. Node Active Sols BestInteger BestBound Gap Time 0 1 4 76.0000000 179.0000000 57.54% 0 0 1 4 76.0000000 77.3333333 1.72% 0 0 0 4 76.0000000 76.0000000 0.00% 0 NOTE: Optimal. NOTE: Objective = 76. NOTE: The output table 'SOLUTION' in caslib 'CASUSER(casuser)' has 5 rows and 6 columns. NOTE: The output table 'DUAL' in caslib 'CASUSER(casuser)' has 6 rows and 4 columns. Out[19]: Selected Rows from Table SOLUTION i var value lb ub rc 0 1.0 get[book] 1.0 -0.0 1.797693e+308 NaN 1 2.0 get[clock] 3.0 -0.0 1.797693e+308 NaN 2 3.0 get[headphone] 2.0 -0.0 1.797693e+308 NaN 3 4.0 get[mug] -0.0 -0.0 1.797693e+308 NaN 4 5.0 get[pen] 2.0 -0.0 1.797693e+308 NaN
Print results:
In [20]: print(solve1.get_solution_summary()) Solution Summary Value Label Solver MILP Algorithm Branch and Cut Objective Function total_value Solution Status Optimal Objective Value 99 Relative Gap 0 Absolute Gap 0 Primal Infeasibility 0 Bound Infeasibility 0 Integer Infeasibility 0 Best Bound 99 Nodes 1 Solutions Found 4 Iterations 7 Presolve Time 0.00 Solution Time 0.18
In [21]: print(print1.get_response()) COL1 get 0 book 2.0 1 clock 3.0 2 headphone 2.0 3 mug -0.0 4 pen 5.0
In [22]: print(solve2.get_solution_summary()) Solution Summary Value Label Solver MILP Algorithm Branch and Cut Objective Function total_value Solution Status Optimal Objective Value 76 Relative Gap 0 Absolute Gap 0 Primal Infeasibility 0 Bound Infeasibility 0 Integer Infeasibility 0 Best Bound 76 Nodes 1 Solutions Found 4 Iterations 3 Presolve Time 0.00 Solution Time 0.19
In [23]: print(print2.get_response()) COL1 get 0 book 1.0 1 clock 3.0 2 headphone 2.0 3 mug -0.0 4 pen 2.0