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