@@ -797,6 +797,136 @@ def sky_ellipse_to_pixel_svd(skycoord, wcs, width_arcsec, height_arcsec,
797797 return center , pixel_width , pixel_height , pixel_angle
798798
799799
800+ def sky_to_pixel_svd_scales (skycoord , wcs ):
801+ """
802+ Compute the pixel center, principal-axis scale factors, and pixel
803+ angle for a sky-to-pixel conversion using SVD of the local Jacobian.
804+
805+ Uses the singular value decomposition (SVD) of the local Jacobian
806+ ``J = d(pixel)/d(sky_arcsec)`` to find the natural principal axes
807+ of the WCS transformation at the given sky position. The singular
808+ values give the scale factors along the major and minor axes of the
809+ ellipse that a unit circle in sky space maps to in pixel space. The
810+ left singular vectors give the directions of those axes in pixel
811+ coordinates.
812+
813+ This is the appropriate method for converting a circular sky region
814+ to a pixel ellipse, as the resulting ellipse accurately represents
815+ the true shape of the WCS mapping (i.e., the tightest-fitting pixel
816+ ellipse that contains the sky circle).
817+
818+ Parameters
819+ ----------
820+ skycoord : `~astropy.coordinates.SkyCoord`
821+ The sky coordinate of the region center.
822+
823+ wcs : WCS object
824+ A world coordinate system (WCS) transformation that
825+ supports the `astropy shared interface for WCS
826+ <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g.,
827+ `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
828+
829+ Returns
830+ -------
831+ center : `~regions.PixCoord`
832+ The pixel center position.
833+
834+ scale_major : float
835+ The scale factor along the major (maximum-stretch) axis
836+ (pixels per arcsec).
837+
838+ scale_minor : float
839+ The scale factor along the minor (minimum-stretch) axis
840+ (pixels per arcsec).
841+
842+ pixel_angle : `~astropy.coordinates.Angle`
843+ The pixel rotation angle of the major axis, measured
844+ counterclockwise from the positive x-axis, wrapped to
845+ [0, 360) degrees.
846+ """
847+ x0 , y0 = wcs .world_to_pixel (skycoord )
848+ center = PixCoord (x = x0 , y = y0 )
849+
850+ jacobian = compute_local_wcs_jacobian (skycoord , wcs )
851+ u_mat , s_vals , _vt = np .linalg .svd (jacobian )
852+
853+ # Pixel angle of the major axis: direction of u_mat[:,0] in pixel
854+ # space. No parity correction needed — pixel space has no axis
855+ # reflection.
856+ pixel_angle = Angle (
857+ np .rad2deg (np .arctan2 (u_mat [1 , 0 ], u_mat [0 , 0 ])) * u .deg ).wrap_at (
858+ 360 * u .deg )
859+
860+ return center , s_vals [0 ], s_vals [1 ], pixel_angle
861+
862+
863+ def pixel_to_sky_svd_scales (pixcoord , wcs ):
864+ """
865+ Compute the sky center, principal-axis scale factors, and sky angle
866+ for a pixel-to-sky conversion using SVD of the inverse Jacobian.
867+
868+ Uses the singular value decomposition (SVD) of the local inverse
869+ Jacobian ``J^{-1} = d(sky)/d(pixel)`` to find the natural principal
870+ axes of the WCS transformation at the given pixel position. The
871+ singular values give the scale factors along the major and minor
872+ axes of the ellipse that a unit circle in pixel space maps to in sky
873+ space. The left singular vectors give the directions of those axes
874+ in tangent-plane coordinates.
875+
876+ This is the appropriate method for converting a circular pixel
877+ region to a sky ellipse, as the resulting ellipse accurately
878+ represents the true shape of the WCS mapping (i.e., the
879+ tightest-fitting sky ellipse that contains the pixel circle).
880+
881+ Parameters
882+ ----------
883+ pixcoord : `~regions.PixCoord`
884+ The pixel coordinate of the region center.
885+
886+ wcs : WCS object
887+ A world coordinate system (WCS) transformation that
888+ supports the `astropy shared interface for WCS
889+ <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g.,
890+ `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
891+
892+ Returns
893+ -------
894+ center : `~astropy.coordinates.SkyCoord`
895+ The sky center position.
896+
897+ scale_major : float
898+ The scale factor along the major (maximum-stretch) axis
899+ (arcsec per pixel).
900+
901+ scale_minor : float
902+ The scale factor along the minor (minimum-stretch) axis
903+ (arcsec per pixel).
904+
905+ sky_angle : `~astropy.coordinates.Angle`
906+ The sky rotation angle of the major axis, measured
907+ counterclockwise from the longitude (RA) axis, wrapped to
908+ [0, 360) degrees.
909+ """
910+ center = wcs .pixel_to_world (pixcoord .x , pixcoord .y )
911+
912+ jacobian = compute_local_wcs_jacobian (center , wcs )
913+ jacobian_inv = np .linalg .inv (jacobian )
914+
915+ # WCS parity: standard WCS has RA increasing to the left, giving
916+ # det(J) < 0. Apply parity to the RA component of U[:,0] to recover
917+ # the correct sky angle from the tangent-plane direction.
918+ parity = np .sign (np .linalg .det (jacobian ))
919+
920+ u_mat , s_vals , _vt = np .linalg .svd (jacobian_inv )
921+
922+ # Sky angle of the major axis in tangent-plane coordinates
923+ sky_angle = Angle (
924+ np .rad2deg (np .arctan2 (u_mat [1 , 0 ], parity * u_mat [0 , 0 ])) * u .deg
925+ ).wrap_at (360 * u .deg )
926+
927+ return center , s_vals [0 ], s_vals [1 ], sky_angle
928+
929+
800930def wcs_pixel_scale_angle (skycoord , wcs ):
801931 """
802932 Calculate the pixel coordinate, scale, and WCS rotation angle at the
0 commit comments