1 package nom.tam.fits.compression.algorithm.quant;
2
3 import nom.tam.fits.compression.algorithm.api.ICompressOption;
4 import nom.tam.fits.compression.provider.param.api.ICompressParameters;
5 import nom.tam.fits.compression.provider.param.base.BundledParameters;
6 import nom.tam.fits.compression.provider.param.quant.QuantizeParameters;
7
8 /*
9 * #%L
10 * nom.tam FITS library
11 * %%
12 * Copyright (C) 1996 - 2024 nom-tam-fits
13 * %%
14 * This is free and unencumbered software released into the public domain.
15 *
16 * Anyone is free to copy, modify, publish, use, compile, sell, or
17 * distribute this software, either in source code form or as a compiled
18 * binary, for any purpose, commercial or non-commercial, and by any
19 * means.
20 *
21 * In jurisdictions that recognize copyright laws, the author or authors
22 * of this software dedicate any and all copyright interest in the
23 * software to the public domain. We make this dedication for the benefit
24 * of the public at large and to the detriment of our heirs and
25 * successors. We intend this dedication to be an overt act of
26 * relinquishment in perpetuity of all present and future rights to this
27 * software under copyright law.
28 *
29 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
32 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
33 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
34 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 * OTHER DEALINGS IN THE SOFTWARE.
36 * #L%
37 */
38
39 /**
40 * Quantization options when they are part of the compression scheme. When compressing tables and images includes
41 * quantization (integer representation of floating point data), users can control how exactly the quantization should
42 * be performed. When reading compressed FITS files, these options will be set automatically based on the header values
43 * recorded in the compressed HDU.
44 *
45 * @see nom.tam.image.compression.hdu.CompressedImageHDU#setQuantAlgorithm(String)
46 * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class)
47 */
48 public class QuantizeOption implements ICompressOption {
49
50 /**
51 * The integer value recommeded by the FITS standard to represent NaN floating-point values in integer compressed
52 * data.
53 */
54 public static final int RECOMMENDED_NAN_INDICATOR = Integer.MIN_VALUE;
55
56 /**
57 * value used to represent zero-valued pixels when dither method 2 is used.
58 */
59 private static final int DITHER2_ZERO_INDICATOR = Integer.MIN_VALUE + 1;
60
61 private static boolean useFMA = false;
62
63 /** Shared configuration across copies */
64 private Config config;
65
66 /** The parameters that represent settings for this option in the FITS headers and/or compressed data columns */
67 protected QuantizeParameters parameters;
68
69 private ICompressOption compressOption;
70
71 private double bScale = Double.NaN;
72
73 private double bZero = Double.NaN;
74
75 private double nullValue = Double.NaN;
76
77 private Integer nullValueIndicator;
78
79 private int intMaxValue;
80
81 private int intMinValue;
82
83 private double maxValue;
84
85 private double minValue;
86
87 private int tileIndex = 0;
88
89 private int tileHeight;
90
91 private int tileWidth;
92
93 /** Quantization constant */
94 private static final int RANDOM_MULTIPLICATOR = 500;
95
96 private static final double DITHER_HALF = 0.5;
97
98 /**
99 * Dither random seed value
100 */
101 private int iseed;
102
103 /**
104 * Next random dither value
105 */
106 private int nextRandom;
107
108 QuantizeOption() {
109 this(null);
110 }
111
112 /**
113 * Creates a new set of quantization options, to be used together with the specified compression options.
114 *
115 * @param compressOption Compression-specific options to pair with these quantization options, or <code>null</code>.
116 *
117 * @since 1.18
118 */
119 public QuantizeOption(ICompressOption compressOption) {
120 parameters = new QuantizeParameters(this);
121 config = new Config();
122 this.compressOption = compressOption;
123 }
124
125 @Override
126 public QuantizeOption copy() {
127 try {
128 QuantizeOption copy = (QuantizeOption) clone();
129 if (compressOption != null) {
130 copy.compressOption = compressOption.copy();
131 }
132 copy.parameters = parameters.copy(copy);
133 return copy;
134 } catch (CloneNotSupportedException e) {
135 throw new IllegalStateException("option could not be cloned", e);
136 }
137 }
138
139 /**
140 * Returns the integer value that represents missing (<code>null</code>) for integer compressed floating-point data.
141 * This funtion was named poorly as it sets the <code>ZBLANK</code> value in the header or in the equivalently named
142 * column.
143 *
144 * @return the integer blanking value (for integer-compressed <code>NaN</code>s). If the returned value is
145 * <code>null</code>, then the recommended value -2147483647 will be used as needed.
146 *
147 * @see #setBNull(Integer)
148 */
149 public Integer getBNull() {
150 return nullValueIndicator;
151 }
152
153 /**
154 * Returns the quantization level for integer compressed floating-point data. This funtion was named poorly as it
155 * sets the <code>ZSCALE</code> parameter value in the named column when compressing floating-point data with an
156 * algorithm that supports integers only. It has nothing to do with the <code>BSCALE</code> header value, which
157 * indicates the integer representation of floating-point data for the <i>uncompressed</i> data.
158 *
159 * @return the floating-point difference between integer levels in the quantized data.
160 *
161 * @see #setBScale(double)
162 * @see #getBZero()
163 */
164 public double getBScale() {
165 return bScale;
166 }
167
168 /**
169 * Returns the quantization offset for integer compressed floating-point data. This funtion was named poorly as it
170 * sets the <code>ZZERO</code> parameter value in the named column when compressing floating-point data with an
171 * algorithm that supports integers only. It has nothing to do with the <code>BZERO</code> header value, which
172 * indicates the integer representation of floating-point data for the <i>uncompressed</i> data.
173 *
174 * @return the floating-point value corresponding to the integer level 0.
175 *
176 * @see #setBZero(double)
177 * @see #getBScale()
178 */
179 public double getBZero() {
180 return bZero;
181 }
182
183 @Override
184 public ICompressParameters getCompressionParameters() {
185 if (compressOption == null) {
186 return parameters;
187 }
188 return new BundledParameters(parameters, compressOption.getCompressionParameters());
189 }
190
191 /**
192 * Returns the compression or quantization options, recast for the selected option class.
193 *
194 * @param <T> the generic type of the compression option
195 * @param clazz the option class for the compression algorithm used with the quantization, or
196 * <code>QunatizeOption.class</code> for our own options.
197 *
198 * @return the recast options for the requested class or <code>null</code> id we do not have access to options
199 * of the requested class.
200 *
201 * @see #getCompressOption()
202 */
203 public <T> T getCompressOption(Class<T> clazz) {
204 return unwrap(clazz);
205 }
206
207 /**
208 * Returns the options for the compression algorithm that accompanies quantization.
209 *
210 * @return the options for the compression algorithm, or <code>null</code>
211 *
212 * @see #getCompressOption(Class)
213 */
214 public final ICompressOption getCompressOption() {
215 return compressOption;
216 }
217
218 /**
219 * Returns the maximum integer level in the quantized representation.
220 *
221 * @return the maximum integer level in the quantized data.
222 *
223 * @see #getMaxValue()
224 * @see #getIntMinValue()
225 */
226 public int getIntMaxValue() {
227 return intMaxValue;
228 }
229
230 /**
231 * Returns the maximum integer level in the quantized representation.
232 *
233 * @return the maximum integer level in the quantized data.
234 *
235 * @see #getMinValue()
236 * @see #getIntMinValue()
237 */
238 public int getIntMinValue() {
239 return intMinValue;
240 }
241
242 /**
243 * Returns the maximum floating-point value in the data
244 *
245 * @return the maximum floating-point value in the data before quantization.
246 *
247 * @see #getIntMaxValue()
248 * @see #getMinValue()
249 */
250 public double getMaxValue() {
251 return maxValue;
252 }
253
254 /**
255 * Returns the minimum floating-point value in the data
256 *
257 * @return the minimum floating-point value in the data before quantization.
258 *
259 * @see #getIntMinValue()
260 * @see #getMaxValue()
261 */
262 public double getMinValue() {
263 return minValue;
264 }
265
266 /**
267 * Returns the floating-point value that indicates missing or invalid data in the image before quantization is
268 * applied. Normally, the FITS standard is that NaN values indicate <code>null</code> values in floating-point
269 * images. While this class allows using other values also, they are not recommended since they are not supported by
270 * FITS in a standard way.
271 *
272 * @return the floating-point value that represents a <code>null</code> value (missing data) in the image before
273 * quantization.
274 *
275 * @see #setNullValue(double)
276 * @see #getBNull()
277 *
278 * @deprecated The FITS standard allows only NaNs to indicate missing / invalid floating-point data.
279 */
280 @Deprecated
281 public double getNullValue() {
282 return nullValue;
283 }
284
285 /**
286 * @deprecated use {@link #getBNull()} instead (duplicate method). Returns the integer value that represents
287 * <code>NaN</code> values in integer-compressed floating-point data.
288 *
289 * @return the integer blanking value (<code>null</code> value).
290 *
291 * @see #setBNull(Integer)
292 */
293 @Deprecated
294 public final Integer getNullValueIndicator() {
295 return getBNull();
296 }
297
298 /**
299 * Returns the quantization resolution level used for automatic qunatization. For Gaussian noise the quantization
300 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensure that
301 * quantization retains just about all of the information in the noisy data.
302 *
303 * @return The current Q value, defined as the number of quantized levels per standard deviation (for Gaussian
304 * noise).
305 *
306 * @see #setQlevel(double)
307 * @see #getBScale()
308 */
309 public double getQLevel() {
310 return config.qlevel;
311 }
312
313 /**
314 * Gets the random seed value used for dithering
315 *
316 * @return the random seed value used for dithering
317 *
318 * @see #setSeed(long)
319 * @see RandomSequence
320 */
321 public long getSeed() {
322 return config.seed;
323 }
324
325 /**
326 * Returns the sequential tile index that this option is currently configured for.
327 *
328 * @return the sequential tile index that the quantization is configured for
329 *
330 * @see #setTileIndex(int)
331 */
332 public long getTileIndex() {
333 return tileIndex;
334 }
335
336 /**
337 * Returns the tile height
338 *
339 * @return the tile height in pixels
340 *
341 * @see #setTileHeight(int)
342 * @see #getTileWidth()
343 */
344 @Override
345 public int getTileHeight() {
346 return tileHeight;
347 }
348
349 /**
350 * Returns the tile width
351 *
352 * @return the tile width in pixels
353 *
354 * @see #setTileWidth(int)
355 * @see #getTileHeight()
356 */
357 @Override
358 public int getTileWidth() {
359 return tileWidth;
360 }
361
362 /**
363 * Checks whether we force the integer quantized level 0 to correspond to a floating-point level 0.0, when using
364 * automatic quantization.
365 *
366 * @return <code>true</code> if we want to keep `ZZERO` at 0.0 when quantizing automatically.
367 *
368 * @see #setCenterOnZero(boolean)
369 */
370 public boolean isCenterOnZero() {
371 return config.centerOnZero;
372 }
373
374 /**
375 * Whether the floating-point data may contain <code>null</code> values (normally NaNs).
376 *
377 * @return <code>true</code> (always since 1.23).
378 *
379 * @see #setBNull(Integer)
380 *
381 * @deprecated Use {@link #getBNull()} instead to see if a custom null-value indicator has been configured.
382 */
383 @Deprecated
384 public final boolean isCheckNull() {
385 return true;
386 }
387
388 /**
389 * Whether automatic quantization treats 0.0 as a special value. The special treatment of 0.0 values is the
390 * distinguishing feature of dither method 2 over method 1.
391 *
392 * @deprecated Use {@link #isDither2()} instead. The special treatent of ero values is the distinghuishing feature
393 * of the <code>SUBTRACTIVE_DITHER_2</code> method, which is otherwise the same as
394 * <code>SUBTRACTIVE_DITHER_1</code>.
395 *
396 * @return <code>true</code> to treat 0.0 (exact) as a special value, or <code>false</code> to treat is as any
397 * other measured value (recommended).
398 *
399 * @see #isDither2()
400 * @see #setDither2(boolean)
401 * @see #getBScale()
402 */
403 @Deprecated
404 public boolean isCheckZero() {
405 return config.checkZero;
406 }
407
408 /**
409 * Whether dithering is enabled
410 *
411 * @return <code>true</code> if dithering is enabled, or else <code>false</code>
412 *
413 * @see #setDither(boolean)
414 * @see #isDither2()
415 */
416 public boolean isDither() {
417 return config.dither;
418 }
419
420 /**
421 * Whether dithering (when enabled) uses method 2, which treats 0.0 values as special.
422 *
423 * @return <code>true</code> if method 2 is used is used for dithering, or else <code>false</code>
424 *
425 * @see #setDither2(boolean)
426 * @see #isDither()
427 */
428 public boolean isDither2() {
429 return config.checkZero;
430 }
431
432 @Override
433 public boolean isLossyCompression() {
434 return true;
435 }
436
437 /**
438 * Sets the integer value that represents missing data (<code>null</code>) for integer compressed floating-point
439 * data. This funtion was named poorly as it sets the <code>ZBLANK</code> value in the header or in the equivalently
440 * named column.
441 *
442 * @param blank the new integer value that denotes <code>NaN</code> when floating-point data is compressed with an
443 * integer-only algorithm. Setting this option to <code>null</code> will set the header
444 * <code>ZBLANK</code> value, when the data contains NaNs, to -2147483647 (i.e., the value
445 * recommended by the FITS standard).
446 *
447 * @return itself
448 *
449 * @see #getBNull()
450 */
451 public QuantizeOption setBNull(Integer blank) {
452 nullValueIndicator = blank;
453 return this;
454 }
455
456 /**
457 * Sets the quantization level for integer compressed floating-point data. This funtion was named poorly as it sets
458 * the <code>ZZERO</code> parameter value in the named column when compressing floating-point data with an algorithm
459 * that supports integers only. It has nothing to do with the <code>BZERO</code> header value, which indicates the
460 * integer representation of floating-point data for the <i>uncompressed</i> data.
461 *
462 * @param value the new floating-point difference between integer levels in the quantized data.
463 *
464 * @return itself
465 *
466 * @see #setQlevel(double)
467 * @see #setBZero(double)
468 * @see #getBScale()
469 */
470 public QuantizeOption setBScale(double value) {
471 bScale = value;
472 return this;
473 }
474
475 /**
476 * Sets the quantization offset for integer compressed floating-point data. This funtion was named poorly as it sets
477 * the <code>ZZERO</code> parameter value in the named column when compressing floating-point data with an algorithm
478 * that supports integers only. It has nothing to do with the <code>BZERO</code> header value, which indicates the
479 * integer representation of floating-point data for the <i>uncompressed</i> data.
480 *
481 * @param value the new floating-point value corresponding to the integer level 0.
482 *
483 * @return itself
484 *
485 * @see #setBScale(double)
486 * @see #getBZero()
487 */
488 public QuantizeOption setBZero(double value) {
489 bZero = value;
490 return this;
491 }
492
493 /**
494 * Enabled or disables keeping `ZZERO` at 0 when using automatic quantization.
495 *
496 * @param value <code>true</code> to keep `ZZERO` at 0 when quantizing automatically, that is keep the integer
497 * quantized level 0 correspond to floating-point level 0.0. Or, <code>false</code> to let the
498 * automatic quantization algorithm determine the optimal quantization offset.
499 *
500 * @return iftself
501 *
502 * @see #isCenterOnZero()
503 */
504 public QuantizeOption setCenterOnZero(boolean value) {
505 config.centerOnZero = value;
506 return this;
507 }
508
509 /**
510 * Obsolete method that used to set whether we should expect the floating-point data to contain <code>null</code>
511 * values (NaNs).
512 *
513 * @deprecated This feature is set automatically as needed.
514 *
515 * @param value (unused since 1.23)
516 *
517 * @return itself
518 *
519 * @see #setBNull(Integer)
520 */
521 @Deprecated
522 public QuantizeOption setCheckNull(boolean value) {
523 return this;
524 }
525
526 /**
527 * Sets whether automatic quantization is to treat 0.0 as a special value. This is the same as
528 * {@link #setDither2(boolean)}. When enabled and dithering is used, then 0.0 values will be denoted with the
529 * special value −2147483647 in the quantized representation.
530 *
531 * @deprecated Use {@link #setDither2(boolean)} instead if you want zero values to be special encoded. The
532 * representation of true zero values is the unique feature of the
533 * <code>SUBTRACTIVE_DITHER_2</code> method that sets it apart from
534 * <code>SUBTRACTIVE_DITHER_1</code>.
535 *
536 * @param value (unused) value whether to treat values around 0.0 as special.
537 *
538 * @return itself
539 *
540 * @see #setDither2(boolean)
541 * @see #isDither2()
542 */
543 @Deprecated
544 public QuantizeOption setCheckZero(boolean value) {
545 return setDither2(value);
546 }
547
548 /**
549 * Enables or disables dithering.
550 *
551 * @param value <code>true</code> to enable dithering, or else <code>false</code> to disable
552 *
553 * @return itself
554 *
555 * @see #isDither()
556 * @see #setDither2(boolean)
557 */
558 public QuantizeOption setDither(boolean value) {
559 config.dither = value;
560 return this;
561 }
562
563 /**
564 * Sets whether dithering is to use method 2, when dithering is enabled. It does not actually enable or disable
565 * dithering itself -- for that you must call {@link #setDither(boolean)}. When dither method 2 is used, then 0.0
566 * values will be denoted with the special value −2147483647 in the quantized representation, whereas dither method
567 * 1 treats 0.0 just like any other decomal value.
568 *
569 * @param value <code>true</code> to use dither method 2, or else <code>false</code> for method 1.
570 *
571 * @return itself
572 *
573 * @see #isDither2()
574 * @see #setDither(boolean)
575 */
576 public QuantizeOption setDither2(boolean value) {
577 config.checkZero = value;
578 return this;
579 }
580
581 /**
582 * Sets the maximum integer level in the quantized representation.
583 *
584 * @param value the new maximum integer level in the quantized data.
585 *
586 * @return itself
587 *
588 * @see #getIntMaxValue()
589 * @see #setIntMinValue(int)
590 */
591 public QuantizeOption setIntMaxValue(int value) {
592 intMaxValue = value;
593 return this;
594 }
595
596 /**
597 * Sets the minimum integer level in the quantized representation.
598 *
599 * @param value the new minimum integer level in the quantized data.
600 *
601 * @return itself
602 *
603 * @see #getIntMinValue()
604 * @see #setIntMaxValue(int)
605 */
606 public QuantizeOption setIntMinValue(int value) {
607 intMinValue = value;
608 return this;
609 }
610
611 /**
612 * Sets the maximum floating-point value in the data
613 *
614 * @param value the maximum floating-point value in the data before quantization.
615 *
616 * @return itself
617 *
618 * @see #getMaxValue()
619 * @see #setMinValue(double)
620 */
621 public QuantizeOption setMaxValue(double value) {
622 maxValue = value;
623 return this;
624 }
625
626 /**
627 * Sets the minimum floating-point value in the data
628 *
629 * @param value the mininum floating-point value in the data before quantization.
630 *
631 * @return itself
632 *
633 * @see #getMinValue()
634 * @see #setMaxValue(double)
635 */
636 public QuantizeOption setMinValue(double value) {
637 minValue = value;
638 return this;
639 }
640
641 /**
642 * Sets the floating-point value that indicates missing data in the floating point image image before quantization
643 * is applied. Normally, the FITS standard is that NaN values indicate <code>null</code> values in floating-point
644 * images. While this class allows using other values also, they are not recommended since they are not supported by
645 * FITS in a standard way.
646 *
647 * @param value the new floating-point value that represents a <code>null</code> value (missing data) in the
648 * image before quantization.
649 *
650 * @return itself
651 *
652 * @see #getNullValue()
653 * @see #setBNull(Integer)
654 *
655 * @deprecated The use of null values other than <code>NaN</code> for floating-point data types is not
656 * standard in FITS. You should therefore avoid using this method, in general.
657 */
658 @Deprecated
659 public QuantizeOption setNullValue(double value) {
660 nullValue = value;
661 return this;
662 }
663
664 @Override
665 public void setParameters(ICompressParameters parameters) {
666 if (parameters instanceof QuantizeParameters) {
667 this.parameters = (QuantizeParameters) parameters.copy(this);
668 } else if (parameters instanceof BundledParameters) {
669 BundledParameters bundle = (BundledParameters) parameters;
670 for (int i = 0; i < bundle.size(); i++) {
671 setParameters(bundle.get(i));
672 }
673 } else if (compressOption != null) {
674 compressOption.setParameters(parameters);
675 }
676 }
677
678 /**
679 * Sets the quantization resolution level to use for automatic quantization. For Gaussian noise the quantization
680 * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensusre that
681 * quantization retains just about all of the information contained in the noisy data.
682 *
683 * @param value The new Q value, defined as the number of quantized levels per standard deviation (for Gaussian
684 * noise).
685 *
686 * @return itself
687 *
688 * @see #getQLevel()
689 * @see #setBScale(double)
690 */
691 public QuantizeOption setQlevel(double value) {
692 config.qlevel = value;
693 return this;
694 }
695
696 /**
697 * Sets the seed value for the dither random generator
698 *
699 * @param value The seed value, as in <code>ZDITHER0</code>, normally a number between 1 and 10000 (inclusive).
700 *
701 * @return itself
702 *
703 * @see #setTileIndex(int)
704 */
705 public QuantizeOption setSeed(long value) {
706 config.seed = value;
707 return this;
708 }
709
710 /**
711 * Sets the tile index for which to initialize the random number generator with the given seed (i.e.
712 * <code>ZDITHER0</code> value).
713 *
714 * @param index The 0-based tile index
715 *
716 * @return itself
717 *
718 * @see #setSeed(long)
719 */
720 public QuantizeOption setTileIndex(int index) {
721 tileIndex = index;
722 return this;
723 }
724
725 @Override
726 public QuantizeOption setTileHeight(int value) {
727 tileHeight = value;
728 if (compressOption != null) {
729 compressOption.setTileHeight(value);
730 }
731 return this;
732 }
733
734 @Override
735 public QuantizeOption setTileWidth(int value) {
736 tileWidth = value;
737 if (compressOption != null) {
738 compressOption.setTileWidth(value);
739 }
740 return this;
741 }
742
743 @Override
744 public <T> T unwrap(Class<T> clazz) {
745 if (clazz.isAssignableFrom(this.getClass())) {
746 return clazz.cast(this);
747 }
748 if (compressOption != null) {
749 if (clazz.isAssignableFrom(compressOption.getClass())) {
750 return clazz.cast(compressOption);
751 }
752 }
753 return null;
754 }
755
756 /**
757 * Re-initialize the dither sequence.
758 */
759 void initDither() {
760 if (isDither() || isDither2()) {
761 iseed = (int) ((getSeed() + tileIndex - 1) % RandomSequence.length());
762 initI1();
763 }
764 }
765
766 private void initI1() {
767 nextRandom = (int) (RandomSequence.get(iseed) * RANDOM_MULTIPLICATOR);
768 }
769
770 private double nextDither() {
771 double d = RandomSequence.get(nextRandom) - DITHER_HALF;
772 nextRandom++;
773
774 if (nextRandom >= RandomSequence.length()) {
775 iseed = (iseed + 1) % RandomSequence.length();
776 initI1();
777 }
778
779 return d;
780 }
781
782 boolean isRegular(double x) {
783 if (!Double.isFinite(x)) {
784 return false;
785 }
786 if (x == nullValue) {
787 return false;
788 }
789 if (isCheckZero() && x == 0.0) {
790 return false;
791 }
792 return true;
793 }
794
795 /**
796 * Converts a floating point value to a quantized integer
797 *
798 * @param d a floating point value
799 *
800 * @return the equivalent quantized integer representation
801 *
802 * @since 1.23
803 */
804 int toInt(double d) {
805 if (Double.isNaN(d) || d == nullValue) {
806 if (nullValueIndicator == null) {
807 nullValueIndicator = RECOMMENDED_NAN_INDICATOR;
808 }
809 return nullValueIndicator;
810 }
811
812 if (isDither() && isDither2() && d == 0.0) {
813 return DITHER2_ZERO_INDICATOR;
814 }
815
816 d -= bZero;
817 d /= bScale;
818 if (isDither()) {
819 d += nextDither();
820 }
821 return (int) Math.round(d);
822 }
823
824 /**
825 * Converts a quantized integer value back to it's floating-point equivalent
826 *
827 * @param i a quantized integer value
828 *
829 * @return the equivalent floating point value
830 *
831 * @since 1.23
832 */
833 double toDouble(int i) {
834 if (isDither() && isDither2() && i == DITHER2_ZERO_INDICATOR) {
835 return 0.0;
836 }
837
838 if (nullValueIndicator != null && i == nullValueIndicator) {
839 return nullValue;
840 }
841
842 double d = i;
843 if (isDither()) {
844 d -= nextDither();
845 }
846
847 return useFMA ? Math.fma(d, bScale, bZero) : d * bScale + bZero;
848 }
849
850 void updateBZeroAndIntLimits() {
851 setBZero(findBZero());
852 setIntMinValue((int) Math.floor((minValue - bZero) / bScale));
853 setIntMaxValue((int) Math.ceil((maxValue - bZero) / bScale));
854 }
855
856 double findBZero() {
857 if (isCenterOnZero()) {
858 // Force ZZERO to be 0.0, as requested
859 return 0.0;
860 }
861
862 // return all positive values, if possible since some compression
863 // algorithms are more efficient that way.
864 if (Math.ceil((maxValue - minValue) / bScale) < Integer.MAX_VALUE) {
865 // fudge the zero point so it is an integer multiple of bScale
866 // This helps to ensure the same scaling will be performed if
867 // the file undergoes multiple fpack/funpack cycles
868 // AK: round to multiple of bScale.
869 double rem = Math.IEEEremainder(minValue, bScale);
870 if (rem < 0.0) {
871 rem += bScale;
872 }
873 return minValue - rem;
874 }
875
876 // center the quantized levels around zero
877 return (minValue + maxValue) / 2.;
878 }
879
880 /**
881 * Selects whether {@link Math#fma(double, double, double)} should be used when converting quantized integers back
882 * to doubles. Othwerwise normal arithmetic is used, which is the default. CFITSIO and astropy both rely on
883 * <code>fma()</code>, which has better precision, but is not supported on some (older) architectures. When hardware
884 * support is lacking, you may expect a significant performance hit from the software implementation.
885 *
886 * @param value <code>true</code> to use <code>fma()</code>, or else <code>false</code> to use regular arithmetics.
887 *
888 * @see #isUseFMA()
889 *
890 * @since 1.23
891 */
892 public static void useFMA(boolean value) {
893 useFMA = value;
894 }
895
896 /**
897 * Checks whether {@link Math#fma(double, double, double)} is used for converting quantized integers back to
898 * doubles.
899 *
900 * @return <code>true</code> if using <code>fma()</code>, or else <code>false</code> is using regular arithmetics.
901 *
902 * @since 1.23
903 */
904 public static final boolean isUseFMA() {
905 return useFMA;
906 }
907
908 /**
909 * Stores configuration in a way that can be shared and modified across enclosing option copies.
910 *
911 * @author Attila Kovacs
912 *
913 * @since 1.18
914 */
915 private static final class Config {
916
917 private boolean centerOnZero;
918
919 private boolean dither;
920
921 private boolean checkZero;
922
923 private double qlevel = 4.0;
924
925 private long seed = 1L;
926 }
927 }