Skip to content

Commit 779ce69

Browse files
authored
Limit Surface bounding error (#1217)
* limit surface error bound * Closes #1216 * added new requirement * considering the number of points, the error bound can be at maximum accurate to 1e-3 * fixed perturbation
1 parent ade7321 commit 779ce69

File tree

9 files changed

+414
-18
lines changed

9 files changed

+414
-18
lines changed

doc/sqa/srs/requirements_list.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@
9797
<requirement id_code="R-RA-7">
9898
<description>RAVEN shall be able to compute probability of failure based on generated data and goal functions</description>
9999
</requirement>
100+
<requirement id_code="R-RA-8">
101+
<description>RAVEN shall be able to estimate the maximum (bounding) error in the computation of the probability of failure based on generated data and goal functions</description>
102+
</requirement>
100103
</requirement_set>
101104

102105
<requirement_set caption="Risk Mitigation">

doc/user_manual/postprocessor.tex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,16 @@ \subsubsection{LimitSurfaceIntegral}
810810
\item \xmlNode{integralType}, \xmlDesc{string, optional field}, specifies the type of integrations that
811811
need to be used. Currently only MonteCarlo integration is available
812812
\default{MonteCarlo}
813+
\item \xmlNode{computeBounds}, \xmlDesc{bool, optional field},
814+
activates the computation of the bounding error of the limit
815+
surface integral ( maximum error in the identification of the
816+
limit surface location). If True, the bounding error is stored
817+
in a variable named as \xmlNode{outputName} appending the suffix
818+
``\_err''. For example, if \xmlNode{outputName} is
819+
``EventProbability'', the bounding error will be stored as
820+
``EventProbability\_err'' (this variable name must be listed as
821+
variable in the output DataObject).
822+
\default{False}
813823
\item \xmlNode{seed}, \xmlDesc{integer, optional field}, specifies the random number generator seed.
814824
\default{20021986}
815825
\item \xmlNode{target}, \xmlDesc{string, optional field}, specifies the target name that represents

framework/DataObjects/DataSet.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,8 +1244,6 @@ def _convertToXrDataset(self):
12441244
self.addMeta('DataSet',{'Hierarchical':{'path':','.join(p)}})
12451245
# clear alignment tracking for indexes
12461246
self._clearAlignment()
1247-
else:
1248-
self.raiseAWarning('No data in DataSet to construct!')
12491247
return self._data
12501248

12511249
def _formatRealization(self,rlz):

framework/Databases.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ def _handleInput(self, paramInput):
9292
self.raiseADebug('HDF5 Read Mode is "'+self.readMode+'".')
9393
if self.readMode == 'overwrite':
9494
# check if self.databaseDir exists or create in case not
95-
if not os.path.exists(self.databaseDir):
96-
os.mkdir(self.databaseDir)
95+
if not os.path.isdir(self.databaseDir):
96+
os.makedirs(self.databaseDir, exist_ok=True)
9797
# get full path
9898
fullpath = os.path.join(self.databaseDir,self.filename)
9999
if os.path.isfile(fullpath):

framework/PostProcessors/LimitSurfaceIntegral.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
import numpy as np
2323
import xarray
2424
import math
25-
import os
25+
import sys
26+
from copy import deepcopy
2627
#External Modules End--------------------------------------------------------------------------------
2728

2829
#Internal Modules------------------------------------------------------------------------------------
@@ -78,6 +79,9 @@ class cls.
7879
LSIOutputNameInput = InputData.parameterInputFactory("outputName", contentType=InputTypes.StringType)
7980
inputSpecification.addSub(LSIOutputNameInput)
8081

82+
LSIOutputNameInput = InputData.parameterInputFactory("computeBounds", contentType=InputTypes.BoolType)
83+
inputSpecification.addSub(LSIOutputNameInput)
84+
8185
return inputSpecification
8286

8387
def __init__(self, messageHandler):
@@ -93,13 +97,15 @@ def __init__(self, messageHandler):
9397
self.integralType = 'montecarlo' # integral type (which alg needs to be used). Either montecarlo or quadrature(quadrature not yet)
9498
self.seed = 20021986 # seed for montecarlo
9599
self.matrixDict = {} # dictionary of arrays and target
96-
self.lowerUpperDict = {}
97-
self.functionS = None
98-
self.computationPrefix = None
100+
self.computeErrrorBounds = False # compute the bounding error?
101+
self.lowerUpperDict = {} # dictionary of lower and upper bounds (used if no distributions are inputted)
102+
self.functionS = None # evaluation classifier for the integration
103+
self.errorModel = None # classifier used for the error estimation
104+
self.computationPrefix = None # output prefix for the storage of the probability and, if requested, bounding error
99105
self.stat = BasicStatistics(self.messageHandler) # instantiation of the 'BasicStatistics' processor, which is used to compute the pb given montecarlo evaluations
100-
self.stat.what = ['expectedValue']
101-
self.addAssemblerObject('distribution','-n', newXmlFlg = True)
102-
self.printTag = 'POSTPROCESSOR INTEGRAL'
106+
self.stat.what = ['expectedValue'] # expected value calculation
107+
self.addAssemblerObject('distribution','-n', newXmlFlg = True) # distributions are optional
108+
self.printTag = 'POSTPROCESSOR INTEGRAL' # print tag
103109

104110
def _localReadMoreXML(self, xmlNode):
105111
"""
@@ -159,17 +165,20 @@ def _handleInput(self, paramInput):
159165
self.target = child.value
160166
elif child.getName() == 'outputName':
161167
self.computationPrefix = child.value
168+
elif child.getName() == 'computeBounds':
169+
self.computeErrrorBounds = child.value
162170
else:
163171
self.raiseAnError(NameError, 'invalid or missing labels after the variables call. Only "variable" is accepted.tag: ' + child.getName())
164172
# if no distribution, we look for the integration domain in the input
165173
if varName != None:
166174
if self.variableDist[varName] == None:
167175
if 'lowerBound' not in self.lowerUpperDict[varName].keys() or 'upperBound' not in self.lowerUpperDict[varName].keys():
168176
self.raiseAnError(NameError, 'either a distribution name or lowerBound and upperBound need to be specified for variable ' + varName)
169-
if self.computationPrefix == None:
177+
if self.computationPrefix is None:
170178
self.raiseAnError(IOError,'The required XML node <outputName> has not been inputted!!!')
171-
if self.target == None:
179+
if self.target is None:
172180
self.raiseAWarning('integral target has not been provided. The postprocessor is going to take the last output it finds in the provided limitsurface!!!')
181+
True
173182

174183
def initialize(self, runInfo, inputs, initDict):
175184
"""
@@ -183,10 +192,29 @@ def initialize(self, runInfo, inputs, initDict):
183192
if self.integralType in ['montecarlo']:
184193
self.stat.toDo = {'expectedValue':[{'targets':set([self.target]), 'prefix':self.computationPrefix}]}
185194
self.stat.initialize(runInfo, inputs, initDict)
186-
self.functionS = LearningGate.returnInstance('SupervisedGate','SciKitLearn', self, **{'SKLtype':'neighbors|KNeighborsClassifier', 'Features':','.join(list(self.variableDist.keys())), 'Target':self.target})
195+
self.functionS = LearningGate.returnInstance('SupervisedGate','SciKitLearn', self,
196+
**{'SKLtype':'neighbors|KNeighborsClassifier',
197+
'Features':','.join(list(self.variableDist.keys())),
198+
'Target':self.target, 'n_jobs': -1})
187199
self.functionS.train(self.matrixDict)
188200
self.raiseADebug('DATA SET MATRIX:')
189201
self.raiseADebug(self.matrixDict)
202+
if self.computeErrrorBounds:
203+
# create a model for computing the "error"
204+
self.errorModel = LearningGate.returnInstance('SupervisedGate','SciKitLearn', self,
205+
**{'SKLtype':'neighbors|KNeighborsClassifier',
206+
'Features':','.join(list(self.variableDist.keys())),
207+
'Target':self.target, 'weights': 'distance', 'n_jobs': -1})
208+
#modify the self.matrixDict to compute half of the "error"
209+
indecesToModifyOnes = np.argwhere(self.matrixDict[self.target] > 0.).flatten()
210+
res = np.concatenate((np.ones(len(indecesToModifyOnes)), np.zeros(len(indecesToModifyOnes))))
211+
modifiedMatrixDict = {}
212+
for key in self.matrixDict:
213+
avg = np.average(self.matrixDict[key][indecesToModifyOnes])
214+
modifiedMatrixDict[key] = np.concatenate((self.matrixDict[key][indecesToModifyOnes], self.matrixDict[key][indecesToModifyOnes]
215+
+ (self.matrixDict[key][indecesToModifyOnes]/avg * 1.e-14))) if key != self.target else res
216+
self.errorModel.train(modifiedMatrixDict)
217+
190218
for varName, distName in self.variableDist.items():
191219
if distName != None:
192220
self.variableDist[varName] = self.retrieveObjectFromAssemblerDict('distribution', distName)
@@ -228,8 +256,9 @@ def run(self, input):
228256
This method executes the postprocessor action. In this case, it performs the computation of the LS integral
229257
@ In, input, object, object contained the data to process. (inputToInternal output)
230258
@ Out, pb, float, integral outcome (probability of the event)
259+
@ Out, boundError, float, optional, error bound (maximum error of the computed probability)
231260
"""
232-
pb = None
261+
pb, boundError = None, None
233262
if self.integralType == 'montecarlo':
234263
tempDict = {}
235264
randomMatrix = np.random.rand(int(math.ceil(1.0 / self.tolerance**2)), len(self.variableDist.keys()))
@@ -241,9 +270,11 @@ def run(self, input):
241270
randomMatrix[:, index] = f(randomMatrix[:, index])
242271
tempDict[varName] = randomMatrix[:, index]
243272
pb = self.stat.run({'targets':{self.target:xarray.DataArray(self.functionS.evaluate(tempDict)[self.target])}})[self.computationPrefix +"_"+self.target]
273+
if self.errorModel:
274+
boundError = abs(pb-self.stat.run({'targets':{self.target:xarray.DataArray(self.errorModel.evaluate(tempDict)[self.target])}})[self.computationPrefix +"_"+self.target])
244275
else:
245276
self.raiseAnError(NotImplemented, "quadrature not yet implemented")
246-
return pb
277+
return pb, boundError
247278

248279
def collectOutput(self, finishedJob, output):
249280
"""
@@ -253,13 +284,18 @@ def collectOutput(self, finishedJob, output):
253284
@ Out, None
254285
"""
255286
evaluation = finishedJob.getEvaluation()
256-
pb = evaluation[1]
287+
pb, boundError = evaluation[1]
257288
lms = evaluation[0][0]
258289
if output.type == 'PointSet':
259290
# we store back the limitsurface
260291
dataSet = lms.asDataset()
261292
loadDict = {key: dataSet[key].values for key in lms.getVars()}
262293
loadDict[self.computationPrefix] = np.full(len(lms), pb)
294+
if self.computeErrrorBounds:
295+
if self.computationPrefix+"_err" not in output.getVars():
296+
self.raiseAWarning('ERROR Bounds have been computed but the output DataObject does not request the variable: "', self.computationPrefix+"_err", '"!')
297+
else:
298+
loadDict[self.computationPrefix+"_err"] = np.full(len(lms), boundError)
263299
output.load(loadDict,'dict')
264300
# NB I keep this commented part in case we want to keep the possibility to have outputfiles for PP
265301
#elif isinstance(output,Files.File):

framework/Samplers/LimitSurfaceSearch.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,5 +850,4 @@ def _formatSolutionExportVariableNames(self, acceptable):
850850
new.append(template.format(RESIDUUM=self.goalFunction.name))
851851
else:
852852
new.append(template)
853-
print('DEBUGG new:', new)
854853
return set(new)

0 commit comments

Comments
 (0)