InterpolatingMicrosphere.java
/*
* 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
*
* https://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.
*/
/*
* This is not the original file distributed by the Apache Software Foundation
* It has been modified by the Hipparchus project
*/
package org.hipparchus.analysis.interpolation;
import java.util.ArrayList;
import java.util.List;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.exception.MathIllegalArgumentException;
import org.hipparchus.exception.MathIllegalStateException;
import org.hipparchus.random.UnitSphereRandomVectorGenerator;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathArrays;
import org.hipparchus.util.MathUtils;
/**
* Utility class for the {@link MicrosphereProjectionInterpolator} algorithm.
*
*/
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 MathIllegalArgumentException if {@code dimension <= 0}
* or {@code size <= 0}.
* @throws MathIllegalArgumentException if {@code darkThreshold < 0}.
* @throws MathIllegalArgumentException 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 MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL_BOUND_EXCLUDED,
dimension, 0);
}
if (size <= 0) {
throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL_BOUND_EXCLUDED,
size, 0);
}
MathUtils.checkRangeInclusive(maxDarkFraction, 0, 1);
if (darkThreshold < 0) {
throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, darkThreshold, 0);
}
this.dimension = dimension;
this.size = size;
this.maxDarkFraction = maxDarkFraction;
this.darkThreshold = darkThreshold;
this.background = background;
microsphere = new ArrayList<>(size);
microsphereData = new ArrayList<>(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 MathIllegalArgumentException if the size of the generated
* vectors does not match the dimension set in the constructor.
* @throws MathIllegalArgumentException if {@code dimension <= 0}
* or {@code size <= 0}.
* @throws MathIllegalArgumentException if {@code darkThreshold < 0}.
* @throws MathIllegalArgumentException 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<>(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 MathIllegalArgumentException if {@code exponent < 0}.
*/
public double value(double[] point,
double[][] samplePoints,
double[] sampleValues,
double exponent,
double noInterpolationTolerance) {
if (exponent < 0) {
throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, exponent, 0);
}
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 MathIllegalArgumentException if the length of {@code n}
* does not match the space dimension.
* @throws MathIllegalStateException 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 MathIllegalStateException(LocalizedCoreFormats.MAX_COUNT_EXCEEDED, size);
}
if (normal.length > dimension) {
throw new MathIllegalArgumentException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
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; // NOPMD - array cloning is taken care of at call site
}
/**
* Return a reference to the vector normal to this facet.
*
* @return the normal vector.
*/
public double[] getNormal() {
return normal; // NOPMD - returning an internal array is intentional and documented here
}
}
/**
* 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;
}
}
}