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