1 //                                               -*- C++ -*-
2 /**
3  *  @brief Implementation of SimulationResult
4  *
5  *  Copyright 2005-2021 Airbus-EDF-IMACS-ONERA-Phimeca
6  *
7  *  This library is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU Lesser General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This library is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public License
18  *  along with this library.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 #include "openturns/SimulationSensitivityAnalysis.hxx"
22 #include "openturns/PersistentObjectFactory.hxx"
23 #include "openturns/Exception.hxx"
24 #include "openturns/MemoizeFunction.hxx"
25 #include "openturns/SobolIndicesAlgorithm.hxx"
26 #include "openturns/Curve.hxx"
27 #include "openturns/Collection.hxx"
28 #include "openturns/Exception.hxx"
29 
30 BEGIN_NAMESPACE_OPENTURNS
31 
32 CLASSNAMEINIT(SimulationSensitivityAnalysis)
33 
34 
35 static const Factory<SimulationSensitivityAnalysis> Factory_SimulationSensitivityAnalysis;
36 
37 /* Default constructor */
SimulationSensitivityAnalysis()38 SimulationSensitivityAnalysis::SimulationSensitivityAnalysis()
39   : PersistentObject()
40 {
41   // Nothing to do
42 }
43 
44 /* Standard constructor */
SimulationSensitivityAnalysis(const RandomVector & event,const Sample & inputSample,const Sample & outputSample)45 SimulationSensitivityAnalysis::SimulationSensitivityAnalysis(const RandomVector & event,
46     const Sample & inputSample,
47     const Sample & outputSample)
48   : PersistentObject(),
49     inputSample_(inputSample),
50     outputSample_(outputSample),
51     event_(event)
52 {
53   const UnsignedInteger inputSize = inputSample.getSize();
54   const UnsignedInteger inputDimension = inputSample.getDimension();
55   // Check if the given sample have compatible size
56   if (inputSize != outputSample.getSize()) throw InvalidArgumentException(HERE) << "Error: the input sample has a size=" << inputSize << " which is not equal to the output sample size=" << outputSample.getSize();
57   // Check if the samples are not empty
58   if (inputSize == 0) throw InvalidArgumentException(HERE) << "Error: cannot perform analysis based on empty samples.";
59   // Check if the iso-probabilistic transformation is compatible with the input sample
60   if (inputDimension != getTransformation().getInputDimension()) throw InvalidArgumentException(HERE) << "Error: the given iso-probabilistic transformation has a dimension=" << getTransformation().getInputDimension() << " that is different from the input sample dimension=" << inputDimension;
61   // Check if the output sample is uni dimensional
62   if (outputSample.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: the given output sample must have a dimension=1, here dimension=" << outputSample.getDimension();
63 }
64 
65 /* Standard constructor */
SimulationSensitivityAnalysis(const ProbabilitySimulationResult & result)66 SimulationSensitivityAnalysis::SimulationSensitivityAnalysis(const ProbabilitySimulationResult & result)
67   : PersistentObject()
68 {
69   *this = SimulationSensitivityAnalysis(result.getEvent());
70 }
71 
72 /* Standard constructor */
SimulationSensitivityAnalysis(const RandomVector & event)73 SimulationSensitivityAnalysis::SimulationSensitivityAnalysis(const RandomVector & event)
74   : PersistentObject(),
75     event_(event)
76 {
77   // Inspect the event to see if it is a composite random vector based event
78   if (!event.isEvent() || !event.isComposite()) throw InvalidArgumentException(HERE) << "Error: cannot perform a sensitivity analysis based on the given event. Check if it is based on a composite random vector.";
79   // Get the input/output sample from the model
80   MemoizeFunction model(event.getFunction());
81   inputSample_  = model.getInputHistory();
82   // Check if the samples are not empty
83   if (inputSample_.getSize() == 0) throw InvalidArgumentException(HERE) << "Error: cannot perform analysis based on empty samples.";
84   // We are sure that the output sample has the same size as the input sample
85   outputSample_ = model.getOutputHistory();
86   inputSample_.setDescription(event.getImplementation()->getAntecedent().getDistribution().getDescription());
87 }
88 
89 /* Virtual constructor */
clone() const90 SimulationSensitivityAnalysis * SimulationSensitivityAnalysis::clone() const
91 {
92   return new SimulationSensitivityAnalysis(*this);
93 }
94 
95 /* Mean point in event domain computation */
computeMeanPointInEventDomain(const Scalar threshold) const96 Point SimulationSensitivityAnalysis::computeMeanPointInEventDomain(const Scalar threshold) const
97 {
98   const UnsignedInteger inputSize = inputSample_.getSize();
99   Sample filteredSample(0, inputSample_.getDimension());
100   // Filter the input points with respect to the considered event
101   for (UnsignedInteger i = 0; i < inputSize; ++i)
102     if (getComparisonOperator()(outputSample_(i, 0), threshold)) filteredSample.add(inputSample_[i]);
103   if (filteredSample.getSize() == 0) throw NotDefinedException(HERE) << "Error: cannont compute the mean point if no point is in the event domain.";
104   return filteredSample.computeMean();
105 }
106 
computeMeanPointInEventDomain() const107 Point SimulationSensitivityAnalysis::computeMeanPointInEventDomain() const
108 {
109   return computeMeanPointInEventDomain(getThreshold());
110 }
111 
112 /* Importance factors computation */
computeImportanceFactors(const Scalar threshold) const113 PointWithDescription SimulationSensitivityAnalysis::computeImportanceFactors(const Scalar threshold) const
114 {
115   PointWithDescription result(getTransformation()(computeMeanPointInEventDomain(threshold)).normalizeSquare());
116   result.setDescription(inputSample_.getDescription());
117   return result;
118 }
119 
computeImportanceFactors() const120 PointWithDescription SimulationSensitivityAnalysis::computeImportanceFactors() const
121 {
122   return computeImportanceFactors(getThreshold());
123 }
124 
computeEventProbabilitySensitivity() const125 PointWithDescription SimulationSensitivityAnalysis::computeEventProbabilitySensitivity() const
126 {
127   const UnsignedInteger dimension = inputSample_.getDimension();
128   const UnsignedInteger size = inputSample_.getSize();
129 
130   // only marginal parameters are handled here as the pdf gradient of copulas is not implemented
131   Description description(event_.getImplementation()->getAntecedent().getDistribution().getParameterDescription());
132   UnsignedInteger parameterDimension = 0;
133   Collection<Distribution> marginals(dimension);
134   for (UnsignedInteger j = 0; j < dimension; ++ j)
135   {
136     marginals[j] = event_.getImplementation()->getAntecedent().getDistribution().getMarginal(j);
137     parameterDimension += marginals[j].getParameter().getSize();
138   }
139   // remove copula parameters
140   description.erase(description.begin() + parameterDimension, description.end());
141   Point sumGrad(parameterDimension);
142   const ComparisonOperator op(getComparisonOperator());
143   const Scalar threshold = getThreshold();
144   for (UnsignedInteger i = 0; i < size; ++ i)
145   {
146     if (op(outputSample_(i, 0), threshold))
147     {
148       Point grad(parameterDimension);
149       UnsignedInteger index = 0;
150       for (UnsignedInteger j = 0; j < dimension; ++ j)
151       {
152         Point gradj(marginals[j].computeLogPDFGradient(Point(1, inputSample_(i, j))));
153         std::copy(gradj.begin(), gradj.end(), grad.begin() + index);
154         index += gradj.getDimension();
155       }
156       sumGrad += grad / size;
157     }
158   }
159   PointWithDescription sensitivity(sumGrad);
160   sensitivity.setDescription(description);
161   return sensitivity;
162 }
163 
164 /* Importance factors drawing */
drawImportanceFactors() const165 Graph SimulationSensitivityAnalysis::drawImportanceFactors() const
166 {
167   OSS oss;
168   oss << "Importance Factors from Simulation - " << outputSample_.getDescription()[0];
169   return SobolIndicesAlgorithm::DrawImportanceFactors(computeImportanceFactors(), oss.str());
170 
171 }
172 
drawImportanceFactorsRange(const Bool probabilityScale,const Scalar lower,const Scalar upper) const173 Graph SimulationSensitivityAnalysis::drawImportanceFactorsRange(const Bool probabilityScale,
174     const Scalar lower,
175     const Scalar upper) const
176 {
177   // Here we choose if we have to go forward or backward through the data
178   // True if < or <=
179   const Bool goForward = getComparisonOperator()(0.0, 1.0);
180   // True if > or >=
181   const Bool goBackward = getComparisonOperator()(1.0, 0.0);
182   // If both are false, the comparison operator checks for equality, for which the method is not implemented
183   if ( (!goForward) && (!goBackward) ) throw InternalException(HERE) << "Error: the drawImportanceFactorsRange is not implemented for an equality comparison operator.";
184   // Load the preconized sample margin to avoid too noisy estimate of importance factors
185   const UnsignedInteger sampleMargin = ResourceMap::GetAsUnsignedInteger("SimulationSensitivityAnalysis-DefaultSampleMargin");
186   const UnsignedInteger size = inputSample_.getSize();
187   if (sampleMargin >= size / 2) throw InternalException(HERE) << "Error: the default sample margin must be in less than half of the sample size, here sample margin=" << sampleMargin << " and sample size=" << size << ". Check the SimulationSensitivityAnalysis-DefaultSampleMargin key value in ResourceMap.";
188   // iStart is the first index of the data to consider
189   SignedInteger iStart = 0;
190   // iStop is the first index outside of the data
191   SignedInteger iStop = size;
192   // iStartDrawing is the first index of the data to draw
193   SignedInteger iStartDrawing = sampleMargin;
194   // iStopDrawing is the first index the is not drawn
195   SignedInteger iStopDrawing = size - iStartDrawing;
196   SignedInteger step = 1;
197   if (goBackward)
198   {
199     iStart = size - 1;
200     iStop = -1;
201     iStartDrawing = size - sampleMargin - 1;
202     iStopDrawing = int(sampleMargin) - 1;
203     step = -1;
204   }
205   // Here, we must take the ties into account in order to get an algorithm that is
206   // both correct AND efficient.
207   // The best way I found is to aggregate the input and output samples in order to sort all the data wrt the output value
208   // Note on the memory management:
209   // + We decided to store the data into two separate samples, one for the input (dimension d), one for the output (dimension 1)
210   // + We want to produce a set of curve showing the evolution of each importance factor with respect either to a threshold value (for all the comparison operators) or to a probability (only for the weak or strict ordering operators)
211   // + The algorithm must duplicate the data at least because of the iso-probabilistic transformation
212   // + In fact, each curve embeds its data, so the input sample is duplicated and the output data is copied d times
213   // + In the case of ties in the output sample, the data stored in the curves are shorter than the initial data
214   const UnsignedInteger inputDimension = inputSample_.getDimension();
215   Sample mergedSample(size, inputDimension + 1);
216   // Use the loop to compute the number of points that compares favorably
217   // to the internal threshold
218   UnsignedInteger good = 0;
219   for (UnsignedInteger i = 0; i < size; ++i)
220   {
221     for (UnsignedInteger j = 0; j < inputDimension; ++j) mergedSample(i, j) = inputSample_(i, j);
222     mergedSample[i][inputDimension] = outputSample_(i, 0);
223     good += getComparisonOperator()(outputSample_(i, 0), getThreshold());
224   }
225   if ((good < sampleMargin) || (good >= size - sampleMargin)) LOGWARN(OSS() << "Warning: the default threshold does not correspond to well-estimated importance factors according to the default sample margin. The number of points defining the event is " << good << " and should be in [" << sampleMargin << ", " << size - sampleMargin - 1 << "] according to the SimulationSensitivityAnalysis-DefaulSampleMargin key value in ResourceMap.");
226   // Sort the merged sample according to its last component
227   mergedSample = mergedSample.sortAccordingToAComponent(inputDimension);
228   // Prepare the data for the curves
229   Collection<Sample> dataCollection(inputDimension, Sample(0, 2));
230   // Now, we can go through the data and accumulate the importance factors. If we just call the computeImportanceFactors() method directly, the cost is O(size^2), which is too expensive for typical situations.
231   // Aggregate the points in the event
232   Point accumulator(inputDimension);
233   // Here, we cannot use a simple loop as we have to deal with ties
234   SignedInteger i = iStart;
235   UnsignedInteger accumulated = 0;
236   Bool mustDraw = false;
237   while (i != iStopDrawing)
238   {
239     SignedInteger iThreshold = i;
240     Scalar currentThreshold = mergedSample[iThreshold][inputDimension];
241     // First, search for a valid threshold, ie one that needs to accumulate more points than the ones already accumulated
242     while (!getComparisonOperator()(mergedSample[i][inputDimension], currentThreshold))
243     {
244       // Accumulate the current threshold candidate, as it will be accepted as soon as a valid threshold will be found
245       const Point current(mergedSample[iThreshold]);
246       for (UnsignedInteger j = 0; j < inputDimension; ++j) accumulator[j] += current[j];
247       ++accumulated;
248       iThreshold += step;
249       // Exit if no other meaningful threshold is available
250       if (iThreshold == iStopDrawing) break;
251       currentThreshold = mergedSample[iThreshold][inputDimension];
252     }
253     // Here, either we have iThreshold == iStopDrawing, in which case there is no other point to add to the graph (for example, the largest values are all equal and we compare using <), or we found a valid new value for the threshold and the associated index iThreshold
254     if (iThreshold == iStopDrawing) break;
255     // The accumulator hes accumulated all the points that didn't compare with the previous threshold value, which means that there are no remaining points if the comparison operator is strict, or there can be additional points to accumulate if the operator is not strict. We have to accumulate all the points associated with a value equals to this threshold
256     // i is the index associated with the last point having a value equal to the threshold. It is iThreshold if the comparison is strict.
257     i = iThreshold;
258     if (getComparisonOperator()(currentThreshold, currentThreshold))
259     {
260       SignedInteger iTies = iThreshold;
261       while (getComparisonOperator()(mergedSample[iTies][inputDimension], currentThreshold))
262       {
263         // Accumulate the current threshold
264         const Point current(mergedSample[iTies]);
265         for (UnsignedInteger j = 0; j < inputDimension; ++j) accumulator[j] += current[j];
266         ++accumulated;
267         iTies += step;
268         // Exit if no other point is available. We have to take into account points of index possibly larger than iStopDrawing because the current threshold could have been reached before index iStopDrawing but could stay the current value for indices after iStopDrawing
269         if (iTies == iStop) break;
270       } // Accumulate points associated with a value equal to the threshold
271       // i is the index associated with the last point having a value equal to the threshold. It is iTies - 1 if the comparison is not strict.
272       i = iTies;
273     }
274     // We must draw the point if the first index associated with the threshold value is equal or after (greater if step = 1, less if step = -1) iStartDrawing.
275     mustDraw |= (iThreshold == iStartDrawing);
276     if (mustDraw)
277     {
278       // Now, augmente the data in the collection
279       // Initialize with values associated with probabilityScale == false
280       Scalar xValue = currentThreshold;
281       // Change the values if probabilityScale == true
282       if (probabilityScale) xValue = Scalar(accumulated) / size;
283       // Check if the point is in the exploration range
284       if ((xValue >= lower) && (xValue <= upper))
285       {
286         Point importanceFactors;
287         // Check if the importance factors are well-defined for the current threshold
288         try
289         {
290           importanceFactors = getTransformation()(accumulator / accumulated).normalizeSquare();
291           const Point ref(computeImportanceFactors(currentThreshold));
292           for (UnsignedInteger j = 0; j < inputDimension; ++j)
293           {
294             Point point(2);
295             point[0] = xValue;
296             point[1] = 100.0 * importanceFactors[j];
297             dataCollection[j].add(point);
298           }
299         }
300         catch (...)
301         {
302           String msg("Warning! The importance factors associated with the");
303           if (probabilityScale) msg += " probability level ";
304           else msg += " threshold ";
305           msg = String(OSS() << msg << xValue << " are not defined");
306           LOGWARN(msg);
307         }
308       } // Within range
309     } // mustDraw
310   } // while (i != iStopDrawing)
311   // Initialize with values associated with probabilityScale == false
312   String xLabel("threshold");
313   Scalar internalX = getThreshold();
314   // Change the values if probabilityScale == true
315   if (probabilityScale)
316   {
317     xLabel = "probability";
318     internalX = static_cast<Scalar>(good) / size;
319   }
320   Graph graph("Importance factors range", xLabel, "Importance (%)", true, "topright");
321   const Description colors(Drawable::BuildDefaultPalette(inputDimension));
322   for (UnsignedInteger j = 0; j < inputDimension; ++j)
323   {
324     Curve curve(dataCollection[j]);
325     curve.setColor(colors[j]);
326     curve.setLegend(inputSample_.getDescription()[j]);
327     graph.add(curve);
328   }
329   // Highlight the default threshold importance factors if stable enough
330   if ((internalX >= lower) && (internalX <= upper) && (good >= sampleMargin) && (good < size - sampleMargin))
331   {
332     Sample data(2, 2);
333     data(0, 0) = internalX;
334     data(0, 1) = 0.0;
335     data(1, 0) = internalX;
336     data(1, 1) = 100.0;
337     Curve curve(data);
338     curve.setLineStyle("dashed");
339     curve.setLineWidth(2);
340     curve.setColor("red");
341     curve.setLegend("current thres.");
342     graph.add(curve);
343   }
344   return graph;
345 }
346 
347 /* Input sample accessors */
getInputSample() const348 Sample SimulationSensitivityAnalysis::getInputSample() const
349 {
350   return inputSample_;
351 }
352 
353 /* Output sample accessors */
getOutputSample() const354 Sample SimulationSensitivityAnalysis::getOutputSample() const
355 {
356   return outputSample_;
357 }
358 
359 /* Threshold accessors */
getThreshold() const360 Scalar SimulationSensitivityAnalysis::getThreshold() const
361 {
362   return event_.getThreshold();
363 }
364 
365 /* Comparison operator accessors */
getComparisonOperator() const366 ComparisonOperator SimulationSensitivityAnalysis::getComparisonOperator() const
367 {
368   return event_.getOperator();
369 }
370 
371 /* Iso-probabilistic transformation accessor */
getTransformation() const372 SimulationSensitivityAnalysis::IsoProbabilisticTransformation SimulationSensitivityAnalysis::getTransformation() const
373 {
374   return event_.getImplementation()->getAntecedent().getDistribution().getIsoProbabilisticTransformation();
375 }
376 
377 /* String converter */
__repr__() const378 String SimulationSensitivityAnalysis::__repr__() const
379 {
380   OSS oss;
381   oss << "class=" << GetClassName()
382       << " inputSample=" << inputSample_
383       << " outputSample=" << outputSample_
384       << " event=" << event_;
385   return oss;
386 }
387 
388 /* Method save() stores the object through the StorageManager */
save(Advocate & adv) const389 void SimulationSensitivityAnalysis::save(Advocate & adv) const
390 {
391   PersistentObject::save(adv);
392   adv.saveAttribute("inputSample_", inputSample_);
393   adv.saveAttribute("outputSample_", outputSample_);
394   adv.saveAttribute("event_", event_);
395 }
396 
397 /* Method load() reloads the object from the StorageManager */
load(Advocate & adv)398 void SimulationSensitivityAnalysis::load(Advocate & adv)
399 {
400   PersistentObject::load(adv);
401   adv.loadAttribute("inputSample_", inputSample_);
402   adv.loadAttribute("outputSample_", outputSample_);
403   adv.loadAttribute("event_", event_);
404 }
405 
406 END_NAMESPACE_OPENTURNS
407