Skip to content

Tolerate an album with no chosen Cluster Strength - #374

Closed
lstein wants to merge 1 commit into
masterfrom
lstein/fix/tolerate-unset-umap-eps
Closed

Tolerate an album with no chosen Cluster Strength#374
lstein wants to merge 1 commit into
masterfrom
lstein/fix/tolerate-unset-umap-eps

Conversation

@lstein

@lstein lstein commented Aug 19, 2026

Copy link
Copy Markdown
Owner

umap_eps — the semantic map's DBSCAN epsilon, shown in the UI as Cluster Strength — was a required float. A newer PhotoMapAI writes umap_eps: null for albums that leave the value to the app, and parsing that into a non-nullable field failed the whole config load:

RuntimeError: Failed to load configuration from config.yaml:
1 validation error for Album
umap_eps  Input should be a valid number [type=float_type, input_value=None]

One optional per-album map setting stopped the app from starting, with no way out but hand-editing YAML. Nothing in a config file should be able to do that — and the two builds share the file whenever a user moves between releases. (This is not hypothetical: it is how the bug was found, on a real config with two such albums.)

What changed

The field is nullable now, meaning "nobody chose one". An unset value is written as an absent key rather than an explicit null, so a config this build writes still loads on one that predates the change.

Everything that consumes the value resolves it through a single resolve_umap_eps helper, which substitutes DEFAULT_UMAP_EPS. Routing both map endpoints through one helper is the point: /umap_data and /cluster_labels must resolve the same epsilon for the same request, or the cluster ids they return describe different clusterings and the hover labels attach to the wrong blobs.

The helper treats zero and negatives as "not chosen" too, because DBSCAN raises on them. /set_umap_eps/ accepted a negative epsilon, stored it happily, and left the map answering 500 until someone edited the file by hand; the request model rejects it now, so a stored value can always be used.

It also names the constant. Two places spelled the fallback out and disagreed — the field default said 0.2, the YAML reader said 0.07.

Tests

Eight cases: an explicit null, an absent key and a chosen value all load; an unset value round-trips as an absent key; /get_umap_eps reports what the map will actually use; /umap_data and /cluster_labels both cluster an album that never chose one; and a non-positive strength is refused. Each was checked by reverting its production hunk and confirming it fails — including the /umap_data fallback, which an adversarial review caught as load-bearing but untested.

Backend 675 passed, frontend 578 passed, ruff clean.

Relationship to the adaptive Cluster Strength work

lstein/feature/adaptive-cluster-eps makes the same field nullable and means something richer by None ("derive an epsilon from the album's own coordinates"). The two are compatible: identical YAML shape, neither writes an explicit null, and configs written by either load on the other. Only the resolution differs — a fixed default here, a derived value there — so they will conflict textually on a few comment lines and nowhere semantically. This branch is the robustness half, and it stands alone: the crash it fixes affects anyone whose config was ever touched by the newer build.

🤖 Generated with Claude Code

``umap_eps`` — the semantic map's DBSCAN epsilon, shown in the UI as
Cluster Strength — was a required float. A newer PhotoMapAI writes
``umap_eps: null`` for albums that leave the value to the app, and
parsing that into a non-nullable field failed the *whole* config load:

    RuntimeError: Failed to load configuration from config.yaml:
    1 validation error for Album
    umap_eps  Input should be a valid number

One optional per-album map setting stopped the app from starting, with
no way out but hand-editing YAML. Nothing about a config file should be
able to do that, and the two builds share the file whenever a user moves
between releases.

The field is nullable now, meaning "nobody chose one". An unset value is
written as an *absent* key rather than an explicit null, so a config this
build writes still loads on one that predates the change. Everything
that consumes the value resolves it through ``resolve_umap_eps``, which
substitutes ``DEFAULT_UMAP_EPS`` — the map and the label endpoints have
to agree here, or the cluster ids they return describe different
clusterings and the hover labels attach to the wrong blobs.

That helper treats zero and negatives as "not chosen" too, because
DBSCAN raises on them: ``/set_umap_eps/`` accepted a negative epsilon,
stored it, and left the map answering 500 until someone edited the file.
It is rejected at the request model now, so the stored value can always
be used.

Also names the constant. Two places spelled the fallback out and
disagreed — the field default said 0.2, the YAML reader said 0.07.

Tests: an explicit null, an absent key and a chosen value all load;
unset round-trips as an absent key; ``/get_umap_eps`` reports what the
map will actually use; ``/umap_data`` and ``/cluster_labels`` cluster an
album that never chose one; and a non-positive strength is refused.

Backend 675 passed, frontend 578 passed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lstein

lstein commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #370 + #375

@lstein lstein closed this Aug 21, 2026
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.

1 participant