Repository: microsoft/torchgeo

Issue:
EuroCrops get_label cannot handle features with None attributes
### Description

The `get_label` method of the EuroCrops dataset is attempting to render features with `None` values, which raises an error. The attribute `EC_hcat_c` is set to `None` for some countries in the dataset, and this is not handled in the source code.


`hcat_code = feature['properties'][self.label_name] # Here, self.label_name is "EC_hcat_c"
`


```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], [line 1](vscode-notebook-cell:?execution_count=7&line=1)
----> [1](vscode-notebook-cell:?execution_count=7&line=1) eurocrops_sk[small_bbox]

File ~\Desktop\torchgeo\torchgeo\datasets\geo.py:759, in VectorDataset.__getitem__(self, query)
    [753](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:753)         for feature in src.filter(bbox=(minx, miny, maxx, maxy)):
    [754](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:754)             # Warp geometries to requested CRS
    [755](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:755)             # print("feature['properties']", feature['properties'])
    [756](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:756)             shape = fiona.transform.transform_geom(
    [757](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:757)                 src.crs, self.crs.to_dict(), feature['geometry']
    [758](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:758)             )
--> [759](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:759)             label = self.get_label(feature)
    [760](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:760)             shapes.append((shape, label))
    [762](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/geo.py:762) # Rasterize geometries

File ~\Desktop\torchgeo\torchgeo\datasets\eurocrops.py:215, in EuroCrops.get_label(self, feature)
    [213](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/eurocrops.py:213)     return self.class_map[hcat_code]
    [214](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/eurocrops.py:214) print("hcat_code", hcat_code)
--> [215](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/eurocrops.py:215) hcat_code_list = list(hcat_code)
    [216](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/eurocrops.py:216) if all(c == '0' for c in hcat_code_list):
    [217](https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/burak/Desktop/torchgeo/docs/tutorials/~/Desktop/torchgeo/torchgeo/datasets/eurocrops.py:217)     break

TypeError: 'NoneType' object is not iterable
```

### Steps to reproduce

```
from torchgeo.datasets import EuroCrops

# Limit to a country with relatively small label size
# It needs to have None values inside, like Slovakia (scroll down): https://github.com/maja601/EuroCrops/wiki/Slovakia
class EuroCropsSK(EuroCrops):
    base_url = 'https://zenodo.org/records/8229128/files/'

    hcat_fname = 'HCAT2.csv'
    hcat_md5 = 'b323e8de3d8d507bd0550968925b6906'
    hcat_code_column = 'HCAT2_code'

    label_name = 'EC_hcat_c'

    filename_glob = '*_EC*.shp'

    filename_regex = r"""
        ^(?P<country>[A-Z]{2})
        (_(?P<region>[A-Z]+))?
        _
        (?P<date>\d{4})
        _
        (?P<suffix>EC(?:21)?)
        \.shp$
    """
    date_format = '%Y'
    zenodo_files: tuple[tuple[str, str], ...] = (
        ('SK_2021.zip', 'c7762b4073869673edc08502e7b22f01'),
    )


# Download the labels
eurocrops_sk = EuroCropsSK('./eurocrops_sk', download=True)

# Split the bounds into two and take the small one for fast debugging
small_bbox, big_bbox = eurocrops_sk.bounds.split(proportion=0.1, horizontal=True)

# Just curious
print(small_bbox.area)

# Trigger __getitem__
eurocrops_sk[small_bbox]
```

### Version

'0.7.0.dev0'

Discussion:
adamjstewart: Nice catch!

It looks like HCATv2 actually has a class for this: 
```csv
"not_known_and_other,3399000000"
```
I wonder if we should use this instead:
```python
hcat_code = feature['properties'][self.label_name] or '3399000000'
```
burakekim: > It looks like HCATv2 actually has a class for this:

