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 }