Skip to content

added UniverSat wrapper support #860

Open
HarshShinde0 wants to merge 9 commits into
opengeos:mainfrom
HarshShinde0:feat/universat
Open

added UniverSat wrapper support #860
HarshShinde0 wants to merge 9 commits into
opengeos:mainfrom
HarshShinde0:feat/universat

Conversation

@HarshShinde0

@HarshShinde0 HarshShinde0 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

#828

Summary by CodeRabbit

  • New Features
    • Added UniverSat model integration for multimodal satellite encoding, inference, and optional training.
    • Supports optical snapshots, optical/SAR time series with date handling, elevation, and additional/unregistered sensors via supplied metadata.
    • Added helpers to compute tile embeddings and generate PCA-based RGB visualizations.
  • Documentation
    • Added UniverSat API docs and an end-to-end examples notebook demonstrating multi-grid encoding and dense embedding visualization.
  • Tests
    • Added unit tests covering public API exposure, preprocessing/batching (including date tensors), GeoTIFF ingestion, encoding calls, embeddings/RGB helpers, and model registry metadata.

Copilot AI review requested due to automatic review settings July 16, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete UniverSat integration with model loading, raster and multimodal encoding, embedding utilities, training support, public exports, tests, registry metadata, and documentation examples.

Changes

UniverSat integration

Layer / File(s) Summary
Model registration and loading
geoai/foundation_models.py, geoai/__init__.py, geoai/universat.py, tests/test_universat.py
Registers UniverSat metadata, exposes lazy APIs, initializes the cached repository, and supports pretrained or hubconf model loading and training execution.
Raster processing and encoding
geoai/universat.py, tests/test_universat.py
Processes GeoTIFF and multimodal inputs, handles dates and unseen-sensor metadata, and forwards batches to model.encode.
Inference and embedding utilities
geoai/universat.py, tests/test_universat.py
Adds inference, tile embedding, and PCA RGB conversion with shape and behavior tests.
Documentation and usage examples
docs/universat.md, docs/examples/universat.ipynb, zensical.toml
Documents the module and demonstrates multimodal encoding, output grids, unseen sensors, embeddings, and visualization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant rasterio
  participant UniverSatProcessor
  participant UniverSatModel
  rasterio->>UniverSatProcessor: read_geotiff raster and metadata
  UniverSatProcessor->>UniverSatProcessor: preprocess_image and format_batch
  UniverSatProcessor->>UniverSatModel: encode formatted modalities
  UniverSatModel-->>UniverSatProcessor: output tokens
Loading

Possibly related PRs

  • opengeos/geoai#811: Shares the foundation-model registry and lazy export integration points.
  • opengeos/geoai#859: Directly overlaps the UniverSat modules, tests, and documentation.

Suggested reviewers: giswqs

Poem

