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   * SparseFloatData implements a sparse data grid for N x M matrices or N x 1 or 1 x N vectors with float values. The sparse grid
12   * 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 SparseFloatDataSi implements DataGridSi<SparseFloatDataSi>
21  {
22      /** The sparse data stored in row-major format, where only non-zero values are stored. */
23      private float[] 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 SparseFloatDataSi(final float[] 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 as double values 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 SparseFloatDataSi(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 one array in row-major format. Note that NO safe copy of the data is stored.
82       * @param denseData the dense data as float values in row-major format
83       * @param rows the number of rows
84       * @param cols the number of columns
85       * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
86       *             or columns is not positive
87       */
88      public SparseFloatDataSi(final float[] denseData, final int rows, final int cols)
89      {
90          Throw.whenNull(denseData, "denseData");
91          Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
92          Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
93          Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
94                  "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
95          this.rows = rows;
96          this.cols = cols;
97          storeSparse(denseData);
98      }
99  
100     /**
101      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
102      * @param denseData the data in row-major format as a double[][]
103      * @throws IllegalArgumentException when the matrix is ragged
104      */
105     @SuppressWarnings("checkstyle:needbraces")
106     public SparseFloatDataSi(final double[][] denseData)
107     {
108         Throw.whenNull(denseData, "denseData");
109         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
110         this.rows = denseData.length;
111         this.cols = denseData[0].length;
112         for (int r = 1; r < this.rows; r++)
113             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
114                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
115                     this.cols);
116         storeSparse(denseData);
117     }
118 
119     /**
120      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
121      * @param denseData the data in row-major format as a float[][]
122      * @throws IllegalArgumentException when the matrix is ragged
123      */
124     @SuppressWarnings("checkstyle:needbraces")
125     public SparseFloatDataSi(final float[][] denseData)
126     {
127         Throw.whenNull(denseData, "denseData");
128         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
129         this.rows = denseData.length;
130         this.cols = denseData[0].length;
131         for (int r = 1; r < this.rows; r++)
132             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
133                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
134                     this.cols);
135         storeSparse(denseData);
136     }
137 
138     /**
139      * Instantiate a data object with one array in row-major format. Note that a safe copy of the data is stored.
140      * @param sparseData the sparse data in row-major format
141      * @param indexes the indexes with the data coordinates, where index = row * cols() + col (0-based)
142      * @param rows the number of rows
143      * @param cols the number of columns
144      * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
145      *             or columns is not positive, or when indexes is not in strictly increasing order
146      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
147      * @param <Q> the quantity type
148      */
149     @SuppressWarnings("checkstyle:needbraces")
150     public <Q extends Quantity<Q>> SparseFloatDataSi(final Q[] sparseData, final int[] indexes, final int rows, final int cols)
151     {
152         Throw.whenNull(sparseData, "sparseData");
153         Throw.whenNull(indexes, "indexes");
154         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
155         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
156         Throw.when(sparseData.length != indexes.length, IllegalArgumentException.class,
157                 "sparseData array (%d) has different length from indexes array (%d)", sparseData.length, indexes.length);
158         this.rows = rows;
159         this.cols = cols;
160         checkIndexes(indexes);
161         this.sparseData = new float[sparseData.length];
162         for (int i = 0; i < sparseData.length; i++)
163             this.sparseData[i] = sparseData[i].floatValue();
164         this.indexes = indexes.clone();
165     }
166 
167     /**
168      * Instantiate a data object with one array in row-major format. Note that a safe copy of the data is stored.
169      * @param denseData the dense data in row-major format
170      * @param rows the number of rows
171      * @param cols the number of columns
172      * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
173      *             or columns is not positive
174      * @param <Q> the quantity type
175      */
176     public <Q extends Quantity<Q>> SparseFloatDataSi(final Q[] denseData, final int rows, final int cols)
177     {
178         Throw.whenNull(denseData, "denseData");
179         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
180         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
181         Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
182                 "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
183         this.rows = rows;
184         this.cols = cols;
185         storeSparse(denseData);
186     }
187 
188     /**
189      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
190      * @param denseData the data in row-major format as a double[][]
191      * @throws IllegalArgumentException when the data matrix is ragged
192      * @param <Q> the quantity type
193      */
194     @SuppressWarnings("checkstyle:needbraces")
195     public <Q extends Quantity<Q>> SparseFloatDataSi(final Q[][] denseData)
196     {
197         Throw.whenNull(denseData, "denseData");
198         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
199         this.rows = denseData.length;
200         this.cols = denseData[0].length;
201         for (int r = 1; r < this.rows; r++)
202             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
203                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
204                     this.cols);
205         storeSparse(denseData);
206     }
207 
208     /**
209      * Instantiate a data object with an indexed collection of values. Note that a safe copy of the data is stored.
210      * @param indexedData the sparse data in an indexed format
211      * @param rows the number of rows
212      * @param cols the number of columns
213      * @throws IndexOutOfBoundsException when a row or column index of an element is out of bounds
214      * @param <Q> the quantity type
215      */
216     @SuppressWarnings("checkstyle:needbraces")
217     public <Q extends Quantity<Q>> SparseFloatDataSi(final Collection<FloatSparseValue<Q>> indexedData, final int rows,
218             final int cols)
219     {
220         Throw.whenNull(indexedData, "indexedData");
221         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
222         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
223         this.rows = rows;
224         this.cols = cols;
225         this.sparseData = new float[indexedData.size()];
226         this.indexes = new int[indexedData.size()];
227         int index = 0;
228         for (var value : indexedData)
229         {
230             Throw.when(value.getRow() >= rows, IndexOutOfBoundsException.class, "Row index for indexed value %s out of bounds",
231                     value.toString());
232             Throw.when(value.getColumn() >= cols, IndexOutOfBoundsException.class,
233                     "Column index for indexed value %s out of bounds", value.toString());
234             this.sparseData[index] = (float) value.si();
235             this.indexes[index++] = value.getRow() * this.cols + value.getColumn();
236         }
237     }
238 
239     /**
240      * Check the correctness of the indexes array.
241      * @param indexArray the indexes with the data coordinates, where index = row * cols() + col (0-based)
242      * @throws IllegalArgumentException when indexes is not in strictly increasing order
243      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
244      */
245     protected void checkIndexes(final int[] indexArray)
246     {
247         for (int i = 0; i < indexArray.length; i++)
248         {
249             if (indexArray[i] < 0 || indexArray[i] >= this.rows * this.cols)
250             {
251                 throw new IndexOutOfBoundsException(
252                         String.format("indexes[%d] out of bounds, %d rows x %d cols; value should be 0..%d", i, this.rows,
253                                 this.cols, this.rows * this.cols - 1));
254             }
255         }
256         for (int i = 1; i < indexArray.length; i++)
257         {
258             if (indexArray[i] <= indexArray[i - 1])
259             {
260                 throw new IllegalArgumentException(
261                         "indexes[] must be strictly increasing, found " + indexArray[i - 1] + " then " + indexArray[i]);
262             }
263         }
264     }
265 
266     /**
267      * Store sparse data[] and indexes[].
268      * @param denseData the dense data in row-major format
269      */
270     @SuppressWarnings("checkstyle:needbraces")
271     public void storeSparse(final double[] denseData)
272     {
273         int nonzero = 0;
274         for (int i = 0; i < denseData.length; i++)
275             if (denseData[i] != 0.0)
276                 nonzero++;
277         this.sparseData = new float[nonzero];
278         this.indexes = new int[nonzero];
279         int index = 0;
280         for (int i = 0; i < denseData.length; i++)
281             if (denseData[i] != 0.0)
282             {
283                 this.sparseData[index] = (float) denseData[i];
284                 this.indexes[index] = i;
285                 index++;
286             }
287     }
288 
289     /**
290      * Store sparse data[] and indexes[].
291      * @param denseData the dense data in row-major format
292      */
293     @SuppressWarnings("checkstyle:needbraces")
294     public void storeSparse(final float[] denseData)
295     {
296         int nonzero = 0;
297         for (int i = 0; i < denseData.length; i++)
298             if (denseData[i] != 0.0f)
299                 nonzero++;
300         this.sparseData = new float[nonzero];
301         this.indexes = new int[nonzero];
302         int index = 0;
303         for (int i = 0; i < denseData.length; i++)
304             if (denseData[i] != 0.0f)
305             {
306                 this.sparseData[index] = denseData[i];
307                 this.indexes[index] = i;
308                 index++;
309             }
310     }
311 
312     /**
313      * Store sparse data[] and indexes[].
314      * @param denseData the dense data in row-major format
315      */
316     @SuppressWarnings("checkstyle:needbraces")
317     public void storeSparse(final double[][] denseData)
318     {
319         int nonzero = 0;
320         for (int i = 0; i < denseData.length; i++)
321             for (int j = 0; j < denseData[i].length; j++)
322                 if (denseData[i][j] != 0.0)
323                     nonzero++;
324         this.sparseData = new float[nonzero];
325         this.indexes = new int[nonzero];
326         int index = 0;
327         for (int i = 0; i < denseData.length; i++)
328             for (int j = 0; j < denseData[i].length; j++)
329                 if (denseData[i][j] != 0.0)
330                 {
331                     this.sparseData[index] = (float) denseData[i][j];
332                     this.indexes[index] = i * this.cols + j;
333                     index++;
334                 }
335     }
336 
337     /**
338      * Store sparse data[] and indexes[].
339      * @param denseData the dense data in row-major format
340      */
341     @SuppressWarnings("checkstyle:needbraces")
342     public void storeSparse(final float[][] denseData)
343     {
344         int nonzero = 0;
345         for (int i = 0; i < denseData.length; i++)
346             for (int j = 0; j < denseData[i].length; j++)
347                 if (denseData[i][j] != 0.0f)
348                     nonzero++;
349         this.sparseData = new float[nonzero];
350         this.indexes = new int[nonzero];
351         int index = 0;
352         for (int i = 0; i < denseData.length; i++)
353             for (int j = 0; j < denseData[i].length; j++)
354                 if (denseData[i][j] != 0.0f)
355                 {
356                     this.sparseData[index] = (float) denseData[i][j];
357                     this.indexes[index] = i * this.cols + j;
358                     index++;
359                 }
360     }
361 
362     /**
363      * Store sparse data[] and indexes[].
364      * @param denseData the dense data in row-major format
365      * @param <Q> the quantity type
366      */
367     @SuppressWarnings("checkstyle:needbraces")
368     public <Q extends Quantity<Q>> void storeSparse(final Q[] denseData)
369     {
370         int nonzero = 0;
371         for (int i = 0; i < denseData.length; i++)
372             if (denseData[i].ne0())
373                 nonzero++;
374         this.sparseData = new float[nonzero];
375         this.indexes = new int[nonzero];
376         int index = 0;
377         for (int i = 0; i < denseData.length; i++)
378             if (denseData[i].ne0())
379             {
380                 this.sparseData[index] = denseData[i].floatValue();
381                 this.indexes[index] = i;
382                 index++;
383             }
384     }
385 
386     /**
387      * Store sparse data[] and indexes[].
388      * @param denseData the dense data in row-major format
389      * @param <Q> the quantity type
390      */
391     @SuppressWarnings("checkstyle:needbraces")
392     public <Q extends Quantity<Q>> void storeSparse(final Q[][] denseData)
393     {
394         int nonzero = 0;
395         for (int i = 0; i < denseData.length; i++)
396             for (int j = 0; j < denseData[i].length; j++)
397                 if (denseData[i][j].ne0())
398                     nonzero++;
399         this.sparseData = new float[nonzero];
400         this.indexes = new int[nonzero];
401         int index = 0;
402         for (int i = 0; i < denseData.length; i++)
403             for (int j = 0; j < denseData[i].length; j++)
404                 if (denseData[i][j].ne0())
405                 {
406                     this.sparseData[index] = denseData[i][j].floatValue();
407                     this.indexes[index] = i * this.cols + j;
408                     index++;
409                 }
410     }
411 
412     @Override
413     public int rows()
414     {
415         return this.rows;
416     }
417 
418     @Override
419     public int cols()
420     {
421         return this.cols;
422     }
423 
424     @Override
425     public boolean isDense()
426     {
427         return false;
428     }
429 
430     @Override
431     public boolean isDouble()
432     {
433         return false;
434     }
435 
436     /**
437      * Check whether the row and column are within bounds.
438      * @param row the row number
439      * @param col the column number
440      * @throws IndexOutOfBoundsException when row &gt; rows() or col &gt; cols() or row &lt; 0 or col &lt; 0
441      */
442     private void checkRowCol(final int row, final int col) throws IndexOutOfBoundsException
443     {
444         Throw.when(row < 0 || row >= this.rows, IndexOutOfBoundsException.class, "row %d not in range 0..%d", row,
445                 this.rows - 1);
446         Throw.when(col < 0 || col >= this.cols, IndexOutOfBoundsException.class, "column %d not in range 0..%d", col,
447                 this.cols - 1);
448     }
449 
450     @Override
451     public double get(final int row, final int col)
452     {
453         checkRowCol(row, col);
454         int index = row * this.cols + col; // zero-based
455         final int pos = Arrays.binarySearch(this.indexes, index);
456         return (pos >= 0) ? this.sparseData[pos] : 0.0;
457     }
458 
459     @SuppressWarnings("checkstyle:needbraces")
460     @Override
461     public double[] getSiArray()
462     {
463         double[] denseData = new double[rows() * cols()];
464         for (int i = 0; i < this.sparseData.length; i++)
465             denseData[this.indexes[i]] = this.sparseData[i];
466         return denseData;
467     }
468 
469     @Override
470     public double[] unsafeSiArray()
471     {
472         return getSiArray();
473     }
474 
475     @Override
476     public SparseFloatDataSi copy()
477     {
478         return new SparseFloatDataSi(this.sparseData.clone(), this.indexes.clone(), rows(), cols());
479     }
480 
481     @SuppressWarnings("checkstyle:needbraces")
482     @Override
483     public int nonZeroCount()
484     {
485         int result = 0;
486         for (int i = 0; i < this.sparseData.length; i++)
487             result += this.sparseData[i] == 0.0 ? 0 : 1;
488         return result;
489     }
490 
491     @Override
492     public SparseFloatDataSi instantiateNew(final double[] denseData)
493     {
494         Throw.when(denseData.length != rows() * cols(), IllegalArgumentException.class,
495                 "Data object length != rows * cols, %d != %d * %d", denseData.length, rows(), cols());
496         return new SparseFloatDataSi(denseData, rows(), cols());
497     }
498 
499     @Override
500     public SparseFloatDataSi instantiateNew(final double[] denseData, final int newRows, final int newCols)
501     {
502         Throw.when(denseData.length != newRows * newCols, IllegalArgumentException.class,
503                 "Data object length != rows * cols, %d != %d * %d", denseData.length, newRows, newCols);
504         return new SparseFloatDataSi(denseData, newRows, newCols);
505     }
506 
507     @Override
508     public int hashCode()
509     {
510         final int prime = 31;
511         int result = 1;
512         result = prime * result + Arrays.hashCode(unsafeSiArray());
513         result = prime * result + Objects.hash(this.cols, this.rows);
514         return result;
515     }
516 
517     @SuppressWarnings("checkstyle:needbraces")
518     @Override
519     public boolean equals(final Object obj)
520     {
521         if (this == obj)
522             return true;
523         if (obj == null)
524             return false;
525         if (getClass() != obj.getClass())
526         {
527             if (obj instanceof DataGridSi dg)
528                 return this.cols == dg.cols() && this.rows == dg.rows() && Arrays.equals(unsafeSiArray(), dg.unsafeSiArray());
529             return false;
530         }
531         SparseFloatDataSi other = (SparseFloatDataSi) obj;
532         return this.cols == other.cols && this.rows == other.rows && Arrays.equals(this.sparseData, other.sparseData)
533                 && Arrays.equals(this.indexes, other.indexes);
534     }
535 
536 }