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