View Javadoc
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 }