Repository: pytroll/pyorbital

Issue:
avhrr_gac instrument definition is broken
It appears to be mostly copy+paste from the standard avhrr with an updated frequency (1/6 -> 1/2 second). But it is still assuming 2048 pixels across track. As a result it only returns geometry for the first 1/5th of the swath

Discussion:

Solution patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7362d1c..a3fab1f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,7 +3,7 @@ fail_fast: false
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
     # Ruff version.
-    rev: 'v0.9.9'
+    rev: 'v0.11.4'
     hooks:
       - id: ruff
   - repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/pyorbital/etc/platforms.txt b/pyorbital/etc/platforms.txt
index 0a9d767..6caeb70 100644
--- a/pyorbital/etc/platforms.txt
+++ b/pyorbital/etc/platforms.txt
@@ -19,6 +19,7 @@ DMSP-F18 35951
 DMSP-F19 39630
 EOS-Aqua 27424
 EOS-Aura 28376
+EOS-6 (OCEANSAT-3) 54361
 EOS-Terra 25994
 FY-2D 29640
 FY-2E 33463
@@ -74,7 +75,9 @@ NOAA-18 28654
 NOAA-19 33591
 NOAA-20 43013
 NOAA-21 54234
+OTB-3-GAZELLE 54023
 RadarSat-2 32382
+SARAL 39086
 Sentinel-1A 39634
 Sentinel-1B 41456
 Sentinel-2A 40697
diff --git a/pyorbital/geoloc_instrument_definitions.py b/pyorbital/geoloc_instrument_definitions.py
index 6d901a4..12c683f 100644
--- a/pyorbital/geoloc_instrument_definitions.py
+++ b/pyorbital/geoloc_instrument_definitions.py
@@ -37,6 +37,7 @@ different pixels.
 Both scan angles and scan times are then combined into a ScanGeometry object.
 """
 
+import warnings
 import numpy as np
 
 from pyorbital.geoloc import ScanGeometry
@@ -82,6 +83,8 @@ def avhrr_gac(scan_times, scan_points,
     Source: NOAA KLM User's Guide, Appendix J
     http://www.ncdc.noaa.gov/oa/pod-guide/ncdc/docs/klm/html/j/app-j.htm
     """
+    warnings.warn("avhrr_gac is replaced with avhrr_from_times or avhrr_gac_from_times",
+                  DeprecationWarning)
     try:
         offset = np.array([(t - scan_times[0]).seconds +
                            (t - scan_times[0]).microseconds / 1000000.0 for t in scan_times])
@@ -100,6 +103,63 @@ def avhrr_gac(scan_times, scan_points,
              + np.expand_dims(offset, 1))
     return ScanGeometry(avhrr_inst, times)
 
+
+def avhrr_from_times(scan_times, scan_points, scan_angle=55.37):
+    """Definition of the avhrr instrument.
+
+    Source: NOAA KLM User's Guide, Appendix J
+    http://www.ncdc.noaa.gov/oa/pod-guide/ncdc/docs/klm/html/j/app-j.htm
+
+    :scan_times: Observation times (datetime object)
+    :scan_points: Across track pixel positions
+    :scan_angle: Maximum scan angle of the outermost FOV
+    """
+    offset = np.array([(t - scan_times[0]).total_seconds() for t in scan_times])
+    scan_points = np.asanyarray(scan_points)
+    scans_nb = len(offset)
+
+    avhrr_inst = np.vstack([(scan_points / 1023.5 - 1) * np.deg2rad(-scan_angle),
+                            np.zeros(len(scan_points))])
+
+    avhrr_inst = np.tile(avhrr_inst[:, np.newaxis, :], [1, scans_nb, 1])
+    # building the corresponding times array
+    times = (np.tile(scan_points * 0.000025, [scans_nb, 1])
+             + np.expand_dims(offset, 1))
+    return ScanGeometry(avhrr_inst, times)
+
+
+def avhrr_gac_from_times(scan_times, scan_points, scan_angle=55.37):
+    """Definition of the avhrr instrument, gac version.
+
+    Source: NOAA KLM User's Guide, Appendix J
+    http://www.ncdc.noaa.gov/oa/pod-guide/ncdc/docs/klm/html/j/app-j.htm
+
+    :scan_times: Observation times (datetime object)
+    :scan_points: Across track pixel positions
+    :scan_angle: Maximum scan angle of the outermost FOV
+    """
+    offset = np.array([(t - scan_times[0]).total_seconds() for t in scan_times])
+    scan_points = np.asanyarray(scan_points)
+    scans_nb = len(offset)
+
+    # The AVHRR swath is +/- 55.4 degrees, which puts the outermost FOV at
+    #   55.4 * 1023.5 / 1024 = 55.373
+    # GAC pixels are the average of four FOVs with sampling: ..1111.2222.----.NNNN..
+    # so we need to reduce the scan_angle by a factor (1023.5 - 3.5) / 1023.5
+    # to calculate the GAC pixel centres
+    gac_angle = scan_angle * (1023.5 - 3.5) / 1023.5
+
+    avhrr_inst = np.vstack([(scan_points / 204 - 1) * np.deg2rad(-gac_angle),
+                            np.zeros(len(scan_points))])
+
+    avhrr_inst = np.tile(
+        avhrr_inst[:, np.newaxis, :], [1, np.int32(scans_nb), 1])
+    # building the corresponding times array
+    times = (np.tile(scan_points * 0.000125, [scans_nb, 1])
+             + np.expand_dims(offset, 1))
+    return ScanGeometry(avhrr_inst, times)
+
+
 ################################################################
 # avhrr, all pixels
 


