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 }