Under [Harmonisation with HCAT](https://github.com/maja601/EuroCrops?tab=readme-ov-file#hamonisation-with-hcat-), it seems `EC_hcat_c` is already the label itself, and that is the variable that is assigned as `None`. If I understand correctly, `EC_hcat_c` with `None` should have been discarded from the features since they are not assigned any value, including `not_known_and_other`.

I am in favor of removing the print statement that notifies the user each time we hit `None`, as it becomes very verbose during training.
favyen2: > do you remember why you added 0? Just to appease mypy?

Do you mean for the HCAT code or for the label returned by the `get_label` function?

I don't think I ran into cases where HCAT code was None or 0 so it may be in a new version of the data or in a geographic area that I did not test.

For returning 0 from `get_label`, the function needs to return some integer for cases where the HCAT code is not in the list of codes that the user provides (i.e. the list they care about). Currently it returns the background class 0, which is also used for pixels that are not covered by any polygon. There may be cases where the user wants to use a different code for background class but for this use case I think it would work for them to add a catch-all class to the list of classes passed to the constructor since it is hierarchical (e.g. `classes=["1234", "5678", "0000"]`, now there is a separate "other" category that will be labeled as 3 after rasterization distinct from the background class 0).
adamjstewart: Should we use 3399000000 instead of 0? I can also ask the EuroCrops folks if necessary.
favyen2: As far as I know, `get_label` converts the HCAT code into a class integer suitable for training the segmentation model. So after the conversion these returned class indices should correspond to output neurons from the neural model rather than the original codes.
favyen2: Oh, I guess you are saying that some users might still find it useful to train on the feature, mapping it to an "other" category rather than to 0 which matches the background category.

I'm not sure the answer to this, it may depend on why there are these weird features in the dataset that don't have a code. I guess the safest would be to mark them invalid but otherwise without digging more into the data I think both are reasonable solutions.
adamjstewart: > Oh, I guess you are saying that some users might still find it useful to train on the feature, mapping it to an "other" category (which first setting the code to 3399000000 and then mapping it based on the user-provided category list would enable) rather than being limited to mapping it to 0 which matches the background category.

I'm actually saying the exact opposite. I'm wondering if the background class, None, and "not_known_and_other" should all be mapped to the same value. Otherwise, users will have to add all three to `ignore_index` during training.
burakekim: I think it would be worth reaching out to EuroCrops people to ask whether `None` and `not_known_and_other` serve any specific purpose across the entire dataset. It is best to find out if there is any reason behind this design choice
favyen2: > I'm actually saying the exact opposite. I'm wondering if the background class, None, and "not_known_and_other" should all be mapped to the same value. Otherwise, users will have to add all three to ignore_index during training.

I think there would be cases where users want to ignore None / not_known_and_other but not background, to try to have the model distinguish areas that are not crop fields.
burakekim: > some of the None fields either represent duplicated field parcels

> These are duplicate field parcels with either the same or different crop labels, since most people only wanted one label per parcel

Does this mean some fields have multiple labels, and only one is used while the rest are mapped to None? If so, it is better not to map None to any other label.

> Regarding not_known_and_other these are predominately either artificial built-up areas or water bodies.

This feels more like background to me.

adamjstewart: I guess I don't have strong opinions at the moment. We should make a decision here and get this merged though.
burakekim: There does not seem to be `background` class in HCAT. I am guessing `not_known_and_other` is best treated as background, and I suggest we keep this as a class (not map to 0) to help model differentiate between what we are interested in and what we are not (assuming the task is formed from this perspective)

Shortly, this is what the current PR does: keep the `not_known_and_other` as -background- class and do not render `None`. I say we can merge this as is
burakekim: On a side note:
HCAT here for convenience: [HCAT2.csv](https://github.com/user-attachments/files/18587301/HCAT2.csv). Here, there is another class with a rather-vague description: `unmaintained - 3308000000`

Currently, we treat this as a separate class. If this class is what I speculate it is (the source the annotation is received is no longer maintained?), it might be best to map to `None`. Once they release the new HCAT, it would be nice to sync with the EuroCrops folks to understand their decision making process

Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 87cafc97..2ca67341 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -2,7 +2,7 @@
 {
   "name": "Torchgeo DevContainer",
   // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
-  "image": "mcr.microsoft.com/devcontainers/python:3.12",
+  "image": "mcr.microsoft.com/devcontainers/python:3.13",
 
   // Features to add to the dev container. More info: https://containers.dev/features.
   "features": {
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 2dbe9954..9fd6890d 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -13,7 +13,7 @@ jobs:
       - name: Set up python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Install pip dependencies
         run: pip install build
       - name: List pip dependencies
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 301ce26e..3529f70f 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -20,7 +20,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
@@ -49,7 +49,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml
index efdd8b50..c71507bf 100644
--- a/.github/workflows/style.yaml
+++ b/.github/workflows/style.yaml
@@ -22,7 +22,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
@@ -48,7 +48,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml
index de0da385..7dde991f 100644
--- a/.github/workflows/tutorials.yaml
+++ b/.github/workflows/tutorials.yaml
@@ -24,7 +24,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bc948edc..e1bd1a15 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,7 +29,7 @@ repos:
           - pillow>=10.4.0
           - pytest>=6.1.2
           - scikit-image>=0.22.0
-          - torch>=2.3
+          - torch>=2.6
           - torchmetrics>=0.10
           - torchvision>=0.18
         exclude: (build|data|dist|logo|logs|output)/
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 36f098ea..6bf047ba 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -8,6 +8,7 @@ version: 2
 build:
   os: ubuntu-lts-latest
   tools:
+    # Sphinx 6.1 and older do not support Python 3.13+
     python: '3.12'
 
 # Configuration of the Python environment to be used
diff --git a/docs/tutorials/customization.rst b/docs/tutorials/customization.rst
index 07b08706..290ef56a 100644
--- a/docs/tutorials/customization.rst
+++ b/docs/tutorials/customization.rst
@@ -9,7 +9,7 @@ The following tutorials will teach you how to customize TorchGeo to meet your ne
 * `Custom Raster Datasets <custom_raster_dataset.ipynb>`_: How to create a new RasterDataset
 * `Custom Data Module <contribute_datamodule.ipynb>`_: How to create and contribute a new DataModule
 
-TorchGeo is a community-driven open source library. If there is a feature missing that you would like to add, please open a pull request to add it. See the ref:`contributing` guidelines to get started.
+TorchGeo is a community-driven open source library. If there is a feature missing that you would like to add, please open a pull request to add it. See the :ref:`contributing` guidelines to get started.
 
 .. toctree::
    :hidden:
diff --git a/pyproject.toml b/pyproject.toml
index bbcab8b4..df0c51cd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,7 @@ classifiers = [
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
     "Topic :: Scientific/Engineering :: Artificial Intelligence",
     "Topic :: Scientific/Engineering :: GIS",
 ]
@@ -247,6 +248,8 @@ filterwarnings = [
     # https://github.com/pytorch/pytorch/issues/136264
     "ignore:__array__ implementation doesn't accept a copy keyword:DeprecationWarning",
     "ignore:__array_wrap__ must accept context and return_scalar arguments:DeprecationWarning",
+    # https://github.com/pydantic/pydantic/pull/11377
+    "ignore:Failing to pass a value to the 'type_params' parameter of 'typing.*' is deprecated:DeprecationWarning:pydantic",
 
     # Expected warnings
     # Lightning warns us about using num_workers=0, but it's faster on macOS
diff --git a/requirements/required.txt b/requirements/required.txt
index b00e5df8..f9484bd1 100644
--- a/requirements/required.txt
+++ b/requirements/required.txt
@@ -15,8 +15,8 @@ pyproj==3.7.0
 rasterio==1.4.3
 rtree==1.3.0
 segmentation-models-pytorch==0.4.0
-shapely==2.0.6
+shapely==2.0.7
 timm==1.0.14
-torch==2.5.1
+torch==2.6.0
 torchmetrics==1.6.1
-torchvision==0.20.1
+torchvision==0.21.0
diff --git a/requirements/style.txt b/requirements/style.txt
index a6d9974f..e8cf6b77 100644
--- a/requirements/style.txt
+++ b/requirements/style.txt
@@ -1,3 +1,3 @@
 # style
 mypy==1.14.1
-ruff==0.9.3
+ruff==0.9.4
diff --git a/torchgeo/datasets/eurocrops.py b/torchgeo/datasets/eurocrops.py
index 5f438143..8832905c 100644
--- a/torchgeo/datasets/eurocrops.py
+++ b/torchgeo/datasets/eurocrops.py
@@ -204,6 +204,9 @@ class EuroCrops(VectorDataset):
         # We go up the class hierarchy until there is a match.
         # (Parent code is computed by replacing rightmost non-0 character with 0.)
         hcat_code = feature['properties'][self.label_name]
+        if hcat_code is None:
+            return 0
+
         while True:
             if hcat_code in self.class_map:
                 return self.class_map[hcat_code]
diff --git a/torchgeo/datasets/landsat.py b/torchgeo/datasets/landsat.py
index 8fb33b7c..acef3117 100644
--- a/torchgeo/datasets/landsat.py
+++ b/torchgeo/datasets/landsat.py
@@ -196,7 +196,7 @@ class Landsat7(Landsat):
 
     filename_glob = 'LE07_*_{}.*'
 
-    default_bands = ('SR_B1', 'SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7')
+    default_bands = ('SR_B1', 'SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B7')
     rgb_bands = ('SR_B3', 'SR_B2', 'SR_B1')
 
 


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f8fd0d15..96fdb226 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -18,7 +18,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ['3.10', '3.11', '3.12']
+        python-version: ['3.10', '3.11', '3.12', '3.13']
     steps:
       - name: Clone repo
         uses: actions/checkout@v4.2.2
@@ -91,7 +91,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.12'
+          python-version: '3.13'
       - name: Cache dependencies
         uses: actions/cache@v4.2.0
         id: cache
diff --git a/tests/conftest.py b/tests/conftest.py
index d55a972c..3f117bdb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -27,4 +27,4 @@ def matplotlib_backend() -> None:
 
 @pytest.fixture(autouse=True)
 def torch_hub(tmp_path: Path) -> None:
-    torch.hub.set_dir(tmp_path)  # type: ignore[no-untyped-call]
+    torch.hub.set_dir(tmp_path)
diff --git a/tests/datasets/test_eurocrops.py b/tests/datasets/test_eurocrops.py
index 3b2d4fc6..63896e23 100644
--- a/tests/datasets/test_eurocrops.py
+++ b/tests/datasets/test_eurocrops.py
@@ -82,6 +82,11 @@ class TestEuroCrops:
         ):
             dataset[query]
 
+    def test_get_label_with_none_hcat_code(self, dataset: EuroCrops) -> None:
+        mock_feature = {'properties': {dataset.label_name: None}}
+        label = dataset.get_label(mock_feature)
+        assert label == 0, "Expected label to be 0 when 'EC_hcat_c' is None."
+
     def test_integrity_error(self, dataset: EuroCrops) -> None:
         dataset.zenodo_files = (('AA.zip', 'invalid'),)
         assert not dataset._check_integrity()
diff --git a/tests/models/test_croma.py b/tests/models/test_croma.py
index 83f63770..ba857a57 100644
--- a/tests/models/test_croma.py
+++ b/tests/models/test_croma.py
@@ -18,7 +18,7 @@ from torchgeo.models import (
 )
 
 
-def save_model(model: torch.nn.Module, path: Path) -> None:
+def save_model(model: CROMA, path: Path) -> None:
     state_dict = {
         's1_encoder': model.s1_encoder.state_dict(),
         's1_GAP_FFN': model.s1_GAP_FFN.state_dict(),