Test patch:
diff --git a/pyorbital/tests/test_geoloc.py b/pyorbital/tests/test_geoloc.py
index f07f777..1efb9fb 100644
--- a/pyorbital/tests/test_geoloc.py
+++ b/pyorbital/tests/test_geoloc.py
@@ -28,7 +28,7 @@ import datetime as dt
 import numpy as np
 
 from pyorbital.geoloc import ScanGeometry, geodetic_lat, qrotate, subpoint
-from pyorbital.geoloc_instrument_definitions import amsua, ascat, atms, avhrr, hirs4, mhs, slstr_nadir, viirs
+from pyorbital.geoloc_instrument_definitions import amsua, ascat, atms, avhrr, hirs4, mhs, slstr_nadir, viirs, avhrr_from_times, avhrr_gac_from_times
 
 
 class TestQuaternion:
@@ -174,6 +174,50 @@ class TestGeolocDefs:
         np.testing.assert_allclose(np.rad2deg(avh.fovs[0]),
                                    np.array([[10, 0, -10]]))
 
+    def test_avhrr_from_times(self):
+        avh = avhrr_from_times([dt.datetime(2000,1,1,0,0,0)], [0, 1023.5, 2047])
+        result = np.rad2deg(avh.fovs[0])
+        expected = np.array([[55.37, 0, -55.37]])
+        np.testing.assert_allclose(result, expected, rtol=1e-7, atol=1e-7)
+        result = avh.times(dt.date(2000,1,1))
+        expected = ((np.array([[0, 1023.5, 2047]]) * 25000).astype('timedelta64[ns]')
+                    + np.datetime64('2000-01-01T00:00:00'))
+        np.testing.assert_equal(result, expected)
+
+        avh = avhrr_from_times([dt.datetime(2000,1,1,0,0,0)], np.array([0, 1023.5, 2047]), 10)
+        np.testing.assert_allclose(np.rad2deg(avh.fovs[0]),
+                                   np.array([[10, 0, -10]]))
+
+        avh = avhrr_from_times([dt.datetime(2000,1,1,0,0,0), dt.datetime(2000,1,1,0,1,0)],
+                    [0, 2047])
+        times = avh.times(dt.datetime(2001,1,1))
+        expected = (np.array([[0,51175000],[60000000000, 60051175000]]).astype('timedelta64[ns]')
+                    + np.datetime64('2001-01-01'))
+        np.testing.assert_equal(times, expected)
+
+
+    def test_avhrr_gac_from_times(self):
+        avh = avhrr_gac_from_times([dt.datetime(2000,1,1,0,0,0)], [0, 204, 408])
+        result = np.rad2deg(avh.fovs[0])
+        expected = np.array([[55.180655, 0, -55.180655]])
+        np.testing.assert_allclose(result, expected, rtol=1e-7, atol=1e-7)
+        result = avh.times(dt.date(2000,1,1))
+        expected = ((np.array([[0, 204, 408]]) * 125000).astype('timedelta64[ns]')
+                    + np.datetime64('2000-01-01T00:00:00'))
+        np.testing.assert_equal(result, expected)
+
+        avh = avhrr_gac_from_times([dt.datetime(2000,1,1,0,0,0)], np.array([0, 204, 408]), 10)
+        np.testing.assert_allclose(np.rad2deg(avh.fovs[0]),
+                                   np.array([[9.965804, 0, -9.965804]]))
+
+        avh = avhrr_gac_from_times([dt.datetime(2000,1,1,0,0,0), dt.datetime(2000,1,1,0,1,0)],
+                    [0, 408])
+        times = avh.times(dt.datetime(2001,1,1))
+        expected = (np.array([[0,51000000],[60000000000, 60051000000]]).astype('timedelta64[ns]')
+                    + np.datetime64('2001-01-01'))
+        np.testing.assert_equal(times, expected)
+
+
     def test_viirs(self):
         """Test the definition of the viirs instrument."""
         geom = viirs(1, np.array([0, 3200, 6399]))