1 package nom.tam.util;
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 nom.tam.fits.Header;
35 import nom.tam.fits.HeaderCard;
36 import nom.tam.fits.header.Standard;
37
38 /**
39 * <p>
40 * Quantizes floating point values as integers. FITS allows representing
41 * floating-point values as integers, e.g. to allow more compact storage at some
42 * tolerable level of precision loss. For example, you may store floating-point
43 * values (4 bytes) discretized into 64k levels as 16-bit integers. The
44 * conversion involves a linear transformation:
45 * </p>
46 *
47 * <pre>
48 * {float-value}= {scaling} * {int-value} + {offset}
49 * </pre>
50 * <p>
51 * and the inverse transformation:
52 * </p>
53 *
54 * <pre>
55 * {int-value} = round(({float-value} - {offset}) / {scaling})
56 * </pre>
57 * <p>
58 * The latter floating-point to integer conversion naturally results in some
59 * loss of precision, comparable to the level of the scaling factor, i.e. the
60 * peration of discrete levels at which information is preserved.
61 * </p>
62 * <p>
63 * In addition to the scaling conversion, FITS also allows designating an
64 * integer blanking value to indicate missing or invalid data, which is mapped
65 * to NaN in the floating point representation.
66 * </p>
67 * <p>
68 * Fits allows for quantized representations of floating-point data both in
69 * image HDUs and for columns in binary table HDUs. The quantization parameters
70 * are stored differently for the two types of HDUs, using the BSCALE, BZERO,
71 * and BLANK keywords for images, and the TSCALn, TZEROn, and TNULLn keywords
72 * for individual columns in a table.
73 * </p>
74 *
75 * @author Attila Kovacs
76 * @since 1.20
77 */
78 public class Quantizer {
79
80 private Long blankingValue;
81
82 private double scale = 1.0;
83
84 private double offset;
85
86 /**
87 * Constructs a new decimal/integer conversion rule.
88 *
89 * @param scale
90 * The scaling value, that is the spacing of the qunatized levels
91 * @param offset
92 * The floating-point value that corresponds to an integer value
93 * of 0 (zero).
94 * @param blankingValue
95 * The value to use to represent NaN values in the integer
96 * representation, that is missing or invalid data.
97 */
98 public Quantizer(double scale, double offset, int blankingValue) {
99 this(scale, offset, (long) blankingValue);
100 }
101
102 /**
103 * Constructs a new decimal/integer conversion rule.
104 *
105 * @param scale
106 * The scaling value, that is the spacing of the qunatized levels
107 * @param offset
108 * The floating-point value that corresponds to an integer value
109 * of 0 (zero).
110 * @param blankingValue
111 * The value to use to represent NaN values in the integer
112 * representation, that is missing or invalid data. It may be
113 * <code>null</code> if the floating-point data is not expected
114 * to contain NaN values ever.
115 */
116 public Quantizer(double scale, double offset, Long blankingValue) {
117 this.scale = scale;
118 this.offset = offset;
119 this.blankingValue = blankingValue;
120 }
121
122 /**
123 * Converts a floating point value to its integer representation using the
124 * quantization.
125 *
126 * @param value
127 * the floating point value
128 * @return the corresponding qunatized integer value
129 * @see #toDouble(long)
130 */
131 public long toLong(double value) {
132 if (!Double.isFinite(value)) {
133 if (blankingValue == null) {
134 throw new IllegalStateException("No blanking value was defined.");
135 }
136 return blankingValue;
137 }
138 return Math.round((value - offset) / scale);
139 }
140
141 /**
142 * Converts an integer value to the floating-point value it represents under
143 * the qunatization.
144 *
145 * @param value
146 * the integer value
147 * @return the corresponding floating-point value, which may be NaN.
148 * @see #toLong(double)
149 */
150 public double toDouble(long value) {
151 if (blankingValue != null && value == blankingValue) {
152 return Double.NaN;
153 }
154 return scale * value + offset;
155 }
156
157 /**
158 * Checks if the quantization is the same as the default quantization. For
159 * example, maybe we don't need to (want to) write the quantization keywords
160 * into the FITS headers if these are irrelevant and/or not meaningful. So
161 * this method might help us decide when quantization is necessary /
162 * meaningful vs when it is irrelevant.
163 *
164 * @return <code>true</code> if the scaling is 1.0, the offset 0.0, and the
165 * blanking value is <code>null</code>. Otherwise <code>false</code>
166 * .
167 */
168 public boolean isDefault() {
169 return scale == 1.0 && offset == 0.0 && blankingValue == null;
170 }
171
172 /**
173 * Adds the quantization parameters to an image header,
174 *
175 * @param h
176 * the image header.
177 * @see #fromImageHeader(Header)
178 * @see #editTableHeader(Header, int)
179 */
180 public void editImageHeader(Header h) {
181 h.addValue(Standard.BSCALE, scale);
182 h.addValue(Standard.BZERO, offset);
183
184 if (blankingValue != null) {
185 h.addValue(Standard.BLANK, blankingValue);
186 } else {
187 h.deleteKey(Standard.BLANK);
188 }
189 }
190
191 /**
192 * Adds the quantization parameters to a binaty table header,
193 *
194 * @param h
195 * the binary table header.
196 * @param col
197 * the zero-based Java column index
198 * @see #fromTableHeader(Header, int)
199 * @see #editImageHeader(Header)
200 */
201 public void editTableHeader(Header h, int col) {
202 Cursor<String, HeaderCard> c = h.iterator();
203 c.setKey(Standard.TFORMn.n(col + 1).key());
204
205 c.add(HeaderCard.create(Standard.TSCALn.n(col + 1), scale));
206 c.add(HeaderCard.create(Standard.TZEROn.n(col + 1), offset));
207
208 if (blankingValue != null) {
209 c.add(HeaderCard.create(Standard.TNULLn.n(col + 1), blankingValue));
210 } else {
211 h.deleteKey(Standard.TNULLn.n(col + 1));
212 }
213 }
214
215 /**
216 * Returns the quantizer that is described by an image header.
217 *
218 * @param h
219 * an image header
220 * @return the quantizer that id described by the header. It may be the
221 * default quantizer if the header does not contain any of the
222 * quantization keywords.
223 * @see #editImageHeader(Header)
224 * @see #fromTableHeader(Header, int)
225 * @see #isDefault()
226 */
227 public static Quantizer fromImageHeader(Header h) {
228 return new Quantizer(h.getDoubleValue(Standard.BSCALE, 1.0), h.getDoubleValue(Standard.BZERO, 0.0), //
229 h.containsKey(Standard.BLANK) ? h.getLongValue(Standard.BLANK) : null);
230 }
231
232 /**
233 * Returns the quantizer that is described by a binary table header.
234 *
235 * @param h
236 * a binary table header
237 * @param col
238 * the zero-based Java column index
239 * @return the quantizer that id described by the header. It may be the
240 * default quantizer if the header does not contain any of the
241 * quantization keywords.
242 * @see #editTableHeader(Header, int)
243 * @see #fromImageHeader(Header)
244 * @see #isDefault()
245 */
246 public static Quantizer fromTableHeader(Header h, int col) {
247 return new Quantizer(h.getDoubleValue(Standard.TSCALn.n(col + 1), 1.0), //
248 h.getDoubleValue(Standard.TZEROn.n(col + 1), 0.0), //
249 h.containsKey(Standard.TNULLn.n(col + 1)) ? h.getLongValue(Standard.TNULLn.n(col + 1)) : null);
250 }
251 }