package org.opensha2.data; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkElementIndex; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static org.opensha2.data.IntervalData.checkDataState; import static org.opensha2.data.IntervalData.indexOf; import static org.opensha2.data.IntervalData.keys; import org.opensha2.data.IntervalData.AbstractArray; import org.opensha2.data.IntervalData.DefaultArray; import com.google.common.primitives.Doubles; import java.util.Arrays; import java.util.List; /** * An array of immutable, double-valued data that is arranged according to * increasing and uniformly spaced double-valued keys. Interval arrays are used * to represent binned data, and so while row keys are bin centers, indexing is * managed internally using bin edges. This simplifies issues related to * rounding/precision errors that occur when indexing according to explicit * double values. * * <p>To create an instance of an {@code IntervalArray}, use a {@link Builder}. * * <p>Internally, an {@code IntervalArray} is backed by a {@code double[]} where * a 'row' maps to an index in the backing array. * * <p>Note that interval arrays are not intended for use with very high * precision data and keys are currently limited to a precision of 4 decimal * places. This may change in the future. * * @author Peter Powers * @see IntervalData * @see IntervalTable * @see IntervalVolume */ public interface IntervalArray { /** * Return the value of the bin that maps to the supplied row value. Do not * confuse this method with {@link #get(int)} by row index. * * @param rowValue of bin to retrieve * @throws IndexOutOfBoundsException if value is out of range */ double get(double rowValue); /** * Return the value of the bin that maps to the supplied row index. Do not * confuse this method with {@link #get(double)} by row value. * * @param rowIndex of bin to retrieve * @throws IndexOutOfBoundsException if index is out of range */ double get(int rowIndex); /** * Return an immutable view of {@code this} as an {@link XySequence}. */ XySequence values(); /** * Return the lower edge of the lowermost bin. */ double rowMin(); /** * Return the upper edge of the uppermost bin. */ double rowMax(); /** * Return the row bin discretization. */ double rowΔ(); /** * Return an immutable list <i>view</i> of the row keys (bin centers). */ List<Double> rows(); /** * Return the sum of the values in this array. */ double sum(); /** * Return the index of the bin with smallest value. */ int minIndex(); /** * Return the index of the bin with largest value. */ int maxIndex(); /** * A supplier of values with which to fill an {@code IntervalArray}. */ interface Loader { /** * Compute the value corresponding to the supplied row key (bin center). * * @param row value */ public double compute(double row); } /** * A builder of immutable {@code IntervalArray}s. * * <p>Rows and columns must be specified before any data can be added. Note * that any supplied {@code max} values may not correspond to the final upper * edge of the uppermost bins if {@code max - min} is not evenly divisible by * {@code Δ} . */ public static final class Builder { private double[] data; private double rowMin; private double rowMax; private double rowΔ; private double[] rows; private boolean built = false; private boolean initialized = false; /** * Create a new builder. */ public Builder() {} /** * Create a new builder with the structure and content identical to that of * the supplied array. * * @param array to copy */ public static Builder copyOf(IntervalArray array) { /* Safe covariant cast. */ DefaultArray defaultArray = (DefaultArray) array; Builder builder = copyStructure(defaultArray); builder.data = Arrays.copyOf( defaultArray.data, defaultArray.data.length); builder.init(); return builder; } /** * Create a new builder with a structure identical to that of the supplied * model. * * @param model interval array */ public static Builder fromModel(IntervalArray model) { /* Safe covariant cast. */ Builder builder = copyStructure((AbstractArray) model); builder.init(); return builder; } private static Builder copyStructure(AbstractArray from) { Builder to = new Builder(); to.rowMin = from.rowMin; to.rowMax = from.rowMax; to.rowΔ = from.rowΔ; to.rows = from.rows; return to; } /** * Define the array intervals. * * @param min lower edge of lowermost row bin * @param max upper edge of uppermost row bin * @param Δ bin discretization */ public static Builder withRows(double min, double max, double Δ) { Builder b = new Builder(); b.rowMin = min; b.rowMax = max; b.rowΔ = Δ; b.rows = keys(min, max, Δ); b.init(); return b; } private void init() { checkState(!initialized, "Builder has already been initialized"); if (data == null) { data = new double[rows.length]; } initialized = true; } /** * Return the index of the row that would contain the supplied value. * @param row value */ public int rowIndex(double row) { return indexOf(rowMin, rowΔ, row, rows.length); } /** * Set the value at the specified row. Be careful not to confuse this with * {@link #set(int, double)}. * * @param row key * @param value to set */ public Builder set(double row, double value) { return set(rowIndex(row), value); } /** * Set the value at the specified row. Be careful not to confuse this with * {@link #set(double, double)}. * * @param row index * @param value to set */ public Builder set(int row, double value) { data[row] = value; return this; } /** * Add to the existing value at the specified row. Be careful not to confuse * this with {@link #add(int, double)}. * * @param row key * @param value to add */ public Builder add(double row, double value) { return add(rowIndex(row), value); } /** * Add to the existing value at the specified row. Be careful not to confuse * this with {@link #add(double, double)}. * * @param row index * @param value to add */ public Builder add(int row, double value) { data[row] += value; return this; } /** * Add to the array being built. * * @param values to add * @throws IndexOutOfBoundsException if values overrun array */ public Builder add(double[] values) { checkElementIndex(values.length - 1, rows.length, "Supplied values overrun array"); for (int i = 0; i < values.length; i++) { data[i] += values[i]; } return this; } /** * Add to the array being built. * * @param values to add * @throws IndexOutOfBoundsException if values overrun array */ public Builder add(List<Double> values) { return add(Doubles.toArray(values)); } /** * Add the y-values of the supplied sequence to the array being built. * * @param sequence to add * @throws IndexOutOfBoundsException if values overrun array */ public Builder add(XySequence sequence) { // safe covariant cast return add(((ImmutableXySequence) sequence).ys); } /** * Add to the array being built starting at the specified row. * * @param row key from which to start adding values * @param values to add * @throws IndexOutOfBoundsException if values overrun array */ public Builder add(double row, double[] values) { int rowIndex = rowIndex(row); checkElementIndex(rowIndex + values.length - 1, rows.length, "Supplied values overrun end of row"); for (int i = 0; i < values.length; i++) { data[rowIndex + i] = values[i]; } return this; } /** * Add to the array being built starting at the specified row. * * @param row key from which to start adding values * @param values to add * @throws IndexOutOfBoundsException if values will overrun array */ public Builder add(double row, List<Double> values) { return add(row, Doubles.toArray(values)); } /** * Add the values in the supplied array to this builder. This operation is * very efficient if this builder and the supplied array are sourced from * the same model. * * @param array to add * @throws IllegalArgumentException if the rows of the supplied array do not * match those of this array * @see #fromModel(IntervalArray) */ public Builder add(IntervalArray array) { // safe covariant cast validateArray((AbstractArray) array); // safe covariant cast until other concrete implementations exist Data.uncheckedAdd(data, ((DefaultArray) array).data); return this; } /** * Add each value-pair of the supplied sequence to the appropriate interval. * * @param sequence to add */ public Builder addEach(XySequence sequence) { for (XyPoint xy : sequence) { add(xy.x(), xy.y()); } return this; } /** * Multiply ({@code scale}) all values in this builder. * @param scale factor */ public Builder multiply(double scale) { Data.multiply(scale, data); return this; } /* * Check hash codes of row arrays in case fromModel or copyOf has been used, * otherwise check array equality. */ AbstractArray validateArray(AbstractArray that) { checkArgument(this.rows.hashCode() == that.rows.hashCode() || Arrays.equals(this.rows, that.rows)); return that; } /* * Data is not copied on build() so we dereference data arrays to prevent * lingering builders from further modifying data. */ private void dereference() { data = null; rows = null; } /** * Return a newly-created, immutable, 2-dimensional data container populated * with values computed by the supplied loader. Calling this method will * overwrite any values already supplied via {@code set*} or {@code add*} * methods. * * @param loader that will compute values */ public IntervalArray build(Loader loader) { checkNotNull(loader); for (int i = 0; i < rows.length; i++) { data[i] = loader.compute(rows[i]); } return build(); } /** * Return a newly-created, immutable, interval data array populated with the * contents of this {@code Builder}. */ public IntervalArray build() { checkState(built != true, "This builder has already been used"); checkDataState(rows); IntervalArray array = new DefaultArray( rowMin, rowMax, rowΔ, rows, data); dereference(); return array; } } }