mixle.utils.hvis.affinity module

Factor/affinity computation for model-based (hierarchical) t-SNE/UMAP.

This module turns a fitted mixture model (or pre-built factors) into the per-pair log affinities that drive the embedding: it builds the affinity factor list for each affinity mode, computes the dense log-affinity matrix, and converts a log-affinity matrix into row-conditional t-SNE probabilities. See the package docstring for the affinity definitions.

balanced_factors(mix_model, data, field_weights=None)[source]

Per-field Bhattacharyya affinity factors for heterogeneous models.

The joint posterior is dominated by whichever field has the largest log-likelihood contrast across components - sharp categorical fields, long token-sequence fields, or collapsed continuous components can contribute many nats of contrast while overlapping continuous fields contribute fractions of one. The drowned fields’ relationships then become invisible to any affinity computed from the joint posterior.

‘balanced’ fixes the scale problem at the affinity level: a field- restricted posterior z^f is computed from each field’s likelihoods alone (fields are the model’s flattened leaves - nested composites, sequence element/length models, and optional wrappers all decompose; see _field_log_densities), and the affinity combines per-field Bhattacharyya coefficients, so every field contributes comparably regardless of its likelihood scale. field_weights apply as exponents on whole field coefficients, i.e. weights on log field-affinities. Combined with an evidence cap (see model_log_affinity) no single field can veto a pair’s similarity either.

local_factors(mix_model, data, field_weights=None)[source]

Per-field local statistical affinity factors.

Each leaf field is first represented by its field-restricted component posterior, and EVERY field also carries within-component local geometry: continuous/count leaves (and averages of such leaves inside sequences) use their native coordinates; every other leaf – HMMs, Markov chains, categoricals, sequence-of-discrete element fields – uses typicality coordinates (per-component log-density; per-token rate plus a log-length axis for sequence-valued leaves, see _typicality_coordinates). Without that universal fallback, sharp posteriors make all same-component pairs exact ties and clusters render as tiny structureless points – the collapse this affinity exists to prevent. The factor carries component-local inverse covariances estimated from the realized data (which also makes heterogeneous fields dimensionless, so continuous, discrete, and sequence evidence are commensurate). Pair affinities then use

sum_k sqrt(z_ik z_jk) exp(-delta_ijk / 8),

where delta_ijk is the component-local Mahalanobis distance in that field’s coordinates. This is the local Fisher quadratic in the plug-in model, with posterior overlap handling component uncertainty.

fisher_factors(model, data=None, enc_data=None, metric='diagonal', ridge=1.0e-8, weight=1.0, information='observed')[source]

Fisher-vector affinity factor for a model and observations.

The model supplies posterior-expected sufficient statistics through to_fisher(). By default those statistics are treated as observed score vectors and whitened by their empirical observed Fisher covariance. Set information=’model’ to use the view’s model Fisher metric directly. Pair affinities are s_ij = exp(-0.5 ||v_i - v_j||^2).

Parameters:
model_log_affinity(posterior_mat, ll_mat=None, affinity='bhattacharyya', evidence_cap=None)[source]

Dense n x n matrix of log affinities (see module docstring) with -inf diagonal.

Rows are comparable up to a per-row shift, which both the row-conditional normalization and per-row perplexity calibration are invariant to.

evidence_cap bounds the dissimilarity evidence any single factor (field) may contribute: each factor’s log affinity is floored at -evidence_cap nats before the factors are summed. Without the cap a single sharp field with (near-)disjoint per-field posteriors drives its log affinity to -inf and vetoes the pair no matter what every other field says; with it, a field can at most testify “these differ by evidence_cap nats”. The cap is only applied to multi-factor (per-field) affinities - for a single factor it could only create ties.

Parameters:
Return type:

ndarray

mixture_coordinates(mix_model, data, field_weights=None)[source]

The observation decomposition made first-class: x -> (posterior, remainder per field).

The mixture describes every observation at two levels, and this returns both explicitly: "posterior" – the (n, K) component posterior, literally barycentric coordinates on the simplex whose vertices are the components (the between-cluster geometry); "fields" – one entry per flattened leaf field with its per-component log-densities, its within-component coordinates ("coords": native value coordinates where the leaf has them, universal typicality coordinates otherwise), and "native" recording which. This is exactly the decomposition the ‘local’ affinity is built from; exposing it lets a caller inspect or plot the two levels directly (e.g. a ternary plot of the posterior for K=3) instead of trusting the embedding blindly.

Return type:

dict

component_map(z, emb_dim=2, *, method='nerve', edge_threshold=0.02)[source]

