mixle.inference.torsion module

Twisted composition for mixture components with a shared base density.

Mixture components can share one base density modulo a declared group action, instead of each independently learning a density from scratch on its own slice of data. Concretely, CyclicGroup acts on a periodic coordinate by rotation of its (cos, sin) embedding (an exact, Jacobian-1 change of variables, so a fitted embedding density scores identically whichever group element aligned a point into it); fit_twisted_mixture() pools every group’s data into ONE shared base density after undoing each group’s twist, so the shared density is fit on the union – effectively |groups| times the data for the same parameter count as fitting one group alone.

Use this as an experimental modeling option. If the shared-base model does not beat independently fit per-group models at matched per-component capacity on held-out per-group log likelihood, keep the independent baseline as the default.

class CyclicGroup(order, period=1.0)[source]

Bases: object

Z_``order`` acting on a periodic real-valued coordinate of period period by rotation.

Each group element k in {0, ..., order - 1} is realized concretely as a rotation of the coordinate’s (cos, sin) embedding by angle 2*pi*k/order – an orthogonal (norm- and Jacobian-preserving) transform, so composing group elements is exactly addition mod order (compose()), and a density fit on the embedding is unaffected by which element aligned a point into it (the twist is undone before scoring, not baked into the density).

Parameters:
embed(x)[source]

The periodic coordinate’s (cos, sin) embedding, shape (..., 2).

Parameters:

x (Sequence[float])

Return type:

ndarray

act(embedded, k)[source]

Rotate an (..., 2) embedding by group element k (the forward twist).

Parameters:
Return type:

ndarray

inverse_act(embedded, k)[source]

Undo group element k’s twist – act(inverse_act(v, k), k) == v.

Parameters:
Return type:

ndarray

compose(k1, k2)[source]

The group element equivalent to applying k1 then k2 – addition mod order.

Parameters:
Return type:

int

class TwistedMixtureResult(base_density, group)[source]

Bases: object

A single shared base density plus the group whose elements twist it into each group’s local factor.

Parameters:
  • base_density (Any)

  • group (CyclicGroup)

log_density(x, k)[source]

log p(x | group=k): undo k’s twist, then score under the shared base density.

Parameters:
Return type:

ndarray

fit_twisted_mixture(group, data_by_group, *, n_components=2, seed=0, max_its=50)[source]

Fit ONE base density on every group’s data pooled together after undoing each group’s twist.

data_by_group maps a group element k to that group’s (small) sample of the periodic coordinate. Every sample is embedded and rotated back by its own group’s inverse_act before pooling – so the fitted n_components-component density sees sum(len(v) for v in data_by_group.values()) points, not just one group’s slice, for the same parameter count as fit_independent_mixtures() spends on a SINGLE group.

Parameters:
Return type:

TwistedMixtureResult

fit_independent_mixtures(group, data_by_group, *, n_components=2, seed=0, max_its=50)[source]

The untwisted baseline: one independently-fit n_components-component density per group.

Same per-group parameter count as the shared base density in fit_twisted_mixture(), but |groups| times the total parameters overall, and each fit sees only its own group’s (small) sample – the comparison fit_twisted_mixture() is measured against.

Parameters:
Return type:

dict[int, Any]

independent_log_density(models, group, x, k)[source]

Score x under group k’s independently-fit density (the baseline sibling of TwistedMixtureResult.log_density()).

Parameters:
Return type:

ndarray