View Javadoc
1   package org.djunits.vecmat.storage;
2   
3   import java.util.Arrays;
4   import java.util.Collection;
5   import java.util.Objects;
6   
7   import org.djunits.quantity.def.Quantity;
8   import org.djunits.unit.UnitInterface;
9   import org.djutils.exceptions.Throw;
10  
11  /**
12   * SparseDoubleData implements a sparse data grid for N x M matrices or N x 1 or 1 x N vectors with double values. The sparse
13   * grid is implemented with an index array that indicates the position of the data values in the dense array. Any index that is
14   * missing indicates a data value of 0.
15   * <p>
16   * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
17   * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
18   * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
19   * @author Alexander Verbraeck
20   */
21  public class SparseDoubleDataSi implements DataGridSi<SparseDoubleDataSi>
22  {
23      /** The sparse data stored in row-major format, where only non-zero values are stored. */
24      private double[] sparseData;
25  
26      /** The index array, giving the position in the dense data array of the values in data. */
27      private int[] indexes;
28  
29      /** The number of rows. */
30      private final int rows;
31  
32      /** the number of columns. */
33      private final int cols;
34  
35      /**
36       * Instantiate a data object with one array in row-major format. Note that NO safe copy of the data is stored. Also note
37       * that get(r, c) methods will be 0-based in this underlying class, whereas the Matrix.get(r, c) method is 1-based.
38       * @param sparseData the sparse data values
39       * @param indexes the indexes with the data coordinates, where index = row * cols() + col (0-based)
40       * @param rows the number of rows (1-based)
41       * @param cols the number of columns (1-based)
42       * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
43       *             or columns is not positive, or when indexes is not in strictly increasing order
44       * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
45       */
46      protected SparseDoubleDataSi(final double[] sparseData, final int[] indexes, final int rows, final int cols)
47      {
48          Throw.whenNull(sparseData, "sparseData");
49          Throw.whenNull(indexes, "indexes");
50          Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
51          Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
52          Throw.when(sparseData.length != indexes.length, IllegalArgumentException.class,
53                  "sparseData array (%d) has different length from indexes array (%d)", sparseData.length, indexes.length);
54          this.rows = rows;
55          this.cols = cols;
56          checkIndexes(indexes);
57          this.sparseData = sparseData;
58          this.indexes = indexes;
59      }
60  
61      /**
62       * Instantiate a data object with one array in row-major format. Note that NO safe copy of the data is stored.
63       * @param denseData the dense data in row-major format
64       * @param rows the number of rows
65       * @param cols the number of columns
66       * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
67       *             or columns is not positive
68       */
69      public SparseDoubleDataSi(final double[] denseData, final int rows, final int cols)
70      {
71          Throw.whenNull(denseData, "denseData");
72          Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
73          Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
74          Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
75                  "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
76          this.rows = rows;
77          this.cols = cols;
78          storeSparse(denseData);
79      }
80  
81      /**
82       * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
83       * @param denseData the data in row-major format as a double[][]
84       * @throws IllegalArgumentException when the matrix is ragged
85       */
86      @SuppressWarnings("checkstyle:needbraces")
87      public SparseDoubleDataSi(final double[][] denseData)
88      {
89          Throw.whenNull(denseData, "denseData");
90          Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
91          this.rows = denseData.length;
92          this.cols = denseData[0].length;
93          for (int r = 1; r < this.rows; r++)
94              Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
95                      "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
96                      this.cols);
97          storeSparse(denseData);
98      }
99  
100     /**
101      * Instantiate a data object with one array in row-major format. Note that a safe copy of the data is stored.
102      * @param sparseData the sparse data in row-major format
103      * @param indexes the indexes with the data coordinates, where index = row * cols() + col (0-based)
104      * @param rows the number of rows
105      * @param cols the number of columns
106      * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
107      *             or columns is not positive, or when indexes is not in strictly increasing order
108      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
109      * @param <Q> the quantity type
110      * @param <U> the unit type
111      */
112     @SuppressWarnings("checkstyle:needbraces")
113     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseDoubleDataSi(final Q[] sparseData,
114             final int[] indexes, final int rows, final int cols)
115     {
116         Throw.whenNull(sparseData, "sparseData");
117         Throw.whenNull(indexes, "indexes");
118         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
119         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
120         Throw.when(sparseData.length != indexes.length, IllegalArgumentException.class,
121                 "sparseData array (%d) has different length from indexes array (%d)", sparseData.length, indexes.length);
122         this.rows = rows;
123         this.cols = cols;
124         checkIndexes(indexes);
125         this.sparseData = new double[sparseData.length];
126         for (int i = 0; i < sparseData.length; i++)
127             this.sparseData[i] = sparseData[i].si();
128         this.indexes = indexes.clone();
129     }
130 
131     /**
132      * Instantiate a data object with one array in row-major format. Note that a safe copy of the data is stored.
133      * @param denseData the dense data in row-major format
134      * @param rows the number of rows
135      * @param cols the number of columns
136      * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
137      *             or columns is not positive
138      * @param <Q> the quantity type
139      * @param <U> the unit type
140      */
141     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseDoubleDataSi(final Q[] denseData, final int rows,
142             final int cols)
143     {
144         Throw.whenNull(denseData, "denseData");
145         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
146         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
147         Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
148                 "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
149         this.rows = rows;
150         this.cols = cols;
151         storeSparse(denseData);
152     }
153 
154     /**
155      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
156      * @param denseData the data in row-major format as a double[][]
157      * @throws IllegalArgumentException when the data matrix is ragged
158      * @param <Q> the quantity type
159      * @param <U> the unit type
160      */
161     @SuppressWarnings("checkstyle:needbraces")
162     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseDoubleDataSi(final Q[][] denseData)
163     {
164         Throw.whenNull(denseData, "denseData");
165         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
166         this.rows = denseData.length;
167         this.cols = denseData[0].length;
168         for (int r = 1; r < this.rows; r++)
169             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
170                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
171                     this.cols);
172         storeSparse(denseData);
173     }
174 
175     /**
176      * Instantiate a data object with an indexed collection of values. Note that a safe copy of the data is stored.
177      * @param indexedData the sparse data in an indexed format
178      * @param rows the number of rows
179      * @param cols the number of columns
180      * @throws IndexOutOfBoundsException when a row or column index of an element is out of bounds
181      * @param <Q> the quantity type
182      * @param <U> the unit type
183      */
184     @SuppressWarnings("checkstyle:needbraces")
185     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseDoubleDataSi(
186             final Collection<DoubleSparseValue<Q, U>> indexedData, final int rows, final int cols)
187     {
188         Throw.whenNull(indexedData, "indexedData");
189         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
190         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
191         this.rows = rows;
192         this.cols = cols;
193         this.sparseData = new double[indexedData.size()];
194         this.indexes = new int[indexedData.size()];
195         int index = 0;
196         for (var value : indexedData)
197         {
198             Throw.when(value.getRow() >= rows, IndexOutOfBoundsException.class, "Row index for indexed value %s out of bounds",
199                     value.toString());
200             Throw.when(value.getColumn() >= cols, IndexOutOfBoundsException.class,
201                     "Column index for indexed value %s out of bounds", value.toString());
202             this.sparseData[index] = value.si();
203             this.indexes[index++] = value.getRow() * this.cols + value.getColumn();
204         }
205     }
206 
207     /**
208      * Check the correctness of the indexes array.
209      * @param indexArray the indexes with the data coordinates, where index = row * cols() + col (0-based)
210      * @throws IllegalArgumentException when indexes is not in strictly increasing order
211      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
212      */
213     protected void checkIndexes(final int[] indexArray)
214     {
215         for (int i = 0; i < indexArray.length; i++)
216         {
217             if (indexArray[i] < 0 || indexArray[i] >= this.rows * this.cols)
218             {
219                 throw new IndexOutOfBoundsException(
220                         String.format("indexes[%d] out of bounds, %d rows x %d cols; value should be 0..%d", i, this.rows,
221                                 this.cols, this.rows * this.cols - 1));
222             }
223         }
224         for (int i = 1; i < indexArray.length; i++)
225         {
226             if (indexArray[i] <= indexArray[i - 1])
227             {
228                 throw new IllegalArgumentException(
229                         "indexes[] must be strictly increasing, found " + indexArray[i - 1] + " then " + indexArray[i]);
230             }
231         }
232     }
233 
234     /**
235      * Store sparse data[] and indexes[].
236      * @param denseData the dense data in row-major format
237      */
238     @SuppressWarnings("checkstyle:needbraces")
239     public void storeSparse(final double[] denseData)
240     {
241         int nonzero = 0;
242         for (int i = 0; i < denseData.length; i++)
243             if (denseData[i] != 0.0)
244                 nonzero++;
245         this.sparseData = new double[nonzero];
246         this.indexes = new int[nonzero];
247         int index = 0;
248         for (int i = 0; i < denseData.length; i++)
249             if (denseData[i] != 0.0)
250             {
251                 this.sparseData[index] = denseData[i];
252                 this.indexes[index] = i;
253                 index++;
254             }
255     }
256 
257     /**
258      * Store sparse data[] and indexes[].
259      * @param denseData the dense data in row-major format
260      */
261     @SuppressWarnings("checkstyle:needbraces")
262     public void storeSparse(final double[][] denseData)
263     {
264         int nonzero = 0;
265         for (int i = 0; i < denseData.length; i++)
266             for (int j = 0; j < denseData[i].length; j++)
267                 if (denseData[i][j] != 0.0)
268                     nonzero++;
269         this.sparseData = new double[nonzero];
270         this.indexes = new int[nonzero];
271         int index = 0;
272         for (int i = 0; i < denseData.length; i++)
273             for (int j = 0; j < denseData[i].length; j++)
274                 if (denseData[i][j] != 0.0)
275                 {
276                     this.sparseData[index] = denseData[i][j];
277                     this.indexes[index] = i * this.cols + j;
278                     index++;
279                 }
280     }
281 
282     /**
283      * Store sparse data[] and indexes[].
284      * @param denseData the dense data in row-major format
285      * @param <Q> the quantity type
286      * @param <U> the unit type
287      */
288     @SuppressWarnings("checkstyle:needbraces")
289     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> void storeSparse(final Q[] denseData)
290     {
291         int nonzero = 0;
292         for (int i = 0; i < denseData.length; i++)
293             if (denseData[i].ne0())
294                 nonzero++;
295         this.sparseData = new double[nonzero];
296         this.indexes = new int[nonzero];
297         int index = 0;
298         for (int i = 0; i < denseData.length; i++)
299             if (denseData[i].ne0())
300             {
301                 this.sparseData[index] = denseData[i].si();
302                 this.indexes[index] = i;
303                 index++;
304             }
305     }
306 
307     /**
308      * Store sparse data[] and indexes[].
309      * @param denseData the dense data in row-major format
310      * @param <Q> the quantity type
311      * @param <U> the unit type
312      */
313     @SuppressWarnings("checkstyle:needbraces")
314     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> void storeSparse(final Q[][] denseData)
315     {
316         int nonzero = 0;
317         for (int i = 0; i < denseData.length; i++)
318             for (int j = 0; j < denseData[i].length; j++)
319                 if (denseData[i][j].ne0())
320                     nonzero++;
321         this.sparseData = new double[nonzero];
322         this.indexes = new int[nonzero];
323         int index = 0;
324         for (int i = 0; i < denseData.length; i++)
325             for (int j = 0; j < denseData[i].length; j++)
326                 if (denseData[i][j].ne0())
327                 {
328                     this.sparseData[index] = denseData[i][j].si();
329                     this.indexes[index] = i * this.cols + j;
330                     index++;
331                 }
332     }
333 
334     @Override
335     public int rows()
336     {
337         return this.rows;
338     }
339 
340     @Override
341     public int cols()
342     {
343         return this.cols;
344     }
345 
346     @Override
347     public boolean isDense()
348     {
349         return false;
350     }
351 
352     @Override
353     public boolean isDouble()
354     {
355         return true;
356     }
357 
358     /**
359      * Check whether the row and column are within bounds.
360      * @param row the row number
361      * @param col the column number
362      * @throws IndexOutOfBoundsException when row &gt; rows() or col &gt; cols() or row &lt; 0 or col &lt; 0
363      */
364     private void checkRowCol(final int row, final int col) throws IndexOutOfBoundsException
365     {
366         Throw.when(row < 0 || row >= this.rows, IndexOutOfBoundsException.class, "row %d not in range 0..%d", row,
367                 this.rows - 1);
368         Throw.when(col < 0 || col >= this.cols, IndexOutOfBoundsException.class, "column %d not in range 0..%d", col,
369                 this.cols - 1);
370     }
371 
372     @Override
373     public double get(final int row, final int col)
374     {
375         checkRowCol(row, col);
376         int index = row * this.cols + col; // zero-based
377         final int pos = Arrays.binarySearch(this.indexes, index);
378         return (pos >= 0) ? this.sparseData[pos] : 0.0;
379     }
380 
381     @SuppressWarnings("checkstyle:needbraces")
382     @Override
383     public double[] getDataArray()
384     {
385         double[] denseData = new double[rows() * cols()];
386         for (int i = 0; i < this.sparseData.length; i++)
387             denseData[this.indexes[i]] = this.sparseData[i];
388         return denseData;
389     }
390 
391     @Override
392     public SparseDoubleDataSi copy()
393     {
394         return new SparseDoubleDataSi(this.sparseData.clone(), this.indexes.clone(), rows(), cols());
395     }
396 
397     @SuppressWarnings("checkstyle:needbraces")
398     @Override
399     public int cardinality()
400     {
401         int result = 0;
402         for (int i = 0; i < this.sparseData.length; i++)
403             result += this.sparseData[i] == 0.0 ? 0 : 1;
404         return result;
405     }
406 
407     @Override
408     public SparseDoubleDataSi instantiateNew(final double[] denseData)
409     {
410         Throw.when(denseData.length != rows() * cols(), IllegalArgumentException.class,
411                 "Data object length != rows * cols, %d != %d * %d", denseData.length, rows(), cols());
412         return new SparseDoubleDataSi(denseData, rows(), cols());
413     }
414 
415     @Override
416     public SparseDoubleDataSi instantiateNew(final double[] denseData, final int newRows, final int newCols)
417     {
418         Throw.when(denseData.length != newRows * newCols, IllegalArgumentException.class,
419                 "Data object length != rows * cols, %d != %d * %d", denseData.length, newRows, newCols);
420         return new SparseDoubleDataSi(denseData, newRows, newCols);
421     }
422 
423     @Override
424     public int hashCode()
425     {
426         final int prime = 31;
427         int result = 1;
428         result = prime * result + Arrays.hashCode(getDataArray());
429         result = prime * result + Objects.hash(this.cols, this.rows);
430         return result;
431     }
432 
433     @SuppressWarnings("checkstyle:needbraces")
434     @Override
435     public boolean equals(final Object obj)
436     {
437         if (this == obj)
438             return true;
439         if (obj == null)
440             return false;
441         if (getClass() != obj.getClass())
442         {
443             if (obj instanceof DataGridSi dg)
444                 return this.cols == dg.cols() && this.rows == dg.rows() && Arrays.equals(getDataArray(), dg.getDataArray());
445             return false;
446         }
447         SparseDoubleDataSi other = (SparseDoubleDataSi) obj;
448         return this.cols == other.cols && this.rows == other.rows && Arrays.equals(this.sparseData, other.sparseData)
449                 && Arrays.equals(this.indexes, other.indexes);
450     }
451 
452 }