summaryrefslogtreecommitdiff
path: root/src/main/java/org/apache/commons/math3/analysis/interpolation/InterpolatingMicrosphere.java
blob: dc600bde8666c9775b10888dd94031b7e459adcf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.math3.analysis.interpolation;

import java.util.List;
import java.util.ArrayList;
import org.apache.commons.math3.random.UnitSphereRandomVectorGenerator;
import org.apache.commons.math3.exception.DimensionMismatchException;
import org.apache.commons.math3.exception.NotPositiveException;
import org.apache.commons.math3.exception.NotStrictlyPositiveException;
import org.apache.commons.math3.exception.MaxCountExceededException;
import org.apache.commons.math3.exception.OutOfRangeException;
import org.apache.commons.math3.util.FastMath;
import org.apache.commons.math3.util.MathArrays;

/**
 * Utility class for the {@link MicrosphereProjectionInterpolator} algorithm.
 *
 * @since 3.6
 */
public class InterpolatingMicrosphere {
    /** Microsphere. */
    private final List<Facet> microsphere;
    /** Microsphere data. */
    private final List<FacetData> microsphereData;
    /** Space dimension. */
    private final int dimension;
    /** Number of surface elements. */
    private final int size;
    /** Maximum fraction of the facets that can be dark. */
    private final double maxDarkFraction;
    /** Lowest non-zero illumination. */
    private final double darkThreshold;
    /** Background value. */
    private final double background;

    /**
     * Create an unitialiazed sphere.
     * Sub-classes are responsible for calling the {@code add(double[]) add}
     * method in order to initialize all the sphere's facets.
     *
     * @param dimension Dimension of the data space.
     * @param size Number of surface elements of the sphere.
     * @param maxDarkFraction Maximum fraction of the facets that can be dark.
     * If the fraction of "non-illuminated" facets is larger, no estimation
     * of the value will be performed, and the {@code background} value will
     * be returned instead.
     * @param darkThreshold Value of the illumination below which a facet is
     * considered dark.
     * @param background Value returned when the {@code maxDarkFraction}
     * threshold is exceeded.
     * @throws NotStrictlyPositiveException if {@code dimension <= 0}
     * or {@code size <= 0}.
     * @throws NotPositiveException if {@code darkThreshold < 0}.
     * @throws OutOfRangeException if {@code maxDarkFraction} does not
     * belong to the interval {@code [0, 1]}.
     */
    protected InterpolatingMicrosphere(int dimension,
                                       int size,
                                       double maxDarkFraction,
                                       double darkThreshold,
                                       double background) {
        if (dimension <= 0) {
            throw new NotStrictlyPositiveException(dimension);
        }
        if (size <= 0) {
            throw new NotStrictlyPositiveException(size);
        }
        if (maxDarkFraction < 0 ||
            maxDarkFraction > 1) {
            throw new OutOfRangeException(maxDarkFraction, 0, 1);
        }
        if (darkThreshold < 0) {
            throw new NotPositiveException(darkThreshold);
        }

        this.dimension = dimension;
        this.size = size;
        this.maxDarkFraction = maxDarkFraction;
        this.darkThreshold = darkThreshold;
        this.background = background;
        microsphere = new ArrayList<Facet>(size);
        microsphereData = new ArrayList<FacetData>(size);
    }

