Optimization Action Set
Milk Collection with CASL Evaluation
This section contains PROC CAS code.
Note: Input data must be accessible in your CAS session, either as a CAS table or as a transient-scope table. A CAS table has a two-level name: the first level is your CAS engine libref, and the second level is the table name. You refer to this table in the CAS procedure by specifying only the second level. For more information about two-level names, see Chapter 3, Shared Concepts (SAS Optimization: Mathematical Optimization Procedures). A transient-scope table is called directly from the action and exists in memory for the duration of the action. For more information about accessing data, see SAS Viya: System Programming Guide. For more information about PROC CAS and programming in CASL, see SAS Cloud Analytic Services: CASL Programmer’s Guide and SAS Cloud Analytic Services: CASL Reference.
This example illustrates how to use the runOptmodel action with CASL evaluation to solve the periodic vehicle routing problem described in Chapter 23, Milk Collection: How to Route and Assign Milk Collection Lorries to Farms (SAS/OR User's Guide: Mathematical Programming Examples). A truck with a limited capacity must collect milk from a set of farms. Not all farms must be visited each day. Each farm has a required collection frequency and production quantity. The objective is to minimize total transportation costs while scheduling the collections, respecting the requirements.
The following DATA step and macro variables set up the input data needed by this example:
data mycas.farmdata;
farm = _N_;
input east north frequency requirement;
datalines;
0 0 2 0
-3 3 2 5
1 11 2 4
4 7 2 3
-5 9 2 6
-5 -2 2 7
-4 -7 2 3
6 0 2 4
3 -6 2 6
-1 -3 2 5
0 -6 1 4
6 4 1 7
2 5 1 3
-2 8 1 4
6 10 1 5
1 8 1 6
-3 1 1 8
-6 5 1 5
2 9 1 7
-6 -5 1 6
5 -4 1 6
;
%let distance_scale = 10;
%let num_days = 2;
%let capacity = 80;
Output 2.11.1 shows the farm locations.
Output 2.11.1: Farm Map

You can use the following statements to determine an optimal collection schedule:
proc cas;
source pgm;
/* original problem sets and parameters */
num distance_scale = &distance_scale;
num num_days = &num_days;
num capacity = &capacity;
set NODES;
num east {NODES};
num north {NODES};
num frequency {NODES};
num requirement {NODES};
read data farmdata into NODES=[farm] east north frequency requirement;
set EDGES = {i in NODES, j in NODES: i < j};
num distance {<i,j> in EDGES} =
distance_scale * sqrt((east[i]-east[j])^2+(north[i]-north[j])^2);
set DAYS = 1..num_days;
create data LinksAll(promote=yes) from [from to] weight=distance;
string evalCode;
/* solve tsp for each day */
source evalCode end=endsrc;
f['obj'] = 0;
num_days = dim(UseNode[1]);
do d = 1 to num_days;
/* convert UseNode[i,d] array into list of nodes for this d */
NODES_THIS = {};
do i = 1 to dim(UseNode);
if UseNode[i,d] > 0.5 then NODES_THIS = NODES_THIS + i;
end;
/* construct induced subgraph link data for this d */
numLinks = comb(dim(UseNode), 2);
fetch result=r / table='LinksAll' maxRows=numLinks to=numLinks;
linkSetIn = findTable(r).where(from in NODES_THIS and
to in NODES_THIS);
saveresult linkSetIn casout="LinkSetIn" replace;
/* call tsp action */
if enableOutput ne . then
action optNetwork.tsp result=r / links={name="LinkSetIn"}
out={name="tourdata"||d};
else
action optNetwork.tsp result=r / links={name="LinkSetIn"};
f['obj'] = f['obj'] + r['objective'];
end;
send_response(f);
endsrc;
/* declare master MILP problem with black-box objective function */
var UseNode {NODES, DAYS} binary;
con Capacity_con {d in DAYS}:
sum {i in NODES} requirement[i] * UseNode[i,d] <= capacity;
con Frequency_con {i in NODES}:
sum {d in DAYS} UseNode[i,d] = frequency[i];
con Symmetry {d in DAYS diff {1}}:
sum {i in NODES} UseNode[i,d]
<= sum {i in NODES} UseNode[i,d-1];
min MasterObjective = caslevaln(evalCode, 'obj', UseNode);
/* call black-box solver */
solve with blackbox;
/* save the best tour data */
submit UseNode enableOutput=1 / code=evalCode;
set<num,num,num> TOUR;
for {d in DAYS} do;
read data ("tourdata"||d) into TOUR=[from to tsp_order];
create data ("soldata"||d) from [i j k]=TOUR
x1=east[i] y1=north[i] x2=east[j] y2=north[j];
end;
endsource;
action optimization.runOptmodel / printlevel=0 code=pgm;
quit;
The black-box solver searches for assignments to UseNode that satisfy the capacity and frequency constraints. The Symmetry constraint reduces the search space by breaking the symmetry of the day assignments.
The evaluation code in evalCode returns the sum of the TSP tour costs for all the days. The cost for each day is returned as the 'objective' result object from the tsp action. The tsp action requires a links data table as input. The CASL code uses the UseNode array to filter the links from the LinksAll data table into a results table. The CASL SAVERESULT statement saves the results table into a transient data table, LinkSetIn.
The evaluation code also takes an argument, enableOutput, that controls when the tours from the tsp action are saved. Only the total tour cost is required for most calls to the evaluation code. After the black-box solver finds the best assignments, the code calls the SUBMIT statement with enableOutput specified in order to save the TSP tours for processing in the runOptmodel action session.
The following SAS macro calls PROC SGPLOT to plot the solution for each day:
%macro showPlots;
%do d = 1 %to &num_days;
/* create annotate data set to draw tours */
data sganno(keep=drawspace linethickness function x1 y1 x2 y2);
retain drawspace "datavalue" linethickness 1;
set mycas.soldata&d;
function = 'line';
run;
title1 "day = &d";
title2;
proc sgplot data=mycas.farmdata sganno=sganno;
styleattrs datasymbols=(circle plus);
scatter y=north x=east / group=frequency grouporder=ascending
datalabel=farm;
xaxis display=(nolabel);
yaxis display=(nolabel);
run;
%end;
%mend showPlots;
%showPlots;
Output 2.11.2 and Output 2.11.3 show the resulting tours for each day of the collection schedule.
Output 2.11.2: Milk Collection Day 1

Output 2.11.3: Milk Collection Day 2

Milk Collection with CASL Evaluation
This section contains Lua code for the analysis in the CASL version of this example, which contains details about the results.
Note: In order to run this code, the data that are described in the CASL version need to be accessible to the CAS server. One way to do this is to convert the farmdata data to the comma-separated-value (CSV) file farmdata.csv and then use the following code to load the CSV file into CAS:
s:loadtable{casLib="casuser", path="farmdata.csv"}
For more information about coding in Lua, see Getting Started with SAS Viya for Lua and SAS Viya: System Programming Guide.
This example illustrates how to use the runOptmodel action with CASL evaluation to solve the periodic vehicle routing problem described in Chapter 23, Milk Collection: How to Route and Assign Milk Collection Lorries to Farms (SAS/OR User's Guide: Mathematical Programming Examples). A truck with a limited capacity must collect milk from a set of farms. Not all farms must be visited each day. Each farm has a required collection frequency and production quantity. The objective is to minimize total transportation costs while scheduling the collections, respecting the requirements.
You can use the following statements to determine an optimal collection schedule:
s:optimization_runOptmodel{
printlevel=0,
code=[[
/* original problem sets and parameters */
num distance_scale = 10;
num num_days = 2;
num capacity = 80;
set NODES;
num east {NODES};
num north {NODES};
num frequency {NODES};
num requirement {NODES};
read data farmdata into NODES=[farm] east north frequency requirement;
set EDGES = {i in NODES, j in NODES: i < j};
num distance {<i,j> in EDGES} =
distance_scale * sqrt((east[i]-east[j])^2+(north[i]-north[j])^2);
set DAYS = 1..num_days;
create data LinksAll(promote=yes) from [from to] weight=distance;
string evalCode;
/* solve tsp for each day */
source evalCode end=endsrc;
f['obj'] = 0;
num_days = dim(UseNode[1]);
do d = 1 to num_days;
/* convert UseNode[i,d] array into list of nodes for this d */
NODES_THIS = {};
do i = 1 to dim(UseNode);
if UseNode[i,d] > 0.5 then NODES_THIS = NODES_THIS + i;
end;
/* construct induced subgraph link data for this d */
numLinks = comb(dim(UseNode), 2);
fetch result=r / table='LinksAll' maxRows=numLinks to=numLinks;
linkSetIn = findTable(r).where(from in NODES_THIS and
to in NODES_THIS);
saveresult linkSetIn casout="LinkSetIn" replace;
/* call tsp action */
if enableOutput ne . then
action optNetwork.tsp result=r / links={name="LinkSetIn"}
out={name="tourdata"||d};
else
action optNetwork.tsp result=r / links={name="LinkSetIn"};
f['obj'] = f['obj'] + r['objective'];
end;
send_response(f);
endsrc;
/* declare master MILP problem with black-box objective function */
var UseNode {NODES, DAYS} binary;
con Capacity_con {d in DAYS}:
sum {i in NODES} requirement[i] * UseNode[i,d] <= capacity;
con Frequency_con {i in NODES}:
sum {d in DAYS} UseNode[i,d] = frequency[i];
con Symmetry {d in DAYS diff {1}}:
sum {i in NODES} UseNode[i,d]
<= sum {i in NODES} UseNode[i,d-1];
min MasterObjective = caslevaln(evalCode, 'obj', UseNode);
/* call black-box solver */
solve with blackbox;
/* save the best tour data */
submit UseNode enableOutput=1 / code=evalCode;
set<num,num,num> TOUR;
for {d in DAYS} do;
read data ("tourdata"||d) into TOUR=[from to tsp_order];
create data ("soldata"||d) from [i j k]=TOUR
x1=east[i] y1=north[i] x2=east[j] y2=north[j];
end;
]] }
The black-box solver searches for assignments to UseNode that satisfy the capacity and frequency constraints. The Symmetry constraint reduces the search space by breaking the symmetry of the day assignments.
The evaluation code in evalCode returns the sum of the TSP tour costs for all the days. The cost for each day is returned as the 'objective' result object from the tsp action. The tsp action requires a links data table as input. The CASL code uses the UseNode array to filter the links from the LinksAll data table into a results table. The CASL SAVERESULT statement saves the results table into a transient data table, LinkSetIn.
The evaluation code also takes an argument, enableOutput, that controls when the tours from the tsp action are saved. Only the total tour cost is required for most calls to the evaluation code. After the black-box solver finds the best assignments, the code calls the SUBMIT statement with enableOutput specified in order to save the TSP tours for processing in the runOptmodel action session.
Milk Collection with CASL Evaluation
This section contains Python code for the analysis in the CASL version of this example, which contains details about the results.
Note: In order to run this code, the data that are described in the CASL version need to be accessible to the CAS server. One way to do this is to convert the farmdata data to the comma-separated-value (CSV) file farmdata.csv and then use the following code to load the CSV file into CAS:
s.upload_file('farmdata.csv')
For more information about coding in Python, see Getting Started with SAS Viya for Python and SAS Viya: System Programming Guide.
This example illustrates how to use the runOptmodel action with CASL evaluation to solve the periodic vehicle routing problem described in Chapter 23, Milk Collection: How to Route and Assign Milk Collection Lorries to Farms (SAS/OR User's Guide: Mathematical Programming Examples). A truck with a limited capacity must collect milk from a set of farms. Not all farms must be visited each day. Each farm has a required collection frequency and production quantity. The objective is to minimize total transportation costs while scheduling the collections, respecting the requirements.
You can use the following statements to determine an optimal collection schedule:
s.optimization.runoptmodel(
code='''
/* original problem sets and parameters */
num distance_scale = 10;
num num_days = 2;
num capacity = 80;
set NODES;
num east {NODES};
num north {NODES};
num frequency {NODES};
num requirement {NODES};
read data farmdata into NODES=[farm] east north frequency requirement;
set EDGES = {i in NODES, j in NODES: i < j};
num distance {<i,j> in EDGES} =
distance_scale * sqrt((east[i]-east[j])^2+(north[i]-north[j])^2);
set DAYS = 1..num_days;
create data LinksAll(promote=yes) from [from to] weight=distance;
string evalCode;
/* solve tsp for each day */
source evalCode end=endsrc;
f['obj'] = 0;
num_days = dim(UseNode[1]);
do d = 1 to num_days;
/* convert UseNode[i,d] array into list of nodes for this d */
NODES_THIS = {};
do i = 1 to dim(UseNode);
if UseNode[i,d] > 0.5 then NODES_THIS = NODES_THIS + i;
end;
/* construct induced subgraph link data for this d */
numLinks = comb(dim(UseNode), 2);
fetch result=r / table='LinksAll' maxRows=numLinks to=numLinks;
linkSetIn = findTable(r).where(from in NODES_THIS and
to in NODES_THIS);
saveresult linkSetIn casout="LinkSetIn" replace;
/* call tsp action */
if enableOutput ne . then
action optNetwork.tsp result=r / links={name="LinkSetIn"}
out={name="tourdata"||d};
else
action optNetwork.tsp result=r / links={name="LinkSetIn"};
f['obj'] = f['obj'] + r['objective'];
end;
send_response(f);
endsrc;
/* declare master MILP problem with black-box objective function */
var UseNode {NODES, DAYS} binary;
con Capacity_con {d in DAYS}:
sum {i in NODES} requirement[i] * UseNode[i,d] <= capacity;
con Frequency_con {i in NODES}:
sum {d in DAYS} UseNode[i,d] = frequency[i];
con Symmetry {d in DAYS diff {1}}:
sum {i in NODES} UseNode[i,d]
<= sum {i in NODES} UseNode[i,d-1];
min MasterObjective = caslevaln(evalCode, 'obj', UseNode);
/* call black-box solver */
solve with blackbox;
/* save the best tour data */
submit UseNode enableOutput=1 / code=evalCode;
set<num,num,num> TOUR;
for {d in DAYS} do;
read data ("tourdata"||d) into TOUR=[from to tsp_order];
create data ("soldata"||d) from [i j k]=TOUR
x1=east[i] y1=north[i] x2=east[j] y2=north[j];
end;
''',
printlevel='0')
The black-box solver searches for assignments to UseNode that satisfy the capacity and frequency constraints. The Symmetry constraint reduces the search space by breaking the symmetry of the day assignments.
The evaluation code in evalCode returns the sum of the TSP tour costs for all the days. The cost for each day is returned as the 'objective' result object from the tsp action. The tsp action requires a links data table as input. The CASL code uses the UseNode array to filter the links from the LinksAll data table into a results table. The CASL SAVERESULT statement saves the results table into a transient data table, LinkSetIn.
The evaluation code also takes an argument, enableOutput, that controls when the tours from the tsp action are saved. Only the total tour cost is required for most calls to the evaluation code. After the black-box solver finds the best assignments, the code calls the SUBMIT statement with enableOutput specified in order to save the TSP tours for processing in the runOptmodel action session.
Milk Collection with CASL Evaluation
This section contains R code for the analysis in the CASL version of this example, which contains details about the results.
Note: In order to run this code, the data that are described in the CASL version need to be accessible to the CAS server. One way to do this is to convert the farmdata data to the comma-separated-value (CSV) file farmdata.csv and then use the following code to load the CSV file into CAS:
m <- cas.read.csv(s, "farmdata.csv", casOut=list(name="farmdata"))
For more information about coding in R, see Getting Started with SAS Viya for R and SAS Viya: System Programming Guide.
This example illustrates how to use the runOptmodel action with CASL evaluation to solve the periodic vehicle routing problem described in Chapter 23, Milk Collection: How to Route and Assign Milk Collection Lorries to Farms (SAS/OR User's Guide: Mathematical Programming Examples). A truck with a limited capacity must collect milk from a set of farms. Not all farms must be visited each day. Each farm has a required collection frequency and production quantity. The objective is to minimize total transportation costs while scheduling the collections, respecting the requirements.
You can use the following statements to determine an optimal collection schedule:
cas.optimization.runOptmodel(s,
code='
/* original problem sets and parameters */
num distance_scale = 10;
num num_days = 2;
num capacity = 80;
set NODES;
num east {NODES};
num north {NODES};
num frequency {NODES};
num requirement {NODES};
read data farmdata into NODES=[farm] east north frequency requirement;
set EDGES = {i in NODES, j in NODES: i < j};
num distance {<i,j> in EDGES} =
distance_scale * sqrt((east[i]-east[j])^2+(north[i]-north[j])^2);
set DAYS = 1..num_days;
create data LinksAll(promote=yes) from [from to] weight=distance;
string evalCode;
/* solve tsp for each day */
source evalCode end=endsrc;
f[\'obj\'] = 0;
num_days = dim(UseNode[1]);
do d = 1 to num_days;
/* convert UseNode[i,d] array into list of nodes for this d */
NODES_THIS = {};
do i = 1 to dim(UseNode);
if UseNode[i,d] > 0.5 then NODES_THIS = NODES_THIS + i;
end;
/* construct induced subgraph link data for this d */
numLinks = comb(dim(UseNode), 2);
fetch result=r / table=\'LinksAll\' maxRows=numLinks to=numLinks;
linkSetIn = findTable(r).where(from in NODES_THIS and
to in NODES_THIS);
saveresult linkSetIn casout="LinkSetIn" replace;
/* call tsp action */
if enableOutput ne . then
action optNetwork.tsp result=r / links={name="LinkSetIn"}
out={name="tourdata"||d};
else
action optNetwork.tsp result=r / links={name="LinkSetIn"};
f[\'obj\'] = f[\'obj\'] + r[\'objective\'];
end;
send_response(f);
endsrc;
/* declare master MILP problem with black-box objective function */
var UseNode {NODES, DAYS} binary;
con Capacity_con {d in DAYS}:
sum {i in NODES} requirement[i] * UseNode[i,d] <= capacity;
con Frequency_con {i in NODES}:
sum {d in DAYS} UseNode[i,d] = frequency[i];
con Symmetry {d in DAYS diff {1}}:
sum {i in NODES} UseNode[i,d]
<= sum {i in NODES} UseNode[i,d-1];
min MasterObjective = caslevaln(evalCode, \'obj\', UseNode);
/* call black-box solver */
solve with blackbox;
/* save the best tour data */
submit UseNode enableOutput=1 / code=evalCode;
set<num,num,num> TOUR;
for {d in DAYS} do;
read data ("tourdata"||d) into TOUR=[from to tsp_order];
create data ("soldata"||d) from [i j k]=TOUR
x1=east[i] y1=north[i] x2=east[j] y2=north[j];
end;
',
printlevel='0')
The black-box solver searches for assignments to UseNode that satisfy the capacity and frequency constraints. The Symmetry constraint reduces the search space by breaking the symmetry of the day assignments.
The evaluation code in evalCode returns the sum of the TSP tour costs for all the days. The cost for each day is returned as the 'objective' result object from the tsp action. The tsp action requires a links data table as input. The CASL code uses the UseNode array to filter the links from the LinksAll data table into a results table. The CASL SAVERESULT statement saves the results table into a transient data table, LinkSetIn.
The evaluation code also takes an argument, enableOutput, that controls when the tours from the tsp action are saved. Only the total tour cost is required for most calls to the evaluation code. After the black-box solver finds the best assignments, the code calls the SUBMIT statement with enableOutput specified in order to save the TSP tours for processing in the runOptmodel action session.