View Javadoc
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  
19  import java.util.List;
20  import java.util.ArrayList;
21  import org.apache.commons.numbers.core.Norm;
22  import org.apache.commons.numbers.angle.CosAngle;
23  import org.apache.commons.rng.sampling.UnitSphereSampler;
24  import org.apache.commons.math4.legacy.exception.DimensionMismatchException;
25  import org.apache.commons.math4.legacy.exception.NotPositiveException;
26  import org.apache.commons.math4.legacy.exception.NotStrictlyPositiveException;
27  import org.apache.commons.math4.legacy.exception.MaxCountExceededException;
28  import org.apache.commons.math4.legacy.exception.OutOfRangeException;
29  import org.apache.commons.math4.core.jdkmath.JdkMath;
30  import org.apache.commons.math4.legacy.core.MathArrays;
31  
32  /**
33   * Utility class for the {@link MicrosphereProjectionInterpolator} algorithm.
34   *
35   * @since 3.6
36   */
37  public class InterpolatingMicrosphere {
38      /** Microsphere. */
39      private final List<Facet> microsphere;
40      /** Microsphere data. */
41      private final List<FacetData> microsphereData;
42      /** Space dimension. */
43      private final int dimension;
44      /** Number of surface elements. */
45      private final int size;
46      /** Maximum fraction of the facets that can be dark. */
47      private final double maxDarkFraction;
48      /** Lowest non-zero illumination. */
49      private final double darkThreshold;
50      /** Background value. */
51      private final double background;
52  
53      /**
54       * Create an uninitialized sphere.
55       * Sub-classes are responsible for calling the {@code add(double[]) add}
56       * method in order to initialize all the sphere's facets.
57       *
58       * @param dimension Dimension of the data space.
59       * @param size Number of surface elements of the sphere.
60       * @param maxDarkFraction Maximum fraction of the facets that can be dark.
61       * If the fraction of "non-illuminated" facets is larger, no estimation
62       * of the value will be performed, and the {@code background} value will
63       * be returned instead.
64       * @param darkThreshold Value of the illumination below which a facet is
65       * considered dark.
66       * @param background Value returned when the {@code maxDarkFraction}
67       * threshold is exceeded.
68       * @throws NotStrictlyPositiveException if {@code dimension <= 0}
69       * or {@code size <= 0}.
70       * @throws NotPositiveException if {@code darkThreshold < 0}.
71       * @throws OutOfRangeException if {@code maxDarkFraction} does not
72       * belong to the interval {@code [0, 1]}.
73       */
74      protected InterpolatingMicrosphere(int dimension,
75                                         int size,
76                                         double maxDarkFraction,
77                                         double darkThreshold,
78                                         double background) {
79          if (dimension <= 0) {
80              throw new NotStrictlyPositiveException(dimension);
81          }
82          if (size <= 0) {
83              throw new NotStrictlyPositiveException(size);
84          }
85          if (maxDarkFraction < 0 ||
86              maxDarkFraction > 1) {
87              throw new OutOfRangeException(maxDarkFraction, 0, 1);
88          }
89          if (darkThreshold < 0) {
90              throw new NotPositiveException(darkThreshold);
91          }
92  
93          this.dimension = dimension;
94          this.size = size;
95          this.maxDarkFraction = maxDarkFraction;
96          this.darkThreshold = darkThreshold;
97          this.background = background;
98          microsphere = new ArrayList<>(size);
99          microsphereData = new ArrayList<>(size);
100     }
101 
102     /**
103      * Create a sphere from randomly sampled vectors.
104      *
105      * @param dimension Dimension of the data space.
106      * @param size Number of surface elements of the sphere.
107      * @param rand Unit vector generator for creating the microsphere.
108      * @param maxDarkFraction Maximum fraction of the facets that can be dark.
109      * If the fraction of "non-illuminated" facets is larger, no estimation
110      * of the value will be performed, and the {@code background} value will
111      * be returned instead.
112      * @param darkThreshold Value of the illumination below which a facet
113      * is considered dark.
114      * @param background Value returned when the {@code maxDarkFraction}
115      * threshold is exceeded.
116      * @throws DimensionMismatchException if the size of the generated
117      * vectors does not match the dimension set in the constructor.
118      * @throws NotStrictlyPositiveException if {@code dimension <= 0}
119      * or {@code size <= 0}.
120      * @throws NotPositiveException if {@code darkThreshold < 0}.
121      * @throws OutOfRangeException if {@code maxDarkFraction} does not
122      * belong to the interval {@code [0, 1]}.
123      */
124     public InterpolatingMicrosphere(int dimension,
125                                     int size,
126                                     double maxDarkFraction,
127                                     double darkThreshold,
128                                     double background,
129                                     UnitSphereSampler rand) {
130         this(dimension, size, maxDarkFraction, darkThreshold, background);
131 
132         // Generate the microsphere normals, assuming that a number of
133         // randomly generated normals will represent a sphere.
134         for (int i = 0; i < size; i++) {
135             add(rand.sample(), false);
136         }
137     }
138 
139     /**
140      * Copy constructor.
141      *
142      * @param other Instance to copy.
143      */
144     protected InterpolatingMicrosphere(InterpolatingMicrosphere other) {
145         dimension = other.dimension;
146         size = other.size;
147         maxDarkFraction = other.maxDarkFraction;
148         darkThreshold = other.darkThreshold;
149         background = other.background;
150 
151         // Field can be shared.
152         microsphere = other.microsphere;
153 
154         // Field must be copied.
155         microsphereData = new ArrayList<>(size);
156         for (FacetData fd : other.microsphereData) {
157             microsphereData.add(new FacetData(fd.illumination(), fd.sample()));
158         }
159     }
160 
161     /**
162      * Perform a copy.
163      *
164      * @return a copy of this instance.
165      */
166     public InterpolatingMicrosphere copy() {
167         return new InterpolatingMicrosphere(this);
168     }
169 
170     /**
171      * Get the space dimensionality.
172      *
173      * @return the number of space dimensions.
174      */
175     public int getDimension() {
176         return dimension;
177     }
178 
179     /**
180      * Get the size of the sphere.
181      *
182      * @return the number of surface elements of the microspshere.
183      */
184     public int getSize() {
185         return size;
186     }
187 
188     /**
189      * Estimate the value at the requested location.
190      * This microsphere is placed at the given {@code point}, contribution
191      * of the given {@code samplePoints} to each sphere facet is computed
192      * (illumination) and the interpolation is performed (integration of
193      * the illumination).
194      *
195      * @param point Interpolation point.
196      * @param samplePoints Sampling data points.
197      * @param sampleValues Sampling data values at the corresponding
198      * {@code samplePoints}.
199      * @param exponent Exponent used in the power law that computes
200      * the weights (distance dimming factor) of the sample data.
201      * @param noInterpolationTolerance When the distance between the
202      * {@code point} and one of the {@code samplePoints} is less than
203      * this value, no interpolation will be performed, and the value
204      * of the sample will just be returned.
205      * @return the estimated value at the given {@code point}.
206      * @throws NotPositiveException if {@code exponent < 0}.
207      */
208     public double value(double[] point,
209                         double[][] samplePoints,
210                         double[] sampleValues,
211                         double exponent,
212                         double noInterpolationTolerance) {
213         if (exponent < 0) {
214             throw new NotPositiveException(exponent);
215         }
216 
217         clear();
218 
219         // Contribution of each sample point to the illumination of the
220         // microsphere's facets.
221         final int numSamples = samplePoints.length;
222         for (int i = 0; i < numSamples; i++) {
223             // Vector between interpolation point and current sample point.
224             final double[] diff = MathArrays.ebeSubtract(samplePoints[i], point);
225             final double diffNorm = Norm.L2.of(diff);
226 
227             if (JdkMath.abs(diffNorm) < noInterpolationTolerance) {
228                 // No need to interpolate, as the interpolation point is
229                 // actually (very close to) one of the sampled points.
230                 return sampleValues[i];
231             }
232 
233             final double weight = JdkMath.pow(diffNorm, -exponent);
234             illuminate(diff, sampleValues[i], weight);
235         }
236 
237         return interpolate();
238     }
239 
240     /**
241      * Replace {@code i}-th facet of the microsphere.
242      * Method for initializing the microsphere facets.
243      *
244      * @param normal Facet's normal vector.
245      * @param copy Whether to copy the given array.
246      * @throws DimensionMismatchException if the length of {@code n}
247      * does not match the space dimension.
248      * @throws MaxCountExceededException if the method has been called
249      * more times than the size of the sphere.
250      */
251     protected void add(double[] normal,
252                        boolean copy) {
253         if (microsphere.size() >= size) {
254             throw new MaxCountExceededException(size);
255         }
256         if (normal.length > dimension) {
257             throw new DimensionMismatchException(normal.length, dimension);
258         }
259 
260         microsphere.add(new Facet(copy ? normal.clone() : normal));
261         microsphereData.add(new FacetData(0d, 0d));
262     }
263 
264     /**
265      * Interpolation.
266      *
267      * @return the value estimated from the current illumination of the
268      * microsphere.
269      */
270     private double interpolate() {
271         // Number of non-illuminated facets.
272         int darkCount = 0;
273 
274         double value = 0;
275         double totalWeight = 0;
276         for (FacetData fd : microsphereData) {
277             final double iV = fd.illumination();
278             if (iV != 0d) {
279                 value += iV * fd.sample();
280                 totalWeight += iV;
281             } else {
282                 ++darkCount;
283             }
284         }
285 
286         final double darkFraction = darkCount / (double) size;
287 
288         return darkFraction <= maxDarkFraction ?
289             value / totalWeight :
290             background;
291     }
292 
293     /**
294      * Illumination.
295      *
296      * @param sampleDirection Vector whose origin is at the interpolation
297      * point and tail is at the sample location.
298      * @param sampleValue Data value of the sample.
299      * @param weight Weight.
300      */
301     private void illuminate(double[] sampleDirection,
302                             double sampleValue,
303                             double weight) {
304         for (int i = 0; i < size; i++) {
305             final double[] n = microsphere.get(i).getNormal();
306             final double cos = CosAngle.value(n, sampleDirection);
307 
308             if (cos > 0) {
309                 final double illumination = cos * weight;
310 
311                 if (illumination > darkThreshold &&
312                     illumination > microsphereData.get(i).illumination()) {
313                     microsphereData.set(i, new FacetData(illumination, sampleValue));
314                 }
315             }
316         }
317     }
318 
319     /**
320      * Reset the all the {@link Facet facets} data to zero.
321      */
322     private void clear() {
323         for (int i = 0; i < size; i++) {
324             microsphereData.set(i, new FacetData(0d, 0d));
325         }
326     }
327 
328     /**
329      * Microsphere "facet" (surface element).
330      */
331     private static final class Facet {
332         /** Normal vector characterizing a surface element. */
333         private final double[] normal;
334 
335         /**
336          * @param n Normal vector characterizing a surface element
337          * of the microsphere. No copy is made.
338          */
339         Facet(double[] n) {
340             normal = n;
341         }
342 
343         /**
344          * Return a reference to the vector normal to this facet.
345          *
346          * @return the normal vector.
347          */
348         public double[] getNormal() {
349             return normal;
350         }
351     }
352 
353     /**
354      * Data associated with each {@link Facet}.
355      */
356     private static final class FacetData {
357         /** Illumination received from the sample. */
358         private final double illumination;
359         /** Data value of the sample. */
360         private final double sample;
361 
362         /**
363          * @param illumination Illumination.
364          * @param sample Data value.
365          */
366         FacetData(double illumination, double sample) {
367             this.illumination = illumination;
368             this.sample = sample;
369         }
370 
371         /**
372          * Get the illumination.
373          * @return the illumination.
374          */
375         public double illumination() {
376             return illumination;
377         }
378 
379         /**
380          * Get the data value.
381          * @return the data value.
382          */
383         public double sample() {
384             return sample;
385         }
386     }
387 }