View Javadoc
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   * A 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: circular 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-handed (input 1) and a left-handed (input 2) wave component */
83      RL(-3),
84  
85      /** circular cross-polarization between a left-handed (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. For values
141      *                                       1--4, the singular Stokes parameters I, Q, U, or V is returned. For
142      *                                       negative values, -1 through -8, the cross polarization parameters are
143      *                                       returned. For all other values and `IndexOutOfBoundsException` is thrown
144      * 
145      * @throws IndexOutOfBoundsException if the coordinate value is 0 or out of the range of acceptable Stokes
146      *                                       coordinate values.
147      * 
148      * @see                              #getCoordinateValue()
149      */
150     public static Stokes forCoordinateValue(int value) throws IndexOutOfBoundsException {
151         if (value == 0) {
152             throw new IndexOutOfBoundsException(
153                     "Value must be in the range [1:4] for single-ended or else [-1:-8] for cross-polarization.");
154         }
155         return ordered[value - YX.getCoordinateValue()];
156     }
157 
158     /**
159      * Helper class for setting or interpreting a set of measured Stokes parameters stored along an array dimension. Two
160      * instances of Stokes parameters are considered equal if they measure the same polarization terms, in the same
161      * order.
162      * 
163      * @author Attila Kovacs
164      * 
165      * @since  1.20
166      */
167     public static final class Parameters {
168         private int flags;
169         private int offset;
170         private int step;
171         private int count;
172 
173         private Parameters(int flags) {
174             this.flags = flags;
175 
176             boolean reversed = (flags & REVERSED_ORDER) != 0;
177 
178             step = reversed ? -1 : 1;
179             count = STANDARD_PARAMETER_COUNT;
180 
181             if ((flags & FULL_CROSS_POLARIZATION) == 0) {
182                 offset = reversed ? Stokes.V.getCoordinateValue() : Stokes.I.getCoordinateValue();
183             } else {
184                 step = -step;
185 
186                 if ((flags & CIRCULAR_CROSS_POLARIZATION) == 0) {
187                     offset = reversed ? Stokes.YX.getCoordinateValue() : Stokes.XX.getCoordinateValue();
188                 } else if ((flags & LINEAR_CROSS_POLARIZATION) == 0) {
189                     offset = reversed ? Stokes.LR.getCoordinateValue() : Stokes.RR.getCoordinateValue();
190                 } else {
191                     offset = reversed ? Stokes.YX.getCoordinateValue() : Stokes.RR.getCoordinateValue();
192                     count = FULL_PARAMETER_COUNT;
193                 }
194             }
195         }
196 
197         private Parameters(int offset, int step, int n) {
198             this.flags = 0;
199             this.offset = offset;
200             this.step = step;
201             this.count = n;
202 
203             if (offset < 0) {
204                 int end = offset + (n - 1) * step;
205 
206                 if (Math.min(offset, end) <= XX.index) {
207                     flags |= LINEAR_CROSS_POLARIZATION;
208                 }
209 
210                 if (Math.max(offset, end) > XX.index) {
211                     flags |= CIRCULAR_CROSS_POLARIZATION;
212                 }
213 
214                 step = -step;
215             }
216 
217             if (step < 0) {
218                 flags |= REVERSED_ORDER;
219             }
220         }
221 
222         @Override
223         public int hashCode() {
224             return flags ^ Integer.hashCode(offset) ^ Integer.hashCode(step);
225         }
226 
227         @Override
228         public boolean equals(Object o) {
229             if (!(o instanceof Parameters)) {
230                 return false;
231             }
232             Parameters p = (Parameters) o;
233             if (p.flags != flags) {
234                 return false;
235             }
236             if (p.offset != offset) {
237                 return false;
238             }
239             if (p.step != step) {
240                 return false;
241             }
242             return true;
243         }
244 
245         boolean isReversedOrder() {
246             return (flags & REVERSED_ORDER) != 0;
247         }
248 
249         /**
250          * Checks if the parameters are for measuring cross-polarization between two inputs.
251          * 
252          * @return <code>true</code> if it is for cross polarization, otherwise <code>false</code>.
253          */
254         public boolean isCrossPolarization() {
255             return (flags & FULL_CROSS_POLARIZATION) != 0;
256         }
257 
258         /**
259          * Checks if the parameters include linear polarization terms.
260          * 
261          * @return <code>true</code> if linear polarization is measured, otherwise <code>false</code>.
262          */
263         public boolean hasLinearPolarization() {
264             return (flags & FULL_CROSS_POLARIZATION) != CIRCULAR_CROSS_POLARIZATION;
265         }
266 
267         /**
268          * Checks if the parameters include circular polarization term(s).
269          * 
270          * @return <code>true</code> if cirular cross polarization is measured, otherwise <code>false</code>.
271          */
272         public boolean hasCircularPolarization() {
273             return (flags & FULL_CROSS_POLARIZATION) != LINEAR_CROSS_POLARIZATION;
274         }
275 
276         /**
277          * Returns the Stokes parameter for a given Java array index for a dimension that corresponds to the Stokes
278          * parameters described by this instance.
279          * 
280          * @param  idx                       the zero-based Java array index, typically [0:3] for single-ended
281          *                                       polarization or circular or linear-only cross-polarization, or else
282          *                                       [0:7] for full cross-polarization.
283          * 
284          * @return                           The specific Stokes parameter corresponding to the specified array index.
285          * 
286          * @throws IndexOutOfBoundsException if the index is outside of the expected range.
287          * 
288          * @see                              #getAvailableParameters()
289          * 
290          * @since                            1.19.1
291          */
292         public Stokes getParameter(int idx) throws IndexOutOfBoundsException {
293             if (idx < 0 || idx >= count) {
294                 throw new IndexOutOfBoundsException();
295             }
296             return Stokes.forCoordinateValue(offset + step * idx);
297         }
298 
299         /**
300          * Returns the ordered list of parameters, which can be used to translate array indexes to Stokes values,
301          * supported by this parameter set.
302          * 
303          * @return the ordered list of available Stokes parameters in this measurement set.
304          * 
305          * @see    #getParameter(int)
306          */
307         public ArrayList<Stokes> getAvailableParameters() {
308             ArrayList<Stokes> list = new ArrayList<>(count);
309             for (int i = 0; i < count; i++) {
310                 list.add(getParameter(i));
311             }
312             return list;
313         }
314 
315         /**
316          * Returns the Java array index corresponding to a given Stokes parameters for this set of parameters.
317          * 
318          * @param  s the Stokes parameter of interest
319          * 
320          * @return   the zero-based Java array index corresponding to the given Stokes parameter.
321          * 
322          * @see      #getParameter(int)
323          * 
324          * @since    1.20
325          */
326         public int getArrayIndex(Stokes s) {
327             return (s.getCoordinateValue() - offset) / step;
328         }
329 
330         /**
331          * Adds WCS description for the coordinate axis containing Stokes parameters. The header must already contain a
332          * NAXIS keyword specifying the dimensionality of the data, or else a FitsException will be thrown.
333          * 
334          * @param  header                    the FITS header to populate (it must already have an NAXIS keyword
335          *                                       present).
336          * @param  coordinateIndex           The 0-based Java coordinate index for the array dimension that corresponds
337          *                                       to the stokes parameter.
338          * 
339          * @throws IndexOutOfBoundsException if the coordinate index is negative or out of bounds for the array
340          *                                       dimensions
341          * @throws FitsException             if the header does not contain an NAXIS keyword, or if the header is not
342          *                                       accessible
343          * 
344          * @see                              #fillTableHeader(Header, int, int)
345          * @see                              Stokes#fromImageHeader(Header)
346          * 
347          * @since                            1.20
348          */
349         public void fillImageHeader(Header header, int coordinateIndex) throws FitsException {
350             int n = header.getIntValue(Standard.NAXIS);
351             if (n == 0) {
352                 throw new FitsException("Missing NAXIS in header");
353             }
354             if (coordinateIndex < 0 || coordinateIndex >= n) {
355                 throw new IndexOutOfBoundsException(
356                         "Invalid Java coordinate index " + coordinateIndex + " (for " + n + " dimensions)");
357             }
358 
359             int i = n - coordinateIndex;
360 
361             header.addValue(WCS.CTYPEna.n(i), Stokes.CTYPE);
362             header.addValue(WCS.CRPIXna.n(i), 1);
363             header.addValue(WCS.CRVALna.n(i), offset);
364             header.addValue(WCS.CDELTna.n(i), step);
365         }
366 
367         /**
368          * Adds WCS description for the coordinate axis containing Stokes parameters to a table column containign
369          * images.
370          * 
371          * @param  header                    the binary table header to populate (it should already contain a TDIMn
372          *                                       keyword for the specified column, or else 1D data is assumed).
373          * @param  column                    the zero-based Java column index containing the 'image' array.
374          * @param  coordinateIndex           the zero-based Java coordinate index for the array dimension that
375          *                                       corresponds to the stokes parameter.
376          * 
377          * @throws IndexOutOfBoundsException if the coordinate index is negative or out of bounds for the array
378          *                                       dimensions, or if the column index is invalid.
379          * @throws FitsException             if the header does not specify the dimensionality of the array elements, or
380          *                                       if the header is not accessible
381          * 
382          * @see                              #fillImageHeader(Header, int)
383          * @see                              Stokes#fromTableHeader(Header, int)
384          * 
385          * @since                            1.20
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     @SuppressWarnings({"unchecked", "rawtypes"})
510     public static Map.Entry<Integer, Parameters> fromImageHeader(Header header) throws FitsException {
511         int n = header.getIntValue(Standard.NAXIS);
512         if (n <= 0) {
513             throw new FitsException("Missing, invalid, or insufficient NAXIS in header");
514         }
515 
516         for (int i = 1; i <= n; i++) {
517             if (Stokes.CTYPE.equalsIgnoreCase(header.getStringValue(WCS.CTYPEna.n(i)))) {
518                 if (header.getDoubleValue(WCS.CRPIXna.n(i), 1.0) != 1.0) {
519                     throw new FitsException("Invalid Stokes " + WCS.CRPIXna.n(i).key() + " value: "
520                             + header.getDoubleValue(WCS.CRPIXna.n(i)) + ", expected 1");
521                 }
522 
523                 Parameters p = forCoords(header.getDoubleValue(WCS.CRVALna.n(i), 0.0),
524                         header.getDoubleValue(WCS.CDELTna.n(i), 1.0), header.getIntValue(Standard.NAXISn.n(i), 1));
525 
526                 return new AbstractMap.SimpleImmutableEntry(n - i, p);
527             }
528         }
529 
530         return null;
531     }
532 
533     /**
534      * Returns a mapping of a Java array dimension to a set of Stokes parameters, based on the WCS coordinate
535      * description in the image header.
536      * 
537      * @param  header                    the FITS header to populate.
538      * @param  column                    the zero-based Java column index containing the 'image' array.
539      * 
540      * @return                           A mapping from a zero-based Java array dimension which corresponds to the
541      *                                       Stokes dimension of the data, to the set of stokes Parameters defined in
542      *                                       that dimension; or <code>null</code> if the header does not contain a fully
543      *                                       valid description of a Stokes coordinate axis.
544      * 
545      * @throws IndexOutOfBoundsException if the column index is invalid.
546      * @throws FitsException             if the header does not contain an TDIMn keyword for the column, necessary for
547      *                                       translating Java array indices to FITS array indices, or if the iCRVLn,
548      *                                       iCRPXn or iCDLTn values for the 'STOKES' dimension are inconsistent with a
549      *                                       Stokes coordinate definition.
550      * 
551      * @see                              #fromImageHeader(Header)
552      * @see                              Parameters#fillTableHeader(Header, int, int)
553      * 
554      * @since                            1.20
555      */
556     @SuppressWarnings({"unchecked", "rawtypes"})
557     public static Map.Entry<Integer, Parameters> fromTableHeader(Header header, int column)
558             throws IndexOutOfBoundsException, FitsException {
559         if (column < 0) {
560             throw new IndexOutOfBoundsException("Invalid Java column index " + column);
561         }
562 
563         String dims = header.getStringValue(Standard.TDIMn.n(++column));
564         if (dims == null) {
565             throw new FitsException("Missing TDIM" + column + " in header");
566         }
567 
568         StringTokenizer tokens = new StringTokenizer(dims, "(, )");
569         int n = tokens.countTokens();
570 
571         for (int i = 1; i <= n; i++) {
572             String d = tokens.nextToken();
573 
574             if (Stokes.CTYPE.equalsIgnoreCase(header.getStringValue(WCS.nCTYPn.n(i, column)))) {
575                 if (header.getDoubleValue(WCS.nCRPXn.n(i, column), 1.0) != 1.0) {
576                     throw new FitsException("Invalid Stokes " + WCS.nCRPXn.n(i, column).key() + " value: "
577                             + header.getDoubleValue(WCS.nCRPXn.n(i, column)) + ", expected 1");
578                 }
579 
580                 try {
581                     Parameters p = forCoords(header.getDoubleValue(WCS.nCRVLn.n(i, column), 0.0),
582                             header.getDoubleValue(WCS.nCDLTn.n(i, column), 1.0), Integer.parseInt(d));
583                     return new AbstractMap.SimpleImmutableEntry(n - i, p);
584                 } catch (NumberFormatException e) {
585                     throw new FitsException("Invalid " + Standard.TDIMn.n(column).key() + " value: '" + dims
586                             + "' (component: '" + d + "')");
587                 }
588             }
589         }
590 
591         return null;
592     }
593 }