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.AbstractTable;
import org.opensha2.data.IntervalData.DefaultTable;
import com.google.common.primitives.Doubles;
import java.util.Arrays;
import java.util.List;
/**
* A 2-dimensional table of immutable, double-valued data that is arranged
* according to increasing and uniformly spaced double-valued keys. Interval
* tables are used to represent binned data, and so while row and column 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 IntervalTable}, use a {@link Builder}.
*
* <p>Internally, an {@code IntervalTable} is backed by a {@code double[][]}
* where a 'row' maps to the 1st dimension and a 'column' the 2nd.
*
* <p>Note that interval tables 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 IntervalArray
* @see IntervalVolume
*/
public interface IntervalTable {
/**
* Return the value of the bin that maps to the supplied row and column
* values. Do not confuse this method with {@link #get(int, int)} by row
* index.
*
* @param rowValue of bin to retrieve
* @param columnValue of bin to retrieve
* @throws IndexOutOfBoundsException if either value is out of range
*/
double get(double rowValue, double columnValue);
/**
* Return the value of the bin that maps to the supplied row and column
* indices. Do not confuse this method with {@link #get(double, double)} by
* row value.
*
* @param rowIndex of bin to retrieve
* @param columnIndex of bin to retrieve
* @throws IndexOutOfBoundsException if either index is out of range
*/
double get(int rowIndex, int columnIndex);
/**
* Return an immutable view of the values that map to the supplied row value.
* Do not confuse with {@link #row(int)} retrieval by index.
*
* @param rowValue of bin to retrieve
*/
XySequence row(double rowValue);
/**
* Return an immutable view of the values that map to the supplied row index.
* Do not confuse with {@link #row(double)} retrieval by value.
*
* @param rowIndex of bin to retrieve
*/
XySequence row(int rowIndex);
/**
* Return the lower edge of the lowermost row bin.
*/
double rowMin();
/**
* Return the upper edge of the uppermost row 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 lower edge of the lowermost column bin.
*/
double columnMin();
/**
* Return the upper edge of the uppermost column bin.
*/
double columnMax();
/**
* Return the column bin discretization.
*/
double columnΔ();
/**
* Return an immutable list <i>view</i> of the column keys (bin centers).
*/
List<Double> columns();
/**
* Return a new {@code IntervalArray} created by summing the columns of this
* table.
*/
IntervalArray collapse();
/**
* Return the indices of the bin with smallest value in the form
* {@code [rowIndex, columnIndex]}.
*/
int[] minIndex();
/**
* Return the indices of the bin with largest value in the form
* {@code [rowIndex, columnIndex]}.
*/
int[] maxIndex();
/**
* A supplier of values with which to fill a {@code IntervalTable}.
*/
interface Loader {
/**
* Compute the value corresponding to the supplied row and column keys (bin
* centers).
*
* @param row value
* @param column value
*/
public double compute(double row, double column);
}
/**
* A builder of immutable {@code IntervalTable}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 double columnMin;
private double columnMax;
private double columnΔ;
private double[] columns;
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 table.
*
* @param table to copy
*/
public static Builder copyOf(IntervalTable table) {
/* Safe covariant cast. */
DefaultTable defaultTable = (DefaultTable) table;
Builder builder = copyStructure(defaultTable);
builder.data = Data.copyOf(defaultTable.data);
builder.init();
return builder;
}
/**
* Create a new builder with a structure identical to that of the supplied
* model.
*
* @param model data table
*/
public static Builder fromModel(IntervalTable model) {
/* Safe covariant cast. */
Builder builder = copyStructure((AbstractTable) model);
builder.init();
return builder;
}
private static Builder copyStructure(AbstractTable from) {
Builder to = new Builder();
to.rowMin = from.rowMin;
to.rowMax = from.rowMax;
to.rowΔ = from.rowΔ;
to.rows = from.rows;
to.columnMin = from.columnMin;
to.columnMax = from.columnMax;
to.columnΔ = from.columnΔ;
to.columns = from.columns;
return to;
}
/**
* Define the table row intervals.
*
* @param min lower edge of lowermost row bin
* @param max upper edge of uppermost row bin
* @param Δ bin discretization
*/
public Builder rows(double min, double max, double Δ) {
rowMin = min;
rowMax = max;
rowΔ = Δ;
rows = keys(min, max, Δ);
init();
return this;
}
/**
* Define the table column intervals.
*
* @param min lower edge of lowermost column bin
* @param max upper edge of uppermost column bin
* @param Δ bin discretization
*/
public Builder columns(double min, double max, double Δ) {
columnMin = min;
columnMax = max;
columnΔ = Δ;
columns = keys(min, max, Δ);
init();
return this;
}
private void init() {
checkState(!initialized, "Builder has already been initialized");
if (rows != null && columns != null) {
if (data == null) {
data = new double[rows.length][columns.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);
}
/**
* Return the index of the column that would contain the supplied value.
* @param column value
*/
public int columnIndex(double column) {
return indexOf(columnMin, columnΔ, column, columns.length);
}
/**
* Set the value at the specified row and column. Be careful not to confuse
* this with {@link #set(int, int, double)}.
*
* @param row key
* @param column key
* @param value to set
*/
public Builder set(double row, double column, double value) {
return set(rowIndex(row), columnIndex(column), value);
}
/**
* Set the value at the specified row and column indices. Be careful not to
* confuse this with {@link #set(double, double, double)}.
*
* @param row index
* @param column index
* @param value to set
*/
public Builder set(int row, int column, double value) {
data[row][column] = value;
return this;
}
/**
* Add to the existing value at the specified row and column. Be careful not
* to confuse this with {@link #add(int, int, double)}.
*
* @param row key
* @param column key
* @param value to add
*/
public Builder add(double row, double column, double value) {
return add(rowIndex(row), columnIndex(column), value);
}
/**
* Add to the existing value at the specified row and column indices. Be
* careful not to confuse this with {@link #add(double, double, double)} .
*
* @param row index
* @param column index
* @param value to add
*/
public Builder add(int row, int column, double value) {
data[row][column] += value;
return this;
}
/**
* Add to the values in the specified row.
*
* @param row key
* @param values to add
* @throws IndexOutOfBoundsException if values overrun row
*/
public Builder add(double row, double[] values) {
checkElementIndex(values.length - 1, columns.length,
"Supplied values overrun end of row");
double[] rowData = data[rowIndex(row)];
for (int i = 0; i < values.length; i++) {
rowData[i] += values[i];
}
return this;
}
/**
* Add to the values in the specified row.
*
* @param row key
* @param values to add
* @throws IndexOutOfBoundsException if values overrun row
*/
public Builder add(double row, List<Double> values) {
return add(row, Doubles.toArray(values));
}
/**
* Add the y-values of the supplied sequence to the values in the specified
* row.
*
* @param row key
* @param sequence to add
* @throws IndexOutOfBoundsException if values overrun row
*/
public Builder add(double row, XySequence sequence) {
// safe covariant cast
return add(row, ((ImmutableXySequence) sequence).ys);
}
/**
* Add to the values in the specified row starting at the specified column.
*
* @param row key
* @param column key from which to start adding values
* @param values to add
* @throws IndexOutOfBoundsException if values overrun row
*/
public Builder add(double row, double column, double[] values) {
int columnIndex = columnIndex(column);
checkElementIndex(columnIndex + values.length - 1, columns.length,
"Supplied values overrun end of row");
double[] rowData = data[rowIndex(row)];
for (int i = 0; i < values.length; i++) {
rowData[columnIndex + i] = values[i];
}
return this;
}
/**
* Add to the values in the specified row starting at the specified column.
*
* @param row key
* @param column key from which to start adding values
* @param values to add
* @throws IndexOutOfBoundsException if values will overrun row
*/
public Builder add(double row, double column, List<Double> values) {
return add(row, column, Doubles.toArray(values));
}
/**
* Add the values in the supplied table to this builder. This operation is
* very efficient if this builder and the supplied table are sourced from
* the same model.
*
* @param table to add
* @throws IllegalArgumentException if the rows and columns of the supplied
* table do not match those of this table
* @see #fromModel(IntervalTable)
*/
public Builder add(IntervalTable table) {
// safe covariant cast
validateTable((AbstractTable) table);
// safe covariant cast until other concrete implementations exist
Data.uncheckedAdd(data, ((DefaultTable) table).data);
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 and column arrays in case fromModel or copyOf has
* been used, otherwise check array equality.
*/
AbstractTable validateTable(AbstractTable that) {
checkArgument((this.rows.hashCode() == that.rows.hashCode() &&
this.columns.hashCode() == that.columns.hashCode()) ||
(Arrays.equals(this.rows, that.rows) &&
Arrays.equals(this.columns, that.columns)));
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;
columns = 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 IntervalTable build(Loader loader) {
checkNotNull(loader);
for (int i = 0; i < rows.length; i++) {
double row = rows[i];
for (int j = 0; j < columns.length; j++) {
data[i][j] = loader.compute(row, columns[j]);
}
}
return build();
}
/**
* Return a newly-created, immutable, 2-dimensional data container populated
* with the contents of this {@code Builder}.
*/
public IntervalTable build() {
checkState(built != true, "This builder has already been used");
checkDataState(rows, columns);
IntervalTable table = new DefaultTable(
rowMin, rowMax, rowΔ, rows,
columnMin, columnMax, columnΔ, columns,
data);
dereference();
return table;
}
}
}