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:

  1. 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
    
  2. 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.
    
  3. 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()
    
  4. 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;
    
  5. 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:

  1. 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()
    
  2. 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
    
  3. 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