Repository: gammapy/gammapy

Issue:
Show reference point in WcsGeom.__repr__
Currently when printing a `WcsGeom` the reference point is not shown in the print out, so two `WcsGeom` can look equivalent in the printout, while they are actually not equivalent. Here is an example to reproduce:
```python3
from gammapy.maps import WcsGeom

geom_1 = WcsGeom.create(skydir=(0, 0), width=10)
geom_2 = WcsGeom.create(skydir=(4.75, -4.75), width=10, refpix=(1, 1))

print(geom_1)
print(geom_2)
print(geom_1 == geom_2)
``` 
Which prints:
```
WcsGeom

	axes       : ['lon', 'lat']
	shape      : (20, 20)
	ndim       : 2
	frame      : icrs
	projection : CAR
	center     : 0.0 deg, 0.0 deg
	width      : 10.0 deg x 10.0 deg

WcsGeom

	axes       : ['lon', 'lat']
	shape      : (20, 20)
	ndim       : 2
	frame      : icrs
	projection : CAR
	center     : 0.0 deg, 0.0 deg
	width      : 10.0 deg x 10.0 deg

False
```
A good solution would be to include the reference point of the WCS projection in the printout.
Show reference point in WcsGeom.__repr__
Currently when printing a `WcsGeom` the reference point is not shown in the print out, so two `WcsGeom` can look equivalent in the printout, while they are actually not equivalent. Here is an example to reproduce:
```python3
from gammapy.maps import WcsGeom

geom_1 = WcsGeom.create(skydir=(0, 0), width=10)
geom_2 = WcsGeom.create(skydir=(4.75, -4.75), width=10, refpix=(1, 1))

print(geom_1)
print(geom_2)
print(geom_1 == geom_2)
``` 
Which prints:
```
WcsGeom

	axes       : ['lon', 'lat']
	shape      : (20, 20)
	ndim       : 2
	frame      : icrs
	projection : CAR
	center     : 0.0 deg, 0.0 deg
	width      : 10.0 deg x 10.0 deg

WcsGeom

	axes       : ['lon', 'lat']
	shape      : (20, 20)
	ndim       : 2
	frame      : icrs
	projection : CAR
	center     : 0.0 deg, 0.0 deg
	width      : 10.0 deg x 10.0 deg

False
```
A good solution would be to include the reference point of the WCS projection in the printout.


Discussion:
It would also be nice to be able to see pinpoint exactly where exactly the two `geoms` differ. 
Often, the `print(geom)` returns the same info, but is actually different.
eg: if one non-spatial axis is node centered, other one is edge centered
In this case, one has to test explicitly `geom1._node_type == geom2._node_type` or `geom1._interp == geom2._interp`
This is not very nice, it basically duplicates code inside `Geom.__eq__`

But I do not know a good way to provide this. Maybe implementing a `MapAxis.__eq__` can help?
@AtreyeeS `MapAxis.__eq__` already exists: https://github.com/gammapy/gammapy/blob/master/gammapy/maps/geom.py#L828
I guess the proposal is to improve the error messages...
Yes indeed - what I meant is that `print(geom)` prints much less info than what is checked in `Geom.__eq__`, so it becomes difficult to know why the equality failed.
It would also be nice to be able to see pinpoint exactly where exactly the two `geoms` differ. 
Often, the `print(geom)` returns the same info, but is actually different.
eg: if one non-spatial axis is node centered, other one is edge centered
In this case, one has to test explicitly `geom1._node_type == geom2._node_type` or `geom1._interp == geom2._interp`
This is not very nice, it basically duplicates code inside `Geom.__eq__`

But I do not know a good way to provide this. Maybe implementing a `MapAxis.__eq__` can help?
@AtreyeeS `MapAxis.__eq__` already exists: https://github.com/gammapy/gammapy/blob/master/gammapy/maps/geom.py#L828
I guess the proposal is to improve the error messages...
Yes indeed - what I meant is that `print(geom)` prints much less info than what is checked in `Geom.__eq__`, so it becomes difficult to know why the equality failed.

Resolving patch:
diff --git a/gammapy/maps/wcs.py b/gammapy/maps/wcs.py
--- a/gammapy/maps/wcs.py
+++ b/gammapy/maps/wcs.py
@@ -13,6 +13,7 @@
     celestial_frame_to_wcs,
     proj_plane_pixel_scales,
     wcs_to_celestial_frame,
