Skip to content

feat(embeddings): AlphaEarth's years on the time slider (embeddings.addCube) (#1720) - #1749

Draft
elpidioassinaturas wants to merge 10 commits into
opengeos:mainfrom
elpidioassinaturas:feat/1720-embeddings-cube
Draft

elpidioassinaturas wants to merge 10 commits into
opengeos:mainfrom
elpidioassinaturas:feat/1720-embeddings-cube

Conversation

@elpidioassinaturas

@elpidioassinaturas elpidioassinaturas commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1720: AlphaEarth's years on the time slider (embeddings.addCube). Similarity and change (§3.2 of the issue) will come in a second PR, to keep this one small.

What it does

Today embeddings.search and embeddings.add (#542) show one AlphaEarth tile for one year, and each year is a separate layer. This PR shows a tile's years as one layer on the time slider, one year per step. It reuses the mechanism stac.addCube (#559) builds on: a tile layer with time_urls, one cloud-optimised GeoTIFF per date.

  • embeddings.addCube {"dataset": "alphaearth", "bbox"?, "start"?, "end"?, "bands"?, "stretch"?, "name"?} adds one tile layer per AlphaEarth tile where the map is. Each layer reads its file for the year the slider is at. If the slider is off, it turns on over the years (unit: year, format: YYYY). If it's already on, it takes the years in, as stac.addCube does.
  • Search over the years (crates/tiles/src/embeddings.rs): aef_series / search_aef_series use the same single read of the GeoParquet index as search_aef, since its rows already hold every year. Each year's export gives its files a new name prefix (for example xdwdu82ch9yffuj03-0000008192-0000000000.tiff in 2017 and x1i7lpngfblt9wshv-0000008192-0000000000.tiff in 2024). So a tile's years are matched by place: the same CRS, and footprint corners that agree to 1e-4° (about 10 m, far below a tile's 82 km), so no rounding boundary splits a tile in two. A {YYYY} template can't describe the series. aef_tiles now shares the row parsing (aef_rows) and behaves as before.
  • Satellite Embeddings pane: a new All years, on the time slider checkbox. With it ticked, the search lists each tile over 2017–2025 ("AlphaEarth 2017–2025 · UTM 23S", "9 of 9 years"), and Add years hands that tile's files to embeddings.addCube (found). In the app nothing is read on the frame: the search runs off it, as before, and the layers are added without waiting for a header. The pane also gets a Cancel button while a search or read runs. It drops the result, and for a data read it releases the download.
  • stac.rs: the slider part of add_cube moved into cube_slider() with no behaviour change, and both cubes use it.
  • Fix found along the way (map.setTiles): a new look (bands, range, dequantize), for example from the pane's Apply to layers, changed only the file shown. The slider brought the old look back at the next step. It now goes to every file in time_urls too, as it already did for a time_url template. This also affects stac.addCube's cubes.

Never silent

  • A year a tile has no file for: the layer shows nothing that year, and the command says how many tile-years are missing ("N of the M tile-years have no file in AlphaEarth's index…"). When the search read only part of the index, the notice says the gap is in the part that was read, and to zoom in (the pane hands this over as complete).
  • Files handed over (found) outside the years asked for, and tiles left with none, are left out, and the command says how many of each.
  • A partial read of the index, tiles past MAX_TILES (60), and a second file for a tile-year (the first is kept) are each reported once, with a count: as notices from the command, and on the pane's info line.
  • More tiles than the map has room for (MAX_TILE_LAYERS = 32) is refused, asking for a smaller area, as stac.addCube does. A cube that fails partway removes the layers it added.

Known limits and choices

  • Waiting: the command line, Python and the assistant wait for the one index read, plus one COG header (the first tile's latest year). That's the same as embeddings.search / embeddings.add today, and Python holds the GIL meanwhile, as for any command. The app's assistant runs its tools on the frame, so there the window waits too, as it does today for embeddings.search, layer.addUrl and the other commands that read the network; moving the assistant's tools off the frame would be a change for all of them. A plugin's command in the app (leave_downloads) is refused rather than waiting on the frame. The pane hands its own search over instead.
  • Web app: with found (the pane), the command works in the browser too. Without it, the search needs a blocking read, so it is refused there with a pointer to the pane.
  • Dates: each year is 1 January UTC (year_start_ms).
  • Bands: the 64 values have no physical meaning on their own, so per-band statistics over the years (cube.reduce, cube.trend) aren't offered for this cube. The useful analyses are cosine similarity and change between years, which come in the follow-up PR.

Tests

  • rustgis-tiles: index_rows_to_series. It covers matching years by place (including a footprint a hair off across a 1e-5° rounding boundary), another place making another tile, years outside the range, a duplicate year counted, titles and details, year_start_ms (1970, 2017, 2024), and aef_place.
  • rustgis-engine: parameters (13 refused addCube inputs, with the map left unchanged) and alphaearth_years_on_the_time_slider. The second checks two tiles, files in the slider's order with the AlphaEarth look plus cube=1, slider dates and YYYY, the missing-years notice, a tile hidden in a year it has no file for while the other shows, no notice when every year is there, an incomplete read's wording, a tile with no year in range left out and said, and other bands holding at every year after map.setTiles. That last check fails without the tiles.rs fix. These run offline, through found.
  • Checked by hand with this branch's rustgis-cli against the real index at Viçosa (MG, Brazil): embeddings.addCube gave {"tiles": 2, "years": 9, "missing": 0} in about 13 s, and share.exportMap at 2017 and 2025 gives different images.

Local runs, Windows: cargo fmt, and cargo clippy -p rustgis-tiles -p rustgis-engine -p rustgis-ui-egui --all-targets -- -D warnings clean. cargo test --lib: rustgis-tiles 428 passed; rustgis-ui-egui 789 passed (record_tests included); rustgis-engine 1225 passed, 1 failed: gdal_formats_open_when_gdal_is_installed, which fails on this PC because its GDAL is older than main needs (3.8), and is untouched here. After the first review: rustgis-tiles 428, rustgis-engine cmd:: 245, rustgis-ui-egui 789, all passed. cargo check --locked --target wasm32-unknown-unknown -p rustgis-web with RUSTFLAGS=-D warnings (LLVM's clang for zstd-sys) passes.

Screenshots

New UI (ui_shot, World Atlas sample zoomed to Viçosa): All years, on the time slider and the tiles listed over 2017–2025 with Add years:

After Add years, the time slider at 2017:

The same layer, slider at 2025:

No code was copied from other projects. AlphaEarth data: CC BY 4.0, "The AlphaEarth Foundations Satellite Embedding dataset is produced by Google and Google DeepMind."

🤖 Generated with Claude Code

…ddCube) (opengeos#1720)

- tiles: search_aef_series / aef_series find each AlphaEarth tile's file of
  every year in one read of the GeoParquet index, matched by place (each
  year's export names its files afresh); year_start_ms, aef_place.
- engine: embeddings.addCube adds a tile layer per tile with a COG per year
  in time_urls, the slider on over the years (YYYY), as stac.addCube does;
  missing tile-years, a partial index read, tiles past a search's list and
  a second file for a year are said. The slider part of stac.addCube is
  shared as cube_slider.
- map.setTiles gives a new look (bands, range) to every file of a data
  cube, not only the one shown.
- ui: Satellite Embeddings › All years, on the time slider, Add years, and
  Cancel while a search or read runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor
Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 thread crates/engine/src/cmd/embeddings.rs Outdated

/// What a search over the years left out: rows past one read, tiles past a search's list, and a
/// tile's second file for a year.
fn series_notices(s: &mut Session, f: &emb::SeriesFound) {

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.

Bug (wasm CI break), confidence high. series_notices is only called from the #[cfg(not(target_arch = "wasm32"))] block in search_series (line 333), so on wasm32 it is never used. ci.yml sets RUSTFLAGS: -D warnings for the wasm build (cargo build --target wasm32-unknown-unknown -p rustgis-web), so this dead_code warning fails that job.

Small fix: gate the function the same way as its only caller.

Suggested change
fn series_notices(s: &mut Session, f: &emb::SeriesFound) {
#[cfg(not(target_arch = "wasm32"))]
fn series_notices(s: &mut Session, f: &emb::SeriesFound) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a3bf404: series_notices is gated with #[cfg(not(target_arch = "wasm32"))], like its caller. cargo check --locked --target wasm32-unknown-unknown -p rustgis-web with RUSTFLAGS=-D warnings passes locally.

undo(s);
return Err(e);
}
if missing > 0 {

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.

Quality / accuracy of the notice, confidence moderate. missing counts every tile-year without a file among the rows that were read, and the message says these have "no file in AlphaEarth's index". When the read was cut short (SeriesFound.complete is false, which search_series reports only in its own notice), a tile-year can be missing because the read stopped, not because the index has no file. The count and the wording then disagree with what happened.

Suggest carrying complete from search_series into this message, e.g. "…have no file in the rows of AlphaEarth's index that were read (zoom in to read the rest)" when the read was incomplete. A test for an incomplete read would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a3bf404. search_series now returns whether the read was whole. With found, the pane hands over its search's own complete; it is a new internal parameter that defaults to true. When the read was cut short, the notice says "…have no file in the part of AlphaEarth's index that was read (zoom in to read it whole)…". alphaearth_years_on_the_time_slider covers it with "complete": false.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • wasm build breaks under -D warnings (high confidence). series_notices in crates/engine/src/cmd/embeddings.rs is only called from the non-wasm branch of search_series. On wasm32 it is dead code, and ci.yml builds rustgis-web with RUSTFLAGS: -D warnings. Fix: gate the function with #[cfg(not(target_arch = "wasm32"))]. Posted inline with a suggestion.

Security

  • No new issues found. The new found input is limited to https:// addresses of at most 4000 bytes, and embeddings.addCube carries the same Network effect as embeddings.add.

Performance

  • Desktop command route blocks the UI thread (low–medium confidence). embeddings.addCube run directly through Session::run without found does a blocking index read and a blocking header probe. The ribbon and command search are routed to the dialog by chrome.rs, so this mostly affects the assistant, scripts and Python. embeddings.search and embeddings.add already work this way, so this extends an existing pattern. The add_cube doc comment should say why this command runs on the caller's thread, as AGENTS requires.
  • The pane's Cancel drops the result but does not stop the index read or the header read (low confidence). Existing searches behave the same way, so this is not new.

Quality

  • The missing notice wording blames the index even when the read was incomplete (moderate confidence). Posted inline.
  • place_key rounds footprints to 1e-5°. Two years whose footprints differ by float noise at a rounding boundary would split into two series (low confidence, about 20%). A tolerance-based match or a comment on the boundary case would settle it.
  • found() silently drops tiles whose years all fall outside start..end, and the returned tiles count reflects only the kept ones (low severity). This is a nit, grouped here.

AGENTS.md

  • Metadata-only check: the never silent rules are followed for the new notices. The tiles.rs setTiles change keeps the cube=1 and AlphaEarth flags, because the new look is derived from the layer's own address. time.goTo with date (used in the docs example) and MAX_TILE_LAYERS = 32 both check out. Option::is_none_or is within the repo's MSRV of 1.90.
  • The UI strings use tl! and i18n::fmt, as required.

Prior reviews: no review threads exist on this PR, so nothing was left out as already raised.

Change size

Compared with the base branch (merge base e7d26e4 to PR head fd0a0fd, the whole PR, not the latest push).

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 321.3 KB
Size after 350.9 KB
Total size change +28.2 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 31.4 KB +13.6 KB
crates/tiles/src/embeddings.rs 33.7 KB 42.6 KB +8.9 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 27.8 KB +4.6 KB
docs/user-guide/web-data.md 48.5 KB 49.3 KB +0.8 KB
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +0.2 KB
crates/engine/src/cmd/stac.rs 62.5 KB 62.6 KB +0.1 KB
crates/ui-egui/src/chrome.rs 76.6 KB 76.6 KB +0.02 KB

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

📖 Docs preview

Item Value
Preview https://preview.rustgis.app/pr-1749/
Web app https://preview.rustgis.app/pr-1749/app/
Commit 71825ee

GitHub Pages takes a minute or two to publish a new commit.

elpidiofilho and others added 2 commits November 10, 2026 20:11
- series_notices is native only, as its one caller: the wasm32 build
  passes with -D warnings.
- add_cube's doc says why it runs on its caller's thread without found.
- The missing-years notice tells an incomplete read from the index's own
  gap; the pane hands its search's completeness over (complete).
- A tile handed over with no file in the years asked for is left out and
  said.
- A tile's years are matched by footprints agreeing to 1e-4 degrees, not
  by a rounded key a rounding boundary could split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elpidioassinaturas

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All of it is in a3bf404, and main is merged in 9f0338f.

  • wasm build (series_notices): fixed. The function is now #[cfg(not(target_arch = "wasm32"))], like its only caller. cargo check --locked --target wasm32-unknown-unknown -p rustgis-web with RUSTFLAGS=-D warnings passes locally.
  • Running on the caller's thread: add_cube's doc comment now says why. Without found, it waits for the index and one header on its caller's thread, as embeddings.search and embeddings.add do. The command line, Python and the assistant have no frame to hold, and they want the layers there when the command returns. The app's own routes open the pane, which searches off the frame and hands the tiles over. A plugin's command in the app (leave_downloads) is refused.
  • Cancel: right, it drops the result and doesn't stop the read. The existing searches do the same, and the read ends at its own limits (one index read, MAX_INDEX_ROWS). Stopping a read in flight would be a change for every search, so it isn't in this PR.
  • missing wording: fixed, see the inline thread.
  • place_key: the series are now matched by tolerance instead of a rounded key. Two rows are one tile when they have the same CRS and footprint corners within 1e-4° (about 10 m). That is well below a tile's 82 km, so no rounding boundary can split a tile. A read holds at most MAX_INDEX_ROWS rows, so comparing each row with the tiles found so far is cheap. The test now has a 2024 row 5.1e-6° off, across a 1e-5° boundary, and it still joins its tile.
  • found() dropping tiles with no year in range: it now says so: "N of the M tiles have no file in start–end and were left out". This is tested.

Comment thread crates/ui-egui/src/embeddingsui.rs Outdated
});
}
if cancel {
// The read runs on, but what it brings is dropped with its slot.

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.

AGENTS.md (Cancel), confidence moderate. Cancel only drops the result. For a search or series read (Read::Search / Read::Series) nothing tells query_url to stop, so the index read keeps running after the button is pressed. The read is bounded (MAX_INDEX_ROWS), so this is a cost, not a hang. But the repo rule is that Cancel stops the work, and this comment is the only thing that says it doesn't. Either pass a cancel flag into the query, or state in the doc comment why a bounded read is left running.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right: for a search or a series read, Cancel drops the result and nothing tells query_url to stop. The read is bounded (one read of the index, at most MAX_INDEX_ROWS rows), and every existing search in this pane works the same way, so this PR keeps that behaviour. 7b4131f updates the comment at the Cancel button to say this. #1726 brings per-download progress and Cancel in the status bar. Once it lands, these reads can move onto it in a follow-up PR, so that Cancel stops the read too.

for (y, u) in t.get("years").and_then(Value::as_object).ok_or_else(bad)? {
let y: i32 = y.trim().parse().map_err(|_| bad())?;
let u = u.as_str().map(str::trim).filter(|u| u.starts_with("https://") && u.len() <= 4000).ok_or_else(bad)?;
if (start..=end).contains(&y) {

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.

AGENTS.md (Never silent), confidence moderate. A year outside start..=end in a handed-over tile is dropped without a notice. A caller (CLI, assistant, plugin) that passes a 2016 file with start: 2017 loses that file silently. Either count the dropped years and report them through s.notify, or say in the doc comment that the filter is deliberate. The pane never triggers this, since its years come from the same range.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, and fixed in 7b4131f. Before the fix, the existing test's 2016 file (with start 2017) went without a notice: a new assertion failed on it. found() now counts the files outside start..=end, and the command says "N files handed over are outside start–end and were left out", next to its other notices. alphaearth_years_on_the_time_slider covers it.

Comment thread crates/engine/src/cmd/embeddings.rs Outdated
/// of the time slider. Each tile is a tile layer reading its file of the year the slider is at
/// (`time_urls`, as `stac.addCube` makes them); a year a tile has no file for shows nothing, said.
/// The tiles come from one read of the index, which holds every year (the command line waits for
/// it), or from the app's search off the frame (`found`). Natively only the first tile's latest

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.

Nit (quality, left for the release quality pass): the doc says natively only the first tile's header is read before the layers are added. With found, probe is false (line 242), so the header is not read at all on the pane path. Say so here, or the doc will mislead the next reader.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, thanks. Fixed in 2ad3136: the doc now says that without found only the first tile's latest header is read, and that with found (the pane, on the frame) none is.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None confirmed. The found path skips the COG header probe (probe is false when found is given, embeddings.rs:242). A non-COG file from that path would fail later, as a broken layer rather than an error at add time. Low confidence this matters in practice, since pane results come from the index.

Security

  • None found. Handed-over URLs are restricted to https:// and length-capped, and the command carries the same Network effect as embeddings.add.

Performance

  • Low. same_place (tiles/src/embeddings.rs:375) allocates two strings per comparison over a linear scan of tiles. At MAX_INDEX_ROWS = 4000 that is roughly 1–2M allocations, so probably tens of ms and off the frame. A keyed map would be cleaner. Confidence low.

Quality

  • Nit, left for the release quality pass: the add_cube doc comment at embeddings.rs:180 says the first tile's header is read natively. On the found path it is not (see inline comment).
  • The PR body says footprints are matched "rounded to about a metre". The code uses a 1e-4° tolerance (about 10 m), which matches its own doc comment.

AGENTS.md

  • Cancel in the pane (embeddingsui.rs:581) drops the result but does not stop the index read. Moderate confidence. Inline comment posted.
  • found() (embeddings.rs:323) drops out-of-range years without a notice, against "Never silent". Moderate confidence. Inline comment posted.

Already raised earlier and fixed in this head, so not re-posted: the wasm dead_code on series_notices (now cfg-gated) and the incomplete-read wording of the missing-years notice (now branched on complete). That is 2 earlier concerns left alone.

Verified by reading, not by running: the year_start_ms arithmetic, the time.goTo doc example ("date": "2017" parses), MAX_TILE_LAYERS = 32, and the stac.rs refactor (cube_slider gives the same slider payloads as before).

Change size

Compared with main (merge base 41315de to PR head 9f0338f): the whole PR, not the latest push.

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 322.1 KB
Size after 352.7 KB
Total size change +30.1 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 33.6 KB +15.7 KB
crates/tiles/src/embeddings.rs 33.7 KB 43.4 KB +9.6 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 28.1 KB +4.9 KB
docs/user-guide/web-data.md 48.5 KB 49.3 KB +0.8 KB
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +0.2 KB
crates/engine/src/cmd/stac.rs 62.5 KB 62.6 KB +0.1 KB
crates/ui-egui/src/chrome.rs 76.6 KB 76.6 KB +0.0 KB

opengeos#1720)

embeddings.addCube's found dropped a file outside start..=end without a
word: a caller passing a 2016 file with start 2017 lost it silently. The
files left out are now counted and said. The pane's Cancel comment says
why a search's bounded index read is left to run on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread crates/tiles/src/embeddings.rs Outdated
/// Whether two rows are one tile's: the same coordinate system, and footprints whose corners
/// agree to [`SAME_PLACE`].
fn same_place(a: &Tile, b: &Tile) -> bool {
let crs = |t: &Tile| t.detail.split(" · ").next().unwrap_or("").to_string();

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.

Nit (performance, confidence low-moderate). same_place runs for every index row against every tile seen so far (up to MAX_INDEX_ROWS = 4000 rows, so roughly rows × tiles calls), and each call allocates two Strings for the CRS prefix. Comparing borrowed &strs avoids that without changing behaviour:

Suggested change
let crs = |t: &Tile| t.detail.split(" · ").next().unwrap_or("").to_string();
fn crs(t: &Tile) -> &str {
t.detail.split(" · ").next().unwrap_or("")
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in 2ad3136. same_place now compares borrowed &strs, and it checks the corners first, so most non-matching rows fail at the first float comparison and never reach the text. For scale: one read holds at most MAX_INDEX_ROWS = 4000 rows, and each tile has up to 9 yearly rows, so a full read is about 450 tiles. That makes about 4000 × 450 / 2 ≈ 0.9 M comparisons, and the theoretical worst case (every row a different tile) about 8 M. Each one is a few float subtractions, which is milliseconds on the search thread, off the frame. I didn't benchmark it separately.

Comment thread crates/engine/src/cmd/embeddings.rs Outdated
}
}
}
let name = match (many, emb::aef_place(&file)) {

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.

Quality (confidence moderate). The place is appended to the layer name only when one call adds several tiles (many). The pane's **Add years' calls one tile at a time, so two tiles added from the pane get the same name, "AlphaEarth 2017–2025 (2, 17, 10)", and the layer list can't tell them apart. embeddings.add has the same pattern with the year. Suggest appending the place whenever aef_place gives one, or only when the name would clash with an existing tile layer. Not a data-loss issue, so this may wait for the release quality pass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed in 2ad3136: the place now goes in the layer name when the cube has several tiles, or when a tile layer with the same name is already on the map. So the pane's second Add years for another tile gives "AlphaEarth 2017–2025 (2, 17, 10) · 23S 0000008192-0000000000", and the first keeps its short name. alphaearth_years_on_the_time_slider covers two tiles added one at a time. I left embeddings.add (one year) as it is, since it's outside this PR.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

I found no bugs, security problems or AGENTS.md violations in the changed lines. Two low-stakes quality items are posted inline. I checked the three earlier concerns against the head code and they are addressed; I left all five prior threads alone.

Bugs: none found. I traced the cube assembly and its undo path, the found hand-over, year_start_ms (checked against 1970 and 2017), aef_place, the map.setTiles look propagation, and the stac.rs refactor into cube_slider. Bounds are handled with get/saturating_sub/try_from, with no panics on the new paths.

Security: none found. found URLs must be https:// and at most 4000 bytes, and the command keeps the existing Effect::Network declaration.

Performance

  • same_place allocates two Strings per call, and aef_series calls it for every index row against every tile seen so far (up to 4000 rows). Low to moderate confidence; the cost is probably around 100 ms at worst. Inline comment on crates/tiles/src/embeddings.rs:376.

Quality

  • Layer names only get the place suffix when one call adds several tiles, so two tiles added from the pane share a name. Moderate confidence; not data loss, so it can wait for the release quality pass. Inline comment on crates/engine/src/cmd/embeddings.rs:270.
  • found() silently overwrites a year key that repeats after trimming (for example "2017" and " 2017"). Very low confidence and only reachable through the internal parameter, so I did not post it.
  • On the desktop, a caller that is not the pane (the assistant, for example) runs the index read on the caller's thread. The doc comment and the existing embeddings.search behaviour both say so. I did not confirm whether the assistant dispatches on the UI thread, so the AGENTS.md "Never hold the window" question stays open. Low confidence.

AGENTS.md: the new command has a doc comment explaining why it can wait, the Cancel comment is updated, the wasm gating is in place, the notices are counted, and the tl! wrappers are used in the UI. The docs example uses time.goTo with a year, which parses. No violations found.

Prior threads (5): left alone. Three are fixed in the head: series_notices is gated for wasm, the incomplete-read notice wording is in place, and outside-range found files are counted and reported. The probe-doc nit is still open and belongs to the quality pass. The Cancel thread was answered, and the comment at the Cancel button is updated.

Change size

Compared with main (merge base 41315de to PR head 7b4131f). The whole PR, by file size.

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 321.5 KB
Size after 353.9 KB
Total size change +32.7 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 34.6 KB +16.8 KB
crates/tiles/src/embeddings.rs 33.7 KB 43.4 KB +9.6 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 28.4 KB +5.1 KB
docs/user-guide/web-data.md 48.5 KB 49.3 KB +0.8 KB
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +0.2 KB
crates/engine/src/cmd/stac.rs 62.5 KB 62.6 KB +0.1 KB
crates/ui-egui/src/chrome.rs 76.5 KB 76.6 KB +0.02 KB

…pengeos#1720)

- A cube whose name a tile layer already has takes its tile's place in
  it, so two tiles added from the pane one at a time can be told apart.
- add_cube's doc says no header is read with found.
- same_place compares borrowed CRS text, after the corners, with no
  allocation per comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread crates/engine/src/cmd/embeddings.rs Outdated
Comment on lines +198 to +202
let year_param = |k: &str, default: i32| match f(p, k) {
None => Ok(default),
Some(y) if y.fract() == 0.0 && y >= f64::from(first) && y <= f64::from(last) => Ok(y as i32),
Some(_) => Err(format!("{k} is a year from {first} to {last}")),
};

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.

Never silent, confidence high. f(p, k) returns None for any value that isn't a number or a numeric string. The None arm here then falls back to the default, so "start": "2020-01-01", "start": "twenty" or "start": true silently gives the full range 2017–2025 with no error and no notice. The caller asked for a narrower period and gets a different result. Only an absent or null key should take the default. Anything else present should be refused, as the out-of-range case already is.

Suggested change
let year_param = |k: &str, default: i32| match f(p, k) {
None => Ok(default),
Some(y) if y.fract() == 0.0 && y >= f64::from(first) && y <= f64::from(last) => Ok(y as i32),
Some(_) => Err(format!("{k} is a year from {first} to {last}")),
};
let year_param = |k: &str, default: i32| match p.get(k).filter(|v| !v.is_null()) {
None => Ok(default),
Some(_) => match f(p, k) {
Some(y) if y.fract() == 0.0 && y >= f64::from(first) && y <= f64::from(last) => Ok(y as i32),
_ => Err(format!("{k} is a year from {first} to {last}")),
},
};

A regression case belongs in the parameters test, e.g. json!({"dataset": "alphaearth", "start": "2020-01-01", "found": tile}), which should be an error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 8fe204e. Before the fix, "start": "2020-01-01" went through and the cube covered 2017–2025: the new parameters case failed on it. Now only a missing or null year takes the default, and anything else given is refused ("start is a year from 2017 to 2025"). "start": "2020-01-01" and "end": true are in parameters.

let y: i32 = y.trim().parse().map_err(|_| bad())?;
let u = u.as_str().map(str::trim).filter(|u| u.starts_with("https://") && u.len() <= 4000).ok_or_else(bad)?;
if (start..=end).contains(&y) {
out.insert(y, u.to_string());

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.

Never silent, confidence moderate. Two keys that trim to the same year (for example "2017" and " 2017") both pass y.trim().parse(), and out.insert keeps only the last one. The other file is dropped without a notice or an error, which is the silent loss the rule forbids. The simplest fix is to refuse a tile whose years map repeats a year, using out.contains_key(&y) before the insert and returning bad(). A test would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 8fe204e. A tile whose years names one year twice ("2017" and " 2017") used to be accepted, and one of the files was lost. The new parameters case failed on it. It is now refused, with "found: a tile names the year 2017 twice", and it is in parameters.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

I read the full diff and the staged AGENTS.md, then checked the cross-file assumptions it depends on. Those were the add_cog probe flag, setTiles keeping cube=1, time::set honouring unit and format, download_failed, f() parsing, and the leave_downloads contexts. I could not compile or run the tests here, so compile and test results come from the author's notes, not from me.

Bugs

  • No confirmed bugs. The wasm dead-code thread is fixed (series_notices is gated), and the setTiles fix keeps cube=1 because the new look is derived from the old URL.
  • Low confidence: same_place assumes a tile's footprint corners agree across years to within 1e-4°. The tests only cover a 5e-6° jitter, and the real-index check was one area (Viçosa). A tile that falls outside the tolerance becomes a separate card with partial years ("N of 9 years"). That is visible, not a silent loss, so I'm noting it rather than requesting a change.

Security

  • None found. found URLs are limited to https and 4000 characters, the list is capped at MAX_TILES, and the existing embeddings.add already accepts arbitrary https URLs.

Performance

  • None new. The prior same_place concern is addressed.

Quality

  • Moderate-high, Never silent: a non-numeric start or end silently falls back to the default year range (inline comment on embeddings.rs:198, suggestion included).
  • Moderate, Never silent: duplicate years in a handed-over found tile silently overwrite each other (inline comment on embeddings.rs:346).

AGENTS.md

  • Checked and compliant: no new unwrap/expect, interface strings use tl!/i18n::fmt, the Cancel comment is updated, the wasm gating is correct, and screenshots are attached.
  • The blocking index read when found is absent is documented in the doc comment, as the rule requires for commands that must wait. I did not raise it.

Prior threads

  • 7 earlier threads on this PR. All were answered with fixes or explanations, and I checked the fixes are present in the head. I left all 7 alone, since none of the new findings repeats them.

Confidence: the two inline findings are high and moderate. The footprint-tolerance note is low.

Change size

Compared with the base branch (merge base 41315de to PR head 2ad3136).

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 329.0 KB
Size after 363.4 KB
Total size change +34.3 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 18.3 KB 36.3 KB +18.0 KB
crates/tiles/src/embeddings.rs 34.6 KB 44.4 KB +9.9 KB
crates/ui-egui/src/embeddingsui.rs 23.8 KB 29.1 KB +5.2 KB
docs/user-guide/web-data.md 49.6 KB 50.5 KB +841 B
crates/engine/src/cmd/tiles.rs 60.4 KB 60.6 KB +253 B
crates/engine/src/cmd/stac.rs 64.0 KB 64.1 KB +105 B
crates/ui-egui/src/chrome.rs 78.4 KB 78.4 KB +23 B

elpidiofilho and others added 2 commits November 10, 2026 21:46
stac.rs: add_cube keeps calling cube_slider (main's slider block was unchanged), and main's range sampling (opengeos#1646) follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pengeos#1720)

- start or end given as something other than a year ("2020-01-01",
  true) took the default 2017-2025 without a word; only a missing or
  null one does now, anything else is refused.
- A found tile naming one year twice (" 2017" and "2017") kept one of
  its files and lost the other silently; it is refused.

Both reproduced in the parameters test first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread crates/engine/src/cmd/embeddings.rs Outdated

/// AlphaEarth's stretch a command names: dequantised values shown over ±stretch.
fn stretch(p: &Value) -> Result<f32, String> {
let stretch = f(p, "stretch").map(|v| v as f32).unwrap_or(emb::AEF_DEFAULT_STRETCH);

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.

Never silent, confidence high. f(p, "stretch") returns None for any non-numeric value, so "stretch": "big" or "stretch": true silently falls back to the default 0.3 and the cube is built with a look the caller didn't ask for. addCube is the new caller, and it takes the same default. Only an absent or null key should take the default; anything else given should be refused, as the range check already does. This is the same class of problem the start/end thread fixed.

Suggested change
let stretch = f(p, "stretch").map(|v| v as f32).unwrap_or(emb::AEF_DEFAULT_STRETCH);
let stretch = match p.get("stretch").filter(|v| !v.is_null()) { None => emb::AEF_DEFAULT_STRETCH, Some(_) => f(p, "stretch").ok_or("stretch is a number above 0, at most 1").map(|v| v as f32)? };

A regression case belongs in the parameters test, e.g. json!({"dataset": "alphaearth", "stretch": "big", "found": tile}).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 792df07. Before the fix, "stretch": "big" went through as 0.3: the new parameters case failed on it. Now only a missing or null stretch takes the default, and anything else given is refused ("stretch is a number above 0, at most 1"). stretch() is shared, so this covers embeddings.add as well as addCube. "big" and true are in parameters.

Comment thread crates/engine/src/cmd/embeddings.rs Outdated
for (y, u) in t.get("years").and_then(Value::as_object).ok_or_else(bad)? {
let y: i32 = y.trim().parse().map_err(|_| bad())?;
// Two keys of one year (" 2017" and "2017"): one file would be lost, so refused.
if seen.contains(&y) {

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.

Performance, confidence low-moderate. seen is a Vec checked with contains for every key of a tile's years object, so the duplicate check is quadratic in the number of keys. The object is not bounded in size, so a crafted found with many thousands of keys in one tile costs seconds of CPU on the calling thread. A BTreeSet<i32> (or a BTreeMap of all parsed keys, checked with contains_key) makes it O(n log n) without changing behaviour. If a size cap on years is preferred, a count check against the years in range would also do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in 792df07: the repeat check is now a BTreeSet<i32> (insert returns false for a year already seen), and the behaviour is unchanged.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

PR #1749 adds embeddings.addCube, which puts AlphaEarth's years on the time slider, plus the matching search, pane changes, and a map.setTiles fix. I read the full diff, the staged metadata and prior threads, and the code the diff calls into (add_cog_with, tiles::add, time.goTo, f(), download_failed, and stac.rs). I found two new issues, both posted inline.

Bugs

  • None found. I checked the dated path in add_cog_with: a cube tile carries no time_url on this route, so the probed flag is honoured and the pane path does not block on a header read.

Security

  • None found. found URLs are limited to https://, at most 4000 characters, and years are range-checked. This adds no new surface beyond embeddings.add {url}.

Performance

  • found() checks duplicate years with Vec::contains, which is quadratic in the number of keys in a tile's years object (embeddings.rs:348). Confidence low-moderate. It only matters for a crafted, very large input.

Quality

  • stretch() silently falls back to 0.3 for a non-numeric stretch, the same class as the start/end thread that was fixed (embeddings.rs:169). Confidence high. This is the one finding I'd fix before merge.
  • Nits, left for the release quality pass:
    • The years field in the result counts distinct years, while missing counts tile-years. The name is ambiguous.
    • The pane's info line concatenates translated fragments (" · {n} more left out…"), which breaks translation.
    • series_notices fires before the command's refusal checks, so a refused cube can still say "tiles past the limit were left out".

AGENTS.md

  • The non-found path blocks the caller's thread for the index read and one header. The doc comment explains why, and embeddings.search already works this way. The lib.rs comment says the assistant also runs commands that wait, so an assistant-run addCube could hold the window in the desktop app. Confidence low; I could not confirm whether the assistant runs on the frame.
  • The wasm gating of series_notices and the Cancel behaviour are both consistent with AGENTS.md.

Already raised, left alone (9 prior threads): the wasm dead-code warning, the partial-read notice, the silent extra years, the duplicate-year refusal, the Cancel behaviour, the header-read doc, the same_place cost, the layer naming, and the start/end junk values. All are addressed in the proposed code or answered by the author. I did not re-raise the Cancel concern, since the author's reply defers it to #1726 and documents it, which AGENTS.md allows.

Change size

Compared with the base branch (merge base 33debef to PR head 8fe204e).

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 332 KB
Size after 367 KB
Total size change +34.8 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 36.3 KB +18.5 KB
crates/tiles/src/embeddings.rs 33.7 KB 43.4 KB +9.6 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 28.4 KB +5.1 KB
docs/user-guide/web-data.md 52.5 KB 53.3 KB +822 B
crates/engine/src/cmd/stac.rs 69.2 KB 69.8 KB +537 B
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +247 B
crates/ui-egui/src/chrome.rs 76.6 KB 76.6 KB +23 B

elpidiofilho and others added 2 commits November 10, 2026 22:40
… gaps only for a cube added (opengeos#1720)

- stretch given as something other than a number ("big", true) took the
  default 0.3 without a word, in embeddings.add and addCube; only a
  missing or null one does now. Reproduced in the parameters test first.
- What a search over the years left out is said once the cube is added,
  not before a refusal that adds nothing.
- A tile's years are checked for repeats with a set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elpidioassinaturas

Copy link
Copy Markdown
Contributor Author

Thanks. Notes on the rest of this review (792df07, with main merged in 845f1ed, which includes #1751):

  • stretch: fixed, see the inline thread.
  • Repeated years: now a set, see the inline thread.
  • series_notices before a refusal: confirmed in the code. The notices were sent before the "no tiles" and "no room" refusals, so a refused cube could still say "tiles past the limit were left out". This path needs a live index read, so it can't be reproduced offline. The search now returns its notes, and add_cube says them only after the layers and the slider are in place.
  • years vs missing: years is the number of slider steps the cube brings (distinct years), and missing counts tile-years, as the notice says ("N of the M tile-years"). I'd rather keep the names than change the command's output now. The docs and the notice spell out each one.
  • Pane info line: each piece is a whole translated phrase. Only the " · " separator joins them, so a translator gets complete sentences. Merging them into one key for each combination would mean four keys for one line.
  • Caller's thread (AGENTS.md): same reason as embeddings.search and embeddings.add. The command line, Python and the assistant want the layers to be there when the command returns, and the doc comment on add_cube says so. In the app, the ribbon and the command search open the pane, which searches off the frame and hands its tiles over (found), so no header is read there. A plugin's command (leave_downloads) is refused rather than wait on the frame. The assistant goes through the same Session::run as embeddings.search today. If it ever runs on the frame, that would apply to every network command, not only this one.

/// reads one tile's; with `found` (the pane, on the frame) no header is read. The rest are read in
/// place as the map draws them.
///
/// Without `found` the command runs on its caller's thread and waits there for the index and that

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.

AGENTS.md (Never hold the window), confidence moderate. The justification here is that the assistant "ha[s] no frame to hold". But Session::run_leaving_downloads says the assistant's commands do wait, and the assistant runs inside the desktop app. So an in-app assistant call without found blocks the window for one index read plus a COG header (about 13 s in the PR's own check). A plugin's command in the app is refused for exactly this reason, but the assistant is not. Either refuse the waiting route in the app for the assistant too, or route the assistant to the pane as the ribbon does. The doc comment should match whichever is chosen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right that the in-app assistant runs on the frame, and the doc was wrong about it. The path is assistantpane::poll → with_agent → Agent::receive_result → Agent::run_tool → tools::run_with_budget → Session::run, on the UI thread with leave_downloads unset. So an assistant-run embeddings.addCube without found holds the window for the index read and one header.

The same is true today for every command that waits for the network when the assistant runs it: embeddings.search (search_aef_blocking), embeddings.add, layer.addUrl. Session::run_leaving_downloads documents it as deliberate ("the command line, scripts and the assistant run commands that wait, and see what was added"). There's no off-frame route for the assistant that this command could copy. Refusing only addCube for the assistant would leave the others as they are, and running the assistant's tools with leave_downloads or off the frame would change all of them. That belongs in its own issue, not in this PR.

71825ee corrects the doc comment and the PR text to say exactly this. I'm leaving this thread open for the maintainers' call on that wider change.

)
});
};
if dropped > 0 {

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.

AGENTS.md (Never silent), confidence low-moderate. dropped (tiles past MAX_TILES) and duplicates (second files for a tile-year) are reported only on this pane info line. They never go through Session::notify, and the found path of embeddings.addCube never sees them, so series_notes is only reached by the command-line route. The info line is replaced by the next search. Consider emitting these as session notices from the pane's add (for example by passing the counts to the command), or confirm the info line is the intended report.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the pane the info line is the intended report, and it is visible where the decision is made. It is drawn right above the result cards (embeddingsui.rs, if let Some(i) = &st.info), from when the search lands until the next search, a dataset change or Cancel. Adding a tile doesn't clear it, so "AlphaEarth: N found · 12 more left out: zoom in for them" or "· 3 tile-years had a second file; the first was taken" sits above the tiles the user is picking from. Nothing is lost silently on the pane route: a tile past MAX_TILES isn't listed, so it can't be added, and a duplicate tile-year was resolved at search time, before found is built. That's the moment the line reports it.

series_notes reaches the session's notices only on the command route, because there is no pane there to show it. The pane's single-year search reports complete the same way, so this follows that pattern. If you'd rather have the counts as session notices too, I can pass them to the command.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • No new correctness bugs found in the changed lines. I checked the year and stretch validation, the found parsing, the undo path, same_place, year_start_ms (Hinnant's algorithm), aef_place, the set() look propagation in tiles.rs, and the cube_slider extraction in stac.rs. The probed argument to add_cog matches its contract.

Security

  • None found. found accepts only https:// addresses of at most 4000 characters, and embeddings.add already accepts arbitrary https URLs, so this adds no new exposure. Network effects are declared and the new command is in NETWORK.

Performance

  • same_place is O(rows × tiles) over at most 4000 rows. The PR's estimate of about 0.9 M comparisons is plausible, and the work runs off the frame. Nothing new to raise.

Quality

  • Nit (for the release quality pass): years in the result counts distinct years across tiles, not tile-years, which reads ambiguously next to missing.
  • Nit (for the release quality pass): the cube_slider error for the slider-off case ("N dates; the time slider takes M at most") no longer names the cube, unlike the old message.

AGENTS.md

  • Never hold the window, confidence moderate: an in-app assistant call to embeddings.addCube without found blocks the window for the index read and a header. The doc comment's justification does not hold for the assistant. [Inline comment posted.]
  • Never silent, confidence low-moderate: the pane path reports dropped and duplicates only on the info line, not through Session::notify. [Inline comment posted.]

Prior reviews: 11 earlier threads. All are either fixed in the code I read (wasm gating, notice wording, outside-range and duplicate-year refusals, stretch and start/end parsing, the BTreeSet change, the layer-name tagging, the header doc, the same_place borrow) or answered by the author. The Cancel thread (the index read cannot be stopped) is answered and the comment at the Cancel button now says so, so I left it alone. I raised no concern again.

Change size

Compared with main (merge base a411d53 to PR head 845f1ed), the whole PR.

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 335 KB
Size after 371 KB
Total size change +35.4 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 36.9 KB +19.1 KB
crates/tiles/src/embeddings.rs 33.7 KB 43.4 KB +9.6 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 28.4 KB +5.1 KB
docs/user-guide/web-data.md 52.5 KB 53.3 KB +821 B
crates/engine/src/cmd/stac.rs 69.2 KB 69.8 KB +537 B
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +247 B
crates/ui-egui/src/chrome.rs 79.5 KB 79.5 KB +23 B

… frame (opengeos#1720)

The doc said the assistant has no frame to hold. The app's assistant runs its tools on the frame without leave_downloads, as it does for embeddings.search and layer.addUrl, so addCube without found waits there too. The doc now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/// How far apart (degrees, about 10 m) two years' footprint corners may be and still be one
/// tile's: far above the noise of the index's coordinates, far below a tile's 82 km, so no
/// rounding boundary splits a tile in two.
const SAME_PLACE: f64 = 1e-4;

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.

Matching tolerance, confidence low-moderate (not verified against the index). The comment justifies SAME_PLACE = 1e-4° by "far below a tile's 82 km", but the risk it has to cover is the other way: how much one tile's footprint can move between years. One 10 m pixel is about 9e-5° of latitude and about 1e-4° of longitude at Viçosa's latitude, so a footprint that shifts by two pixels between years would not match. That would split one tile into separate series, and the cube would show two layers with gaps instead of one. The gap would be reported as "tile-years have no file", but the cause would be misreported. The test only covers a 1e-5° offset. Suggest either a test with a 2–3 pixel shift, or a note on what the index's footprints were measured to do across years.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I checked this against the real index. I ran embeddings.search for each year from 2017 to 2025 in three areas of Brazil:

  • around Viçosa, MG: [-43.5, -21.2, -42.3, -20.3]
  • across the UTM 23/24 zone line: [-42.3, -21.0, -41.5, -20.2]
  • inland São Paulo: [-47.5, -23.2, -46.3, -22.3]

Then I grouped the rows by zone, the file's row/column suffix and the area (the name prefix changes every year), and compared the footprints.

Result: 16 tiles, 9 years each, 144 rows. Every tile's footprint is the same in all nine years, bit for bit. The largest corner spread, across all tiles and corners, is 0.0°. The index draws each year's export on the same UTM grid (8192 px tiles from a fixed origin per zone), so a tile doesn't move between years.

So 1e-4° covers float noise in the index's coordinates, which is all there is, and it is far from the 0.74° between neighbouring tiles. The comment's "far below a tile's 82 km" is about not merging neighbours. The other side, how far one tile moves between years, is 0 here. If a future export ever shifted a tile, the series would show up as two tiles with their years split, which the "N of M years" detail would make visible. No change in this PR.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

I read the full diff, the staged AGENTS.md, and the 13 prior review threads. Most of the diff is consistent with the repository rules. I found one new concern, posted inline. Everything else I checked is either already raised in a prior thread or does not hold up on reading.

Bugs

  • Footprint match tolerance may split one tile in two (crates/tiles/src/embeddings.rs:371). SAME_PLACE is 1e-4°, about one 10 m pixel. If a tile's footprint moves by two pixels between years, its years split into separate series. The tile count doubles and the gaps are misreported as missing years. Confidence: low-moderate, since I could not check the index offline. Posted inline.

Security

  • Nothing new. found accepts only https:// addresses up to 4000 characters, and embeddings.add already accepts arbitrary https URLs, so the trust level is unchanged.

Performance

  • Nothing new. The quadratic same_place scan and the duplicate-year check were already raised and fixed in the PR.

Quality

  • The years value in the embeddings.addCube result counts distinct years, not tile-years, and the name is ambiguous next to the "tile-years" notices. Confidence: moderate. Left for the release quality pass.
  • When the time slider is already on, add_cube's unit and format are not applied. The years then display in whatever unit the slider has. This looks intentional, matching stac.addCube. Confidence: low.

AGENTS.md

  • Already raised, not re-posted: an in-app assistant's embeddings.addCube without found waits on the frame. The author answered and pointed to a separate issue.
  • Already raised, not re-posted: Cancel on a search or series read does not stop the index query. The author answered and documented it.
  • Already raised, not re-posted: dropped and duplicates from the pane's search are shown only on the info line, not as session notices. The thread has no reply.

Prior threads: 13 in total. I left all 13 alone, because each is either answered and fixed or already raised and still open.

Change size

Compared: the whole PR against main (merge base a411d53 to PR head 71825ee).

Measure Value
Files changed 7 (0 added, 7 modified, 0 removed, 0 renamed)
Size before 335.0 KB
Size after 370.8 KB
Total size change +35.8 KB
Path Before After Change
crates/engine/src/cmd/embeddings.rs 17.9 KB 37.3 KB +19.4 KB
crates/tiles/src/embeddings.rs 33.7 KB 43.4 KB +9.6 KB
crates/ui-egui/src/embeddingsui.rs 23.3 KB 28.4 KB +5.1 KB
docs/user-guide/web-data.md 52.5 KB 53.3 KB +0.8 KB
crates/engine/src/cmd/stac.rs 69.2 KB 69.8 KB +0.5 KB
crates/engine/src/cmd/tiles.rs 58.9 KB 59.2 KB +0.2 KB
crates/ui-egui/src/chrome.rs 79.5 KB 79.5 KB +23 B

This branch has not been deployed

No deployments
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.

2 participants