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 }