InterpolatingMicrosphere.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.math4.legacy.analysis.interpolation;

  18. import java.util.List;
  19. import java.util.ArrayList;
  20. import org.apache.commons.numbers.core.Norm;
  21. import org.apache.commons.numbers.angle.CosAngle;
  22. import org.apache.commons.rng.sampling.UnitSphereSampler;
  23. import org.apache.commons.math4.legacy.exception.DimensionMismatchException;
  24. import org.apache.commons.math4.legacy.exception.NotPositiveException;
  25. import org.apache.commons.math4.legacy.exception.NotStrictlyPositiveException;
  26. import org.apache.commons.math4.legacy.exception.MaxCountExceededException;
  27. import org.apache.commons.math4.legacy.exception.OutOfRangeException;
  28. import org.apache.commons.math4.core.jdkmath.JdkMath;
  29. import org.apache.commons.math4.legacy.core.MathArrays;

  30. /**
  31.  * Utility class for the {@link MicrosphereProjectionInterpolator} algorithm.
  32.  *
  33.  * @since 3.6
  34.  */
  35. public class InterpolatingMicrosphere {
  36.     /** Microsphere. */
  37.     private final List<Facet> microsphere;
  38.     /** Microsphere data. */
  39.     private final List<FacetData> microsphereData;
  40.     /** Space dimension. */
  41.     private final int dimension;
  42.     /** Number of surface elements. */
  43.     private final int size;
  44.     /** Maximum fraction of the facets that can be dark. */
  45.     private final double maxDarkFraction;
  46.     /** Lowest non-zero illumination. */
  47.     private final double darkThreshold;
  48.     /** Background value. */
  49.     private final double background;

  50.     /**
  51.      * Create an uninitialized sphere.
  52.      * Sub-classes are responsible for calling the {@code add(double[]) add}
  53.      * method in order to initialize all the sphere's facets.
  54.      *
  55.      * @param dimension Dimension of the data space.
  56.      * @param size Number of surface elements of the sphere.
  57.      * @param maxDarkFraction Maximum fraction of the facets that can be dark.
  58.      * If the fraction of "non-illuminated" facets is larger, no estimation
  59.      * of the value will be performed, and the {@code background} value will
  60.      * be returned instead.
  61.      * @param darkThreshold Value of the illumination below which a facet is
  62.      * considered dark.
  63.      * @param background Value returned when the {@code maxDarkFraction}
  64.      * threshold is exceeded.
  65.      * @throws NotStrictlyPositiveException if {@code dimension <= 0}
  66.      * or {@code size <= 0}.
  67.      * @throws NotPositiveException if {@code darkThreshold < 0}.
  68.      * @throws OutOfRangeException if {@code maxDarkFraction} does not
  69.      * belong to the interval {@code [0, 1]}.
  70.      */
  71.     protected InterpolatingMicrosphere(int dimension,
  72.                                        int size,
  73.                                        double maxDarkFraction,
  74.                                        double darkThreshold,
  75.                                        double background) {
  76.         if (dimension <= 0) {
  77.             throw new NotStrictlyPositiveException(dimension);
  78.         }
  79.         if (size <= 0) {
  80.             throw new NotStrictlyPositiveException(size);
  81.         }
  82.         if (maxDarkFraction < 0 ||
  83.             maxDarkFraction > 1) {
  84.             throw new OutOfRangeException(maxDarkFraction, 0, 1);
  85.         }
  86.         if (darkThreshold < 0) {
  87.             throw new NotPositiveException(darkThreshold);
  88.         }

  89.         this.dimension = dimension;
  90.         this.size = size;
  91.         this.maxDarkFraction = maxDarkFraction;
  92.         this.darkThreshold = darkThreshold;
  93.         this.background = background;
  94.         microsphere = new ArrayList<>(size);
  95.         microsphereData = new ArrayList<>(size);
  96.     }

  97.     /**
  98.      * Create a sphere from randomly sampled vectors.
  99.      *
  100.      * @param dimension Dimension of the data space.
  101.      * @param size Number of surface elements of the sphere.
  102.      * @param rand Unit vector generator for creating the microsphere.
  103.      * @param maxDarkFraction Maximum fraction of the facets that can be dark.
  104.      * If the fraction of "non-illuminated" facets is larger, no estimation
  105.      * of the value will be performed, and the {@code background} value will
  106.      * be returned instead.
  107.      * @param darkThreshold Value of the illumination below which a facet
  108.      * is considered dark.
  109.      * @param background Value returned when the {@code maxDarkFraction}
  110.      * threshold is exceeded.
  111.      * @throws DimensionMismatchException if the size of the generated
  112.      * vectors does not match the dimension set in the constructor.
  113.      * @throws NotStrictlyPositiveException if {@code dimension <= 0}
  114.      * or {@code size <= 0}.
  115.      * @throws NotPositiveException if {@code darkThreshold < 0}.
  116.      * @throws OutOfRangeException if {@code maxDarkFraction} does not
  117.      * belong to the interval {@code [0, 1]}.
  118.      */
  119.     public InterpolatingMicrosphere(int dimension,
  120.                                     int size,
  121.                                     double maxDarkFraction,
  122.                                     double darkThreshold,
  123.                                     double background,
  124.                                     UnitSphereSampler rand) {
  125.         this(dimension, size, maxDarkFraction, darkThreshold, background);

  126.         // Generate the microsphere normals, assuming that a number of
  127.         // randomly generated normals will represent a sphere.
  128.         for (int i = 0; i < size; i++) {
  129.             add(rand.sample(), false);
  130.         }
  131.     }

  132.     /**
  133.      * Copy constructor.
  134.      *
  135.      * @param other Instance to copy.
  136.      */
  137.     protected InterpolatingMicrosphere(InterpolatingMicrosphere other) {
  138.         dimension = other.dimension;
  139.         size = other.size;
  140.         maxDarkFraction = other.maxDarkFraction;
  141.         darkThreshold = other.darkThreshold;
  142.         background = other.background;

  143.         // Field can be shared.
  144.         microsphere = other.microsphere;

  145.         // Field must be copied.
  146.         microsphereData = new ArrayList<>(size);
  147.         for (FacetData fd : other.microsphereData) {
  148.             microsphereData.add(new FacetData(fd.illumination(), fd.sample()));
  149.         }
  150.     }

  151.     /**
  152.      * Perform a copy.
  153.      *
  154.      * @return a copy of this instance.
  155.      */
  156.     public InterpolatingMicrosphere copy() {
  157.         return new InterpolatingMicrosphere(this);
  158.     }

  159.     /**
  160.      * Get the space dimensionality.
  161.      *
  162.      * @return the number of space dimensions.
  163.      */
  164.     public int getDimension() {
  165.         return dimension;
  166.     }

  167.     /**
  168.      * Get the size of the sphere.
  169.      *
  170.      * @return the number of surface elements of the microspshere.
  171.      */
  172.     public int getSize() {
  173.         return size;
  174.     }

  175.     /**
  176.      * Estimate the value at the requested location.
  177.      * This microsphere is placed at the given {@code point}, contribution
  178.      * of the given {@code samplePoints} to each sphere facet is computed
  179.      * (illumination) and the interpolation is performed (integration of
  180.      * the illumination).
  181.      *
  182.      * @param point Interpolation point.
  183.      * @param samplePoints Sampling data points.
  184.      * @param sampleValues Sampling data values at the corresponding
  185.      * {@code samplePoints}.
  186.      * @param exponent Exponent used in the power law that computes
  187.      * the weights (distance dimming factor) of the sample data.
  188.      * @param noInterpolationTolerance When the distance between the
  189.      * {@code point} and one of the {@code samplePoints} is less than
  190.      * this value, no interpolation will be performed, and the value
  191.      * of the sample will just be returned.
  192.      * @return the estimated value at the given {@code point}.
  193.      * @throws NotPositiveException if {@code exponent < 0}.
  194.      */
  195.     public double value(double[] point,
  196.                         double[][] samplePoints,
  197.                         double[] sampleValues,
  198.                         double exponent,
  199.                         double noInterpolationTolerance) {
  200.         if (exponent < 0) {
  201.             throw new NotPositiveException(exponent);
  202.         }

  203.         clear();

  204.         // Contribution of each sample point to the illumination of the
  205.         // microsphere's facets.
  206.         final int numSamples = samplePoints.length;
  207.         for (int i = 0; i < numSamples; i++) {
  208.             // Vector between interpolation point and current sample point.
  209.             final double[] diff = MathArrays.ebeSubtract(samplePoints[i], point);
  210.             final double diffNorm = Norm.L2.of(diff);

  211.             if (JdkMath.abs(diffNorm) < noInterpolationTolerance) {
  212.                 // No need to interpolate, as the interpolation point is
  213.                 // actually (very close to) one of the sampled points.
  214.                 return sampleValues[i];
  215.             }

  216.             final double weight = JdkMath.pow(diffNorm, -exponent);
  217.             illuminate(diff, sampleValues[i], weight);
  218.         }

  219.         return interpolate();
  220.     }

  221.     /**
  222.      * Replace {@code i}-th facet of the microsphere.
  223.      * Method for initializing the microsphere facets.
  224.      *
  225.      * @param normal Facet's normal vector.
  226.      * @param copy Whether to copy the given array.
  227.      * @throws DimensionMismatchException if the length of {@code n}
  228.      * does not match the space dimension.
  229.      * @throws MaxCountExceededException if the method has been called
  230.      * more times than the size of the sphere.
  231.      */
  232.     protected void add(double[] normal,
  233.                        boolean copy) {
  234.         if (microsphere.size() >= size) {
  235.             throw new MaxCountExceededException(size);
  236.         }
  237.         if (normal.length > dimension) {
  238.             throw new DimensionMismatchException(normal.length, dimension);
  239.         }

  240.         microsphere.add(new Facet(copy ? normal.clone() : normal));
  241.         microsphereData.add(new FacetData(0d, 0d));
  242.     }

  243.     /**
  244.      * Interpolation.
  245.      *
  246.      * @return the value estimated from the current illumination of the
  247.      * microsphere.
  248.      */
  249.     private double interpolate() {
  250.         // Number of non-illuminated facets.
  251.         int darkCount = 0;

  252.         double value = 0;
  253.         double totalWeight = 0;
  254.         for (FacetData fd : microsphereData) {
  255.             final double iV = fd.illumination();
  256.             if (iV != 0d) {
  257.                 value += iV * fd.sample();
  258.                 totalWeight += iV;
  259.             } else {
  260.                 ++darkCount;
  261.             }
  262.         }

  263.         final double darkFraction = darkCount / (double) size;

  264.         return darkFraction <= maxDarkFraction ?
  265.             value / totalWeight :
  266.             background;
  267.     }

  268.     /**
  269.      * Illumination.
  270.      *
  271.      * @param sampleDirection Vector whose origin is at the interpolation
  272.      * point and tail is at the sample location.
  273.      * @param sampleValue Data value of the sample.
  274.      * @param weight Weight.
  275.      */
  276.     private void illuminate(double[] sampleDirection,
  277.                             double sampleValue,
  278.                             double weight) {
  279.         for (int i = 0; i < size; i++) {
  280.             final double[] n = microsphere.get(i).getNormal();
  281.             final double cos = CosAngle.value(n, sampleDirection);

  282.             if (cos > 0) {
  283.                 final double illumination = cos * weight;

  284.                 if (illumination > darkThreshold &&
  285.                     illumination > microsphereData.get(i).illumination()) {
  286.                     microsphereData.set(i, new FacetData(illumination, sampleValue));
  287.                 }
  288.             }
  289.         }
  290.     }

  291.     /**
  292.      * Reset the all the {@link Facet facets} data to zero.
  293.      */
  294.     private void clear() {
  295.         for (int i = 0; i < size; i++) {
  296.             microsphereData.set(i, new FacetData(0d, 0d));
  297.         }
  298.     }

  299.     /**
  300.      * Microsphere "facet" (surface element).
  301.      */
  302.     private static final class Facet {
  303.         /** Normal vector characterizing a surface element. */
  304.         private final double[] normal;

  305.         /**
  306.          * @param n Normal vector characterizing a surface element
  307.          * of the microsphere. No copy is made.
  308.          */
  309.         Facet(double[] n) {
  310.             normal = n;
  311.         }

  312.         /**
  313.          * Return a reference to the vector normal to this facet.
  314.          *
  315.          * @return the normal vector.
  316.          */
  317.         public double[] getNormal() {
  318.             return normal;
  319.         }
  320.     }

  321.     /**
  322.      * Data associated with each {@link Facet}.
  323.      */
  324.     private static final class FacetData {
  325.         /** Illumination received from the sample. */
  326.         private final double illumination;
  327.         /** Data value of the sample. */
  328.         private final double sample;

  329.         /**
  330.          * @param illumination Illumination.
  331.          * @param sample Data value.
  332.          */
  333.         FacetData(double illumination, double sample) {
  334.             this.illumination = illumination;
  335.             this.sample = sample;
  336.         }

  337.         /**
  338.          * Get the illumination.
  339.          * @return the illumination.
  340.          */
  341.         public double illumination() {
  342.             return illumination;
  343.         }

  344.         /**
  345.          * Get the data value.
  346.          * @return the data value.
  347.          */
  348.         public double sample() {
  349.             return sample;
  350.         }
  351.     }
  352. }