# 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