    /**
     * Create a sphere from randomly sampled vectors.
     *
     * @param dimension Dimension of the data space.
     * @param size Number of surface elements of the sphere.
     * @param rand Unit vector generator for creating the microsphere.
     * @param maxDarkFraction Maximum fraction of the facets that can be dark.
     * If the fraction of "non-illuminated" facets is larger, no estimation
     * of the value will be performed, and the {@code background} value will
     * be returned instead.
     * @param darkThreshold Value of the illumination below which a facet
     * is considered dark.
     * @param background Value returned when the {@code maxDarkFraction}
     * threshold is exceeded.
     * @throws DimensionMismatchException if the size of the generated
     * vectors does not match the dimension set in the constructor.
     * @throws NotStrictlyPositiveException if {@code dimension <= 0}
     * or {@code size <= 0}.
     * @throws NotPositiveException if {@code darkThreshold < 0}.
     * @throws OutOfRangeException if {@code maxDarkFraction} does not
     * belong to the interval {@code [0, 1]}.
     */
    public InterpolatingMicrosphere(int dimension,
                                    int size,
                                    double maxDarkFraction,
                                    double darkThreshold,
                                    double background,
                                    UnitSphereRandomVectorGenerator rand) {
        this(dimension, size, maxDarkFraction, darkThreshold, background);

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

    /**
     * Copy constructor.
     *
     * @param other Instance to copy.
     */
    protected InterpolatingMicrosphere(InterpolatingMicrosphere other) {
        dimension = other.dimension;
        size = other.size;
        maxDarkFraction = other.maxDarkFraction;
        darkThreshold = other.darkThreshold;
        background = other.background;

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

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

    /**
     * Perform a copy.
     *
     * @return a copy of this instance.
     */
    public InterpolatingMicrosphere copy() {
        return new InterpolatingMicrosphere(this);
    }

    /**
     * Get the space dimensionality.
     *
     * @return the number of space dimensions.
     */
    public int getDimension() {
        return dimension;
    }

    /**
     * Get the size of the sphere.
     *
     * @return the number of surface elements of the microspshere.
     */
    public int getSize() {
        return size;
    }

    /**
     * Estimate the value at the requested location.
     * This microsphere is placed at the given {@code point}, contribution
     * of the given {@code samplePoints} to each sphere facet is computed
     * (illumination) and the interpolation is performed (integration of
     * the illumination).
     *
     * @param point Interpolation point.
     * @param samplePoints Sampling data points.
     * @param sampleValues Sampling data values at the corresponding
     * {@code samplePoints}.
     * @param exponent Exponent used in the power law that computes
     * the weights (distance dimming factor) of the sample data.
     * @param noInterpolationTolerance When the distance between the
     * {@code point} and one of the {@code samplePoints} is less than
     * this value, no interpolation will be performed, and the value
     * of the sample will just be returned.
     * @return the estimated value at the given {@code point}.
     * @throws NotPositiveException if {@code exponent < 0}.
     */
    public double value(double[] point,
                        double[][] samplePoints,
                        double[] sampleValues,
                        double exponent,
                        double noInterpolationTolerance) {
        if (exponent < 0) {
            throw new NotPositiveException(exponent);
        }

        clear();

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

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

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

        return interpolate();
    }

    /**
     * Replace {@code i}-th facet of the microsphere.
     * Method for initializing the microsphere facets.
     *
     * @param normal Facet's normal vector.
     * @param copy Whether to copy the given array.
     * @throws DimensionMismatchException if the length of {@code n}
     * does not match the space dimension.
     * @throws MaxCountExceededException if the method has been called
     * more times than the size of the sphere.
     */
    protected void add(double[] normal,
                       boolean copy) {
        if (microsphere.size() >= size) {
            throw new MaxCountExceededException(size);
        }
        if (normal.length > dimension) {
            throw new DimensionMismatchException(normal.length, dimension);
        }

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

    /**
     * Interpolation.
     *
     * @return the value estimated from the current illumination of the
     * microsphere.
     */
    private double interpolate() {
        // Number of non-illuminated facets.
        int darkCount = 0;

        double value = 0;
        double totalWeight = 0;
        for (FacetData fd : microsphereData) {
            final double iV = fd.illumination();
            if (iV != 0d) {
                value += iV * fd.sample();
                totalWeight += iV;
            } else {
                ++darkCount;
            }
        }

        final double darkFraction = darkCount / (double) size;

        return darkFraction <= maxDarkFraction ?
            value / totalWeight :
            background;
    }

    /**
     * Illumination.
     *
     * @param sampleDirection Vector whose origin is at the interpolation
     * point and tail is at the sample location.
     * @param sampleValue Data value of the sample.
     * @param weight Weight.
     */
    private void illuminate(double[] sampleDirection,
                            double sampleValue,
                            double weight) {
        for (int i = 0; i < size; i++) {
            final double[] n = microsphere.get(i).getNormal();
            final double cos = MathArrays.cosAngle(n, sampleDirection);

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

                if (illumination > darkThreshold &&
                    illumination > microsphereData.get(i).illumination()) {
                    microsphereData.set(i, new FacetData(illumination, sampleValue));
                }
            }
        }
    }

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

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

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

        /**
         * Return a reference to the vector normal to this facet.
         *
         * @return the normal vector.
         */
        public double[] getNormal() {
            return normal;
        }
    }

    /**
     * Data associated with each {@link Facet}.
     */
    private static class FacetData {
        /** Illumination received from the sample. */
        private final double illumination;
        /** Data value of the sample. */
        private final double sample;

        /**
         * @param illumination Illumination.
         * @param sample Data value.
         */
        FacetData(double illumination, double sample) {
            this.illumination = illumination;
            this.sample = sample;
        }

        /**
         * Get the illumination.
         * @return the illumination.
         */
        public double illumination() {
            return illumination;
        }

        /**
         * Get the data value.
         * @return the data value.
         */
        public double sample() {
            return sample;
        }
    }
}