+    skycoord_to_pixel
 )
 from gammapy.utils.array import round_up_to_odd
 from .geom import (
@@ -459,6 +460,45 @@ def footprint(self):
         coords = self.wcs.calc_footprint()
         return SkyCoord(coords, frame=self.frame, unit="deg")
 
+    @classmethod
+    def from_aligned(cls, geom, skydir, width):
+        """Create an aligned geometry from an existing one
+
+        Parameters
+        ----------
+        geom : `~WcsGeom`
+            A reference WCS geometry object.
+        skydir : tuple or `~astropy.coordinates.SkyCoord`
+            Sky position of map center.  Can be either a SkyCoord
+            object or a tuple of longitude and latitude in deg in the
+            coordinate system of the map.
+        width : float or tuple or list or string
+            Width of the map in degrees.  A tuple will be interpreted
+            as parameters for longitude and latitude axes.  For maps
+            with non-spatial dimensions, list input can be used to
+            define a different map width in each image plane.
+
+        Returns
+        -------
+        geom : `~WcsGeom`
+            An aligned WCS geometry object with specified size and center.
+
+        """
+        width = _check_width(width) * u.deg
+        npix = tuple(np.round(width / geom.pixel_scales).astype(int))
+        xref, yref = geom.to_image().coord_to_pix(skydir)
+        xref = int(np.floor(-xref + npix[0] / 2.)) + geom.wcs.wcs.crpix[0]
+        yref = int(np.floor(-yref + npix[1] / 2.)) + geom.wcs.wcs.crpix[1]
+        return cls.create(
+            skydir=tuple(geom.wcs.wcs.crval),
+            npix=npix,
+            refpix=(xref, yref),
+            frame=geom.frame,
+            binsz=tuple(geom.pixel_scales.deg),
+            axes=geom.axes,
+            proj=geom.projection
+        )
+
     @classmethod
     def from_header(cls, header, hdu_bands=None, format="gadf"):
         """Create a WCS geometry object from a FITS header.
@@ -1009,6 +1049,7 @@ def __repr__(self):
         axes = ["lon", "lat"] + [_.name for _ in self.axes]
         lon = self.center_skydir.data.lon.deg
         lat = self.center_skydir.data.lat.deg
+        lon_ref, lat_ref = self.wcs.wcs.crval
 
         return (
             f"{self.__class__.__name__}\n\n"
@@ -1019,6 +1060,7 @@ def __repr__(self):
             f"\tprojection : {self.projection}\n"
             f"\tcenter     : {lon:.1f} deg, {lat:.1f} deg\n"
             f"\twidth      : {self.width[0][0]:.1f} x {self.width[1][0]:.1f}\n"
+            f"\twcs ref    : {lon_ref:.1f} deg, {lat_ref:.1f} deg\n"
         )
 
     def to_odd_npix(self, max_radius=None):


Test patch:
diff --git a/gammapy/maps/tests/test_wcs.py b/gammapy/maps/tests/test_wcs.py
--- a/gammapy/maps/tests/test_wcs.py
+++ b/gammapy/maps/tests/test_wcs.py
@@ -137,6 +137,59 @@ def test_wcsgeom_contains(npix, binsz, frame, proj, skydir, axes):
         assert_allclose(geom.contains(coords), np.zeros((1,), dtype=bool))
 
 
+@pytest.mark.parametrize(
+    ("npix", "binsz", "frame", "proj", "skydir", "axes"), wcs_test_geoms
+)
+def test_wcs_geom_from_aligned(npix, binsz, frame, proj, skydir, axes):
+    geom = WcsGeom.create(
+        npix=npix, binsz=binsz, skydir=(0, 0), proj=proj, frame=frame, axes=axes
+    )
+
+    aligned_geom = WcsGeom.from_aligned(
+        geom=geom, skydir=(2, 3), width="90 deg"
+    )
+
+    assert aligned_geom.is_aligned(geom)
+
+
+def test_from_aligned_vs_cutout():
+    skydir = SkyCoord(0.12, -0.34, unit="deg", frame="galactic")
+
+    geom = WcsGeom.create(
+        binsz=0.1, skydir=skydir, proj="AIT", frame="galactic"
+    )
+
+    position = SkyCoord("2.23d", "3.102d", frame="galactic")
+
+    width = ("89 deg", "79 deg")
+    aligned_geom = WcsGeom.from_aligned(
+        geom=geom, skydir=position, width=width
+    )
+
+    geom_cutout = geom.cutout(position=position, width=width)
+
+    assert geom_cutout == aligned_geom
+
+
+def test_from_aligned_vs_cutout_tan():
+    skydir = SkyCoord(0, 0, unit="deg", frame="galactic")
+
+    geom = WcsGeom.create(
+        binsz=1, skydir=skydir, proj="TAN", frame="galactic", width=("180d", "90d")
+    )
+
+    position = SkyCoord("53.23d", "22.102d", frame="galactic")
+    width = ("17 deg", "15 deg")
+
+    geom_cutout = geom.cutout(position=position, width=width, mode="partial")
+
+    aligned_geom = WcsGeom.from_aligned(
+        geom=geom, skydir=position, width=width
+    )
+
+    assert aligned_geom == geom_cutout
+    
+
 def test_wcsgeom_solid_angle():
     # Test using a CAR projection map with an extra axis
     binsz = 1.0 * u.deg
@@ -331,7 +384,16 @@ def test_geom_repr():
     geom = WcsGeom.create(
         skydir=(0, 0), npix=(10, 4), binsz=50, frame="galactic", proj="AIT"
     )
-    assert geom.__class__.__name__ in repr(geom)
+
+    str_info = repr(geom)
+    assert geom.__class__.__name__ in str_info
+    assert "wcs ref" in str_info
+    assert "center" in str_info
+    assert "projection" in str_info
+    assert "axes" in str_info
+    assert "shape" in str_info
+    assert "ndim" in str_info
+    assert "width" in str_info
 
 
 def test_geom_refpix():
@@ -418,6 +480,7 @@ def test_check_width(width, out):
     geom = WcsGeom.create(width=width, binsz=1.0)
     assert tuple(geom.npix) == out
 
+
 def test_check_binsz():
     # float
     binsz = _check_binsz(0.1)