View Javadoc
1   package org.djunits.vecmat.table;
2   
3   import java.util.Objects;
4   
5   import org.djunits.quantity.SIQuantity;
6   import org.djunits.quantity.def.Quantity;
7   import org.djunits.unit.UnitInterface;
8   import org.djunits.unit.si.SIUnit;
9   import org.djunits.vecmat.d1.Matrix1x1;
10  import org.djunits.vecmat.d1.Vector1;
11  import org.djunits.vecmat.d2.Matrix2x2;
12  import org.djunits.vecmat.d2.Vector2;
13  import org.djunits.vecmat.d3.Matrix3x3;
14  import org.djunits.vecmat.d3.Vector3;
15  import org.djunits.vecmat.def.VectorMatrix;
16  import org.djunits.vecmat.dn.MatrixNxN;
17  import org.djunits.vecmat.dn.VectorN;
18  import org.djunits.vecmat.storage.DataGridSi;
19  import org.djunits.vecmat.storage.DenseDoubleDataSi;
20  import org.djutils.exceptions.Throw;
21  
22  /**
23   * QuantityTable is a two-dimensonal table with quantities. The QuantityTable allows for Hadamard (element-wise) operations, but
24   * not for vector/matrix operations. A QuantityTable can be transformed to a MatrixNxM or vice versa.
25   * <p>
26   * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
27   * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
28   * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
29   * @author Alexander Verbraeck
30   * @param <Q> the quantity type
31   * @param <U> the unit type
32   */
33  public class QuantityTable<Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>>
34          extends VectorMatrix<Q, U, QuantityTable<Q, U>, QuantityTable<SIQuantity, SIUnit>, QuantityTable<?, ?>>
35  {
36      /** */
37      private static final long serialVersionUID = 600L;
38  
39      /** The data of the table, in SI unit. */
40      private final DataGridSi<?> dataSi;
41  
42      /**
43       * Create a new NxM QuantityTable with a unit, based on a DataGrid storage object. This constructor assumes dataSi stores SI
44       * values. Note: NO safe copy is made.
45       * @param dataSi the data of the matrix, in SI unit.
46       * @param displayUnit the display unit to use
47       */
48      public QuantityTable(final DataGridSi<?> dataSi, final U displayUnit)
49      {
50          super(displayUnit);
51          Throw.whenNull(dataSi, "dataSi");
52          this.dataSi = dataSi;
53      }
54  
55      /**
56       * Create a new NxM QuantityTable with a unit, based on a 1-dimensional double array.
57       * @param valueArrayInUnit the matrix values {a11, a12, ..., a1M, aN2, ..., aNM} expressed in the display unit
58       * @param displayUnit the display unit to use
59       * @param <Q> the quantity type
60       * @param <U> the unit type
61       * @param rows the number of rows in the valueArray
62       * @param cols the number of columns in the valueArray
63       * @return a new NxM QuantityTable with a unit
64       * @throws IllegalArgumentException when rows or cols is not positive, or when the number of entries in valueArray is not
65       *             equal to rows*cols
66       */
67      @SuppressWarnings("checkstyle:needbraces")
68      public static <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> QuantityTable<Q, U> of(
69              final double[] valueArrayInUnit, final int rows, final int cols, final U displayUnit)
70      {
71          Throw.whenNull(valueArrayInUnit, "valueArrayInUnit");
72          Throw.whenNull(displayUnit, "displayUnit");
73          Throw.when(rows <= 0, IllegalArgumentException.class, "rows <= 0");
74          Throw.when(cols <= 0, IllegalArgumentException.class, "cols <= 0");
75          Throw.when(rows * cols != valueArrayInUnit.length, IllegalArgumentException.class,
76                  "valueArrayInUnit does not contain the correct number of entries (%d x %d != %d)", rows, cols,
77                  valueArrayInUnit.length);
78          double[] aSi = new double[rows * cols];
79          for (int i = 0; i < valueArrayInUnit.length; i++)
80              aSi[i] = displayUnit.toBaseValue(valueArrayInUnit[i]);
81          return new QuantityTable<Q, U>(new DenseDoubleDataSi(aSi, rows, cols), displayUnit);
82      }
83  
84      /**
85       * Create a new NxM QuantityTable with a unit, based on a 2-dimensional double grid.
86       * @param valueGridInUnit the matrix values {{a11, a12, a1M}, ..., {aN1, aN2, aNM}} expressed in the display unit
87       * @param displayUnit the display unit to use
88       * @param <Q> the quantity type
89       * @param <U> the unit type
90       * @return a new NxM QuantityTable with a unit
91       * @throws IllegalArgumentException when valueGrid has 0 rows, or when the number of columns for one of the rows is not
92       *             equal to the number of columns in another row
93       */
94      @SuppressWarnings("checkstyle:needbraces")
95      public static <Q extends Quantity<Q, U>,
96              U extends UnitInterface<U, Q>> QuantityTable<Q, U> of(final double[][] valueGridInUnit, final U displayUnit)
97      {
98          Throw.whenNull(valueGridInUnit, "valueGridInUnit");
99          Throw.whenNull(displayUnit, "displayUnit");
100         int rows = valueGridInUnit.length;
101         Throw.when(rows == 0, IllegalArgumentException.class, "valueGridInUnit has 0 rows");
102         int cols = valueGridInUnit[0].length;
103         Throw.when(cols == 0, IllegalArgumentException.class, "row 0 in valueGridInUnit has 0 columns");
104         double[] aSi = new double[rows * cols];
105         for (int r = 0; r < rows; r++)
106         {
107             Throw.when(valueGridInUnit[r].length != cols, IllegalArgumentException.class,
108                     "valueGridInUnit is not a NxM array; row %d has a length of %d, not %d", r, valueGridInUnit[r].length,
109                     cols);
110             for (int c = 0; c < cols; c++)
111                 aSi[cols * r + c] = displayUnit.toBaseValue(valueGridInUnit[r][c]);
112         }
113         return new QuantityTable<Q, U>(new DenseDoubleDataSi(aSi, rows, cols), displayUnit);
114     }
115 
116     /**
117      * Create a new NxM QuantityTable with a unit, based on a 2-dimensional quantity grid.
118      * @param quantityGrid the matrix values {{a11, a12, ..., a1M}, {aN2, ..., aNM}}, each with their own unit
119      * @param displayUnit the display unit to use for the resulting matrix
120      * @param <Q> the quantity type
121      * @param <U> the unit type
122      * @return a new NxM QuantityTable with a unit
123      * @throws IllegalArgumentException when rows or cols is not positive, or when the number of entries in quantityGrid is not
124      *             equal to rows*cols
125      */
126     @SuppressWarnings("checkstyle:needbraces")
127     public static <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> QuantityTable<Q, U> of(final Q[][] quantityGrid,
128             final U displayUnit)
129     {
130         return new QuantityTable<Q, U>(new DenseDoubleDataSi(quantityGrid), displayUnit);
131     }
132 
133     @Override
134     public QuantityTable<Q, U> instantiateSi(final double[] siNew)
135     {
136         return new QuantityTable<Q, U>(this.dataSi.instantiateNew(siNew), getDisplayUnit().getBaseUnit())
137                 .setDisplayUnit(getDisplayUnit());
138     }
139 
140     @Override
141     public QuantityTable<SIQuantity, SIUnit> instantiateSi(final double[] siNew, final SIUnit siUnit)
142     {
143         return new QuantityTable<SIQuantity, SIUnit>(this.dataSi.instantiateNew(siNew), siUnit);
144     }
145 
146     /**
147      * Return the internal datagrid object, so we can retrieve data from it.
148      * @return the internal datagrid object
149      */
150     public DataGridSi<?> getDataGrid()
151     {
152         return this.dataSi;
153     }
154 
155     @Override
156     public double[] si()
157     {
158         return this.dataSi.getDataArray();
159     }
160 
161     @Override
162     public double si(final int row, final int col) throws IndexOutOfBoundsException
163     {
164         checkRow(row);
165         checkCol(col);
166         return this.dataSi.get(row, col);
167     }
168 
169     @Override
170     public VectorN.Row<Q, U> getRowVector(final int row)
171     {
172         return VectorN.Row.ofSi(getRowSi(row), getDisplayUnit());
173     }
174 
175     @Override
176     public VectorN.Row<Q, U> mgetRowVector(final int mRow)
177     {
178         return VectorN.Row.ofSi(mgetRowSi(mRow), getDisplayUnit());
179     }
180 
181     @Override
182     public VectorN.Col<Q, U> getColumnVector(final int col)
183     {
184         return VectorN.Col.ofSi(getColumnSi(col), getDisplayUnit());
185     }
186 
187     @Override
188     public VectorN.Col<Q, U> mgetColumnVector(final int mCol)
189     {
190         return VectorN.Col.ofSi(mgetColumnSi(mCol), getDisplayUnit());
191     }
192 
193     @Override
194     public double[] getRowSi(final int row)
195     {
196         checkRow(row);
197         return this.dataSi.getRowArray(row);
198     }
199 
200     @Override
201     public double[] getColumnSi(final int col)
202     {
203         checkCol(col);
204         return this.dataSi.getColArray(col);
205     }
206 
207     @Override
208     public int rows()
209     {
210         return this.dataSi.rows();
211     }
212 
213     @Override
214     public int cols()
215     {
216         return this.dataSi.cols();
217     }
218 
219 
220     // --------------------------------------- AS() FUNCTIONS ---------------------------------
221 
222     @Override
223     public int hashCode()
224     {
225         return Objects.hash(this.dataSi);
226     }
227 
228     @SuppressWarnings("checkstyle:needbraces")
229     @Override
230     public boolean equals(final Object obj)
231     {
232         if (this == obj)
233             return true;
234         if (obj == null)
235             return false;
236         if (getClass() != obj.getClass())
237             return false;
238         QuantityTable<?, ?> other = (QuantityTable<?, ?>) obj;
239         return Objects.equals(this.dataSi, other.dataSi);
240     }
241 
242     /**
243      * Return the QuantityTable 'as' a QuantityTable with a known quantity, using a unit to express the result in. Throw a
244      * Runtime exception when the SI units of this vector and the target vector do not match.
245      * @param targetUnit the unit to convert the quantity table to
246      * @return a quantity table typed in the target quantity table class
247      * @throws IllegalArgumentException when the units do not match
248      * @param <TQ> target quantity type
249      * @param <TU> target unit type
250      */
251     public <TQ extends Quantity<TQ, TU>, TU extends UnitInterface<TU, TQ>> QuantityTable<TQ, TU> as(final TU targetUnit)
252             throws IllegalArgumentException
253     {
254         Throw.when(!getDisplayUnit().siUnit().equals(targetUnit.siUnit()), IllegalArgumentException.class,
255                 "QuantityTable.as(%s) called, but units do not match: %s <> %s", targetUnit,
256                 getDisplayUnit().siUnit().getDisplayAbbreviation(), targetUnit.siUnit().getDisplayAbbreviation());
257         return new QuantityTable<TQ, TU>(this.dataSi.instantiateNew(si()), targetUnit.getBaseUnit()).setDisplayUnit(targetUnit);
258     }
259 
260     /**
261      * Convert this QuantityTable to a {@link Matrix1x1}. The shape must be 1 x 1.
262      * @return a {@code Matrix1x1} with identical SI data and display unit
263      * @throws IllegalStateException if this matrix is not 1 x 1
264      */
265     public Matrix1x1<Q, U> asMatrix1x1()
266     {
267         Throw.when(rows() != 1 || cols() != 1, IllegalStateException.class,
268                 "asMatrix1x1() called, but matrix is no 1x1 but %dx%d", rows(), cols());
269         return Matrix1x1.of(si(), getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
270     }
271 
272     /**
273      * Convert this QuantityTable to a {@link Matrix2x2}. The shape must be 2 x 2.
274      * @return a {@code Matrix2x2} with identical SI data and display unit
275      * @throws IllegalStateException if this matrix is not 2 x 2
276      */
277     public Matrix2x2<Q, U> asMatrix2x2()
278     {
279         Throw.when(rows() != 2 || cols() != 2, IllegalStateException.class,
280                 "asMatrix2x2() called, but matrix is no 2x2 but %dx%d", rows(), cols());
281         return Matrix2x2.of(si(), getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
282     }
283 
284     /**
285      * Convert this QuantityTable to a {@link Matrix3x3}. The shape must be 3 x 3.
286      * @return a {@code Matrix3x3} with identical SI data and display unit
287      * @throws IllegalStateException if this matrix is not 3 x 3
288      */
289     public Matrix3x3<Q, U> asMatrix3x3()
290     {
291         Throw.when(rows() != 3 || cols() != 3, IllegalStateException.class,
292                 "asMatrix3x3() called, but matrix is no 3x3 but %dx%d", rows(), cols());
293         return Matrix3x3.of(si(), getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
294     }
295 
296     /**
297      * Convert this QuantityTable to a {@link MatrixNxN}. The shape must be square.
298      * @return a {@code MatrixNxN} with identical SI data and display unit
299      * @throws IllegalStateException if this matrix is not square
300      */
301     public MatrixNxN<Q, U> asMatrixNxN()
302     {
303         Throw.when(rows() != cols(), IllegalStateException.class, "asMatrixNxN() called, but matrix is no square but %dx%d",
304                 rows(), cols());
305         return new MatrixNxN<Q, U>(new DenseDoubleDataSi(si(), rows(), cols()), getDisplayUnit().getBaseUnit())
306                 .setDisplayUnit(getDisplayUnit());
307     }
308 
309     /**
310      * Convert this QuantityTable to a 1-element column vector. Shape must be 1 x 1.
311      * @return a {@code Vector1} with identical SI data and display unit
312      * @throws IllegalStateException if shape is not 1 x 1
313      */
314     public Vector1<Q, U> asVector1()
315     {
316         Throw.when(rows() != 1 || cols() != 1, IllegalStateException.class, "Matrix is not 1x1");
317         final double[] data = si();
318         return new Vector1<Q, U>(data[0], getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
319     }
320 
321     /**
322      * Convert this QuantityTable to a 2-element column vector. Shape must be 2 x 1.
323      * @return a {@code Vector2.Col} with identical SI data and display unit
324      * @throws IllegalStateException if shape is not 2 x 1
325      */
326     public Vector2.Col<Q, U> asVector2Col()
327     {
328         Throw.when(rows() != 2 || cols() != 1, IllegalStateException.class, "Matrix is not 2x1");
329         final double[] data = si();
330         return new Vector2.Col<Q, U>(data[0], data[1], getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
331     }
332 
333     /**
334      * Convert this QuantityTable to a 3-element column vector. Shape must be 3 x 1.
335      * @return a {@code Vector3.Col} with identical SI data and display unit
336      * @throws IllegalStateException if shape is not 3 x 1
337      */
338     public Vector3.Col<Q, U> asVector3Col()
339     {
340         Throw.when(rows() != 3 || cols() != 1, IllegalStateException.class, "Matrix is not 3x1");
341         final double[] data = si();
342         return new Vector3.Col<Q, U>(data[0], data[1], data[2], getDisplayUnit().getBaseUnit())
343                 .setDisplayUnit(getDisplayUnit());
344     }
345 
346     /**
347      * Convert this QuantityTable to an N-element column vector. Shape must be N x 1.
348      * @return a {@code VectorN.Col} with identical SI data and display unit
349      * @throws IllegalStateException if {@code cols() != 1}
350      */
351     public VectorN.Col<Q, U> asVectorNCol()
352     {
353         Throw.when(cols() != 1, IllegalStateException.class, "Matrix is not Nx1");
354         return VectorN.Col.ofSi(si(), getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
355     }
356 
357     /**
358      * Convert this QuantityTable to a 2-element row vector. Shape must be 1 x 2.
359      * @return a {@code Vector2.Row} with identical SI data and display unit
360      * @throws IllegalStateException if shape is not 1 x 2
361      */
362     public Vector2.Row<Q, U> asVector2Row()
363     {
364         Throw.when(rows() != 1 || cols() != 2, IllegalStateException.class, "Matrix is not 1x2");
365         final double[] data = si();
366         return new Vector2.Row<Q, U>(data[0], data[1], getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
367     }
368 
369     /**
370      * Convert this QuantityTable to a 3-element row vector. Shape must be 1 x 3.
371      * @return a {@code Vector3.Row} with identical SI data and display unit
372      * @throws IllegalStateException if shape is not 1 x 3
373      */
374     public Vector3.Row<Q, U> asVector3Row()
375     {
376         Throw.when(rows() != 1 || cols() != 3, IllegalStateException.class, "Matrix is not 1x3");
377         final double[] data = si();
378         return new Vector3.Row<Q, U>(data[0], data[1], data[2], getDisplayUnit().getBaseUnit())
379                 .setDisplayUnit(getDisplayUnit());
380     }
381 
382     /**
383      * Convert this QuantityTable to an N-element row vector. Shape must be 1 x N.
384      * @return a {@code VectorN.Row} with identical SI data and display unit
385      * @throws IllegalStateException if {@code rows() != 1}
386      */
387     public VectorN.Row<Q, U> asVectorNRow()
388     {
389         Throw.when(rows() != 1, IllegalStateException.class, "Matrix is not 1xN");
390         return VectorN.Row.of(si(), getDisplayUnit().getBaseUnit()).setDisplayUnit(getDisplayUnit());
391     }
392 
393 }