A rabbit hops where new models bloom,
UniverSat tokens light the room.
Sensors gather, grids unfold,
PCA paints embeddings gold.
Docs and tests join the cheer—
Fresh GeoAI magic is here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding UniverSat wrapper support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@geoai/universat.py`:
- Around line 19-20: Extract the duplicated ~/.cache/geoai/UniverSat value into
one module-level constant and update both the module-scope path setup and
universat_train to reuse it, keeping the existing path behavior unchanged.
- Around line 193-215: Update get_pca_rgb and _proj to fit a single PCA(3) model
over all batch tokens, then reuse that fitted model when transforming each tile
so RGB component orientations remain consistent across the batch. Preserve the
existing handling for single 2D inputs, square 3D inputs, normalization, and
output shapes.
- Around line 79-96: Update UniverSatProcessor.__init__ to accept the pretrained
and size options used by universat_inference, then forward them to
load_universat_model when no model instance is supplied. Preserve the existing
device placement and eval_mode behavior for explicitly provided models, while
allowing those options to select non-default pretrained model variants.
- Around line 111-135: Update _process_sample so tensor inputs follow the same
scale normalization and time-series dimension expansion as the str/array paths,
reusing preprocess_image or its equivalent processing. Revise d_val handling so
supplied {mod}_dates for custom modalities are not silently discarded: honor
them when present, or explicitly reject unsupported date metadata; preserve
default dates for recognized time-series modalities.
- Around line 18-37: Refactor the module-level UniverSat setup into an explicit
lazy helper that clones and imports only when requested, rather than during
import of geoai.universat. Pin the Git checkout to a known commit or tag, guard
cloning with a lock, and apply a finite timeout to the clone operation. Keep the
path setup and imports within the helper, and have callers obtain UniverSat and
related registry symbols through that lazy initialization.

In `@tests/test_universat.py`:
- Around line 56-70: The modality_registry mock is applied after geoai.universat
binds INPUT_RES, SUBPATCHES, and WAVELENGTHS, so the test still uses real
values. Update the test setup to install the sys.modules patch before importing
geoai.universat and constructing UniverSatProcessor, or patch the imported
constants directly on geoai.universat before use.

In `@zensical.toml`:
- Line 105: Add the new universat API documentation page to the API Reference
navigation list in zensical.toml, placing the universat module entry alongside
the existing train and utils module entries and pointing it to universat.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 68dc2f21-e716-439c-a91c-0a7e32cf5381

📥 Commits

Reviewing files that changed from the base of the PR and between df71fc8 and a9c157e.

📒 Files selected for processing (7)
  • docs/examples/universat.ipynb
  • docs/universat.md
  • geoai/__init__.py
  • geoai/foundation_models.py
  • geoai/universat.py
  • tests/test_universat.py
  • zensical.toml

Comment thread geoai/universat.py Outdated
Comment thread geoai/universat.py Outdated
Comment thread geoai/universat.py
Comment thread geoai/universat.py
Comment thread geoai/universat.py
Comment thread tests/test_universat.py Outdated
Comment thread zensical.toml
@HarshShinde0
HarshShinde0 requested a review from giswqs July 16, 2026 13:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
geoai/universat.py (1)

19-27: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Pin the Git checkout, add a lock, and make cloning lazy.

As per past review comments, while the timeout and constant were added, the Git clone still executes at module import time, lacks a lock to prevent concurrent import races, and checks out an unpinned main branch. This remains a security and reliability risk. Please move this logic behind an explicit lazy helper function, pin to a specific commit or tag, and use a locking mechanism (e.g., filelock) to prevent cache corruption during concurrent clones.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@geoai/universat.py` around lines 19 - 27, The module-level UniverSat clone
logic should not run during import. Move it into an explicit lazy helper, such
as a dedicated cache-initialization function, that acquires a file lock before
checking or populating UNIVERSAT_CACHE_DIR, clones a pinned commit or tag
instead of the unpinned default branch, and preserves the existing timeout and
sys.path setup after initialization; update callers to invoke the helper when
UniverSat is needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@geoai/universat.py`:
- Around line 19-27: The module-level UniverSat clone logic should not run
during import. Move it into an explicit lazy helper, such as a dedicated
cache-initialization function, that acquires a file lock before checking or
populating UNIVERSAT_CACHE_DIR, clones a pinned commit or tag instead of the
unpinned default branch, and preserves the existing timeout and sys.path setup
after initialization; update callers to invoke the helper when UniverSat is
needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 76cab67c-90a3-4b70-9ae5-8bceeff9c5bb

📥 Commits

Reviewing files that changed from the base of the PR and between a9c157e and 7a55798.

📒 Files selected for processing (1)
  • geoai/universat.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
geoai/universat.py (1)

146-167: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate date metadata consistently across the batch.

For custom modalities, this condition can produce None for samples without {mod}_dates and tensors for samples that provide them. Because format_batch checks only processed[0][1], dates supplied by later samples are silently dropped when the first sample lacks dates, or torch.stack fails when the first sample has dates but a later one does not. Require dates for every sample once any sample supplies them, or synthesize a documented default consistently before stacking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@geoai/universat.py` around lines 146 - 167, Update format_batch and its
date-processing flow around _process_sample so date metadata is consistent for
every sample in a modality before stacking. If any sample supplies mod_dates,
require all samples to provide compatible dates (or apply the documented default
uniformly); otherwise omit the date tensor. Do not rely only on processed[0][1],
and preserve the existing modality batching behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@geoai/universat.py`:
- Around line 23-30: Update the git clone invocation in the module’s cache setup
to resolve the executable explicitly and fail clearly when git is unavailable,
rather than relying on PATH lookup. Update format_batch() to inspect all
processed records for *_dates metadata, preserving later values and ensuring
mixed batches are handled without stacking errors.

---

Outside diff comments:
In `@geoai/universat.py`:
- Around line 146-167: Update format_batch and its date-processing flow around
_process_sample so date metadata is consistent for every sample in a modality
before stacking. If any sample supplies mod_dates, require all samples to
provide compatible dates (or apply the documented default uniformly); otherwise
omit the date tensor. Do not rely only on processed[0][1], and preserve the
existing modality batching behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 740c9302-c9e1-4c80-8595-bda0061b7c03

📥 Commits

Reviewing files that changed from the base of the PR and between 7a55798 and 2c2472c.

📒 Files selected for processing (1)
  • geoai/universat.py

Comment thread geoai/universat.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
geoai/universat.py (1)

200-204: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent TypeError crash when stacking mixed date metadata.

If a batch contains samples where only some include _dates for a specific modality, processed will contain a mix of Tensor and None values for p[1]. Passing this mixed list to torch.stack will raise a TypeError and crash the application.

Backfill the None values with default zero tensors (matching the length of the tensor p[0], exactly as _process_sample does) to ensure consistent batching.

🐛 Proposed fix for stacking logic
-            if any(p[1] is not None for p in processed):
-                batch[f"{mod}_dates"] = torch.stack([p[1] for p in processed]).to(
-                    self.device
-                )
+            if any(p[1] is not None for p in processed):
+                batch[f"{mod}_dates"] = torch.stack([
+                    p[1] if p[1] is not None else torch.zeros(p[0].shape[0], dtype=torch.long)
+                    for p in processed
+                ]).to(self.device)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@geoai/universat.py` around lines 200 - 204, Update the date batching logic
before torch.stack in the processed-result flow to replace each None date with a
zero tensor whose length matches the corresponding p[0] tensor, consistent with
_process_sample. Preserve existing date tensors and stack the normalized values
into the modality’s _dates batch field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@geoai/universat.py`:
- Around line 24-28: Add a module-level threading.Lock and use it in _setup to
guard the _repo_ready check and all repository initialization work, ensuring
only one thread can clone or checkout the cache at a time while preserving the
early return for already-initialized state.

---

Outside diff comments:
In `@geoai/universat.py`:
- Around line 200-204: Update the date batching logic before torch.stack in the
processed-result flow to replace each None date with a zero tensor whose length
matches the corresponding p[0] tensor, consistent with _process_sample. Preserve
existing date tensors and stack the normalized values into the modality’s _dates
batch field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ec5e9309-7dd9-4b9e-9a31-7a9dbf6b731e

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2472c and 38a9595.

📒 Files selected for processing (2)
  • geoai/universat.py
  • tests/test_universat.py

Comment thread geoai/universat.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
geoai/universat.py (1)

29-78: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix lock scope to prevent race condition during initialization.

The with _setup_lock: context manager only wraps the double-check if _repo_ready: return block and releases the lock immediately afterward. The rest of the initialization logic runs entirely outside the lock, which allows multiple threads to race and concurrently execute the git clone and path setup.

Indent the remaining setup logic so it executes while the lock is held.

🔒️ Proposed fix to expand the lock scope
     with _setup_lock:
         if _repo_ready:
             return
 
-    git = shutil.which("git")
-    if not git:
-        raise FileNotFoundError("git not found on PATH")
-
-    if not os.path.exists(UNIVERSAT_CACHE_DIR):
-        subprocess.run(
-            [
-                git,
-                "clone",
-                "--single-branch",
-                "https://github.com/gastruc/UniverSat.git",
-                UNIVERSAT_CACHE_DIR,
-            ],
-            check=True,
-            timeout=300,
-        )
-        # pin to known working commit
-        subprocess.run(
-            [
-                git,
-                "-C",
-                UNIVERSAT_CACHE_DIR,
-                "checkout",
-                "f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
-            ],
-            check=True,
-            timeout=60,
-        )
-
-    sys.path = [UNIVERSAT_CACHE_DIR, _src] + [
-        p for p in sys.path if p not in (UNIVERSAT_CACHE_DIR, _src)
-    ]
-
-    try:
-        import torch._dynamo
-
-        torch._dynamo.config.disable = True
-    except ImportError:
-        pass
-
-    global UniverSat, hubconf, WAVELENGTHS
-    from hubconf import UniverSat  # noqa: F811
-    import hubconf  # noqa: F811
-    from modality_registry import WAVELENGTHS  # noqa: F811
-
-    _repo_ready = True
+        git = shutil.which("git")
+        if not git:
+            raise FileNotFoundError("git not found on PATH")
+
+        if not os.path.exists(UNIVERSAT_CACHE_DIR):
+            subprocess.run(
+                [
+                    git,
+                    "clone",
+                    "--single-branch",
+                    "https://github.com/gastruc/UniverSat.git",
+                    UNIVERSAT_CACHE_DIR,
+                ],
+                check=True,
+                timeout=300,
+            )
+            # pin to known working commit
+            subprocess.run(
+                [
+                    git,
+                    "-C",
+                    UNIVERSAT_CACHE_DIR,
+                    "checkout",
+                    "f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
+                ],
+                check=True,
+                timeout=60,
+            )
+
+        sys.path = [UNIVERSAT_CACHE_DIR, _src] + [
+            p for p in sys.path if p not in (UNIVERSAT_CACHE_DIR, _src)
+        ]
+
+        try:
+            import torch._dynamo
+
+            torch._dynamo.config.disable = True
+        except ImportError:
+            pass
+
+        global UniverSat, hubconf, WAVELENGTHS
+        from hubconf import UniverSat  # noqa: F811
+        import hubconf  # noqa: F811
+        from modality_registry import WAVELENGTHS  # noqa: F811
+
+        _repo_ready = True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@geoai/universat.py` around lines 29 - 78, Expand the _setup_lock context in
the initialization function to cover all setup operations, including git
discovery, repository cloning and checkout, sys.path updates, torch
configuration, imports, and setting _repo_ready. Keep the existing early return
under the same lock so only one thread can perform initialization while others
observe the completed state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@geoai/universat.py`:
- Around line 29-78: Expand the _setup_lock context in the initialization
function to cover all setup operations, including git discovery, repository
cloning and checkout, sys.path updates, torch configuration, imports, and
setting _repo_ready. Keep the existing early return under the same lock so only
one thread can perform initialization while others observe the completed state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 459423e1-ed25-46f0-bca0-ea4f6568c6bd

📥 Commits

Reviewing files that changed from the base of the PR and between 38a9595 and 71f5f69.

📒 Files selected for processing (1)
  • geoai/universat.py

Comment thread geoai/universat.py
Comment on lines +25 to +78
def _setup():
global _repo_ready
if _repo_ready:
return
with _setup_lock:
if _repo_ready:
return

git = shutil.which("git")
if not git:
raise FileNotFoundError("git not found on PATH")

if not os.path.exists(UNIVERSAT_CACHE_DIR):
subprocess.run(
[
git,
"clone",
"--single-branch",
"https://github.com/gastruc/UniverSat.git",
UNIVERSAT_CACHE_DIR,
],
check=True,
timeout=300,
)
# pin to known working commit
subprocess.run(
[
git,
"-C",
UNIVERSAT_CACHE_DIR,
"checkout",
"f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
],
check=True,
timeout=60,
)

sys.path = [UNIVERSAT_CACHE_DIR, _src] + [
p for p in sys.path if p not in (UNIVERSAT_CACHE_DIR, _src)
]

try:
import torch._dynamo

torch._dynamo.config.disable = True
except ImportError:
pass

global UniverSat, hubconf, WAVELENGTHS
from hubconf import UniverSat # noqa: F811
import hubconf # noqa: F811
from modality_registry import WAVELENGTHS # noqa: F811

_repo_ready = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (high confidence): the double-checked-locking pattern doesn't actually protect the setup work.

The with _setup_lock: block only wraps the inner if _repo_ready: return check (lines 29-31); the with statement ends right after that, so the git clone, sys.path mutation, dynamic imports, and the final _repo_ready = True assignment (lines 33-78) all run outside the lock. If two threads call _setup() concurrently before the repo is cached, both pass the unlocked check at line 27, both acquire/release the lock in turn without doing any work under it, and both then race to run git clone into the same UNIVERSAT_CACHE_DIR — one clone will fail (destination path already exists and is not an empty directory) or the directory can end up in an inconsistent state. This defeats the purpose of _setup_lock (added in the "threading lock fix" commit).

Fix by moving the setup body inside the with block so the lock actually serializes it:

Suggested change
def _setup():
global _repo_ready
if _repo_ready:
return
with _setup_lock:
if _repo_ready:
return
git = shutil.which("git")
if not git:
raise FileNotFoundError("git not found on PATH")
if not os.path.exists(UNIVERSAT_CACHE_DIR):
subprocess.run(
[
git,
"clone",
"--single-branch",
"https://github.com/gastruc/UniverSat.git",
UNIVERSAT_CACHE_DIR,
],
check=True,
timeout=300,
)
# pin to known working commit
subprocess.run(
[
git,
"-C",
UNIVERSAT_CACHE_DIR,
"checkout",
"f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
],
check=True,
timeout=60,
)
sys.path = [UNIVERSAT_CACHE_DIR, _src] + [
p for p in sys.path if p not in (UNIVERSAT_CACHE_DIR, _src)
]
try:
import torch._dynamo
torch._dynamo.config.disable = True
except ImportError:
pass
global UniverSat, hubconf, WAVELENGTHS
from hubconf import UniverSat # noqa: F811
import hubconf # noqa: F811
from modality_registry import WAVELENGTHS # noqa: F811
_repo_ready = True
def _setup():
global _repo_ready
if _repo_ready:
return
with _setup_lock:
if _repo_ready:
return
git = shutil.which("git")
if not git:
raise FileNotFoundError("git not found on PATH")
if not os.path.exists(UNIVERSAT_CACHE_DIR):
subprocess.run(
[
git,
"clone",
"--single-branch",
"https://github.com/gastruc/UniverSat.git",
UNIVERSAT_CACHE_DIR,
],
check=True,
timeout=300,
)
# pin to known working commit
subprocess.run(
[
git,
"-C",
UNIVERSAT_CACHE_DIR,
"checkout",
"f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
],
check=True,
timeout=60,
)
sys.path = [UNIVERSAT_CACHE_DIR, _src] + [
p for p in sys.path if p not in (UNIVERSAT_CACHE_DIR, _src)
]
try:
import torch._dynamo
torch._dynamo.config.disable = True
except ImportError:
pass
global UniverSat, hubconf, WAVELENGTHS
from hubconf import UniverSat # noqa: F811
import hubconf # noqa: F811
from modality_registry import WAVELENGTHS # noqa: F811
_repo_ready = True

Comment thread geoai/universat.py
Comment on lines +37 to +60
if not os.path.exists(UNIVERSAT_CACHE_DIR):
subprocess.run(
[
git,
"clone",
"--single-branch",
"https://github.com/gastruc/UniverSat.git",
UNIVERSAT_CACHE_DIR,
],
check=True,
timeout=300,
)
# pin to known working commit
subprocess.run(
[
git,
"-C",
UNIVERSAT_CACHE_DIR,
"checkout",
"f6df2eec54955b0f7524cc95fe21a5e80c0239d9",
],
check=True,
timeout=60,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): a failed/interrupted clone permanently wedges the cache.

If the first subprocess.run (clone) fails after git has already created UNIVERSAT_CACHE_DIR (e.g. network interruption, timeout), check=True raises and the exception propagates — but the directory now exists. On every subsequent call, os.path.exists(UNIVERSAT_CACHE_DIR) is True, so cloning/checkout is skipped, sys.path is still updated, and from hubconf import UniverSat fails with ImportError (or worse, imports a stale/partial tree) every time thereafter — with no automatic recovery short of the user manually deleting ~/.cache/geoai/UniverSat. Consider cloning into a temp dir and atomically renaming into place only on success, or removing the partial directory on failure.

Comment thread geoai/universat.py
Comment on lines +262 to +266
if t.ndim == 2:
g = int(t.shape[0] ** 0.5)
return _norm(PCA(3).fit_transform(t.reshape(-1, t.shape[-1])), g)
if t.ndim == 3 and t.shape[0] == t.shape[1]:
return _norm(PCA(3).fit_transform(t.reshape(-1, t.shape[-1])), t.shape[0])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): shape-based dispatch is ambiguous and silently mis-handles a plausible batch shape.

get_pca_rgb distinguishes "single square-grid tile" from "batch of tiles" purely by comparing t.shape[0] == t.shape[1]. But a genuine batch of tiles where the batch size equals the token count per tile is a realistic case — e.g. batching 9 tiles each encoded with output_grid=9 gives tokens.shape == (9, 81, D)... more simply, 9 tiles with output_grid=3 gives (9, 9, D). That tensor satisfies t.ndim == 3 and t.shape[0] == t.shape[1] (9 == 9), so it takes the "single tile" branch at line 265-266 and reshapes the whole (9, 9, D) batch into one (9, 9, 3) "grid", silently producing a nonsensical PCA/RGB result instead of raising an error or going through the per-tile batch path below. Since this fails silently (no exception, just wrong output), it's easy to miss. Consider requiring callers to disambiguate explicitly (e.g. always treat ndim == 3 as batch, and require callers to pass a single 2D (N, D) tensor for one tile — which matches how get_pca_rgb(tokens[0]) is used in the example notebook).

Comment thread geoai/universat.py
Comment on lines +3 to +5
import os, sys, subprocess, shutil, threading
from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np, torch, torch.nn as nn, rasterio

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality nits (low confidence):

  • Union (line 4) is imported but never used in this module.
  • Comma-separated multi-imports on a single line (import os, sys, subprocess, shutil, threading / import numpy as np, torch, torch.nn as nn, rasterio) is PEP 8 (E401) discouraged and inconsistent with the rest of the codebase, which uses one import per line (see geoai/prithvi.py, geoai/dinov3.py).
Suggested change
import os, sys, subprocess, shutil, threading
from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np, torch, torch.nn as nn, rasterio
import os
import shutil
import subprocess
import sys
import threading
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import rasterio
import torch
import torch.nn as nn

Comment thread geoai/universat.py
def preprocess_image(
self, img: np.ndarray, mod: str, scale: Optional[float] = None
) -> torch.Tensor:
scale = scale or 1.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (low confidence): scale = scale or 1.0 treats an explicitly-passed scale=0.0 the same as scale=None (falls back to 1.0) instead of surfacing the caller's mistake (e.g. a ZeroDivisionError/ValueError). Same pattern at line 172 in _process_sample. Probably fine in practice since real scale factors are never 0, but scale if scale is not None else 1.0 would be more defensive/explicit.

@github-actions

Copy link
Copy Markdown

Code review

Reviewed geoai/universat.py (the core new module) plus the supporting registry/docs/test changes in this PR. Findings below; nothing found in geoai/__init__.py, geoai/foundation_models.py, docs/*, or zensical.toml beyond boilerplate registration that looks consistent with existing patterns.

Bugs

  • _setup()'s double-checked locking doesn't actually lock the setup work (geoai/universat.py:25-78): the with _setup_lock: block only wraps the inner readiness check; the git clone, sys.path mutation, dynamic imports, and _repo_ready = True all run outside the lock. Concurrent first calls can race to git clone into the same cache dir. High confidence.
  • A failed/interrupted clone permanently wedges the repo cache (geoai/universat.py:37-60): if git clone fails after creating UNIVERSAT_CACHE_DIR, later calls see the dir already exists, skip re-cloning, and fail on import forever until a human deletes the cache manually. Medium confidence.
  • get_pca_rgb shape-based dispatch is ambiguous for a real batch shape (geoai/universat.py:262-266): when batch size equals per-tile token count (e.g. 9 tiles each with output_grid=3 giving (9, 9, D)), the function silently treats the batch as a single square grid and reshapes/PCA's the wrong axes, producing incorrect output with no error. Medium confidence.

Security

  • No injection, unsafe input handling, or leaked secrets found. subprocess.run calls all use list args (no shell=True), so experiment/overrides in universat_train and the git commands are not vulnerable to shell injection. The cloned dependency is pinned to a fixed commit SHA, which mitigates (but doesn't eliminate) supply-chain risk inherent to cloning-and-importing a third-party repo at runtime. Low confidence, informational only.

Performance

  • No obvious inefficiencies in the hot paths (encode/batch formatting). format_batch uses set() over sample keys purely for dedup, which is fine since it only affects a dict-building loop, not model math.

Quality

  • Unused Union import in geoai/universat.py:4. Low confidence.
  • Comma-separated multi-imports on single lines (geoai/universat.py:3,5) deviate from the one-import-per-line convention used elsewhere in the codebase (e.g. prithvi.py, dinov3.py). Low confidence.
  • scale = scale or 1.0 (geoai/universat.py:157, and similarly line 172) silently coerces an explicit scale=0 to 1.0 rather than surfacing a caller error. Low confidence.

CLAUDE.md

  • No CLAUDE.md file exists in this repository, so there are no project-specific guidelines to check against.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants