Skip to content

feat(math): Clojure-exact base-cluster lineage + warm start (PR-C)#2622

Draft
jucor wants to merge 1 commit into
spr/edge/2b7f93f6from
spr/edge/ca2af3c7
Draft

feat(math): Clojure-exact base-cluster lineage + warm start (PR-C)#2622
jucor wants to merge 1 commit into
spr/edge/2b7f93f6from
spr/edge/ca2af3c7

Conversation

@jucor

@jucor jucor commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Port the warm-start k-means Clojure threads across conv-update ticks
(:last-clusters, clusters.clj:301-312 / conversation.clj:403-445) into a
new module polismath/pca_kmeans_rep/legacy_kmeans.py and wire it into the
'clojure-legacy' engine mode of Conversation._compute_clusters. Improved
mode (default) is byte-for-byte unchanged.

Lineage semantics (all cited to math/src/polismath/math/clusters.clj):

  • cold init = first-k-distinct rows, ids 0..k-1 (init-clusters, :55-65),
    reusing production _get_first_k_distinct_centers so cold init is identical
    to kmeans_sklearn's first-k init.
  • clean-start-clusters (:230-277): safe-recenter drop-vanished + fallback
    (:171-191), uniqify identical centers (:220-227), most-distal split loop
    with new ids = (inc max id) (:202-217, :267).
  • merge keeps the LARGER cluster's id, tie -> later arg (merge-clusters, :194-199).
  • cluster-step drops empty clusters, ties -> later cluster (:142-158).
  • same-clustering sorts centers and zip-TRUNCATES to the shorter (utils/zip),
    reproduced deliberately (:68-76).

Wiring (conversation.py):

  • base level: legacy_kmeans warm-started from prev.base_clusters (base-iters
    = 100). Base-cluster ids are stable across ticks; new participants get
    strictly larger ids.
  • group level: per-k legacy_kmeans over base-cluster CENTERS, weighted by
    base-cluster member counts (:weights base-clusters-weights,
    conversation.clj:444), warm-started from prev per-k clusterings. Clojure
    passes the MISNAMED :cluster-iters key that kmeans ignores, so the group
    level runs kmeans' DEFAULT max-iters (20), not group-iters (100) — matched.
  • self.group_clusterings now holds id-carrying cluster dicts {id,members,center}
    in legacy mode (was the (labels,centers,member_lists,silhouette) tuple), so
    the next tick can warm-start from it. improved mode never writes it (stays {}).
  • recompute() threads prev_base_clusters alongside the existing prev_* state.

Cold-start invariance (HARD GATE): MEASURED on vw (67 in-conv participants ->
67 singleton base clusters; groups k=2..5) — legacy cold clustering is
STRUCTURALLY bit-identical to improved at BOTH levels (same base/group
memberships, ids, counts, and all downstream repness/priorities/group-votes).
Cluster CENTER coordinates differ only at floating point (~1e-13: ported
weighted-mean via np.average vs sklearn centroid on identical memberships).
The existing test_engine_mode cold-identity test is adjusted to compare
numbers with a 1e-6 tolerance and everything else exactly, plus a new
belt-and-braces test that drops center coords and asserts EXACT structural
equality. On near-duplicate projections (a degenerate synthetic set) legacy
and improved DIVERGE at the base level too: legacy keeps each near-dup as its
own exact-init singleton (Clojure-faithful) while sklearn's Lloyd collapses a
pair and leaves an empty cluster; base cold identity therefore holds only when
projections are distinct (vw), and legacy is the more Clojure-faithful side.

RED evidence (before this commit):

  • tests/test_legacy_kmeans.py: ModuleNotFoundError (module absent).
  • tests/test_base_cluster_lineage.py warm-start-threading tests: legacy_kmeans
    never called (0 calls); group_clusterings stored a tuple not id-carrying
    dicts; a new participant received id 0 (no lineage) instead of a larger id.
    GREEN: 22 unit tests (hand-derived from the Clojure rules) + 8 integration
    tests (incl. vw cross-mode cold identity) pass; full suite 511 passed / 17
    skipped / 47 xfailed (was 480/17/47), zero regressions.

