docs: @Override
  	public AbstractLIBORCovarianceModelParametric getCloneCalibrated(final LIBORMarketModel calibrationModel, final CalibrationProduct[] calibrationProducts, Map<String,Object> calibrationParameters) throws CalculationException {
  
  		if(calibrationParameters == null) {
  			calibrationParameters = new HashMap<>();
  		}
  
  		int numberOfPaths	= (Integer)calibrationParameters.getOrDefault("numberOfPaths", 2000);
  		int seed			= (Integer)calibrationParameters.getOrDefault("seed", 31415);
  		int maxIterations	= (Integer)calibrationParameters.getOrDefault("maxIterations", 400);
  		double accuracy		= (Double)calibrationParameters.getOrDefault("accuracy", 1E-7);
  		final BrownianMotion brownianMotion = (BrownianMotion)calibrationParameters.getOrDefault("brownianMotion", new BrownianMotionLazyInit(getTimeDiscretization(), getNumberOfFactors(), numberOfPaths, seed));
  
  		RandomVariable[] initialParameters = this.getParameter();
  		RandomVariable[] lowerBound = new RandomVariable[initialParameters.length];
  		RandomVariable[] upperBound = new RandomVariable[initialParameters.length];
  		RandomVariable[] parameterStep = new RandomVariable[initialParameters.length];
  		Arrays.fill(lowerBound, new RandomVariableFromDoubleArray(Double.NEGATIVE_INFINITY));
  		Arrays.fill(upperBound, new RandomVariableFromDoubleArray(Double.POSITIVE_INFINITY));
  		Double	parameterStepParameter	= (Double)calibrationParameters.get("parameterStep");
  		Arrays.fill(parameterStep,  new RandomVariableFromDoubleArray(parameterStepParameter != null ? parameterStepParameter.doubleValue() : 1E-4));
  
  		int numberOfThreadsForProductValuation = Runtime.getRuntime().availableProcessors();
  		final ExecutorService executor = Executors.newFixedThreadPool(numberOfThreadsForProductValuation);
  
  		StochasticOptimizer.ObjectiveFunction calibrationError = new StochasticOptimizer.ObjectiveFunction() {
  			// Calculate model values for given parameters
  			@Override
  			public void setValues(RandomVariable[] parameters, RandomVariable[] values) throws SolverException {
  
  				AbstractLIBORCovarianceModelParametric calibrationCovarianceModel = AbstractLIBORCovarianceModelParametric.this.getCloneWithModifiedParameters(parameters);
  
  				// Create a LIBOR market model with the new covariance structure.
  				LIBORMarketModel model = calibrationModel.getCloneWithModifiedCovarianceModel(calibrationCovarianceModel);
  				EulerSchemeFromProcessModel process = new EulerSchemeFromProcessModel(brownianMotion);
  				final LIBORMonteCarloSimulationFromLIBORModel liborMarketModelMonteCarloSimulation =  new LIBORMonteCarloSimulationFromLIBORModel(model, process);
  
  				ArrayList<Future<RandomVariable>> valueFutures = new ArrayList<Future<RandomVariable>>(calibrationProducts.length);
  				for(int calibrationProductIndex=0; calibrationProductIndex<calibrationProducts.length; calibrationProductIndex++) {
  					final int workerCalibrationProductIndex = calibrationProductIndex;
  					Callable<RandomVariable> worker = new  Callable<RandomVariable>() {
  						@Override
  						public RandomVariable call() {
  							try {
  								return calibrationProducts[workerCalibrationProductIndex].getProduct().getValue(0.0, liborMarketModelMonteCarloSimulation).sub(calibrationProducts[workerCalibrationProductIndex].getTargetValue()).mult(calibrationProducts[workerCalibrationProductIndex].getWeight());
  							} catch (CalculationException e) {
  								// We do not signal exceptions to keep the solver working and automatically exclude non-working calibration products.
  								return new Scalar(0.0);
  							} catch (Exception e) {
  								// We do not signal exceptions to keep the solver working and automatically exclude non-working calibration products.
  								return new Scalar(0.0);
  							}
  						}
  					};
  					if(executor != null) {
  						Future<RandomVariable> valueFuture = executor.submit(worker);
  						valueFutures.add(calibrationProductIndex, valueFuture);
  					}
  					else {
  						FutureTask<RandomVariable> valueFutureTask = new FutureTask<>(worker);
  						valueFutureTask.run();
  						valueFutures.add(calibrationProductIndex, valueFutureTask);
  					}
  				}
  				for(int calibrationProductIndex=0; calibrationProductIndex<calibrationProducts.length; calibrationProductIndex++) {
  					try {
  						RandomVariable value = valueFutures.get(calibrationProductIndex).get();
  						values[calibrationProductIndex] = value;
  					}
  					catch (InterruptedException e) {
  						throw new SolverException(e);
  					} catch (ExecutionException e) {
  						throw new SolverException(e);
  					}
  				}
  			}
  		};
  
  		/*
  		 * 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;
  		Object optimizerFactory = calibrationParameters.getOrDefault("optimizerFactory", new OptimizerFactoryLevenbergMarquardt(maxIterations, accuracy, numberOfThreads));
  		//		Object optimizerFactory = calibrationParameters.getOrDefault("optimizerFactory", new StochasticPathwiseOptimizerFactoryLevenbergMarquardt(maxIterations, accuracy, numberOfThreads));
  
  		/*
  		 * Obtain besterParameters and numberOfIterations
  		 */
  		AbstractLIBORCovarianceModelParametric calibrationCovarianceModel = null;
  		if(optimizerFactory instanceof StochasticOptimizerFactory) {
  			RandomVariable[] zerosForTargetValues = new RandomVariable[calibrationProducts.length];
  			Arrays.fill(zerosForTargetValues, new RandomVariableFromDoubleArray(0.0));
  			StochasticOptimizer optimizer = ((StochasticOptimizerFactory)optimizerFactory).getOptimizer(calibrationError, initialParameters, lowerBound, upperBound, parameterStep, zerosForTargetValues);
  			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.
  			RandomVariable[] bestParameters = optimizer.getBestFitParameters();
  			int numberOfIterations = optimizer.getIterations();
  			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);
  			}
  		}
  		else if(optimizerFactory instanceof OptimizerFactory) {
  			return getCloneCalibratedLegazy(calibrationModel, calibrationProducts, calibrationParameters);
  			/*
  			double[] zerosForTargetValues = new double[calibrationProducts.length];
  			Arrays.fill(zerosForTargetValues, 0.0);
  			Optimizer optimizer = ((OptimizerFactory)optimizerFactory).getOptimizer(calibrationError, initialParameters, lowerBound, upperBound, parameterStep, zerosForTargetValues);
  			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();
  			int numberOfIterations = optimizer.getIterations();
  			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);
  			}
  			 */
  		}
  		else {
  			throw new IllegalArgumentException(optimizerFactory + " not supported.");
  		}
  
  		return calibrationCovarianceModel;
  	}