View Javadoc
1   package nom.tam.fits;
2   
3   import java.io.PrintStream;
4   import java.lang.reflect.Array;
5   import java.util.ArrayList;
6   import java.util.Hashtable;
7   import java.util.Set;
8   
9   import nom.tam.fits.header.Bitpix;
10  import nom.tam.fits.header.Standard;
11  import nom.tam.util.ArrayDataOutput;
12  import nom.tam.util.ArrayFuncs;
13  import nom.tam.util.FitsOutput;
14  
15  /*
16   * #%L
17   * nom.tam FITS library
18   * %%
19   * Copyright (C) 2004 - 2024 nom-tam-fits
20   * %%
21   * This is free and unencumbered software released into the public domain.
22   *
23   * Anyone is free to copy, modify, publish, use, compile, sell, or
24   * distribute this software, either in source code form or as a compiled
25   * binary, for any purpose, commercial or non-commercial, and by any
26   * means.
27   *
28   * In jurisdictions that recognize copyright laws, the author or authors
29   * of this software dedicate any and all copyright interest in the
30   * software to the public domain. We make this dedication for the benefit
31   * of the public at large and to the detriment of our heirs and
32   * successors. We intend this dedication to be an overt act of
33   * relinquishment in perpetuity of all present and future rights to this
34   * software under copyright law.
35   *
36   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
37   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
38   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
39   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
40   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
41   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
42   * OTHER DEALINGS IN THE SOFTWARE.
43   * #L%
44   */
45  
46  import static nom.tam.fits.header.Standard.BITPIX;
47  import static nom.tam.fits.header.Standard.GCOUNT;
48  import static nom.tam.fits.header.Standard.GROUPS;
49  import static nom.tam.fits.header.Standard.NAXIS;
50  import static nom.tam.fits.header.Standard.NAXISn;
51  import static nom.tam.fits.header.Standard.PCOUNT;
52  import static nom.tam.fits.header.Standard.SIMPLE;
53  import static nom.tam.fits.header.Standard.XTENSION;
54  import static nom.tam.fits.header.Standard.XTENSION_IMAGE;
55  
56  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
57  
58  /**
59   * Random groups header/data unit. Random groups were an early attempt at extending FITS support beyond images, and was
60   * eventually superseded by binary tables, which offer the same functionality and more in a more generic way. The use of
61   * random group HDUs is discouraged, even by the FITS standard. Some old radio data may be packaged in this format. Thus
62   * apart from provided limited support for reading such data, users should not create random groups anew.
63   * {@link BinaryTableHDU} offers a much more flexible and capable way for storing an ensemble of parameters, arrays, and
64   * more.
65   * <p>
66   * Note that the internal storage of random groups is a <code>Object[ngroups][2]</code> array. The first element of each
67   * group (row) is a 1D array of parameter data of a numerical primitive type (e.g. <code>short[]</code>,
68   * <code>double[]</code>). The second element in each group (row) is an image of the same element type as the
69   * parameters. When analyzing group data structure only the first group is examined, but for a valid FITS file all
70   * groups must have the same structure.
71   * <p>
72   * As of version 1.19, we provide support for accessing parameters by names including building up higher-precision
73   * values by combining multiple related parameter conversion recipes through scalings and offsets, as described in the
74   * FITS standard (e.g. combining 3 or 4 related <code>byte</code> parameter values to obtain a full-precision 32-bit
75   * <code>float</code> parameter value when <code>BITPIX</code> is 8).
76   * </p>
77   * 
78   * @see BinaryTableHDU
79   */
80  @SuppressWarnings("deprecation")
81  public class RandomGroupsHDU extends BasicHDU<RandomGroupsData> {
82  
83      private Hashtable<String, Parameter> parameters;
84  
85      @Override
86      protected final String getCanonicalXtension() {
87          return Standard.XTENSION_IMAGE;
88      }
89  
90      /**
91       * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future
92       *
93       * @return                   a random groups data structure from an array of objects representing the data.
94       *
95       * @param      o             the array of object to create the random groups
96       *
97       * @throws     FitsException if the data could not be created.
98       */
99      @Deprecated
100     public static RandomGroupsData encapsulate(Object o) throws FitsException {
101         if (o instanceof Object[][]) {
102             return new RandomGroupsData((Object[][]) o);
103         }
104         throw new FitsException("Attempt to encapsulate invalid data in Random Group");
105     }
106 
107     static Object[] generateSampleRow(Header h) throws FitsException {
108 
109         int ndim = h.getIntValue(NAXIS, 0) - 1;
110         int[] dims = new int[ndim];
111 
112         Class<?> baseClass = Bitpix.fromHeader(h).getNumberType();
113 
114         // Note that we have to invert the order of the axes
115         // for the FITS file to get the order in the array we
116         // are generating. Also recall that NAXIS1=0, so that
117         // we have an 'extra' dimension.
118 
119         for (int i = 0; i < ndim; i++) {
120             long cdim = h.getIntValue(NAXISn.n(i + 2), 0);
121             if (cdim < 0) {
122                 throw new FitsException("Invalid array dimension:" + cdim);
123             }
124             dims[ndim - i - 1] = (int) cdim;
125         }
126 
127         Object[] sample = new Object[2];
128         sample[0] = ArrayFuncs.newInstance(baseClass, h.getIntValue(PCOUNT));
129         sample[1] = ArrayFuncs.newInstance(baseClass, dims);
130 
131         return sample;
132     }
133 
134     /**
135      * Check if this data is compatible with Random Groups structure. Must be an <code>Object[nGroups][2]</code>
136      * structure with both elements of each group having the same base type and the first element being a simple
137      * primitive array. We do not check anything but the first row.
138      *
139      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future
140      *
141      * @param      potentialData data to check
142      *
143      * @return                   is this data compatible with Random Groups structure
144      */
145     @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility")
146     @Deprecated
147     public static boolean isData(Object potentialData) {
148         if (potentialData instanceof Object[][]) {
149             Object[][] o = (Object[][]) potentialData;
150             if (o.length > 0 && o[0].length == 2 && //
151                     ArrayFuncs.getBaseClass(o[0][0]) == ArrayFuncs.getBaseClass(o[0][1])) {
152                 String cn = o[0][0].getClass().getName();
153                 if (cn.length() == 2 && cn.charAt(1) != 'Z' || cn.charAt(1) != 'C') {
154                     return true;
155                 }
156             }
157         }
158         return false;
159     }
160 
161     /**
162      * @deprecated     (<i>for internal use</i>) Will reduce visibility in the future
163      *
164      * @return         Is this a random groups header?
165      *
166      * @param      hdr The header to be tested.
167      */
168     @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility")
169     @Deprecated
170     public static boolean isHeader(Header hdr) {
171 
172         if (hdr.getBooleanValue(SIMPLE)) {
173             return hdr.getBooleanValue(GROUPS);
174         }
175 
176         String xtension = hdr.getStringValue(XTENSION);
177         xtension = xtension == null ? "" : xtension.trim();
178         if (XTENSION_IMAGE.equals(xtension)) {
179             return hdr.getBooleanValue(GROUPS);
180         }
181 
182         return false;
183     }
184 
185     /**
186      * Prepares a data object into which the actual data can be read from an input subsequently or at a later time.
187      *
188      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future
189      *
190      * @param      header        The FITS header that describes the data
191      *
192      * @return                   A data object that support reading content from a stream.
193      *
194      * @throws     FitsException if the data could not be prepared to prescriotion.
195      */
196     @Deprecated
197     public static RandomGroupsData manufactureData(Header header) throws FitsException {
198 
199         int gcount = header.getIntValue(GCOUNT, -1);
200         int pcount = header.getIntValue(PCOUNT, -1);
201 
202         if (!header.getBooleanValue(GROUPS) || header.getIntValue(NAXISn.n(1), -1) != 0 || gcount < 0 || pcount < 0
203                 || header.getIntValue(NAXIS) < 2) {
204             throw new FitsException("Invalid Random Groups Parameters");
205         }
206 
207         return new RandomGroupsData(gcount, generateSampleRow(header));
208     }
209 
210     /**
211      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future
212      *
213      * @return                   Make a header point to the given object.
214      *
215      * @param      d             The random groups data the header should describe.
216      *
217      * @throws     FitsException if the operation failed
218      */
219     @Deprecated
220     static Header manufactureHeader(Data d) throws FitsException {
221 
222         if (d == null) {
223             throw new FitsException("Attempt to create null Random Groups data");
224         }
225         Header h = new Header();
226         d.fillHeader(h);
227         return h;
228 
229     }
230 
231     /**
232      * Creates a random groups HDU from an <code>Object[nGroups][2]</code> array. Prior to 1.18, we used
233      * {@link Fits#makeHDU(Object)} to create random groups HDUs automatically from matching data. However, FITS
234      * recommends using binary tables instead of random groups in general, and this type of HDU is included in the
235      * standard only to support reading some older radio data. Hence, as of 1.18 {@link Fits#makeHDU(Object)} will never
236      * return random groups HDUs any longer, and will instead create binary (or ASCII) table HDUs instead. If the need
237      * arises to create new random group HDUs programatically, beyond reading of older files, then this method can take
238      * its place.
239      * 
240      * @param  data          The random groups table. The second dimension must be 2. The first element in each group
241      *                           (row) must be a 1D numerical primitive array, while the second element may be a
242      *                           multi-dimensional image of the same element type. All rows must consists of arrays of
243      *                           the same primitive numerical types and sized, e.g.
244      *                           <code>{ float[5], float[7][2] }</code> or <code>{ short[3], short[2][2][4] }</code>.
245      * 
246      * @return               a new random groups HDU, which encapsulated the supploed data table.
247      * 
248      * @throws FitsException if the seconds dimension of the array is not 2.
249      * 
250      * @see                  Fits#makeHDU(Object)
251      * 
252      * @since                1.18
253      */
254     public static RandomGroupsHDU createFrom(Object[][] data) throws FitsException {
255         if (!isData(data)) {
256             throw new FitsException("Type or layout of data is not random groups compatible.");
257         }
258         RandomGroupsData d = encapsulate(data);
259         return new RandomGroupsHDU(manufactureHeader(d), d);
260     }
261 
262     private void parseParameters(Header header) {
263         // Parse the parameter descriptions from the header
264         int nparms = header.getIntValue(Standard.PCOUNT);
265 
266         parameters = new Hashtable<>();
267 
268         for (int i = 1; i <= nparms; i++) {
269             String name = header.getStringValue(Standard.PTYPEn.n(i));
270             if (name == null) {
271                 continue;
272             }
273 
274             Parameter p = parameters.get(name);
275             if (p == null) {
276                 p = new Parameter();
277                 parameters.put(name, p);
278             }
279 
280             p.components.add(new ParameterConversion(header, i));
281         }
282     }
283 
284     /**
285      * Create an HDU from the given header and data.
286      * 
287      * @deprecated        (<i>for internal use</i>) Its visibility should be reduced to package level in the future.
288      *
289      * @param      header header to use
290      * @param      data   data to use
291      */
292     public RandomGroupsHDU(Header header, RandomGroupsData data) {
293         super(header, data);
294 
295         if (header == null) {
296             return;
297         }
298 
299         parseParameters(header);
300     }
301 
302     @Override
303     public void info(PrintStream stream) {
304 
305         stream.println("Random Groups HDU");
306         if (myHeader != null) {
307             stream.println("   HeaderInformation:");
308             stream.println("     Ngroups:" + myHeader.getIntValue(GCOUNT));
309             stream.println("     Npar:   " + myHeader.getIntValue(PCOUNT));
310             stream.println("     BITPIX: " + myHeader.getIntValue(BITPIX));
311             stream.println("     NAXIS:  " + myHeader.getIntValue(NAXIS));
312             for (int i = 0; i < myHeader.getIntValue(NAXIS); i++) {
313                 stream.println("      NAXIS" + (i + 1) + "= " + myHeader.getIntValue(NAXISn.n(i + 1)));
314             }
315         } else {
316             stream.println("    No Header Information");
317         }
318 
319         Object[][] data = null;
320         if (myData != null) {
321             try {
322                 data = myData.getData();
323             } catch (FitsException e) {
324                 // nothing to do...
325             }
326         }
327 
328         if (data == null || data.length < 1 || data[0].length != 2) {
329             stream.println("    Invalid/unreadable data");
330         } else {
331             stream.println("    Number of groups:" + data.length);
332             stream.println("    Parameters: " + ArrayFuncs.arrayDescription(data[0][0]));
333             stream.println("    Data:" + ArrayFuncs.arrayDescription(data[0][1]));
334         }
335     }
336 
337     /**
338      * Returns the number of parameter bytes (per data group) accompanying each data object in the group.
339      */
340     @Override
341     public int getParameterCount() {
342         return super.getParameterCount();
343     }
344 
345     /**
346      * Returns the number of data objects (of identical shape and size) that are group together in this HDUs data
347      * segment.
348      */
349     @Override
350     public int getGroupCount() {
351         return super.getGroupCount();
352     }
353 
354     /**
355      * Check that this HDU has a valid header.
356      *
357      * @return <CODE>true</CODE> if this HDU has a valid header.
358      */
359     public boolean isHeader() {
360         return isHeader(myHeader);
361     }
362 
363     /**
364      * Returns the name of the physical unit in which image data are represented.
365      * 
366      * @return the standard name of the physical unit in which the image is expressed, e.g. <code>"Jy beam^{-1}"</code>.
367      */
368     @Override
369     public String getBUnit() {
370         return super.getBUnit();
371     }
372 
373     /**
374      * Returns the integer value that signifies blank (missing or <code>null</code>) data in an integer image.
375      *
376      * @return               the integer value used for identifying blank / missing data in integer images.
377      * 
378      * @throws FitsException if the header does not specify a blanking value or if it is not appropriate for the type of
379      *                           imge (that is not an integer type image)
380      */
381     @Override
382     public long getBlankValue() throws FitsException {
383         if (getBitpix().getHeaderValue() < 0) {
384             throw new FitsException("No integer blanking value in floating-point images.");
385         }
386         return super.getBlankValue();
387     }
388 
389     /**
390      * Returns the floating-point increment between adjacent integer values in the image. Strictly speaking, only
391      * integer-type images should define a quantization scaling, but there is no harm in having this value in
392      * floating-point images also -- which may be interpreted as a hint for quantization, perhaps.
393      * 
394      * @return the floating-point quantum that corresponds to the increment of 1 in the integer data representation.
395      * 
396      * @see    #getBZero()
397      */
398     @Override
399     public double getBScale() {
400         return super.getBScale();
401     }
402 
403     /**
404      * Returns the floating-point value that corresponds to an 0 integer value in the image. Strictly speaking, only
405      * integer-type images should define a quantization scaling, but there is no harm in having this value in
406      * floating-point images also -- which may be interpreted as a hint for quantization, perhaps.
407      * 
408      * @return the floating point value that correspond to the integer 0 in the image data.
409      * 
410      * @see    #getBScale()
411      */
412     @Override
413     public double getBZero() {
414         return super.getBZero();
415     }
416 
417     @Override
418     public void write(ArrayDataOutput stream) throws FitsException {
419         if (stream instanceof FitsOutput) {
420             if (!((FitsOutput) stream).isAtStart()) {
421                 throw new FitsException("Random groups are only permitted in the primary HDU");
422             }
423         }
424         super.write(stream);
425     }
426 
427     /**
428      * Returns a list of parameter names bundled along the images in each group, as extracted from the PTYPE_n_ header
429      * entries.
430      * 
431      * @return A set containing the parameter names contained in this HDU
432      * 
433      * @see    #getParameter(String, int)
434      * 
435      * @since  1.19
436      */
437     public Set<String> getParameterNames() {
438         return parameters.keySet();
439     }
440 
441     /**
442      * Returns the value for a given group parameter.
443      * 
444      * @param  name                           the parameter name
445      * @param  group                          the zero-based group index
446      * 
447      * @return                                the stored parameter value in the specified group, or {@link Double#NaN}
448      *                                            if the there is no such group.
449      * 
450      * @throws ArrayIndexOutOfBoundsException if the group index is out of bounds.
451      * @throws FitsException                  if the deferred parameter data cannot be accessed
452      * 
453      * @see                                   #getParameterNames()
454      * @see                                   RandomGroupsData#getImage(int)
455      * 
456      * @since                                 1.19
457      */
458     public double getParameter(String name, int group) throws ArrayIndexOutOfBoundsException, FitsException {
459         Parameter p = parameters.get(name);
460         if (p == null) {
461             return Double.NaN;
462         }
463 
464         return p.getValue(getData().getParameterArray(group));
465     }
466 
467     /**
468      * A conversion recipe from the native BITPIX type to a floating-point value. Each parameter may have multiple such
469      * recipes, the sum of which can provide the required precision for the parameter regardless the BITPIX storage
470      * type.
471      * 
472      * @author Attila Kovacs
473      * 
474      * @since  1.19
475      */
476     private static final class ParameterConversion {
477         private int index;
478         private double scaling;
479         private double offset;
480 
481         private ParameterConversion(Header h, int n) {
482             index = n - 1;
483             scaling = h.getDoubleValue(Standard.PSCALn.n(n), 1.0);
484             offset = h.getDoubleValue(Standard.PZEROn.n(n), 0.0);
485         }
486     }
487 
488     /**
489      * Represents a single parameter in the random groups data.
490      * 
491      * @author Attila Kovacs
492      * 
493      * @since  1.19
494      */
495     private static final class Parameter {
496         private ArrayList<ParameterConversion> components = new ArrayList<>();
497 
498         private double getValue(Object array) {
499             double value = 0.0;
500             for (ParameterConversion c : components) {
501                 double x = Array.getDouble(array, c.index);
502                 value += c.scaling * x + c.offset;
503             }
504             return value;
505         }
506     }
507 
508 }