docs: public AbstractLIBORCovarianceModelParametric getCloneCalibrated(final LIBORMarketModelInterface calibrationModel, final AbstractLIBORMonteCarloProduct[] calibrationProducts, final RandomVariableInterface[] calibrationTargetValues, double[] calibrationWeights, Map<String,Object> calibrationParameters) throws CalculationException {
  
  		if(calibrationParameters == null) {
  			calibrationParameters = new HashMap<String,Object>();
  		}
  		Integer numberOfPathsParameter	= (Integer)calibrationParameters.get("numberOfPaths");
  		Integer seedParameter			= (Integer)calibrationParameters.get("seed");
  		Integer maxIterationsParameter	= (Integer)calibrationParameters.get("maxIterations");
  		Double	parameterStepParameter	= (Double)calibrationParameters.get("parameterStep");
  		Double	accuracyParameter		= (Double)calibrationParameters.get("accuracy");
  		BrownianMotionInterface brownianMotionParameter	= (BrownianMotionInterface)calibrationParameters.get("brownianMotion");
  
  		double[] initialParameters = this.getParameter();
  		double[] lowerBound = new double[initialParameters.length];
  		double[] upperBound = new double[initialParameters.length];
  		double[] parameterStep = new double[initialParameters.length];
  		double[] zero = new double[calibrationTargetValues.length];
  		Arrays.fill(lowerBound, Double.NEGATIVE_INFINITY);
  		Arrays.fill(upperBound, Double.POSITIVE_INFINITY);
  		Arrays.fill(parameterStep, parameterStepParameter != null ? parameterStepParameter.doubleValue() : 1E-4);
  		Arrays.fill(zero, 0);
  
  		/*
  		 * We allow for 2 simultaneous calibration models.
  		 * Note: In the case of a Monte-Carlo calibration, the memory requirement is that of
  		 * one model with 2 times the number of paths. In the case of an analytic calibration
  		 * memory requirement is not the limiting factor.
  		 */
  		int numberOfThreads = 2;
  		OptimizerFactoryInterface optimizerFactoryParameter = (OptimizerFactoryInterface)calibrationParameters.get("optimizerFactory");
  
  		int numberOfPaths	= numberOfPathsParameter != null ? numberOfPathsParameter.intValue() : 2000;
  		int seed			= seedParameter != null ? seedParameter.intValue() : 31415;
  		int maxIterations	= maxIterationsParameter != null ? maxIterationsParameter.intValue() : 400;
  		double accuracy		= accuracyParameter != null ? accuracyParameter.doubleValue() : 1E-7;
  		final BrownianMotionInterface brownianMotion = brownianMotionParameter != null ? brownianMotionParameter : new BrownianMotion(getTimeDiscretization(), getNumberOfFactors(), numberOfPaths, seed);
  		OptimizerFactoryInterface optimizerFactory = optimizerFactoryParameter != null ? optimizerFactoryParameter : new OptimizerFactoryLevenbergMarquardt(maxIterations, accuracy, numberOfThreads);
  
  		int numberOfThreadsForProductValuation = 2 * Math.max(2, Runtime.getRuntime().availableProcessors());
  		final ExecutorService executor = null;//Executors.newFixedThreadPool(numberOfThreadsForProductValuation);
  
  		ObjectiveFunction calibrationError = new ObjectiveFunction() {
  			// Calculate model values for given parameters
  			@Override
  			public void setValues(double[] parameters, double[] values) throws SolverException {
  
  				AbstractLIBORCovarianceModelParametric calibrationCovarianceModel = AbstractLIBORCovarianceModelParametric.this.getCloneWithModifiedParameters(parameters);
  
  				// Create a LIBOR market model with the new covariance structure.
  				LIBORMarketModelInterface model = calibrationModel.getCloneWithModifiedCovarianceModel(calibrationCovarianceModel);
  				ProcessEulerScheme process = new ProcessEulerScheme(brownianMotion);
  				final LIBORModelMonteCarloSimulation liborMarketModelMonteCarloSimulation =  new LIBORModelMonteCarloSimulation(model, process);
  
  				ArrayList<Future<RandomVariableInterface>> valueFutures = new ArrayList<Future<RandomVariableInterface>>(calibrationProducts.length);
  				for(int calibrationProductIndex=0; calibrationProductIndex<calibrationProducts.length; calibrationProductIndex++) {
  					final int workerCalibrationProductIndex = calibrationProductIndex;
  					Callable<RandomVariableInterface> worker = new  Callable<RandomVariableInterface>() {
  						public RandomVariableInterface call() {
  							try {
  								return calibrationProducts[workerCalibrationProductIndex].getValue(0.0, liborMarketModelMonteCarloSimulation).sub(calibrationTargetValues[workerCalibrationProductIndex]);
  							} catch (CalculationException e) {
  								// We do not signal exceptions to keep the solver working and automatically exclude non-working calibration products.
  								return null;
  							} catch (Exception e) {
  								// We do not signal exceptions to keep the solver working and automatically exclude non-working calibration products.
  								return null;
  							}
  						}
  					};
  					if(executor != null) {
  						Future<RandomVariableInterface> valueFuture = executor.submit(worker);
  						valueFutures.add(calibrationProductIndex, valueFuture);
  					}
  					else {
  						FutureTask<RandomVariableInterface> valueFutureTask = new FutureTask<RandomVariableInterface>(worker);
  						valueFutureTask.run();
  						valueFutures.add(calibrationProductIndex, valueFutureTask);
  					}
  				}
  				for(int calibrationProductIndex=0; calibrationProductIndex<calibrationProducts.length; calibrationProductIndex++) {
  					try {
  						RandomVariableInterface value = valueFutures.get(calibrationProductIndex).get();
  						values[calibrationProductIndex] = value != null ? value.getAverage() : 0.0;;
  					}
  					catch (InterruptedException e) {
  						throw new SolverException(e);
  					} catch (ExecutionException e) {
  						throw new SolverException(e);
  					}
  				}
  			}
  		};
  
  		OptimizerInterface optimizer = optimizerFactory.getOptimizer(calibrationError, initialParameters, lowerBound, upperBound, parameterStep, zero);
  		try {
  			optimizer.run();
  		}
  		catch(SolverException e) {
  			throw new CalculationException(e);
  		}
  		finally {
  			if(executor != null) {
  				executor.shutdown();
  			}
  		}
  
  		// Get covariance model corresponding to the best parameter set.
  		double[] bestParameters = optimizer.getBestFitParameters();
  		AbstractLIBORCovarianceModelParametric calibrationCovarianceModel = this.getCloneWithModifiedParameters(bestParameters);
  
  		// Diagnostic output
  		if (logger.isLoggable(Level.FINE)) {
  			logger.fine("The solver required " + optimizer.getIterations() + " iterations. The best fit parameters are:");
  
  			String logString = "Best parameters:";
  			for(int i=0; i<bestParameters.length; i++) {
  				logString += "\tparameter["+i+"]: " + bestParameters[i];
  			}
  			logger.fine(logString);
  		}
  
  		return calibrationCovarianceModel;
  	}