Lay out the K components as vertices by their overlap geometry on the data.

method='nerve' (default): geodesic layout of the cover’s nerve – edge lengths are -log BC on STRONG edges only (see mixle.utils.hvis.topology.fuzzy_nerve()), all-pairs shortest paths give the target metric, and deterministic stress majorization embeds it. This is Isomap on the nerve: a ring of components renders as a ring and a chain as a line, where bare MDS on the clipped dense -log BC matrix (every non-overlapping pair saturating at the same huge distance) distorts both – the classic horseshoe failure. Disconnected pieces of the nerve are laid out separately and placed side by side with an explicit gap; their on-screen separation is a RENDERING choice, which mixle.utils.hvis.topology.nerve_report() also says outright.

method='mds': the previous behavior – classical MDS on the dense clipped -log BC matrix. Kept as the fallback and for comparison.

Component confusability itself is unchanged: the Bhattacharyya coefficient between the components’ responsibility profiles. These vertices anchor barycentric_init() and mixle.utils.hvis.direct.model_map().

Parameters:
Return type:

ndarray

barycentric_init(z, emb_dim=2, *, jitter=0.15, seed=None)[source]

Initial embedding coordinates from the barycentric reading of the posterior.

Each observation starts at z @ vertices – its posterior-weighted combination of the component vertices from component_map() – so the layout’s GLOBAL arrangement (which clusters sit near which, where mixed-membership points fall) is decided by the model’s own geometry rather than by the random seed, and t-SNE’s optimization refines locally from there.

jitter is a fraction of the smallest nonzero inter-vertex distance and matters more than it looks: sharp posteriors put every same-regime point EXACTLY on its vertex, and t-SNE from near-coincident starts is chaotic (microscopic noise decides the layout) and slow to develop local structure. The decomposition needs both levels even at init time – the barycentric base supplies the between geometry, the jitter stands in for the within spread the optimization then makes real. Rescaled to the conventional 1e-4 standard deviation so optimizer dynamics (early exaggeration, learning rates) match the random-init path.

Parameters:
Return type:

ndarray

log_affinity_block(factors, row_idx, col_idx, evidence_cap=None)[source]

Rectangular (rows x cols) log-affinity block – model_log_affinity() for a sub-block.

Mirrors the square path exactly: per-factor similarity blocks, log, per-factor evidence cap (multi-factor affinities only), weighted sum. Used by streaming placement (new points x landmarks) and by affinity_health() (subsampled diagnostics).

Parameters:
Return type:

ndarray

affinity_health(mix_model, data, *, affinity='auto', perplexity=30.0, field_weights=None, evidence_cap=1.0, max_rows=400, seed=0)[source]

Receipts for “why does my embedding look like this”: measure the affinity’s degeneracies BEFORE spending an optimization on them.

The classic failure this catches is posterior collapse: sharp posteriors make every same-component pair an exact tie, rows cannot reach the requested perplexity, and t-SNE renders each cluster as a tiny structureless point. That is a property of the AFFINITY, measurable in milliseconds – not a property of the optimizer, discoverable after a thousand iterations.

Returns a dict with per-field entries (geometry: 'local'/'fisher'/ 'posterior-only'; posterior_sharpness: mean max field-posterior, 1.0 = fully hard) and overall numbers on a row subsample of at most max_rows:

  • top_tie_fraction – mean fraction of each row’s neighbors tied (within 1e-9) with its best neighbor. Near 0 is healthy; large means nearest-neighbor structure is degenerate.

  • row_entropy_deficit_nats – mean shortfall between the requested log(perplexity) and the entropy each row can actually reach (ties saturate the calibration). 0 is healthy.

  • diagnosis – plain-language findings, empty when healthy.

Parameters:
  • perplexity (float | None)

  • evidence_cap (float | None)

  • max_rows (int)

  • seed (int)

Return type:

dict

conditional_pmat(log_aff, perplexity=None)[source]

Row-conditional probabilities p_{j|i} from log affinities (diagonal -inf).

With perplexity set, each row is calibrated so its entropy equals log(perplexity); otherwise the raw row softmax is used.

Parameters:
Return type:

ndarray

get_pmat(posterior_mat, ll_mat=None, targ_perplexity=None, vlen=False, affinity='bhattacharyya', evidence_cap=None)[source]

Symmetrized t-SNE input probabilities from model posteriors (and optionally component log-likelihoods, for affinity=’likelihood’).

The vlen flag is kept for backward compatibility and ignored.

Parameters:
  • affinity (str)

  • evidence_cap (float | None)