Not ported: the agg-bucket-votes unknown-pid edge (conv_edge_cases_test.clj:
110-127) is not applicable — Python base-cluster members are always the current
in-conv pids (subset of rating_mat.index) and _compute_group_votes already
skips unknown pids defensively (conversation.py:1481-1486).

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

commit-id:ca2af3c7


Stack:


⚠️ Part of a stack created by spr. Do not merge manually using the UI - doing so may have unexpected results.

@jucor
jucor marked this pull request as draft July 18, 2026 02:29
@jucor
jucor force-pushed the spr/edge/2b7f93f6 branch from ec7b7a2 to b44a080 Compare July 18, 2026 13:28
@jucor
jucor force-pushed the spr/edge/ca2af3c7 branch from 99c120b to 380f97a Compare July 18, 2026 13:28
@jucor
jucor force-pushed the spr/edge/2b7f93f6 branch from b44a080 to 98736b7 Compare July 18, 2026 13:32
@jucor
jucor force-pushed the spr/edge/ca2af3c7 branch 2 times, most recently from 6a3085f to eb3dcb5 Compare July 18, 2026 13:35
@jucor
jucor requested a review from Copilot July 21, 2026 08: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.

Pull request overview

Adds a Clojure-faithful, lineage-preserving k-means implementation for the clojure-legacy engine mode and wires it into the Delphi Conversation clustering pipeline, enabling warm-start clustering across ticks while keeping the default improved path unchanged.

Changes:

  • Introduces legacy_kmeans.py, a Python port of Clojure’s clusters.clj k-means semantics (lineage IDs, clean-start behavior, weighted recentering).
  • Wires legacy warm-start clustering into Conversation._compute_clusters (base + group levels) and threads previous-tick base/group clustering state via recompute().
  • Adds/updates unit + integration tests to validate lineage behavior and cold-start invariance (with float-tolerant center comparisons).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
delphi/polismath/pca_kmeans_rep/legacy_kmeans.py New Clojure-faithful k-means + lineage implementation used in legacy engine mode.
delphi/polismath/conversation/conversation.py Integrates legacy k-means warm-start at base/group levels; threads previous tick state through recompute.
delphi/tests/test_legacy_kmeans.py Unit tests covering the ported Clojure k-means rules and lineage semantics.
delphi/tests/test_base_cluster_lineage.py Integration tests validating warm-start threading and base-cluster id lineage across ticks.
delphi/tests/test_engine_mode.py Updates cold-start invariance checks to allow float noise in cluster centers; adds structural equality check.
delphi/tests/test_group_k_smoother.py Adds tests for legacy smoother behavior on degenerate ticks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread delphi/tests/test_engine_mode.py
Comment thread delphi/tests/test_legacy_kmeans.py Outdated
Comment on lines 894 to 896
else:
self.group_clusters = []
self.subgroup_clusters = {}
@jucor
jucor force-pushed the spr/edge/2b7f93f6 branch from 0eea394 to fd4aa0d Compare July 21, 2026 11:11
@jucor
jucor force-pushed the spr/edge/ca2af3c7 branch from eb3dcb5 to 45d5fca Compare July 21, 2026 11:11
@jucor
jucor force-pushed the spr/edge/2b7f93f6 branch from fd4aa0d to 4f39d5e Compare July 21, 2026 19:49
@jucor
jucor force-pushed the spr/edge/ca2af3c7 branch from 45d5fca to 46286fc Compare July 21, 2026 19:49
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 00:51
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 02:01
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 02:02
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 02:10
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 02:10
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 03:54
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 03:54
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 06:16
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 06:16
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 06:50
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 06:50
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 08:11
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 08:11
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 09:58
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 09:58
@jucor
jucor changed the base branch from spr/edge/2b7f93f6 to edge July 22, 2026 11:21
@jucor
jucor changed the base branch from edge to spr/edge/2b7f93f6 July 22, 2026 11:21
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