Models¶
Creating a model¶
You can create an empty model by using the Model
constructor:
In [1]: import sasoptpy as so
In [2]: m = so.Model(name='model1')
NOTE: Initialized model model1.
Adding new components to a model¶
Add a variable:
In [3]: x = m.add_variable(name='x', vartype=so.BIN)
In [4]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (1): [
x
]
Constraints (0): [
]
]
In [5]: y = m.add_variable(name='y', lb=1, ub=10)
In [6]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (0): [
]
]
Add a constraint:
In [7]: c1 = m.add_constraint(x + 2 * y <= 10, name='c1')
In [8]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (1): [
x + 2 * y <= 10
]
]
Adding existing components to a model¶
A new model can use existing variables. The typical way to include a variable
is to use the Model.include()
method:
In [9]: new_model = so.Model(name='new_model')
NOTE: Initialized model new_model.
In [10]: new_model.include(x, y)
In [11]: print(new_model)
Model: [
Name: new_model
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (0): [
]
]
In [12]: new_model.include(c1)
In [13]: print(new_model)
Model: [
Name: new_model
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (1): [
x + 2 * y <= 10
]
]
In [14]: z = so.Variable(name='z', vartype=so.INT, lb=3)
In [15]: new_model.include(z)
In [16]: print(new_model)
Model: [
Name: new_model
Objective: MIN [0]
Variables (3): [
x
y
z
]
Constraints (1): [
x + 2 * y <= 10
]
]
Note that variables are added to Model
objects by reference.
Therefore, after Model.solve()
is called, the values of variables are
replaced with optimal values.
Accessing components¶
You can access a list of model variables by using the Model.get_variables()
method:
In [17]: print(m.get_variables())
[sasoptpy.Variable(name='x', lb=0, ub=1, vartype='BIN'), sasoptpy.Variable(name='y', lb=1, ub=10, vartype='CONT')]
Similarly, you can access a list of constraints by using the
Model.get_constraints()
method:
In [18]: c2 = m.add_constraint(2 * x - y >= 1, name='c2')
In [19]: print(m.get_constraints())
[sasoptpy.Constraint(x + 2 * y <= 10, name='c1'), sasoptpy.Constraint(2 * x - y >= 1, name='c2')]
To access a certain constraint by using its name, you can use the
Model.get_constraint()
method:
In [20]: print(m.get_constraint('c2'))
2 * x - y >= 1
Dropping components¶
You can drop a variable inside a model by using
Model.drop_variable()
method. Similarly, you can drop a set of variables
by using the Model.drop_variables()
method.
In [21]: m.drop_variable(y)
In [22]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (1): [
x
]
Constraints (2): [
x + 2 * y <= 10
2 * x - y >= 1
]
]
You can drop a constraint by using the Model.drop_constraint()
method.
Similarly, you can drop a set of constraints by using the
Model.drop_constraints()
method.
In [23]: m.drop_constraint(c1)
In [24]: m.drop_constraint(c2)
In [25]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (0): [
]
]
In [26]: m.include(c1)
In [27]: print(m)
Model: [
Name: model1
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (1): [
x + 2 * y <= 10
]
]
Copying a model¶
You can copy an existing model by including the Model
object itself.
In [28]: copy_model = so.Model(name='copy_model')
NOTE: Initialized model copy_model.
In [29]: copy_model.include(m)
In [30]: print(copy_model)
Model: [
Name: copy_model
Objective: MIN [0]
Variables (2): [
x
y
]
Constraints (1): [
x + 2 * y <= 10
]
]
Note that all variables and constraints are included by reference.
Solving a model¶
A model is solved by using the Model.solve()
method. This method converts
Python definitions into OPTMODEL language and submits using SWAT or SASPy packages.
>>> m.solve()
NOTE: Initialized model model_1
NOTE: Added action set 'optimization'.
NOTE: Converting model model_1 to OPTMODEL.
...
NOTE: Optimal.
NOTE: Objective = 124.343.
NOTE: The Dual Simplex solve time is 0.01 seconds.
Solve options¶
Solver Options¶
You can pass either solve options from the OPTMODEL procedure or solve parameter from the solveLp
and solveMilp
actions by using the options
parameter of the Model.solve()
method.
>>> m.solve(options={'with': 'milp', 'maxtime': 600})
>>> m.solve(options={'with': 'lp', 'algorithm': 'ipm'})
The parameter with
is used to specificy the optimization solver in OPTMODEL procedure.
If the with
parameter is not passed, PROC OPTMODEL chooses a solver that depends on the problem type.
Possible with
values are listed in the SAS/OR documentation.
You can find specific solver options in the SAS Optimization documentation:
Black-box solver options (formerly called LSO solver)
The options
parameter can also pass solveLp
and solveMilp
action
parameter when frame=True
is used when the Model.solve()
method is called.
Call parameters¶
Besides the options
parameter, you can pass following parameters into the Model.solve()
method:
name: Name of the uploaded problem information
drop: Drops the data from server after the solve
replace: Replaces an existing data with the same name
- primalin: Uses the current values of the variables as an initial solution.
When the value of this parameter is
True
, the solve method grabsVariable
objects’_init
fields. You can modify this field by using theVariable.set_init()
method.
submit: Calls the CAS action or SAS procedure
frame: Uses the frame (MPS) method. If the value of this parameter is
False
, then the method uses OPTMODEL codes.verbose: Prints the generated PROC OPTMODEL code or MPS DataFrame object before the solve
Getting solutions¶
After the solve is completed, all variable and constraint values are parsed
automatically.
You can access a summary of the problem by using the
Model.get_problem_summary()
method,
and a summary of the solution by using the
Model.get_solution_summary()
method.
To print the values of any object, you can use the get_solution_table()
method:
>>> print(so.get_solution_table(x, y))
All variables and constraints that are passed into this method are returned on the basis of their indices. See Examples for more details.
Tuning MILP model parameters¶
SAS Optimization solvers provide a variety of settings. However, it might be difficult to find the best settings for a particular model. In order to compare parameters and make a good choice, you can use the optimization.tune action for mixed integer linear optimization problems.
The Model.tune_parameters()
method is a wrapper for the tune action. Consider the following knapsack problem example:
In [31]: def get_model():
....: m = so.Model(name='knapsack_with_tuner', session=cas_conn)
....: 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']).set_index(['item'])
....: ITEMS = df.index
....: value = df['value']
....: weight = df['weight']
....: limit = df['limit']
....: total_weight = 55
....: get = m.add_variables(ITEMS, name='get', vartype=so.INT)
....: m.add_constraints((get[i] <= limit[i] for i in ITEMS), name='limit_con')
....: m.add_constraint(so.expr_sum(weight[i] * get[i] for i in ITEMS) <= total_weight, name='weight_con')
....: total_value = so.expr_sum(value[i] * get[i] for i in ITEMS)
....: m.set_objective(total_value, name='total_value', sense=so.MAX)
....: return m
....:
In [32]: m = get_model()
NOTE: Initialized model knapsack_with_tuner.
For this problem, you can compare configurations as follows:
In [33]: results = m.tune_parameters(tunerParameters={'maxConfigs': 10})
NOTE: Added action set 'optimization'.
NOTE: Uploading the problem DataFrame to the server.
NOTE: Cloud Analytic Services made the uploaded file available as table KNAPSACK_WITH_TUNER in caslib CASUSER(casuser).
NOTE: The table KNAPSACK_WITH_TUNER has been created in caslib CASUSER(casuser) from binary data uploaded to Cloud Analytic Services.
NOTE: Start to tune the MILP
SolveCalls Configurations BestTime Time
1 1 0.19* 0.27
2 2 0.19* 0.52
3 3 0.19* 0.73
4 4 0.19* 0.93
5 5 0.19* 1.14
6 6 0.19* 1.35
7 7 0.19* 1.56
8 8 0.19* 1.79
9 9 0.19* 2.00
10 10 0.19* 2.22
NOTE: Configuration limit reached.
NOTE: The tuning time is 2.22 seconds.
In [34]: print(results)
Configuration conflictSearch cutGomory cutMiLifted cutStrategy \
0 0.0 automatic automatic automatic automatic
1 1.0 moderate none automatic automatic
2 2.0 none none moderate aggressive
3 3.0 automatic none none moderate
4 4.0 aggressive none aggressive none
5 5.0 aggressive none none aggressive
6 6.0 moderate none automatic moderate
7 7.0 none moderate moderate none
8 8.0 aggressive none none moderate
9 9.0 aggressive none none moderate
cutZeroHalf heuristics nodelSel presolver probe restarts \
0 automatic automatic automatic automatic automatic automatic
1 none automatic automatic none none basic
2 none automatic bestEstimatedepth moderate none basic
3 moderate automatic automatic none automatic none
4 moderate automatic bestBound moderate automatic none
5 moderate none depth automatic automatic none
6 aggressive none bestBound moderate automatic none
7 aggressive none depth automatic automatic automatic
8 moderate moderate bestBound basic none moderate
9 moderate moderate bestBound moderate none moderate
symmetry varSel Mean of Run Times Sum of Run Times \
0 automatic automatic 0.19 0.19
1 automatic pseudo 0.20 0.20
2 none automatic 0.18 0.18
3 basic minInfeas 0.18 0.18
4 moderate ryanFoster 0.17 0.17
5 aggressive minInfeas 0.17 0.17
6 automatic ryanFoster 0.18 0.18
7 none minInfeas 0.19 0.19
8 aggressive strong 0.19 0.19
9 aggressive strong 0.18 0.18
Percentage Successful
0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
5 0.0
6 0.0
7 0.0
8 0.0
9 0.0
Model.tune_parameters()
accepts three main arguments
milpParameters
tunerParameters
tuningParameters
For a full set of tuning parameters and acceptable values of these arguments, see the SAS Optimization documentation.
For the example problem, you can tune the presolver, cutStrategy, and strongIter settings, by using initial values and candidate values, and limit the maximum number of configurations and maximum running time as follows:
In [35]: results = m.tune_parameters(
....: milpParameters={'maxtime': 10},
....: tunerParameters={'maxConfigs': 20, 'logfreq': 5},
....: tuningParameters=[
....: {'option': 'presolver', 'initial': 'none', 'values': ['basic', 'aggressive', 'none']},
....: {'option': 'cutStrategy'},
....: {'option': 'strongIter', 'initial': -1, 'values': [-1, 100, 1000]}
....: ])
....:
NOTE: Added action set 'optimization'.
NOTE: Uploading the problem DataFrame to the server.
NOTE: Cloud Analytic Services made the uploaded file available as table KNAPSACK_WITH_TUNER in caslib CASUSER(casuser).
NOTE: The table KNAPSACK_WITH_TUNER has been created in caslib CASUSER(casuser) from binary data uploaded to Cloud Analytic Services.
NOTE: Start to tune the MILP
SolveCalls Configurations BestTime Time
5 5 0.20* 1.09
10 10 0.20* 2.15
15 15 0.20* 3.19
20 20 0.20* 4.25
NOTE: Configuration limit reached.
NOTE: The tuning time is 4.25 seconds.
In [36]: print(results)
Configuration conflictSearch cutGomory cutMiLifted cutStrategy \
0 0.0 automatic automatic automatic automatic
1 1.0 moderate none automatic automatic
2 2.0 none none moderate aggressive
3 3.0 automatic none none moderate
4 4.0 aggressive none aggressive none
5 5.0 aggressive none none aggressive
6 6.0 moderate none automatic moderate
7 7.0 none moderate moderate none
8 8.0 none none moderate aggressive
9 9.0 none none moderate aggressive
10 10.0 none none aggressive aggressive
11 11.0 none none moderate aggressive
12 12.0 none none moderate aggressive
13 13.0 none automatic moderate aggressive
14 14.0 none none moderate aggressive
15 15.0 none none moderate aggressive
16 16.0 none none moderate aggressive
17 17.0 automatic automatic automatic automatic
18 18.0 automatic automatic automatic automatic
19 19.0 automatic automatic automatic automatic
cutZeroHalf heuristics nodelSel presolver probe \
0 automatic automatic automatic automatic automatic
1 none automatic automatic none none
2 none automatic bestEstimatedepth moderate none
3 moderate automatic automatic none automatic
4 moderate automatic bestBound moderate automatic
5 moderate none depth automatic automatic
6 aggressive none bestBound moderate automatic
7 aggressive none depth automatic automatic
8 moderate basic depth none automatic
9 aggressive basic bestEstimatedepth none automatic
10 moderate basic bestEstimatedepth none automatic
11 moderate basic bestEstimatedepth none automatic
12 moderate basic bestEstimatedepth none basic
13 moderate basic bestEstimatedepth none automatic
14 moderate basic automatic none automatic
15 moderate basic bestEstimatedepth moderate automatic
16 moderate basic bestEstimatedepth none automatic
17 automatic automatic bestBound automatic automatic
18 automatic aggressive automatic automatic automatic
19 automatic automatic automatic automatic automatic
restarts symmetry varSel Mean of Run Times Sum of Run Times \
0 automatic automatic automatic 0.19 0.19
1 basic automatic pseudo 0.19 0.19
2 basic none automatic 0.18 0.18
3 none basic minInfeas 0.18 0.18
4 none moderate ryanFoster 0.20 0.20
5 none aggressive minInfeas 0.18 0.18
6 none automatic ryanFoster 0.19 0.19
7 automatic none minInfeas 0.18 0.18
8 none automatic maxInfeas 0.18 0.18
9 none automatic maxInfeas 0.18 0.18
10 none automatic maxInfeas 0.18 0.18
11 none automatic minInfeas 0.17 0.17
12 none automatic maxInfeas 0.18 0.18
13 none automatic maxInfeas 0.18 0.18
14 none automatic maxInfeas 0.17 0.17
15 none automatic maxInfeas 0.18 0.18
16 none automatic maxInfeas 0.17 0.17
17 automatic automatic automatic 0.19 0.19
18 automatic automatic automatic 0.18 0.18
19 automatic none automatic 0.18 0.18
Percentage Successful
0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
5 0.0
6 0.0
7 0.0
8 0.0
9 0.0
10 0.0
11 0.0
12 0.0
13 0.0
14 0.0
15 0.0
16 0.0
17 0.0
18 0.0
19 0.0
You can retrieve full details by using the Model.get_tuner_results()
method.
Exporting models¶
sasoptpy can return problem representation as an OPTMODEL string using Model.to_optmodel()
method:
In [37]: print(m.to_optmodel())
proc optmodel;
var get {{'clock','mug','headphone','book','pen'}} integer;
con limit_con_clock : get['clock'] <= 3;
con limit_con_mug : get['mug'] <= 5;
con limit_con_headphone : get['headphone'] <= 2;
con limit_con_book : get['book'] <= 10;
con limit_con_pen : get['pen'] <= 15;
con weight_con : 4 * get['clock'] + 6 * get['mug'] + 7 * get['headphone'] + 12 * get['book'] + get['pen'] <= 55;
max total_value = 8 * get['clock'] + 10 * get['mug'] + 15 * get['headphone'] + 20 * get['book'] + get['pen'];
solve;
quit;
An MPS representation of the model is available for LP and MILP problems using Model.to_mps()
method:
In [38]: print(m.to_mps())
Field1 Field2 Field3 Field4 \
0 NAME knapsack_with_tuner 0.0
1 ROWS NaN
2 MAX total_value NaN
3 L limit_con['clock'] NaN
4 L limit_con['mug'] NaN
5 L limit_con['headphone'] NaN
6 L limit_con['book'] NaN
7 L limit_con['pen'] NaN
8 L weight_con NaN
9 COLUMNS NaN
10 MARK0000 'MARKER' NaN
11 get[clock] total_value 8.0
12 get[clock] weight_con 4.0
13 get[mug] total_value 10.0
14 get[mug] weight_con 6.0
15 get[headphone] total_value 15.0
16 get[headphone] weight_con 7.0
17 get[book] total_value 20.0
18 get[book] weight_con 12.0
19 get[pen] total_value 1.0
20 get[pen] weight_con 1.0
21 MARK0001 'MARKER' NaN
22 RHS NaN
23 RHS limit_con['clock'] 3.0
24 RHS limit_con['headphone'] 2.0
25 RHS limit_con['pen'] 15.0
26 RANGES NaN
27 BOUNDS NaN
28 FR BND get[clock] NaN
29 FR BND get[mug] NaN
30 FR BND get[headphone] NaN
31 FR BND get[book] NaN
32 FR BND get[pen] NaN
33 ENDATA 0.0
Field5 Field6 _id_
0 0.0 1
1 NaN 2
2 NaN 3
3 NaN 4
4 NaN 5
5 NaN 6
6 NaN 7
7 NaN 8
8 NaN 9
9 NaN 10
10 'INTORG' NaN 11
11 limit_con['clock'] 1.0 12
12 NaN 13
13 limit_con['mug'] 1.0 14
14 NaN 15
15 limit_con['headphone'] 1.0 16
16 NaN 17
17 limit_con['book'] 1.0 18
18 NaN 19
19 limit_con['pen'] 1.0 20
20 NaN 21
21 'INTEND' NaN 22
22 NaN 23
23 limit_con['mug'] 5.0 24
24 limit_con['book'] 10.0 25
25 weight_con 55.0 26
26 NaN 27
27 NaN 28
28 NaN 29
29 NaN 30
30 NaN 31
31 NaN 32
32 NaN 33
33 0.0 34
Finally, a model can be exported into an MPS file using Model.export_mps()
method.
You can use filename argument in this method to write the multi-line Python string into a file on disk.
When fetch argument is used, it returns the generated string back to user.
In [39]: print(m.export_mps(fetch=True))
NAME knapsack_with_tuner
ROWS
MAX total_value
L limit_con['clock']
L limit_con['mug']
L limit_con['headphone']
L limit_con['book']
L limit_con['pen']
L weight_con
COLUMNS
MARK0000 'MARKER' 'INTORG'
get[clock] total_value 8.0 limit_con['clock'] 1.0
get[clock] weight_con 4.0
get[mug] total_value 10.0 limit_con['mug'] 1.0
get[mug] weight_con 6.0
get[headphone] total_value 15.0 limit_con['headphone'] 1.0
get[headphone] weight_con 7.0
get[book] total_value 20.0 limit_con['book'] 1.0
get[book] weight_con 12.0
get[pen] total_value 1.0 limit_con['pen'] 1.0
get[pen] weight_con 1.0
MARK0001 'MARKER' 'INTEND'
RHS
RHS limit_con['clock'] 3.0 limit_con['mug'] 5.0
RHS limit_con['headphone'] 2.0 limit_con['book'] 10.0
RHS limit_con['pen'] 15.0 weight_con 55.0
RANGES
BOUNDS
FR BND get[clock]
FR BND get[mug]
FR BND get[headphone]
FR BND get[book]
FR BND get[pen]
ENDATA