Skip to content

Incompatible results from WCS.pixel_to_world between astropy 3.x and 4.x (distortion in WCSLIB 6.3 + Cutout2D) #11216

Closed

Description

@JulienPeloton

EDIT: The cause identified in #11216 (comment)

Description

The use of WCS.pixel_to_world gives different results when using astropy 3.x or 4.x (same environment).

Expected behavior

The two astropy versions should give the same results, or expected incompatibilities should be quoted explicitly in the documentation.

Actual behavior

The two astropy versions give different results, and I could not find a documentation on it.

Steps to Reproduce

The common.fits can be found here (37 KB).

from astropy.io import fits
from astropy.wcs import WCS

fn = "common.fits"

header = fits.open(fn)[0].header
w = WCS(header)

print(w.pixel_to_world([[0, 0]], 0))
# astropy 3.2.2: 
# <SkyCoord (FK5: equinox=2000.0): (ra, dec) in deg
#   [[(250.51824533, -75.47419369), (250.51824533, -75.47419369)]]>
# astropy 4.0.1.post1:
# <SkyCoord (FK5: equinox=2000.0): (ra, dec) in deg
#   [[(251.99786293, -75.83653121), (251.99786293, -75.83653121)]]>

I am not sure what's happening here. I have the impression the information from the header is used differently in the two astropy versions, but I cannot find what has changed explicitly in the documentation.

System Details

Darwin-20.2.0-x86_64-i386-64bit
Python 3.7.1 (default, Oct 23 2018, 14:07:42)
[Clang 4.0.1 (tags/RELEASE_401/final)]
Numpy 1.16.2
astropy 3.2.2
Scipy 1.4.1
Matplotlib 3.0.1
Darwin-20.2.0-x86_64-i386-64bit
Python 3.7.1 (default, Oct 23 2018, 14:07:42)
[Clang 4.0.1 (tags/RELEASE_401/final)]
Numpy 1.16.2
astropy 4.0.1.post1
Scipy 1.4.1
Matplotlib 3.0.1

