BarrierAlignedSpotGridFactory.java

package net.finmath.finitedifference.grids;

import java.util.Arrays;

/**
 * Utility factory for spot grids used in barrier option PDE problems.
 *
 * <p>
 * Main features:
 * </p>
 * <ul>
 *   <li>forces the barrier to be an exact grid node,</li>
 *   <li>can build either a uniform or a clustered grid,</li>
 *   <li>clustered grid puts more points near the barrier.</li>
 * </ul>
 *
 * <p>
 * The clustered grid is piecewise-defined:
 * on each side of the barrier, nodes are generated with a power transform.
 * For clusteringExponent {@code > 1.0}, points concentrate near the barrier.
 * For clusteringExponent {@code = 1.0}, the grid is piecewise uniform.
 * </p>
 *
 * <p>
 * This class is intended especially for direct barrier pricing under
 * Heston/SABR,
 * where barrier alignment can materially improve knock-in accuracy.
 * </p>
 *
 * @author Alessandro Gnoatto
 */
public final class BarrierAlignedSpotGridFactory {

	private BarrierAlignedSpotGridFactory() {
	}

	/**
	 * Creates a uniform grid with the barrier exactly on a node.
	 *
	 * <p>
	 * The spacing is chosen so that:
	 * </p>
	 * <ul>
	 *   <li>the barrier lies exactly on a node,</li>
	 *   <li>the lower/upper bounds are preserved,</li>
	 *   <li>the number of intervals is exactly {@code numberOfSteps}.</li>
	 * </ul>
	 *
	 * <p>
	 * This method requires the barrier to lie strictly inside the interval.
	 * </p>
	 *
	 * @param numberOfSteps Number of grid intervals.
	 * @param sMin Lower grid boundary.
	 * @param sMax Upper grid boundary.
	 * @param barrier Barrier level.
	 * @return A grid with the barrier exactly on a node.
	 */
	public static Grid createBarrierAlignedUniformGrid(
			final int numberOfSteps,
			final double sMin,
			final double sMax,
			final double barrier) {

		validateInputs(numberOfSteps, sMin, sMax, barrier);

		final int barrierIndex = chooseBarrierIndex(numberOfSteps, sMin, sMax, barrier);

		final double[] grid = new double[numberOfSteps + 1];
		final double dxLeft = (barrier - sMin) / barrierIndex;
		final double dxRight = (sMax - barrier) / (numberOfSteps - barrierIndex);

		/*
		 * If dxLeft and dxRight differ, one cannot have a globally uniform grid
		 * with barrier fixed as an exact node and both endpoints preserved.
		 *
		 * So this "uniform" builder is best understood as:
		 * - uniform on the left of the barrier
		 * - uniform on the right of the barrier
		 * - exact barrier alignment
		 *
		 * This is often already sufficient and far better than missing the
		 * barrier.
		 */
		for (int i = 0; i <= barrierIndex; i++) {
			grid[i] = sMin + i * dxLeft;
		}
		for (int i = barrierIndex + 1; i <= numberOfSteps; i++) {
			grid[i] = barrier + (i - barrierIndex) * dxRight;
		}

		grid[0] = sMin;
		grid[barrierIndex] = barrier;
		grid[numberOfSteps] = sMax;

		return new ArrayGrid(grid);
	}

	/**
	 * Creates a barrier-aligned clustered grid.
	 *
	 * <p>
	 * The barrier is exactly on a node. Points are concentrated near the
	 * barrier
	 * using a power transform on each side:
	 * </p>
	 *
	 * <pre>
	 * x = barrier - (barrier - sMin) * (1 - u)^p    on the left side
	 * x = barrier + (sMax - barrier) * u^p          on the right side
	 * </pre>
	 *
	 * <p>
	 * where {@code p = clusteringExponent}.
	 * </p>
	 *
	 * <ul>
	 *   <li>{@code clusteringExponent = 1.0}: piecewise uniform</li>
	 * <li>{@code clusteringExponent > 1.0}: points cluster near the
	 * barrier</li>
	 * </ul>
	 *
	 * @param numberOfSteps Number of grid intervals.
	 * @param sMin Lower grid boundary.
	 * @param sMax Upper grid boundary.
	 * @param barrier Barrier level.
	 * @param clusteringExponent Clustering strength, must be >= 1.0.
	 * @return A clustered grid with the barrier exactly on a node.
	 */
	public static Grid createBarrierAlignedClusteredGrid(
			final int numberOfSteps,
			final double sMin,
			final double sMax,
			final double barrier,
			final double clusteringExponent) {

		validateInputs(numberOfSteps, sMin, sMax, barrier);

		if (clusteringExponent < 1.0) {
			throw new IllegalArgumentException("clusteringExponent must be >= 1.0");
		}

		final int barrierIndex = chooseBarrierIndex(numberOfSteps, sMin, sMax, barrier);

		final double[] grid = new double[numberOfSteps + 1];

		/*
		 * Left side: i = 0,...,barrierIndex
		 * u runs from 0 to 1, with x(barrierIndex) = barrier.
		 */
		for (int i = 0; i <= barrierIndex; i++) {
			final double u = barrierIndex == 0 ? 0.0 : ((double)i) / barrierIndex;
			grid[i] = barrier - (barrier - sMin) * Math.pow(1.0 - u, clusteringExponent);
		}

		/*
		 * Right side: i = barrierIndex,...,numberOfSteps
		 * u runs from 0 to 1, with x(barrierIndex) = barrier.
		 */
		for (int i = barrierIndex; i <= numberOfSteps; i++) {
			final double u = numberOfSteps == barrierIndex ? 0.0
					: ((double)(i - barrierIndex)) / (numberOfSteps - barrierIndex);
			grid[i] = barrier + (sMax - barrier) * Math.pow(u, clusteringExponent);
		}

		grid[0] = sMin;
		grid[barrierIndex] = barrier;
		grid[numberOfSteps] = sMax;

		enforceStrictMonotonicity(grid);

		return new ArrayGrid(grid);
	}

	/**
	 * Returns the index of the barrier node in a grid previously built by this
	 * factory.
	 *
	 * @param grid The grid array.
	 * @param barrier The barrier level.
	 * @return The barrier node index.
	 */
	public static int findBarrierIndex(final double[] grid, final double barrier) {
		for (int i = 0; i < grid.length; i++) {
			if (Math.abs(grid[i] - barrier) < 1E-12) {
				return i;
			}
		}
		throw new IllegalArgumentException("Barrier is not a grid node.");
	}

	/**
	 * Convenience overload.
	 *
	 * @param grid The grid.
	 * @param barrier The barrier level.
	 * @return The barrier node index.
	 */
	public static int findBarrierIndex(final Grid grid, final double barrier) {
		return findBarrierIndex(grid.getGrid(), barrier);
	}

	/**
	 * Chooses the barrier node index so that the node count allocation roughly
	 * matches the geometric position of the barrier inside the interval.
	 * @param numberOfSteps The value.
	 * @param sMin The value.
	 * @param sMax The value.
	 * @param barrier The value.
	 * @return The value.
	 */
	private static int chooseBarrierIndex(
			final int numberOfSteps,
			final double sMin,
			final double sMax,
			final double barrier) {

		final double relativePosition = (barrier - sMin) / (sMax - sMin);
		int barrierIndex = (int)Math.round(relativePosition * numberOfSteps);

		/*
		 * Force strict interior placement.
		 */
		barrierIndex = Math.max(1, barrierIndex);
		barrierIndex = Math.min(numberOfSteps - 1, barrierIndex);

		return barrierIndex;
	}

	private static void validateInputs(
			final int numberOfSteps,
			final double sMin,
			final double sMax,
			final double barrier) {

		if (numberOfSteps < 2) {
			throw new IllegalArgumentException("numberOfSteps must be at least 2.");
		}
		if (!(sMin < sMax)) {
			throw new IllegalArgumentException("Require sMin < sMax.");
		}
		if (!(barrier > sMin && barrier < sMax)) {
			throw new IllegalArgumentException("Barrier must lie strictly inside (sMin, sMax).");
		}
	}

	/**
	 * Guards against duplicate nodes caused by extreme clustering and floating-
	 * point rounding.
	 * @param grid The value.
	 */
	private static void enforceStrictMonotonicity(final double[] grid) {
		final double epsilon = 1E-12;

		for (int i = 1; i < grid.length; i++) {
			if (grid[i] <= grid[i - 1]) {
				grid[i] = grid[i - 1] + epsilon;
			}
		}
	}

	/**
	 * Simple immutable array-backed grid.
	 */
	private static final class ArrayGrid extends AbstractGrid {

		/**
		 * The grid.
		 */
		private final double[] grid;
		/**
		 * The delta.
		 */
		private double[] delta;

		private ArrayGrid(final double[] grid) {
			this.grid = Arrays.copyOf(grid, grid.length);
		}

		@Override
		public double[] getGrid() {
			return Arrays.copyOf(grid, grid.length);
		}

		@Override
		public double[] getDelta(final double[] valuesOnGrid) {

			if (delta == null) {
				if (valuesOnGrid == null) {
					throw new IllegalArgumentException("valuesOnGrid must not be null.");
				}
				if (valuesOnGrid.length != grid.length) {
					throw new IllegalArgumentException(
							"valuesOnGrid length must match grid length."
							);
				}

				delta = new double[valuesOnGrid.length - 1];
				for (int i = 0; i < valuesOnGrid.length - 1; i++) {
					delta[i] = valuesOnGrid[i + 1] - valuesOnGrid[i];
				}
			}

			return Arrays.copyOf(delta, delta.length);
		}
	}
}