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.

The rotation is Jacobian-1, but the (cos, sin) embedding is not a change of variables at all: it maps the 1-D coordinate onto a measure-zero circle in R^2. An ambient 2-D density evaluated on that circle is therefore NOT a density over x – it integrates over one period to an arbitrary constant that differs from fit to fit. Every scoring entry point here (TwistedMixtureResult.log_density(), independent_log_density()) divides out that constant via log_circular_normalizer(), so the returned values are genuine log densities on [0, period) and the twisted-vs-independent held-out likelihood comparison this module exists to make is between comparable quantities.

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.

NORMALIZER_GRID = 4096

Quadrature points per period used by log_circular_normalizer(). The integrand is smooth and periodic, so the uniform-grid (periodic trapezoid) rule converges spectrally – a few thousand nodes pin the normalizer far below any difference the twisted/independent comparison turns on.

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).

order must be an exact positive integer and period a finite positive number: a zero order divides by zero in the rotation law, a negative or fractional one describes no cyclic group at all, and a zero/non-finite period makes every embedding NaN. {0, ..., order - 1} is the whole element set, and it is checked – an out-of-range k used to be silently wrapped into a different element, so -1, order, order + 1 and True all acted as undeclared aliases.

Parameters:
element(k)[source]

Validate k as a declared group element and return it as a plain int.

Parameters:

k (Any)

Return type:

int

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

log_circular_normalizer(density, group, *, grid=NORMALIZER_GRID)[source]

log ∫_0^period q(embed(t)) dt – what turns an ambient embedding score into a density on x.

embed places the periodic coordinate on a measure-zero circle in R^2, so an ambient 2-D density q restricted to it does not integrate to one over a period; it integrates to a fit-specific constant. Subtracting this log constant from log q(embed(x)) yields a normalized density on [0, period).

The value does not depend on the group element: inverse_act(embed(x), k) == embed(x - k*period/order), and integrating over a whole period is invariant to that shift – so one normalizer serves every k.

Parameters:
  • density (Any)

  • group (CyclicGroup)

  • grid (int)

Return type:

float

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_normalizer()[source]

The (cached, k-independent) log constant that normalizes the shared base density over a period.

Return type:

float

log_density(x, k)[source]

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

A normalized density on the periodic coordinate – exp of this integrates to 1 over one period – so values are comparable against independent_log_density() and across fits.

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()).

Normalized over one period by that model’s own log_circular_normalizer() – each independent fit has a different embedding normalizer, so without this the baseline comparison would be decided by normalization rather than by fit quality.

Parameters:
Return type:

ndarray