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   * 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
13   * 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 SparseFloatDataSi implements DataGridSi<SparseFloatDataSi>
22  {
23      /** The sparse data stored in row-major format, where only non-zero values are stored. */
24      private float[] 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 SparseFloatDataSi(final float[] 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 as double values 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 SparseFloatDataSi(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 one array in row-major format. Note that NO safe copy of the data is stored.
83       * @param denseData the dense data as float values in row-major format
84       * @param rows the number of rows
85       * @param cols the number of columns
86       * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
87       *             or columns is not positive
88       */
89      public SparseFloatDataSi(final float[] denseData, final int rows, final int cols)
90      {
91          Throw.whenNull(denseData, "denseData");
92          Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
93          Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
94          Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
95                  "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
96          this.rows = rows;
97          this.cols = cols;
98          storeSparse(denseData);
99      }
100 
101     /**
102      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
103      * @param denseData the data in row-major format as a double[][]
104      * @throws IllegalArgumentException when the matrix is ragged
105      */
106     @SuppressWarnings("checkstyle:needbraces")
107     public SparseFloatDataSi(final double[][] denseData)
108     {
109         Throw.whenNull(denseData, "denseData");
110         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
111         this.rows = denseData.length;
112         this.cols = denseData[0].length;
113         for (int r = 1; r < this.rows; r++)
114             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
115                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
116                     this.cols);
117         storeSparse(denseData);
118     }
119 
120     /**
121      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
122      * @param denseData the data in row-major format as a float[][]
123      * @throws IllegalArgumentException when the matrix is ragged
124      */
125     @SuppressWarnings("checkstyle:needbraces")
126     public SparseFloatDataSi(final float[][] denseData)
127     {
128         Throw.whenNull(denseData, "denseData");
129         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
130         this.rows = denseData.length;
131         this.cols = denseData[0].length;
132         for (int r = 1; r < this.rows; r++)
133             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
134                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
135                     this.cols);
136         storeSparse(denseData);
137     }
138 
139     /**
140      * Instantiate a data object with one array in row-major format. Note that a safe copy of the data is stored.
141      * @param sparseData the sparse data in row-major format
142      * @param indexes the indexes with the data coordinates, where index = row * cols() + col (0-based)
143      * @param rows the number of rows
144      * @param cols the number of columns
145      * @throws IllegalArgumentException when the size of the data object is not equal to rows*cols, or when the number of rows
146      *             or columns is not positive, or when indexes is not in strictly increasing order
147      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
148      * @param <Q> the quantity type
149      * @param <U> the unit type
150      */
151     @SuppressWarnings("checkstyle:needbraces")
152     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseFloatDataSi(final Q[] sparseData,
153             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      * @param <U> the unit type
179      */
180     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseFloatDataSi(final Q[] denseData, final int rows,
181             final int cols)
182     {
183         Throw.whenNull(denseData, "denseData");
184         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
185         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
186         Throw.when(denseData.length != rows * cols, IllegalArgumentException.class,
187                 "denseData.length (%d) != rows x cols (%d x %d)", denseData.length, rows, cols);
188         this.rows = rows;
189         this.cols = cols;
190         storeSparse(denseData);
191     }
192 
193     /**
194      * Instantiate a data object with a dense double[rows][cols]. A sparse, safe copy of the data is stored.
195      * @param denseData the data in row-major format as a double[][]
196      * @throws IllegalArgumentException when the data matrix is ragged
197      * @param <Q> the quantity type
198      * @param <U> the unit type
199      */
200     @SuppressWarnings("checkstyle:needbraces")
201     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseFloatDataSi(final Q[][] denseData)
202     {
203         Throw.whenNull(denseData, "denseData");
204         Throw.when(denseData.length == 0, IllegalArgumentException.class, "Number of rows in the data matrix = 0");
205         this.rows = denseData.length;
206         this.cols = denseData[0].length;
207         for (int r = 1; r < this.rows; r++)
208             Throw.when(denseData[r].length != this.cols, IllegalArgumentException.class,
209                     "Number of columns in row %d (%d) is not equal to number of columns in row 0 (%d)", r, denseData[r].length,
210                     this.cols);
211         storeSparse(denseData);
212     }
213 
214     /**
215      * Instantiate a data object with an indexed collection of values. Note that a safe copy of the data is stored.
216      * @param indexedData the sparse data in an indexed format
217      * @param rows the number of rows
218      * @param cols the number of columns
219      * @throws IndexOutOfBoundsException when a row or column index of an element is out of bounds
220      * @param <Q> the quantity type
221      * @param <U> the unit type
222      */
223     @SuppressWarnings("checkstyle:needbraces")
224     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> SparseFloatDataSi(
225             final Collection<FloatSparseValue<Q, U>> indexedData, final int rows, final int cols)
226     {
227         Throw.whenNull(indexedData, "indexedData");
228         Throw.when(rows <= 0, IllegalArgumentException.class, "Number of rows <= 0");
229         Throw.when(cols <= 0, IllegalArgumentException.class, "Number of columns <= 0");
230         this.rows = rows;
231         this.cols = cols;
232         this.sparseData = new float[indexedData.size()];
233         this.indexes = new int[indexedData.size()];
234         int index = 0;
235         for (var value : indexedData)
236         {
237             Throw.when(value.getRow() >= rows, IndexOutOfBoundsException.class, "Row index for indexed value %s out of bounds",
238                     value.toString());
239             Throw.when(value.getColumn() >= cols, IndexOutOfBoundsException.class,
240                     "Column index for indexed value %s out of bounds", value.toString());
241             this.sparseData[index] = (float) value.si();
242             this.indexes[index++] = value.getRow() * this.cols + value.getColumn();
243         }
244     }
245 
246     /**
247      * Check the correctness of the indexes array.
248      * @param indexArray the indexes with the data coordinates, where index = row * cols() + col (0-based)
249      * @throws IllegalArgumentException when indexes is not in strictly increasing order
250      * @throws IndexOutOfBoundsException when one of the entries in indexes is out of bounds
251      */
252     protected void checkIndexes(final int[] indexArray)
253     {
254         for (int i = 0; i < indexArray.length; i++)
255         {
256             if (indexArray[i] < 0 || indexArray[i] >= this.rows * this.cols)
257             {
258                 throw new IndexOutOfBoundsException(
259                         String.format("indexes[%d] out of bounds, %d rows x %d cols; value should be 0..%d", i, this.rows,
260                                 this.cols, this.rows * this.cols - 1));
261             }
262         }
263         for (int i = 1; i < indexArray.length; i++)
264         {
265             if (indexArray[i] <= indexArray[i - 1])
266             {
267                 throw new IllegalArgumentException(
268                         "indexes[] must be strictly increasing, found " + indexArray[i - 1] + " then " + indexArray[i]);
269             }
270         }
271     }
272 
273     /**
274      * Store sparse data[] and indexes[].
275      * @param denseData the dense data in row-major format
276      */
277     @SuppressWarnings("checkstyle:needbraces")
278     public void storeSparse(final double[] denseData)
279     {
280         int nonzero = 0;
281         for (int i = 0; i < denseData.length; i++)
282             if (denseData[i] != 0.0)
283                 nonzero++;
284         this.sparseData = new float[nonzero];
285         this.indexes = new int[nonzero];
286         int index = 0;
287         for (int i = 0; i < denseData.length; i++)
288             if (denseData[i] != 0.0)
289             {
290                 this.sparseData[index] = (float) denseData[i];
291                 this.indexes[index] = i;
292                 index++;
293             }
294     }
295 
296     /**
297      * Store sparse data[] and indexes[].
298      * @param denseData the dense data in row-major format
299      */
300     @SuppressWarnings("checkstyle:needbraces")
301     public void storeSparse(final float[] denseData)
302     {
303         int nonzero = 0;
304         for (int i = 0; i < denseData.length; i++)
305             if (denseData[i] != 0.0f)
306                 nonzero++;
307         this.sparseData = new float[nonzero];
308         this.indexes = new int[nonzero];
309         int index = 0;
310         for (int i = 0; i < denseData.length; i++)
311             if (denseData[i] != 0.0f)
312             {
313                 this.sparseData[index] = denseData[i];
314                 this.indexes[index] = i;
315                 index++;
316             }
317     }
318 
319     /**
320      * Store sparse data[] and indexes[].
321      * @param denseData the dense data in row-major format
322      */
323     @SuppressWarnings("checkstyle:needbraces")
324     public void storeSparse(final double[][] denseData)
325     {
326         int nonzero = 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                     nonzero++;
331         this.sparseData = new float[nonzero];
332         this.indexes = new int[nonzero];
333         int index = 0;
334         for (int i = 0; i < denseData.length; i++)
335             for (int j = 0; j < denseData[i].length; j++)
336                 if (denseData[i][j] != 0.0)
337                 {
338                     this.sparseData[index] = (float) denseData[i][j];
339                     this.indexes[index] = i * this.cols + j;
340                     index++;
341                 }
342     }
343 
344     /**
345      * Store sparse data[] and indexes[].
346      * @param denseData the dense data in row-major format
347      */
348     @SuppressWarnings("checkstyle:needbraces")
349     public void storeSparse(final float[][] denseData)
350     {
351         int nonzero = 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                     nonzero++;
356         this.sparseData = new float[nonzero];
357         this.indexes = new int[nonzero];
358         int index = 0;
359         for (int i = 0; i < denseData.length; i++)
360             for (int j = 0; j < denseData[i].length; j++)
361                 if (denseData[i][j] != 0.0f)
362                 {
363                     this.sparseData[index] = (float) denseData[i][j];
364                     this.indexes[index] = i * this.cols + j;
365                     index++;
366                 }
367     }
368 
369     /**
370      * Store sparse data[] and indexes[].
371      * @param denseData the dense data in row-major format
372      * @param <Q> the quantity type
373      * @param <U> the unit type
374      */
375     @SuppressWarnings("checkstyle:needbraces")
376     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> void storeSparse(final Q[] denseData)
377     {
378         int nonzero = 0;
379         for (int i = 0; i < denseData.length; i++)
380             if (denseData[i].ne0())
381                 nonzero++;
382         this.sparseData = new float[nonzero];
383         this.indexes = new int[nonzero];
384         int index = 0;
385         for (int i = 0; i < denseData.length; i++)
386             if (denseData[i].ne0())
387             {
388                 this.sparseData[index] = denseData[i].floatValue();
389                 this.indexes[index] = i;
390                 index++;
391             }
392     }
393 
394     /**
395      * Store sparse data[] and indexes[].
396      * @param denseData the dense data in row-major format
397      * @param <Q> the quantity type
398      * @param <U> the unit type
399      */
400     @SuppressWarnings("checkstyle:needbraces")
401     public <Q extends Quantity<Q, U>, U extends UnitInterface<U, Q>> void storeSparse(final Q[][] denseData)
402     {
403         int nonzero = 0;
404         for (int i = 0; i < denseData.length; i++)
405             for (int j = 0; j < denseData[i].length; j++)
406                 if (denseData[i][j].ne0())
407                     nonzero++;
408         this.sparseData = new float[nonzero];
409         this.indexes = new int[nonzero];
410         int index = 0;
411         for (int i = 0; i < denseData.length; i++)
412             for (int j = 0; j < denseData[i].length; j++)
413                 if (denseData[i][j].ne0())
414                 {
415                     this.sparseData[index] = denseData[i][j].floatValue();
416                     this.indexes[index] = i * this.cols + j;
417                     index++;
418                 }
419     }
420 
421     @Override
422     public int rows()
423     {
424         return this.rows;
425     }
426 
427     @Override
428     public int cols()
429     {
430         return this.cols;
431     }
432 
433     @Override
434     public boolean isDense()
435     {
436         return false;
437     }
438 
439     @Override
440     public boolean isDouble()
441     {
442         return false;
443     }
444 
445     /**
446      * Check whether the row and column are within bounds.
447      * @param row the row number
448      * @param col the column number
449      * @throws IndexOutOfBoundsException when row &gt; rows() or col &gt; cols() or row &lt; 0 or col &lt; 0
450      */
451     private void checkRowCol(final int row, final int col) throws IndexOutOfBoundsException
452     {
453         Throw.when(row < 0 || row >= this.rows, IndexOutOfBoundsException.class, "row %d not in range 0..%d", row,
454                 this.rows - 1);
455         Throw.when(col < 0 || col >= this.cols, IndexOutOfBoundsException.class, "column %d not in range 0..%d", col,
456                 this.cols - 1);
457     }
458 
459     @Override
460     public double get(final int row, final int col)
461     {
462         checkRowCol(row, col);
463         int index = row * this.cols + col; // zero-based
464         final int pos = Arrays.binarySearch(this.indexes, index);
465         return (pos >= 0) ? this.sparseData[pos] : 0.0;
466     }
467 
468     @SuppressWarnings("checkstyle:needbraces")
469     @Override
470     public double[] getDataArray()
471     {
472         double[] denseData = new double[rows() * cols()];
473         for (int i = 0; i < this.sparseData.length; i++)
474             denseData[this.indexes[i]] = this.sparseData[i];
475         return denseData;
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 cardinality()
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(getDataArray());
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(getDataArray(), dg.getDataArray());
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 }