Dynamic Bayesian Network Action Set
Fault Detection in a System of Two Tanks
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 2, Shared Concepts (SAS Viya: Machine Learning 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 of two tanks (see Figure 8) and associated flows is developed using a similar scenario of five tanks from Lerner et al. (2000) to illustrate the filtering algorithm. The numerical values are generated using a simulation model for this two-tank scenario. The following notation is used to build the model:
Adding the subscript t to a notation denotes its value at time period t.
Figure 8: Two-Tank Model

Flows are computed using these relationships:
Material balance in tank 1 is represented by this relationship:
Similarly, material balance in tank 2 is represented by the following relationship:
Rearranging terms in these equations gives the following derivatives for ODEs:
A block that develops over time in the pipe connecting tank 1 to tank 2 is specified as
where is the rate at which the block is developing. A leak that develops over time in the pipe connecting tank 1 to tank 2 is specified as
where is the rate at which the leak is developing. The following branches are used to consider behavior under different scenarios for the pipe connecting tank 1 and tank 2:
L1Working: no block or leak in the pipeL2Blocked: a sudden block appearing in the pipe that reduces the conductance to half of normal conductanceL3BwDrift: a gradual block appearing in the pipeL4LwDrift: a gradual leak appearing in the pipeL5B_LwD: a sudden block with a gradual leak appearing in the pipeL6BwD_LwD: a gradual block with a gradual leak appearing in the pipe
The two-time-period Bayesian network for the two-tank model given in Figure 8 is shown in Figure 9, where and
are observed variables at time t. The models for
and
are as follows:
The clique tree for the Bayesian network in Figure 9 is shown in Figure 10.
Figure 9: Two-Period Tank Model

Figure 10: Two-Tank Clique Tree

The dynBnet action requires multiple input data tables. These statements assume that your CAS engine libref is named casuser, but you can substitute any appropriately defined CAS engine libref.
The variables in the two-time-period Bayesian network and their roles are defined in the variableRoles data table as follows:
data casuser.variableRoles;
length varname $20;
input varname $ varrole $;
datalines;
Time TIMEID
H1 HIDDEN
H2 HIDDEN
C12 HIDDEN
Cleak HIDDEN
Fault HIDDEN
F1out_rd OBSERVED
F2out_rd OBSERVED
;
Levels of nominal variables are defined in the variableLevels data table as follows:
data casuser.variableLevels;
length varname $20;
length varlevel $20;
input varname $ varlevel $;
datalines;
Fault L1Working
Fault L2Blocked
Fault L3BwDrift
Fault L4LwDrift
Fault L5B_LwD
Fault L6BwD_LwD
;
The parent-child relationships in the two-time-period Bayesian network are defined using the links data table as follows, where stage defines the parent to be in the current time period (1) or previous time period (2):
data casuser.links;
length parent $20 child $20;
input parent $ child $ stage;
datalines;
Time C12 1
C12 C12 2
Fault C12 1
Time Cleak 1
Cleak Cleak 2
Fault Cleak 1
Time H1 1
C12 H1 2
Cleak H1 2
H1 H1 2
H2 H1 2
Time H2 1
C12 H2 2
H1 H2 2
H2 H2 2
H1 F1out_rd 1
H2 F2out_rd 1
;
The systems of ordinary differential equations in the two-time-period Bayesian network are defined using the variableOdes data table as follows:
data casuser.variableOdes;
length varname $20;
input varname $ system;
datalines;
C12 1
Cleak 1
H1 1
H2 1
;
The initial beliefs that come from the previous time period are defined in the initialBeliefs data table as follows. For nominal variables, incoming probabilities are defined for the levels. For interval variables, the incoming mean and standard deviation are optionally defined by levels of nominal variables.
data casuser.initialBeliefs;
length intervalvar $20 nominalvar $20 varlevel $20;
input Time intervalvar $ mean sdev nominalvar $ varlevel $;
datalines;
1 H1 20 0.001 . .
1 H2 2 0.001 . .
1 C12 1 0.0001 . .
1 C12 0.5 0.0001 Fault L2Blocked
1 C12 0.5 0.0001 Fault L5B_LwD
1 Cleak 0 0.0001 . .
1 . 0.95 . Fault L1Working
1 . 0.01 . Fault L2Blocked
1 . 0.01 . Fault L3BwDrift
1 . 0.01 . Fault L4LwDrift
1 . 0.01 . Fault L5B_LwD
1 . 0.01 . Fault L6BwD_LwD
;
The observations in the tank_data data table, shown in the following code, are for the two observed variables, and they cover the time periods from 1 to 10. Time period 1 corresponds to the maximum time period in the initialBeliefs data table. Hence, the future observations are from time periods 2 to 10. The action generates outgoing beliefs for time periods 2 to 10, given the input beliefs from time period 1 and the observations from time periods 2 to 10. If a model of any child variable uses a parent that is an observed variable from the previous time period, then the observed values from time period 1 are also used by the action.
data casuser.tank_data;
input Time f1out_rd f2out_rd;
datalines;
1 10.000 2.000
2 9.955 2.069
3 9.910 2.137
4 9.866 2.203
5 9.822 2.268
6 9.779 2.332
7 9.736 2.394
8 9.692 2.455
9 9.650 2.514
10 9.607 2.572
;
You need to load the fcmpact action set and invoke the addRoutines action as follows to define the models in the two-time-period Bayesian network:
proc cas;
session sascas1;
loadactionset "fcmpact";
setSessOpt{cmplib="casuser.nonlinears"}; run;
fcmpact.addRoutines /
saveTable = true,
funcTable = {name="nonlinears", caslib="casuser", replace=true},
package = "tank",
routineCode =
"
/****************************************************/
/* subroutine to get derivative of variable C12 */
/* over different branches of nominal variable */
/* sdev is the noise in the ODE model of C12 */
/****************************************************/
subroutine d_C12(Time, C12, Fault $, d_C12, sdev);
outargs d_C12, sdev;
if Fault = 'L1Working' or Fault = 'L2Blocked' or Fault = 'L4LwDrift'
or Fault = 'L5B_LwD' then do;
d_C12 = 0;
sdev = 0.0001;
end;
else do;
d_C12 = -1.0/120;
sdev = 0.0001;
end;
endsub;
/****************************************************/
/* subroutine to get derivative of variable Cleak */
/* over different branches of nominal variable */
/* sdev is the noise in the ODE model of Cleak */
/****************************************************/
subroutine d_Cleak(Time, Cleak, Fault $, d_Cleak, sdev);
outargs d_Cleak, sdev;
if Fault = 'L4LwDrift' or Fault = 'L5B_LwD' or Fault = 'L6BwD_LwD' then do;
d_Cleak = 0.5 / 60;
sdev = 0.0001;
end;
else do;
d_Cleak = 0;
sdev = 0.0001;
end;
endsub;
/**********************************************************/
/* subroutine to get derivative of variable H1 */
/* as a function of variables TIME, C12, Cleak, H1 and H2 */
/* sdev is the noise in the ODE model of H1 */
/**********************************************************/
subroutine d_H1(TIME, C12, Cleak, H1, H2, d_H1, sdev);
outargs d_H1, sdev;
A1 = 200;
f1in = 1;
C1out = 0.5;
d_H1 = (1/A1)*f1in - (C12+Cleak+C1out)/A1 * H1
+ (C12)/A1 * H2;
sdev = 0.001;
endsub;
/**********************************************************/
/* subroutine to get derivative of variable H2 */
/* as a function of variables TIME, C12, H1 and H2 */
/* sdev is the noise in the ODE model of H2 */
/**********************************************************/
subroutine d_H2(TIME, C12, H1, H2, d_H2, sdev);
outargs d_H2, sdev;
A2 = 100;
C2out = 1;
d_H2 = C12/A2 * H1
- ((C12+C2out)/A2) * H2;
sdev = 0.001;
endsub;
/**********************************************************/
/* subroutine to get value of variable F1out_rd */
/* as a function of variable H1 */
/* sdev is the noise in this function */
/**********************************************************/
subroutine F1out_rd(H1, mean, sdev);
outargs mean, sdev;
C1out = 0.5;
mean = H1 * C1out;
sdev = 0.0001;
endsub;
/**********************************************************/
/* subroutine to get value of variable F2out_rd */
/* as a function of variable H2 */
/* sdev is the noise in this function */
/**********************************************************/
subroutine F2out_rd(H2, mean, sdev);
outargs mean, sdev;
C2out = 1;
mean = H2 * C2out;
sdev = 0.0001;
endsub;
";
quit;
Next, you need to load the dynamicBayesianNetwork action set and invoke the dynBnet action as follows:
proc cas;
loadactionset "dynamicBayesianNetwork";
/* invoke dynBnet action, where action returns status code in result = res */
/* status code indicates the success or the cause of failure if any */
action dynBnet result = res /
/* values of observed variables */
table = {data = {name = "tank_data" caslib = "casuser"}}
/* variables in Bayesian network */
varroles = {data = {name = "variableRoles" caslib = "casuser"}}
/* levels of nominal variables */
varlevels = {data = {name = "variableLevels" caslib = "casuser"}}
/* parent-child relationships */
links = {data = {name = "links" caslib = "casuser"}}
/* ODEs */
varodes = {data = {name = "variableOdes" caslib = "casuser"}}
/* incoming beliefs */
initbeliefs = {data = {name = "initialBeliefs" caslib = "casuser"}}
/* output tables */
output = {data = {name = "tank_out" caslib = "casuser" replace = 1}}
outdetails = {data = {name = "tank_outd" caslib = "casuser" replace = 1}}
outputState = {data = {name = "tank_state" caslib = "casuser" replace=1}}
/* method to get linear approx */
linApprox = "EXTENDED"
/* method to approx ODE solution */
odeApprox = "NONE"
;
print res;
run;
Output 12.1.1 shows the results.
Output 12.1.1: dynBnet Action Results
| RowId | Description | cValue | Value |
|---|---|---|---|
| RUNSTATUS | status of filter run | success | 0 |
| NTHREADS | number of threads used | 1 |
You need to invoke the ASTORE procedure as follows to save the state in a file on disk so that it can be accessed as input in future invocations of the dynBnet action:
proc astore;
download rstore=casuser.tank_state store='\Documents\tank_state';
run;
The tank_data data table contains values of observed variables every minute from to
minutes. The data are obtained by simulating the fault for the branch
L2Blocked, where the fault appears at minutes and remains. Models are linearized using the extended approximation method by specifying
linApprox="EXTENDED", and ODEs are not approximated by specifying odeApprox="NONE".
The incoming beliefs specify the L1Working (branch 1) to have a probability of 0.95 and all other branches to have a probability of 0.01.
The following code displays the probabilities of branches over time:
data tank_out;
set casuser.tank_out;
by Time;
Branch = "L1Working";
prob = Fault_L1Working;
output;
Branch = "L2Blocked";
prob = Fault_L2Blocked;
output;
Branch = "L3BwDrift";
prob = Fault_L3BwDrift;
output;
Branch = "L4LwDrift";
prob = Fault_L4LwDrift;
output;
Branch = "L5B_LwD";
prob = Fault_L5B_LwD;
output;
Branch = "L6BwD_LwD";
prob = Fault_L6BwD_LwD;
output;
run;
ods graphics on / attrpriority=none;
proc sgplot data = tank_out;
series x = Time y = prob / group = Branch lineattrs=(thickness=4);
xaxis label = "Time (mins)";
yaxis label = "Probability";
run;
Output 12.1.2 shows the results.
Output 12.1.2: Probabilities of Fault Branches

At minutes, the probability of
L2Blocked (branch 2) is 0.40 and the probability of L5B_Lwd (branch 5) is 0.60. A division of probabilities between L2Blocked and L5B_Lwd is explained by the fact that L5B_Lwd also has a block in the pipe that connects tank 1 and tank 2. At minutes, the probability of
L2Blocked is 0.97 and the probability of L5B_Lwd is 0.03. The higher probability of L2Blocked is explained by the fact that the observations of the variables and
at time
minutes are closer to the
L2Blocked branch than to the L5B_Lwd branch.
Fault Detection in a System of Two Tanks
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 inputtable data to the comma-separated-value (CSV) file inputtable.csv and then use the following code to load the CSV file into CAS:
s:loadtable{casLib="casuser", path="inputtable.csv"}
For more information about coding in Lua, see Getting Started with SAS Viya for Lua and SAS Viya: System Programming Guide.
The dynBnet action requires multiple input data tables. You need to load each data table by replacing inputtable.csv with the appropriate CSV file, such as variableRoles.csv. These statements assume that your CAS engine libref is named casuser, but you can substitute any appropriately defined CAS engine libref.
The models for child variables that relate values of child variables to the values of parent variables are specified using the fcmpact action set.
-- Load fcmpact action set
s:loadactionset{actionset='fcmpact'}
s:sessionProp_setSessOpt{cmplib="casuser.nonlinears"}
-- Specify models for child variables
s:fcmpact_addRoutines{ saveTable = true,
subroutine d_C12(Time, C12, Fault $, d_C12, sdev); \
outargs d_C12, sdev; \
if Fault = 'L1Working' or Fault = 'L2Blocked' or Fault = 'L4LwDrift' \
or Fault = 'L5B_LwD' then do; \
d_C12 = 0; \
sdev = 0.0001; \
end; \
else do; \
d_C12 = -1.0/120; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_Cleak(Time, Cleak, Fault $, d_Cleak, sdev); \
outargs d_Cleak, sdev; \
if Fault = 'L4LwDrift' or Fault = 'L5B_LwD' or Fault = 'L6BwD_LwD' then do; \
d_Cleak = 0.5 / 60; \
sdev = 0.0001; \
end; \
else do; \
d_Cleak = 0; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_H1(TIME, C12, Cleak, H1, H2, d_H1, sdev); \
outargs d_H1, sdev; \
A1 = 200; \
f1in = 1; \
C1out = 0.5; \
d_H1 = (1/A1)*f1in - (C12+Cleak+C1out)/A1 * H1 \
+ (C12)/A1 * H2; \
sdev = 0.001; \
endsub; \
subroutine d_H2(TIME, C12, H1, H2, d_H2, sdev); \
outargs d_H2, sdev; \
A2 = 100; \
C2out = 1; \
d_H2 = C12/A2 * H1 \
- ((C12+C2out)/A2) * H2; \
sdev = 0.001; \
endsub; \
subroutine F1out_rd(H1, mean, sdev); \
outargs mean, sdev; \
C1out = 0.5; \
mean = H1 * C1out; \
sdev = 0.0001; \
endsub; \
subroutine F2out_rd(H2, mean, sdev); \
outargs mean, sdev; \
C2out = 1; \
mean = H2 * C2out; \
sdev = 0.0001; \
endsub;"
}
The dynBnet action is invoked as follows to generate new beliefs by filtering the incoming observations;
-- Load dynamicBayesianNetwork actionset
s:loadactionset{actionset='dynamicBayesianNetwork'}
-- Call dynBnet action to generate outgoing beliefs in CAS table output
myres = s:dynamicBayesianNetwork_dynBnet{table = 'tank_data', \
varRoles = 'variableRoles', \
varLevels = 'variableLevels', \
links = 'links' \
varodes = 'variableOdes', \
initBeliefs = 'initialBeliefs', \
odeApprox = 'NONE', \
output = 'output' }
Fault Detection in a System of Two Tanks
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 inputtable data to the comma-separated-value (CSV) file inputtable.csv and then use the following code to load the CSV file into CAS:
s.upload_file('inputtable.csv')
For more information about coding in Python, see Getting Started with SAS Viya for Python and SAS Viya: System Programming Guide.
The dynBnet action requires multiple input data tables. You need to load each data table by replacing inputtable.csv with the appropriate CSV file, such as variableRoles.csv. These statements assume that your CAS engine libref is named casuser, but you can substitute any appropriately defined CAS engine libref.
The models for child variables that relate values of child variables to the values of parent variables are specified using the fcmpact action set.
s.fcmpact.addRoutines(saveTable="true", \
funcTable = {"name":"nonlinears", "caslib":"casuser", "replace":"true"}, \
package="tank", \
routineCode = {"\
subroutine d_C12(Time, C12, Fault $, d_C12, sdev); \
outargs d_C12, sdev; \
if Fault = 'L1Working' or Fault = 'L2Blocked' or Fault = 'L4LwDrift' \
or Fault = 'L5B_LwD' then do; \
d_C12 = 0; \
sdev = 0.0001; \
end; \
else do; \
d_C12 = -1.0/120; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_Cleak(Time, Cleak, Fault $, d_Cleak, sdev); \
outargs d_Cleak, sdev; \
if Fault = 'L4LwDrift' or Fault = 'L5B_LwD' or Fault = 'L6BwD_LwD' then do; \
d_Cleak = 0.5 / 60; \
sdev = 0.0001; \
end; \
else do; \
d_Cleak = 0; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_H1(TIME, C12, Cleak, H1, H2, d_H1, sdev); \
outargs d_H1, sdev; \
A1 = 200; \
f1in = 1; \
C1out = 0.5; \
d_H1 = (1/A1)*f1in - (C12+Cleak+C1out)/A1 * H1 \
+ (C12)/A1 * H2; \
sdev = 0.001; \
endsub; \
subroutine d_H2(TIME, C12, H1, H2, d_H2, sdev); \
outargs d_H2, sdev; \
A2 = 100; \
C2out = 1; \
d_H2 = C12/A2 * H1 \
- ((C12+C2out)/A2) * H2; \
sdev = 0.001; \
endsub; \
subroutine F1out_rd(H1, mean, sdev); \
outargs mean, sdev; \
C1out = 0.5; \
mean = H1 * C1out; \
sdev = 0.0001; \
endsub; \
subroutine F2out_rd(H2, mean, sdev); \
outargs mean, sdev; \
C2out = 1; \
mean = H2 * C2out; \
sdev = 0.0001; \
endsub;"})
The dynBnet action is invoked as follows to generate new beliefs by filtering the incoming observations;
myres = s.dynamicBayesianNetwork.dynBnet(table = 'tank_data', \
varRoles = 'variableRoles', \
varLevels = 'variableLevels', \
links = 'links', \
varodes = 'variableOdes', \
initBeliefs = 'initialBeliefs', \
odeApprox = 'NONE', \
output = 'output')
Fault Detection in a System of Two Tanks
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 inputtable data to the comma-separated-value (CSV) file inputtable.csv and then use the following code to load the CSV file into CAS:
m <- cas.read.csv(s, "inputtable.csv", casOut=list(name="inputtable"))
For more information about coding in R, see Getting Started with SAS Viya for R and SAS Viya: System Programming Guide.
The dynBnet action requires multiple input data tables. You need to load each data table by replacing inputtable.csv with the appropriate CSV file, such as variableRoles.csv. These statements assume that your CAS engine libref is named casuser, but you can substitute any appropriately defined CAS engine libref.
The models for child variables that relate values of child variables to the values of parent variables are specified using the fcmpact action set.
results <- cas.fcmpact.addRoutines(s,
saveTable=TRUE,
funcTable = c(name = "nonlinears",
caslib = "casuser", replace = TRUE),
package = "tank",
routineCode = c("\
subroutine d_C12(Time, C12, Fault $, d_C12, sdev); \
outargs d_C12, sdev; \
if Fault = 'L1Working' or Fault = 'L2Blocked' or Fault = 'L4LwDrift' \
or Fault = 'L5B_LwD' then do; \
d_C12 = 0; \
sdev = 0.0001; \
end; \
else do; \
d_C12 = -1.0/120; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_Cleak(Time, Cleak, Fault $, d_Cleak, sdev); \
outargs d_Cleak, sdev; \
if Fault = 'L4LwDrift' or Fault = 'L5B_LwD' or Fault = 'L6BwD_LwD' then do; \
d_Cleak = 0.5 / 60; \
sdev = 0.0001; \
end; \
else do; \
d_Cleak = 0; \
sdev = 0.0001; \
end; \
endsub; \
subroutine d_H1(TIME, C12, Cleak, H1, H2, d_H1, sdev); \
outargs d_H1, sdev; \
A1 = 200; \
f1in = 1; \
C1out = 0.5; \
d_H1 = (1/A1)*f1in - (C12+Cleak+C1out)/A1 * H1 \
+ (C12)/A1 * H2; \
sdev = 0.001; \
endsub; \
subroutine d_H2(TIME, C12, H1, H2, d_H2, sdev); \
outargs d_H2, sdev; \
A2 = 100; \
C2out = 1; \
d_H2 = C12/A2 * H1 \
- ((C12+C2out)/A2) * H2; \
sdev = 0.001; \
endsub; \
subroutine F1out_rd(H1, mean, sdev); \
outargs mean, sdev; \
C1out = 0.5; \
mean = H1 * C1out; \
sdev = 0.0001; \
endsub; \
subroutine F2out_rd(H2, mean, sdev); \
outargs mean, sdev; \
C2out = 1; \
mean = H2 * C2out; \
sdev = 0.0001; \
endsub;"))
The dynBnet action is invoked as follows to generate new beliefs by filtering the incoming observations;
myres = cas.dynamicBayesianNetwork.dynbnet(s, table = 'tank_data',
varRoles = 'variableRoles',
varLevels = 'variableLevels',
links = 'links',
varodes = 'variableOdes',
initBeliefs = 'initialBeliefs',
odeApprox = 'NONE',
output = 'output')