1 package nom.tam.fits.header; 2 3 /*- 4 * #%L 5 * nom.tam.fits 6 * %% 7 * Copyright (C) 1996 - 2024 nom-tam-fits 8 * %% 9 * This is free and unencumbered software released into the public domain. 10 * 11 * Anyone is free to copy, modify, publish, use, compile, sell, or 12 * distribute this software, either in source code form or as a compiled 13 * binary, for any purpose, commercial or non-commercial, and by any 14 * means. 15 * 16 * In jurisdictions that recognize copyright laws, the author or authors 17 * of this software dedicate any and all copyright interest in the 18 * software to the public domain. We make this dedication for the benefit 19 * of the public at large and to the detriment of our heirs and 20 * successors. We intend this dedication to be an overt act of 21 * relinquishment in perpetuity of all present and future rights to this 22 * software under copyright law. 23 * 24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 30 * OTHER DEALINGS IN THE SOFTWARE. 31 * #L% 32 */ 33 34 import java.util.AbstractMap; 35 import java.util.ArrayList; 36 import java.util.Map; 37 import java.util.StringTokenizer; 38 39 import nom.tam.fits.FitsException; 40 import nom.tam.fits.Header; 41 42 /** 43 * <p> 44 * A mapping of image coordinate values for a coordinate axis with {@link WCS#CTYPEna} = <code>'STOKES'</code> (or 45 * equivalent), specifying polarization (or cross-polarization) data products along the image direction. The FITS 46 * standard (4.0) defines a mapping of pixel coordinate values along an image imension to Stokes parameters, and this 47 * enum provides an implementation of that for this library. 48 * </p> 49 * <p> 50 * An dataset may typically contain 4 or 8 Stokes parameters (or fewer), which depending on the type of measurement can 51 * be (I, Q, U, [V]), or (RR, LL, RL, LR) and/or (XX, YY, XY, YX). As such, the corresponding {@link WCS#CRPIXna} is 52 * typically 0 and {@link WCS#CDELTna} is +/- 1, and depending on the type of measurement {@link WCS#CRVALna} is 1, or 53 * -1, or -5. You can use the {@link Parameters} subclass to help populate or interpret Stokes parameters in headers. 54 * </p> 55 * 56 * @author Attila Kovacs 57 * 58 * @since 1.20 59 * 60 * @see WCS 61 * @see #parameters() 62 */ 63 public enum Stokes { 64 /** Stokes I: total (polarized + unpolarized) power */ 65 I(1), 66 67 /** Stokes Q: linear polarization Q component */ 68 Q(2), 69 70 /** Stokes U: linear polarization U component */ 71 U(3), 72 73 /** Stokes V: circularly polarization */ 74 V(4), 75 76 /** circular cross-polarization between two right-handed wave components */ 77 RR(-1), 78 79 /** circular cross-polarization between two left-handed wave components */ 80 LL(-2), 81 82 /** circular cross-polarization between a right-handled (input 1) and a left-handed (input 2) wave component */ 83 RL(-3), 84 85 /** circular cross-polarization between a left-handled (input 1) and a right-handed (input 2) wave component */ 86 LR(-4), 87 88 /** linear cross-polarization between two 'horizontal' wave components (in local orientation) */ 89 XX(-5), 90 91 /** linear cross-polarization between two 'vertical' wave components (in local orientation) */ 92 YY(-6), 93 94 /** 95 * linear cross-polarization between a 'horizontal' (input 1) and a 'vertical' (input 2) wave component (in local 96 * orientation) 97 */ 98 XY(-7), 99 100 /** 101 * linear cross-polarization between a 'vertical' (input 1) and a 'horizontal' (input 2) wave component (in local 102 * orientation) 103 */ 104 YX(-8); 105 106 /** The value to use for CTYPE type keywords to indicate Stokes parameter data */ 107 public static final String CTYPE = "STOKES"; 108 109 private int index; 110 111 private static Stokes[] ordered = {YX, XY, YY, XX, LR, RL, LL, RR, null, I, Q, U, V}; 112 113 private static final int STANDARD_PARAMETER_COUNT = 4; 114 private static final int FULL_PARAMETER_COUNT = 8; 115 116 Stokes(int value) { 117 this.index = value; 118 } 119 120 /** 121 * Returns the WCS coordinate value corresponding to this Stokes parameter for an image coordinate with 122 * {@link WCS#CTYPEna} = <code>'STOKES'</code>. 123 * 124 * @return the WCS coordinate value corresponding to this Stokes parameter. 125 * 126 * @see #forCoordinateValue(int) 127 * @see WCS#CTYPEna 128 * @see WCS#CRVALna 129 */ 130 public final int getCoordinateValue() { 131 return index; 132 } 133 134 /** 135 * Returns the Stokes parameter for the given pixel coordinate value for an image coordinate with 136 * {@link WCS#CTYPEna} = <code>'STOKES'</code>. 137 * 138 * @param value The image coordinate value 139 * 140 * @return The Stokes parameter, which corresponds to that coordinate value. 141 * 142 * @throws IndexOutOfBoundsException if the coordinate value is outt of the range of acceptable Stokes coordinate 143 * values. 144 * 145 * @see #getCoordinateValue() 146 */ 147 public static Stokes forCoordinateValue(int value) throws IndexOutOfBoundsException { 148 return ordered[value - YX.getCoordinateValue()]; 149 } 150 151 /** 152 * Helper class for setting or interpreting a set of measured Stokes parameters stored along an array dimension. Two 153 * instances of Stokes parameters are considered equal if they measure the same polarization terms, in the same 154 * order. 155 * 156 * @author Attila Kovacs 157 * 158 * @since 1.20 159 */ 160 public static final class Parameters { 161 private int flags; 162 private int offset; 163 private int step; 164 private int count; 165 166 private Parameters(int flags) { 167 this.flags = flags; 168 169 boolean reversed = (flags & REVERSED_ORDER) != 0; 170 171 step = reversed ? -1 : 1; 172 count = STANDARD_PARAMETER_COUNT; 173 174 if ((flags & FULL_CROSS_POLARIZATION) == 0) { 175 offset = reversed ? Stokes.V.getCoordinateValue() : Stokes.I.getCoordinateValue(); 176 } else { 177 step = -step; 178 179 if ((flags & CIRCULAR_CROSS_POLARIZATION) == 0) { 180 offset = reversed ? Stokes.YX.getCoordinateValue() : Stokes.XX.getCoordinateValue(); 181 } else if ((flags & LINEAR_CROSS_POLARIZATION) == 0) { 182 offset = reversed ? Stokes.LR.getCoordinateValue() : Stokes.RR.getCoordinateValue(); 183 } else { 184 offset = reversed ? Stokes.YX.getCoordinateValue() : Stokes.RR.getCoordinateValue(); 185 count = FULL_PARAMETER_COUNT; 186 } 187 } 188 } 189 190 private Parameters(int offset, int step, int n) { 191 this.offset = offset; 192 this.step = step; 193 this.count = n; 194 195 if (offset < 0) { 196 int end = offset + (n - 1) * step; 197 198 if (Math.min(offset, end) <= XX.index) { 199 flags |= LINEAR_CROSS_POLARIZATION; 200 } 201 202 if (Math.max(offset, end) > XX.index) { 203 flags |= CIRCULAR_CROSS_POLARIZATION; 204 } 205 206 step = -step; 207 } 208 209 if (step < 0) { 210 flags |= REVERSED_ORDER; 211 } 212 } 213 214 @Override 215 public int hashCode() { 216 return flags ^ Integer.hashCode(offset) ^ Integer.hashCode(step); 217 } 218 219 @Override 220 public boolean equals(Object o) { 221 if (!(o instanceof Parameters)) { 222 return false; 223 } 224 Parameters p = (Parameters) o; 225 if (p.flags != flags) { 226 return false; 227 } 228 if (p.offset != offset) { 229 return false; 230 } 231 if (p.step != step) { 232 return false; 233 } 234 return true; 235 } 236 237 boolean isReversedOrder() { 238 return (flags & REVERSED_ORDER) != 0; 239 } 240 241 /** 242 * Checks if the parameters are for measuring cross-polarization between two inputs. 243 * 244 * @return <code>true</code> if it is for cross polarization, otherwise <code>false</code>. 245 */ 246 public boolean isCrossPolarization() { 247 return (flags & FULL_CROSS_POLARIZATION) != 0; 248 } 249 250 /** 251 * Checks if the parameters include linear polarization terms. 252 * 253 * @return <code>true</code> if linear polarization is measured, otherwise <code>false</code>. 254 */ 255 public boolean hasLinearPolarization() { 256 return (flags & FULL_CROSS_POLARIZATION) != CIRCULAR_CROSS_POLARIZATION; 257 } 258 259 /** 260 * Checks if the parameters include circular polarization term(s). 261 * 262 * @return <code>true</code> if cirular cross polarization is measured, otherwise <code>false</code>. 263 */ 264 public boolean hasCircularPolarization() { 265 return (flags & FULL_CROSS_POLARIZATION) != LINEAR_CROSS_POLARIZATION; 266 } 267 268 /** 269 * Returns the Stokes parameter for a given Java array index for a dimension that corresponds to the Stokes 270 * parameters described by this instance. 271 * 272 * @param idx the zero-based Java array index, typically [0:3] for single-ended 273 * polarization or circular or linear-only cross-polarization, or else 274 * [0:7] for full cross-polarization. 275 * 276 * @return The specific Stokes parameter corresponding to the specified array index. 277 * 278 * @throws IndexOutOfBoundsException if the index is outside of the expected range. 279 * 280 * @see #getAvailableParameters() 281 * 282 * @since 1.19.1 283 * 284 * @author Attila Kovacs 285 */ 286 public Stokes getParameter(int idx) throws IndexOutOfBoundsException { 287 if (idx < 0 || idx >= count) { 288 throw new IndexOutOfBoundsException(); 289 } 290 return Stokes.forCoordinateValue(offset + step * idx); 291 } 292 293 /** 294 * Returns the ordered list of parameters, which can be used to translate array indexes to Stokes values, 295 * supported by this parameter set. 296 * 297 * @return the ordered list of available Stokes parameters in this measurement set. 298 * 299 * @see #getParameter(int) 300 */ 301 public ArrayList<Stokes> getAvailableParameters() { 302 ArrayList<Stokes> list = new ArrayList<>(count); 303 for (int i = 0; i < count; i++) { 304 list.add(getParameter(i)); 305 } 306 return list; 307 } 308 309 /** 310 * Returns the Java array index corresponding to a given Stokes parameters for this set of parameters. 311 * 312 * @param s the Stokes parameter of interest 313 * 314 * @return the zero-based Java array index corresponding to the given Stokes parameter. 315 * 316 * @see #getParameter(int) 317 * 318 * @since 1.20 319 * 320 * @author Attila Kovacs 321 */ 322 public int getArrayIndex(Stokes s) { 323 return (s.getCoordinateValue() - offset) / step; 324 } 325 326 /** 327 * Adds WCS description for the coordinate axis containing Stokes parameters. The header must already contain a 328 * NAXIS keyword specifying the dimensionality of the data, or else a FitsException will be thrown. 329 * 330 * @param header the FITS header to populate (it must already have an NAXIS keyword 331 * present). 332 * @param coordinateIndex The 0-based Java coordinate index for the array dimension that corresponds 333 * to the stokes parameter. 334 * 335 * @throws IndexOutOfBoundsException if the coordinate index is negative or out of bounds for the array 336 * dimensions 337 * @throws FitsException if the header does not contain an NAXIS keyword, or if the header is not 338 * accessible 339 * 340 * @see #fillTableHeader(Header, int, int) 341 * @see Stokes#fromImageHeader(Header) 342 * 343 * @since 1.20 344 * 345 * @author Attila Kovacs 346 */ 347 public void fillImageHeader(Header header, int coordinateIndex) throws FitsException { 348 int n = header.getIntValue(Standard.NAXIS); 349 if (n == 0) { 350 throw new FitsException("Missing NAXIS in header"); 351 } 352 if (coordinateIndex < 0 || coordinateIndex >= n) { 353 throw new IndexOutOfBoundsException( 354 "Invalid Java coordinate index " + coordinateIndex + " (for " + n + " dimensions)"); 355 } 356 357 int i = n - coordinateIndex; 358 359 header.addValue(WCS.CTYPEna.n(i), Stokes.CTYPE); 360 header.addValue(WCS.CRPIXna.n(i), 1); 361 header.addValue(WCS.CRVALna.n(i), offset); 362 header.addValue(WCS.CDELTna.n(i), step); 363 } 364 365 /** 366 * Adds WCS description for the coordinate axis containing Stokes parameters to a table column containign 367 * images. 368 * 369 * @param header the binary table header to populate (it should already contain a TDIMn 370 * keyword for the specified column, or else 1D data is assumed). 371 * @param column the zero-based Java column index containing the 'image' array. 372 * @param coordinateIndex the zero-based Java coordinate index for the array dimension that 373 * corresponds to the stokes parameter. 374 * 375 * @throws IndexOutOfBoundsException if the coordinate index is negative or out of bounds for the array 376 * dimensions, or if the column index is invalid. 377 * @throws FitsException if the header does not specify the dimensionality of the array elements, or 378 * if the header is not accessible 379 * 380 * @see #fillImageHeader(Header, int) 381 * @see Stokes#fromTableHeader(Header, int) 382 * 383 * @since 1.20 384 * 385 * @author Attila Kovacs 386 */ 387 public void fillTableHeader(Header header, int column, int coordinateIndex) 388 throws IndexOutOfBoundsException, FitsException { 389 if (column < 0) { 390 throw new IndexOutOfBoundsException("Invalid Java column index " + column); 391 } 392 393 String dims = header.getStringValue(Standard.TDIMn.n(++column)); 394 if (dims == null) { 395 throw new FitsException("Missing TDIM" + column + " in header"); 396 } 397 398 StringTokenizer tokens = new StringTokenizer(dims, "(, )"); 399 int n = tokens.countTokens(); 400 401 if (coordinateIndex < 0 || coordinateIndex >= n) { 402 throw new IndexOutOfBoundsException( 403 "Invalid Java coordinate index " + coordinateIndex + " (for " + n + " dimensions)"); 404 } 405 406 int i = n - coordinateIndex; 407 408 header.addValue(WCS.nCTYPn.n(i, column), Stokes.CTYPE); 409 header.addValue(WCS.nCRPXn.n(i, column), 1); 410 header.addValue(WCS.nCRVLn.n(i, column), offset); 411 header.addValue(WCS.nCDLTn.n(i, column), step); 412 } 413 } 414 415 /** 416 * Returns a new set of standard single-input Stokes parameters (I, Q, U, V). 417 * 418 * @return the standard set of I, Q, U, V Stokes parameters. 419 * 420 * @see #parameters(int) 421 */ 422 public static Parameters parameters() { 423 return parameters(0); 424 } 425 426 /** 427 * Returns the set of Stokes parameters for the given bitwise flags, which may specify linear or cicular cross 428 * polarization, or both, and/or if the parameters are stored in reversed index order in the FITS. The flags can be 429 * bitwise OR'd, e.g. {@link #LINEAR_CROSS_POLARIZATION} | {@link #CIRCULAR_CROSS_POLARIZATION} will select Stokes 430 * parameters for measuring circular cross polarization, stored in reversed index order that is: (LR, RL, LL, RR). 431 * 432 * @param flags the bitwise flags specifying the type of Stokes parameters. 433 * 434 * @return the set of Stokes parameters for the given bitwise flags. 435 * 436 * @see #parameters() 437 * @see #LINEAR_CROSS_POLARIZATION 438 * @see #CIRCULAR_CROSS_POLARIZATION 439 * @see #FULL_CROSS_POLARIZATION 440 */ 441 public static Parameters parameters(int flags) { 442 return new Parameters(flags); 443 } 444 445 /** 446 * Bitwise flag for Stokes parameters stored in reversed index order. 447 */ 448 static final int REVERSED_ORDER = 1; 449 450 /** 451 * Bitwise flag for dual-input linear cross polarization Stokes parameters (XX, YY, XY, YX) 452 */ 453 public static final int LINEAR_CROSS_POLARIZATION = 2; 454 455 /** 456 * Bitwise flag for dual-input circular cross polarization Stokes parameters (RR, LL, RL, LR) 457 */ 458 public static final int CIRCULAR_CROSS_POLARIZATION = 4; 459 460 /** 461 * Bitwise flag for dual-input full (linear + circular) cross polarization Stokes parameters (RR, LL, RL, LR, XX, 462 * YY, XY, YX). By definition tme as ({@link #CIRCULAR_CROSS_POLARIZATION} | {@link #LINEAR_CROSS_POLARIZATION}). 463 * 464 * @see #CIRCULAR_CROSS_POLARIZATION 465 * @see #LINEAR_CROSS_POLARIZATION 466 */ 467 public static final int FULL_CROSS_POLARIZATION = LINEAR_CROSS_POLARIZATION | CIRCULAR_CROSS_POLARIZATION; 468 469 private static Parameters forCoords(double start, double delt, int count) throws FitsException { 470 int offset = (int) start; 471 if (start != offset) { 472 throw new FitsException("Invalid (non-integer) Stokes coordinate start: " + start); 473 } 474 475 int step = (int) delt; 476 if (delt != step) { 477 throw new FitsException("Invalid (non-integer) Stokes coordinate step: " + delt); 478 } 479 480 int end = offset + step * (count - 1); 481 if (Math.min(offset, end) <= 0 && Math.max(offset, end) >= 0) { 482 throw new FitsException("Invalid Stokes coordinate range: " + offset + ":" + end); 483 } 484 485 return new Parameters(offset, step, count); 486 } 487 488 /** 489 * Returns a mapping of a Java array dimension to a set of Stokes parameters, based on the WCS coordinate 490 * description in the image header. The header must already contain a NAXIS keyword specifying the dimensionality of 491 * the data, or else a FitsException will be thrown. 492 * 493 * @param header the FITS header to populate (it must already have an NAXIS keyword present). 494 * 495 * @return A mapping from a zero-based Java array dimension which corresponds to the Stokes dimension 496 * of the data, to the set of stokes Parameters defined in that dimension; or 497 * <code>null</code> if the header does not contain a fully valid description of a Stokes 498 * coordinate axis. 499 * 500 * @throws FitsException if the header does not contain an NAXIS keyword, necessary for translating Java array 501 * indices to FITS array indices, or if the CRVALn, CRPIXna or CDELTna values for the 502 * 'STOKES' dimension are inconsistent with a Stokes coordinate definition. 503 * 504 * @see #fromTableHeader(Header, int) 505 * @see Parameters#fillImageHeader(Header, int) 506 * 507 * @since 1.20 508 * 509 * @author Attila Kovacs 510 */ 511 @SuppressWarnings({"unchecked", "rawtypes"}) 512 public static Map.Entry<Integer, Parameters> fromImageHeader(Header header) throws FitsException { 513 int n = header.getIntValue(Standard.NAXIS); 514 if (n <= 0) { 515 throw new FitsException("Missing, invalid, or insufficient NAXIS in header"); 516 } 517 518 for (int i = 1; i <= n; i++) { 519 if (Stokes.CTYPE.equalsIgnoreCase(header.getStringValue(WCS.CTYPEna.n(i)))) { 520 if (header.getDoubleValue(WCS.CRPIXna.n(i), 1.0) != 1.0) { 521 throw new FitsException("Invalid Stokes " + WCS.CRPIXna.n(i).key() + " value: " 522 + header.getDoubleValue(WCS.CRPIXna.n(i)) + ", expected 1"); 523 } 524 525 Parameters p = forCoords(header.getDoubleValue(WCS.CRVALna.n(i), 0.0), 526 header.getDoubleValue(WCS.CDELTna.n(i), 1.0), header.getIntValue(Standard.NAXISn.n(i), 1)); 527 528 return new AbstractMap.SimpleImmutableEntry(n - i, p); 529 } 530 } 531 532 return null; 533 } 534 535 /** 536 * Returns a mapping of a Java array dimension to a set of Stokes parameters, based on the WCS coordinate 537 * description in the image header. 538 * 539 * @param header the FITS header to populate. 540 * @param column the zero-based Java column index containing the 'image' array. 541 * 542 * @return A mapping from a zero-based Java array dimension which corresponds to the 543 * Stokes dimension of the data, to the set of stokes Parameters defined in 544 * that dimension; or <code>null</code> if the header does not contain a fully 545 * valid description of a Stokes coordinate axis. 546 * 547 * @throws IndexOutOfBoundsException if the column index is invalid. 548 * @throws FitsException if the header does not contain an TDIMn keyword for the column, necessary for 549 * translating Java array indices to FITS array indices, or if the iCRVLn, 550 * iCRPXn or iCDLTn values for the 'STOKES' dimension are inconsistent with a 551 * Stokes coordinate definition. 552 * 553 * @see #fromImageHeader(Header) 554 * @see Parameters#fillTableHeader(Header, int, int) 555 * 556 * @since 1.20 557 * 558 * @author Attila Kovacs 559 */ 560 @SuppressWarnings({"unchecked", "rawtypes"}) 561 public static Map.Entry<Integer, Parameters> fromTableHeader(Header header, int column) 562 throws IndexOutOfBoundsException, FitsException { 563 if (column < 0) { 564 throw new IndexOutOfBoundsException("Invalid Java column index " + column); 565 } 566 567 String dims = header.getStringValue(Standard.TDIMn.n(++column)); 568 if (dims == null) { 569 throw new FitsException("Missing TDIM" + column + " in header"); 570 } 571 572 StringTokenizer tokens = new StringTokenizer(dims, "(, )"); 573 int n = tokens.countTokens(); 574 575 for (int i = 1; i <= n; i++) { 576 String d = tokens.nextToken(); 577 578 if (Stokes.CTYPE.equalsIgnoreCase(header.getStringValue(WCS.nCTYPn.n(i, column)))) { 579 if (header.getDoubleValue(WCS.nCRPXn.n(i, column), 1.0) != 1.0) { 580 throw new FitsException("Invalid Stokes " + WCS.nCRPXn.n(i, column).key() + " value: " 581 + header.getDoubleValue(WCS.nCRPXn.n(i, column)) + ", expected 1"); 582 } 583 584 Parameters p = forCoords(header.getDoubleValue(WCS.nCRVLn.n(i, column), 0.0), 585 header.getDoubleValue(WCS.nCDLTn.n(i, column), 1.0), Integer.parseInt(d)); 586 587 return new AbstractMap.SimpleImmutableEntry(n - i, p); 588 } 589 } 590 591 return null; 592 } 593 }