docs: public HybridAssetLIBORModelMonteCarloSimulationInterface getHybridAssetLIBORModel(
  			final LIBORModelMonteCarloSimulationInterface baseModel,
  			final BrownianMotionInterface brownianMotion,
  			final double[] initialValues,
  			final double riskFreeRate,
  			final double[][] correlations,
  			final double[] maturities,
  			final double[] strikes,
  			final double[] volatilities,
  			final DiscountCurveInterface discountCurve) throws CalculationException {
  
  		OptimizerInterface optimizer = new LevenbergMarquardt(volatilities /*initialParameters*/, volatilities /*targetValues*/, 100 /*maxIteration*/, 1 /*numberOfThreads*/) {
  			private static final long serialVersionUID = -9199565564991442848L;
  
  			@Override
  			public void setValues(double[] parameters, double[] values) throws SolverException {
  				AssetModelMonteCarloSimulationInterface model = new MonteCarloMultiAssetBlackScholesModel(brownianMotion, initialValues, riskFreeRate, parameters, correlations);
  				HybridAssetLIBORModelMonteCarloSimulation hybridModel = new HybridAssetLIBORModelMonteCarloSimulation(baseModel, model);
  
  				try {
  					for(int assetIndex=0; assetIndex<values.length; assetIndex++) {
  						double df = hybridModel.getNumeraire(maturities[assetIndex]).invert().getAverage();
  						double spot = hybridModel.getAssetValue(0.0, assetIndex).getAverage();
  						EuropeanOption option = new EuropeanOption(maturities[assetIndex], strikes[assetIndex], assetIndex);
  						double valueOptoin = option.getValue(hybridModel);
  						double impliedVol = AnalyticFormulas.blackScholesOptionImpliedVolatility(spot/df, maturities[assetIndex]/*optionMaturity*/, strikes[assetIndex]/*optionStrike*/, df /*payoffUnit*/, valueOptoin);
  						values[assetIndex] = impliedVol;
  					}
  				} catch (CalculationException e) {
  					throw new SolverException(e);
  				}
  			}
  		};
  
  		try {
  			optimizer.run();
  		} catch (SolverException e) {
  			if(e.getCause() instanceof CalculationException) {
  				throw (CalculationException)e.getCause();
  			} else {
  				throw new CalculationException(e);
  			}
  		}
  
  		AssetModelMonteCarloSimulationInterface model = new MonteCarloMultiAssetBlackScholesModel(brownianMotion, initialValues, riskFreeRate, optimizer.getBestFitParameters(), correlations);
  
  		/*
  		 * Test calibration
  		 */
  		HybridAssetLIBORModelMonteCarloSimulation hybridModelWithoutDiscountAdjustment = new HybridAssetLIBORModelMonteCarloSimulation(baseModel, model, null);
  		for(int assetIndex=0; assetIndex<volatilities.length; assetIndex++) {
  			double df = hybridModelWithoutDiscountAdjustment.getNumeraire(maturities[assetIndex]).invert().getAverage();
  			double spot = hybridModelWithoutDiscountAdjustment.getAssetValue(0.0, assetIndex).getAverage();
  			EuropeanOption option = new EuropeanOption(maturities[assetIndex], strikes[assetIndex], assetIndex);
  			double valueOptoin = option.getValue(hybridModelWithoutDiscountAdjustment);
  			double impliedVol = AnalyticFormulas.blackScholesOptionImpliedVolatility(spot/df, maturities[assetIndex]/*optionMaturity*/, strikes[assetIndex]/*optionStrike*/, df /*payoffUnit*/, valueOptoin);
  			if(Math.abs(impliedVol - volatilities[assetIndex]) > 0.01) {
  				throw new CalculationException("Calibration failed");
  			}
  		}
  
  		/*
  		 * Construct model with discounting (options will then use the discounting spread adjustment).
  		 */
  		HybridAssetLIBORModelMonteCarloSimulation hybridModel = new HybridAssetLIBORModelMonteCarloSimulation(baseModel, model, discountCurve);
  		return hybridModel;
  	}