Checked also using the official docker images (https://hub.docker.com/u/continuumio/): 2019.10 (astropy 3.2.2), 2020.02 (astropy 4.0), 2020.07 (astropy 4.0.1.post1), 2020.11 (astropy 4.0.2).

Activity

  1. pllim commented on Jan 6, 2021

    @pllim
    Member

    Is it possible for you to share the WCS header info here without requiring people to each download that FITS file? Thanks!

  2. JulienPeloton commented on Jan 6, 2021

    @JulienPeloton
    Author

    Thanks - which information in particular do you have in mind? here is the header:

    SIMPLE  =                    T / conforms to FITS standard
    BITPIX  =                   16 / array data type
    NAXIS   =                    2 / number of array dimensions
    NAXIS1  =                  100
    NAXIS2  =                  150
    WCSAXES =                    2 / Number of coordinate axes
    CRPIX1  =      737.32251317135 / Pixel coordinate of reference point
    CRPIX2  =       807.0426298396 / Pixel coordinate of reference point
    PC1_1   =    0.025278924159706 / Coordinate transformation matrix element
    PC1_2   =  0.00037445145190975 / Coordinate transformation matrix element
    PC2_1   = -0.00037626636323055 / Coordinate transformation matrix element
    PC2_2   =    0.025284403492745 / Coordinate transformation matrix element
    CDELT1  =    -0.01867555394435 / [deg] Coordinate increment at reference point
    CDELT2  =     0.01867555394435 / [deg] Coordinate increment at reference point
    CUNIT1  = 'deg'                / Units of coordinate increment and value
    CUNIT2  = 'deg'                / Units of coordinate increment and value
    CTYPE1  = 'RA---TAN'           / Right ascension, gnomonic projection
    CTYPE2  = 'DEC--TAN'           / Declination, gnomonic projection
    CRVAL1  =      249.10970770833 / [deg] Coordinate value at reference point
    CRVAL2  =     -75.102948144444 / [deg] Coordinate value at reference point
    LONPOLE =                180.0 / [deg] Native longitude of celestial pole
    LATPOLE =     -75.102948144444 / [deg] Native latitude of celestial pole
    WCSNAME = 'DSS PLATEID 0194'   / Coordinate system title
    RADESYS = 'FK5'                / Equatorial coordinate system
    EQUINOX =               2000.0 / [yr] Equinox of equatorial coordinates
    MJD-OBS =              42867.0 / [d] MJD of observation matching DATE-OBS
    DATE-OBS= '1976-03-30'         / ISO-8601 observation date matching MJD-OBS
    CQDIS1  = 'TPD'                / Q = sequent, Template Polynomial Distortion
    DQ1     = 'NAXES:  2'          / Two independent variables
    DQ1     = 'AXIS.1: 1'          / 1st independent variable: axis 1 (= u)
    DQ1     = 'AXIS.2: 2'          / 2nd independent variable: axis 2 (= v)
    DQ1     = 'AUX.1.COEFF.0:   -0.030369554325293' / TPD: x = c0 + c1*u + c2*v
    DQ1     = 'AUX.1.COEFF.1:    -0.99999816063804' / TPD: x = c0 + c1*u + c2*v
    DQ1     = 'AUX.1.COEFF.2:    0.014809554960054' / TPD: x = c0 + c1*u + c2*v
    DQ1     = 'AUX.2.COEFF.0:   0.0013505128079274' / TPD: y = d0 + d1*u + d2*v
    DQ1     = 'AUX.2.COEFF.1:    0.014881334702971' / TPD: y = d0 + d1*u + d2*v
    DQ1     = 'AUX.2.COEFF.2:     0.99978145301581' / TPD: y = d0 + d1*u + d2*v
    DQ1     = 'TPD.FWD.0:    -0.030382917644437' / TPD coefficient: 1
    DQ1     = 'TPD.FWD.1:     -0.99978145301581' / TPD coefficient: x
    DQ1     = 'TPD.FWD.2:     0.014809554960054' / TPD coefficient: y
    DQ1     = 'TPD.FWD.4:   -3.321209956147E-08' / TPD coefficient: xx
    DQ1     = 'TPD.FWD.5:   2.1215435903186E-07' / TPD coefficient: xy
    DQ1     = 'TPD.FWD.6:   2.2014764207626E-07' / TPD coefficient: yy
    DQ1     = 'TPD.FWD.7:  -3.3806674482692E-08' / TPD coefficient: xxx
    DQ1     = 'TPD.FWD.8:   9.4293173357228E-10' / TPD coefficient: xxy
    DQ1     = 'TPD.FWD.9:    -3.54701586968E-08' / TPD coefficient: xyy
    DQ1     = 'TPD.FWD.10:  1.2163819157713E-09' / TPD coefficient: yyy
    CQDIS2  = 'TPD'                / Q = sequent, Template Polynomial Distortion
    DQ2     = 'NAXES:  2'          / Two independent variables
    DQ2     = 'AXIS.1: 1'          / 1st independent variable: axis 1 (= u)
    DQ2     = 'AXIS.2: 2'          / 2nd independent variable: axis 2 (= v)
    DQ2     = 'AUX.1.COEFF.0:   0.0013505128079274' / TPD: x = c0 + c1*u + c2*v
    DQ2     = 'AUX.1.COEFF.1:    0.014881334702971' / TPD: x = c0 + c1*u + c2*v
    DQ2     = 'AUX.1.COEFF.2:     0.99978145301581' / TPD: x = c0 + c1*u + c2*v
    DQ2     = 'AUX.2.COEFF.0:   -0.030369554325293' / TPD: y = d0 + d1*u + d2*v
    DQ2     = 'AUX.2.COEFF.1:    -0.99999816063804' / TPD: y = d0 + d1*u + d2*v
    DQ2     = 'AUX.2.COEFF.2:    0.014809554960054' / TPD: y = d0 + d1*u + d2*v
    DQ2     = 'TPD.FWD.0:  -0.00089857082115081' / TPD coefficient: 1
    DQ2     = 'TPD.FWD.1:      0.99999816063804' / TPD coefficient: x
    DQ2     = 'TPD.FWD.2:     0.014881334702971' / TPD coefficient: y
    DQ2     = 'TPD.FWD.4:  -3.2511261465127E-07' / TPD coefficient: xx
    DQ2     = 'TPD.FWD.5:   1.5287025105602E-07' / TPD coefficient: xy
    DQ2     = 'TPD.FWD.6:  -2.2662497174755E-07' / TPD coefficient: yy
    DQ2     = 'TPD.FWD.7:   3.5215497254629E-08' / TPD coefficient: xxx
    DQ2     = 'TPD.FWD.8:   1.8773137889193E-09' / TPD coefficient: xxy
    DQ2     = 'TPD.FWD.9:     3.35202384015E-08' / TPD coefficient: xyy
    DQ2     = 'TPD.FWD.10: -2.5608500457886E-09' / TPD coefficient: yyy
    
  3. pllim commented on Jan 6, 2021

    @pllim
    Member

    At the very least, w.to_header() might give a clue. (Thanks for the info!)

    Also, what about the dev version of astropy? 💭

  4. pllim commented on Jan 6, 2021

    @pllim
    Member

    Also, do you see any warnings at all? Does wcslint complain?

  5. JulienPeloton commented on Jan 6, 2021

    @JulienPeloton
    Author

    I do not see any warning at all (in all astropy versions tested: 3.2.2, 4.0, 4.0.1.post1, 4.2). I did not try the dev version though. I will have a look with the latest one (v4.3.dev).

  6. pllim commented on Jan 6, 2021

    @pllim
    Member

    Another thing you can do is try git bisect, since you know roughly where was the last good commit and where it broke bad. Just remember to only bisect merge commits, so you don't pick up the individual commits inside the same PR.

  7. pllim commented on Jan 6, 2021

    @pllim
    Member

    I don't know if such inconsistency can come from all the way down in wcslib (which is external but bundled, which we have bumped since). Hopefully a bisect would tell you.

  8. JulienPeloton commented on Jan 6, 2021

    @JulienPeloton
    Author

    OK, I will have a look to the code history.

    The dev version 4.3 of astropy gives the same difference with respect to 3.x.

  9. JulienPeloton commented on Jan 6, 2021

    @JulienPeloton
    Author

    But while I'm searching - I wonder why such a widely used thing (pixel_to_world) was not spot before?
    I am wondering if this is not something specific with the header I have. Unfortunately, I am not an expert to tell what could be wrong :-(

  10. pllim commented on Jan 6, 2021

    @pllim
    Member

    Yes, could be specific to something in your header. I cannot reproduce such inconsistency using a drizzled HST/ACS image I obtained from MAST archive.

    >>> import astropy
    >>> from astropy.io import fits
    >>> from astropy.wcs import WCS
    >>> filename = 'jbt7a3020_drz.fits'
    >>> header = fits.getheader(filename, ext=1)
    >>> w = WCS(header)
    >>> astropy.__version__
    '4.3.dev354+g0d9a91698'
    >>> w.pixel_to_world([[0, 0]], 0)
    <SkyCoord (ICRS): (ra, dec) in deg
        [[(169.00258197, 1.5236589), (169.00258197, 1.5236589)]]>
    >>> astropy.__version__
    '3.2.2'
    >>> w.pixel_to_world([[0, 0]], 0)
    <SkyCoord (ICRS): (ra, dec) in deg
        [[(169.00258197, 1.5236589), (169.00258197, 1.5236589)]]>

    Not sure why it would make a difference, but I notice that my image has w.wcs.radesys set to 'ICRS' and not 'FK5' like yours. Who made "common.fits" and how?

  11. dhomeier commented on Jan 6, 2021

    @dhomeier
    Contributor

    I note that the distortion-free parts of the WCS (using the header without the CQDIS/DQ cards) return identical results:
    3.2.2

    >>> w = wcs.WCS(fits.Header.fromstring(hdr[:1802], sep='\n'))
    >>> wd = wcs.WCS(fits.Header.fromstring(hdr, sep='\n'))
    >>> w.pixel_to_world(0, 0)
    <SkyCoord (FK5: equinox=2000.0): (ra, dec) in deg
        (250.51820607, -75.47417884)>
    >>> wd.pixel_to_world(0, 0).separation(w.pixel_to_world(0, 0))
    <Angle 1.78130314e-05 deg>

    4.2

    >>> w = wcs.WCS(fits.Header.fromstring(hdr[:1802], sep='\n'))
    >>> wd = wcs.WCS(fits.Header.fromstring(hdr, sep='\n'))
    >>> w.pixel_to_world(0, 0)
    <SkyCoord (FK5: equinox=2000.0): (ra, dec) in deg
        (250.51820607, -75.47417884)>
    >>> wd.pixel_to_world(0, 0).separation(w.pixel_to_world(0, 0))
    <Angle 0.51542227 deg>
    >>> w.pixel_to_world(736.32251317135, 806.0426298396).separation(w.pixel_to_world(0, 0))
    <Angle 0.51548823 deg>
    >>> wd.pixel_to_world(736.32251317135, 806.0426298396).separation(wd.pixel_to_world(0, 0))
    <Angle 1.0309105 deg>

    IOW in the 4.x version the distortion seems to offset the point by almost exactly the full distance to CRPIX1, CRPIX2 again (instead of ~64 mas, blowing up the field by a factor of 2)! Unless this is an almost pathologically distorted WCS (does not look like it, with diagonal coefficients close to 1.0 AFAICS), this looks like a serious bug.

  12. 48 remaining items

  13. mcara commented on Jan 7, 2021

    @mcara
    Contributor

    Okay... so just to get things straight, WCSLIB 6.3 turns out not to be the true cause, but rather it is a known issue with distortion in Cutout2D that manifests more obviously in WCSLIB 6.3 or later?

    I do not think it is an issue at all with Cutout2D: wcs in this case almost passes through unmodified. I cannot even reproduce the WCS from #11216 (comment) - the one with DQ? coefficients. @pllim Try to run that example, please.

    In own experiment above I am getting the same header from the WCS before and after the call to Cutout2D. It just does not affect WCS at all.

    In [76]: astropy.__version__
    Out[76]: '4.2'
  14. mcara commented on Jan 8, 2021

    @mcara
    Contributor

    And another thing: the warning below refers to not supporting distortion lookup table distortion.

    Ah actually, this is expected -- I just see a huge warning in the doc for nddata.Cutout2D...
    https://docs.astropy.org/en/stable/api/astropy.nddata.Cutout2D.html

    Screenshot 2021-01-07 at 23 23 45

    My bad then... It seems this case is understood.

    This is different from TPD and maybe other distortions. For example, if distortions are described with SIP, then making a cutout requires recomputing coefficients of polynomials OR shifting the center of the coordinate system (CRPIX) which is what Cutout2D actually does. Shifting CRPIX invalidates existing distortion lookup table distortion hence it should be removed after making a cutout (because we do not know how to recompute an arbitrary lookup table). For the same reason, supporting distortions other than other than SIP is not trivial: you need to know how to recompute distortions after making a cutout [and probably there are all kind of models out there, possibly some even non-linear].

    Another difficulty is that SIP is handled by astropy.wcs while all other distortions are handled by WCSLIB and we do not have access to those distortion models (their coefficients). That's just to give an idea of the work involved...

    ... unless, we make the assumption that shifting CRPIX has no negative side-effects.

  15. pllim commented on Jan 8, 2021

    @pllim
    Member

    Thanks for all the details, @mcara !

    So, should we issue a recommendation in Cutout2D to correct for the distortion first before applying cutout? Or perhaps even have Cutout2D throw error if someone attempts to cut out a distorted image?

    I feel like we won't be the only one confused by such behavior in the future.

  16. mcara commented on Jan 8, 2021

    @mcara
    Contributor

    Or perhaps even have Cutout2D throw error if someone attempts to cut out a distorted image?

    The thing is we do not know when an image has distortions other than the ones supported in astropy.wcs itself: SIP and distortion lookup table. Take a look at the original header here - the one containing AMD* and PP* coefficients in #11216 (comment). CTYPE does not contain any indication that distortion is present (for SIP it ends in --SIP).

    So, should we issue a recommendation in Cutout2D to correct for the distortion first before applying cutout?

    No, I do not think this should be recommended. Many users want to work on distorted images (i.e., FLT). I think that warning about distortion lookup table should be expanded to include any other distortions (other than SIP). Or to say that the WCS of the cutout is updated correctly only when it contains no distortions or at most SIP distortions.

  17. JulienPeloton commented on Jan 8, 2021

    @JulienPeloton
    Author

    I do not think it is an issue at all with Cutout2D: wcs in this case almost passes through unmodified. I cannot even reproduce the WCS from #11216 (comment) - the one with DQ? coefficients.

    @mcara The problem appears only when you change astropy version after creating the cutout (which is what happened in my case). Try to run this example to see the mismatch:

    create a cutout with astropy 3.x

    from astropy.io import fits
    from astropy.wcs import WCS
    from astropy.nddata import Cutout2D
    
    import astropy
    print(astropy.__version__)
    # 3.2.2
    
    fn = 'dss.16.54.38.6-76.08.22.5.fits'
    
    header = fits.open(fn)[0].header
    data = fits.open(fn)[0].data
    w = WCS(header)
    
    cutout = Cutout2D(data[:10, :10], position=(5, 5), size=10, wcs=w)
    
    print(w.pixel_to_world([[0, 0]], 0))
    # (258.50340774, -76.99981288)
    
    print(cutout.wcs.pixel_to_world([[0, 0]], 0))
    # (258.50340774, -76.99981288)
    
    fits.writeto('mycutout.fits', cutout.data, cutout.wcs.to_header())

    Both original and cutout images give the same results.

    read this cutout with astropy 4.x

    from astropy.io import fits
    from astropy.wcs import WCS
    
    import astropy
    print(astropy.__version__)
    # 4.2
    
    fn = 'dss.16.54.38.6-76.08.22.5.fits'
    
    header = fits.open(fn)[0].header
    data = fits.open(fn)[0].data
    w = WCS(header)
    
    print(w.pixel_to_world([[0, 0]], 0))
    # (258.50340774, -76.99981288)
    
    fn = 'mycutout.fits'
    
    header = fits.open(fn)[0].header
    data = fits.open(fn)[0].data
    w = WCS(header)
    
    print(w.pixel_to_world([[0, 0]], 0))
    # (270.52329931, -78.44831125)

    You can see that in that case pixel_to_world gives different answers. The original image gives consistent pixel_to_world results compared to astropy 3.x, but not the cutout.

  18. dhomeier commented on Jan 8, 2021

    @dhomeier
    Contributor

    You can see that in that case pixel_to_world gives different answers. The original image gives consistent pixel_to_world results compared to astropy 3.x, but not the cutout.

    Yes, that is expected, since the WCS for the original image is constructed from the AMD[XY] coefficients in the DSS header, but after reading in WCSLIB creates a TPD distortion model. The format this is written by WCSLIB 6.2 is incompatible with 6.3 and later due to the changed defaults for DOCORR.
    For now you can fix this manually by modifying the old files with

    header.insert('CQDIS1', ('DQ1', 'DOCORR:  0'), after=True)
    header.insert('CQDIS2', ('DQ2', 'DOCORR:  0'), after=True)
    

    Creating and reading Cutout2D consistently with newer Astropy versions has its issues as well, though:
    In 4.2 (I assume 4.0.x as well) the actual distortion coefficients are never written back to FITS (at least not the DQ coefficients for a sequent distortion such as here). You can see this in @mcara's example above, where the
    DQ1 = 'DOCORR: 1'
    is correctly set in the header, but meaningless, since nothing else except for the NAXES/AXIS[12] cards is saved.

    So I was actually surprised that your cutouts did include the distortion, but in Astropy 3.2 w.to_header() indeed includes the full distortion coefficients AUX.[12].COEFF.* and TPD.FWD.*.

    With 4.2 on the contrary

    >>> dss = fits.open('dss.16.42.04-75.28.27.fits')
    >>> w = wcs.WCS(dss[0].header)
    [VerifyWarnings]
    WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 42867.000000 from DATE-OBS.
    Changed DATE-OBS from '30/03/76          ' to '1976-03-30''. [astropy.wcs.wcs]
    >>> cutout = Cutout2D(dss[0].data[:10, :10], position=(5, 5), size=10, wcs=w)
    >>> hdulist = cutout.wcs.to_fits()
    >>> hdulist[0].data = cutout.data
    >>> hdulist.writeto('cutout.fits')
    >>> w2 = wcs.WCS(fits.open('cutout.fits')[0].header)
    >>> cutout.wcs.pixel_to_world([[0, 0]], 0).to_string('dms')
    array([['250d51m43.4114s -75d33m13.7085s',
            '250d51m43.4114s -75d33m13.7085s']], dtype='<U31')
    >>> w.pixel_to_world([[0, 0]], 0).to_string('dms')
    array([['250d51m43.4114s -75d33m13.7085s',
            '250d51m43.4114s -75d33m13.7085s']], dtype='<U31')
    >>> w2.pixel_to_world([[0, 0]], 0).to_string('dms')
    array([['250d51m43.1446s -75d33m13.6168s',
            '250d51m43.1446s -75d33m13.6168s']], dtype='<U31')
    

    – using to_fits() here since the to_header docs actually state that

    This function does not write out FITS WCS distortion paper_
    information, since that requires multiple FITS
    header data units. To get a full representation of
    everything in this object, use to_fits.

    I cannot really follow that argument since all WCS coefficients are stored in the same ImageHDU header, but to_fits, which is calling _write_distortion_kw() internally, on 4.x creates the same incomplete header. _write_distortion_kw apparently only considers prior distortion models DP (if at all), ignoring all DQ coefficients.

    So the WCS from the cutout, when read back in, only includes the plain image scale and rotation without any distortions (the differences in this case however are small, as expected).
    That seems to be an internal change in WCSLIB, as creating a header from the same files with wcsware -o produces the same incomplete TPD section now, but I could not identify how, where or when this has changed in wcshdo.

  19. dhomeier commented on Jan 8, 2021

    @dhomeier
    Contributor

    What is different is that with 6.2, WCS.wcs had the following

    iparm[0][]:            1            8           18            0           12
                            0            1            0
    

    compared to 7.3.1

    iparm[0][]:            1            7           18           12            0
                            1            0
    

    seemingly due to the change in

    astropy/cextern/wcslib/C/dis.c

    Lines 1918 to 1923 in 4b242e6

    /* These iparm indices are specific to TPD. */
    #define I_TPDNCO 3 /* No. of TPD coefficients, forward... */
    #define I_TPDINV 4 /* ...and inverse. */
    #define I_TPDAUX 5 /* True if auxiliary variables are used. */
    #define I_TPDRAD 6 /* True if the radial variable is used. */
    #define I_NTPD 7

    when iparm[I_DOCORR] was removed from dis.c; however this should be the same iparm array that is passed to wcshdo, which still has the codes
    #define I_DTYPE 0 /* Distortion type code. */
    #define I_NIPARM 1 /* Full (allocated) length of iparm[]. */
    #define I_NDPARM 2 /* No. of parameters in dparm[], excl. work space. */
    #define I_DOCORR 3 /* True if distortion func computes a correction. */
    #define I_TPDNCO 4 /* No. of TPD coefficients, forward... */
    #define I_TPDINV 5 /* ...and inverse. */
    #define I_TPDAUX 6 /* True if auxiliary variables are used. */
    #define I_TPDRAD 7 /* True if the radial variable is used. */

    so it looks like the WCS by mistake is passing iparm[I_TPDNCO]=0, iparm[I_TPDAUX]=0 to wcshdo, causing it to skip writing of both the AUX.* and TPD.FWD.* coefficients...

  20. pllim commented on Jun 3, 2022

    @pllim
    Member

    Is this still an issue now?

  21. JulienPeloton commented on Jun 3, 2022

    @JulienPeloton
    Author

    Hi @pllim -- thanks for the follow-up. I did not get more into the details since last year, and did not encounter this problem again in my work.

  22. added
    Close?Tell stale bot that this issue/PR is stale
    on Jun 3, 2022
  23. github-actions commented on Jun 4, 2022

    @github-actions
    Contributor

    Hi humans 👋 - this issue was labeled as Close? approximately 8 hours ago. If you think this issue should not be closed, a maintainer should remove the Close? label - otherwise, I will close this issue in 7 days.

    If you believe I commented on this issue incorrectly, please report this here

  24. github-actions commented on Jun 12, 2022

    @github-actions
    Contributor

    I'm going to close this issue as per my previous message, but if you feel that this issue should stay open, then feel free to re-open and remove the Close? label.

    If this is the first time I am commenting on this issue, or if you believe I closed this issue incorrectly, please report this here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions