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 }