API reference¶
The public API is re-exported from the top-level mimirax package. The
modules below are documented from their source docstrings.
Contracts and types¶
The contracts mimirax is written against, and nothing else.
WHY PROTOCOLS. The package’s reason to exist is to outlive any single solver:
its core must never import jaccpot, nornax or odisseo. So the core
is written against structural interfaces – anything with the right methods
satisfies them, whether it is jaccpot’s FMM, an ODISSEO rollout, an analytic
potential, or the doubles in mimirax.testing. This is how the ecosystem
already works: jaccpot/nornax_adapter.py satisfies nornax’s
MutualForceModel without importing nornax, and nornax’s
forces/base.py is the template these follow.
Every protocol here is runtime_checkable so that isinstance (and the
optional beartype hook) can validate an argument structurally. Optional
capabilities are deliberately not protocol members: adding one would make
isinstance reject every implementation that predates it. An implementation
advertises an optional capability by having the attribute; a consumer probes
with hasattr or inspect.signature, never by widening the protocol.
Differentiability is part of every contract below: unless a docstring says
otherwise, an implementation must be traceable by jax.grad in its array
arguments, because the core numerical paths differentiate through it.
- class mimirax.protocols.Constraint(*args, **kwargs)[source]¶
Bases:
ProtocolAn equality constraint, expressed as a defect that is zero when satisfied.
nornax’s multiple-shooting
shooting_defectis one; a fixed total mass is another. Consumed bymimirax.parameters.quadratic_penalty().
- class mimirax.protocols.FoldableObservable(*args, **kwargs)[source]¶
Bases:
ProtocolAn
Observablethat can also be accumulated one snapshot at a time.WHY THIS EXISTS. A time-averaged observable is a reduction over a trajectory, and the cheap way to evaluate a reduction is to fold it into the integration rather than to stack the trajectory and average afterwards. Stacking costs
O(t * n)memory in the forward pass alone – for a made-to-measure fit over many dynamical times that is the term that decides whether the problem fits on a machine, and it has nothing to do with autodiff. Folding costsO(m).So this is a capability, not a replacement. It is a separate protocol because
Observableisruntime_checkableand widening it would makeisinstancereject every observable that predates this; and it is narrower thanObservablebecause not every observable is foldable. A mean folds. Exponential smoothing folds, through the recursionstate <- a * state + value. A median does not, and neither does anything needing the whole sample at once.An implementation must satisfy the fold law: for any trajectory,
result(fold(… fold(initial(s_0), value(s_0)) …, value(s_{t-1})), t)
equals
self(stacked_trajectory), the plainObservablecall. The two paths are then interchangeable and a caller can choose on memory grounds alone.mimirax.observables.TimeAveragesatisfies both protocols and a test pins the law to round-off.valueis separated fromfoldon purpose: a rollout can then evaluatevalueinside its own loop – where the snapshot already is – and stack only the(m,)results instead of the(n, 3)state.- value(snapshot)[source]¶
Evaluate the observable at one snapshot.
- Parameters:
snapshot (PyTree) – A single time slice, with no leading time axis.
- Returns:
(m,)observable values at that snapshot.- Return type:
Array
- initial(snapshot)[source]¶
Return the zero accumulator, shaped from a template snapshot.
Must not depend on the snapshot’s values, only on its shapes and dtypes, so that it can be built with
jax.eval_shape()and carries no gradient.- Parameters:
snapshot (PyTree) – A template time slice.
- Returns:
The accumulator’s identity element.
- Return type:
PyTree
- fold(state, value)[source]¶
Accumulate one snapshot’s value into the running state.
Called in trajectory order, so an implementation may weight by recency. Must be a fixed-shape pytree map, since it runs as a
lax.scancarry.- Parameters:
state (PyTree) – The running accumulator.
value (Array) –
(m,)result ofvalue()at the next snapshot.
- Returns:
The updated accumulator.
- Return type:
PyTree
- result(state, num_steps)[source]¶
Finish the reduction, given how many snapshots went into it.
- Parameters:
state (PyTree) – The final accumulator.
num_steps (int) – How many snapshots were folded. Static, so a normalization may depend on it.
- Returns:
(m,)reduced observable, equal to the plainObservablecall on the stacked trajectory.- Return type:
Array
- class mimirax.protocols.ForceModel(*args, **kwargs)[source]¶
Bases:
ProtocolA gravitational (or any pairwise) acceleration provider.
The smallest interface a solver has to offer for mimirax to build a forward model on it: accelerations at
positionsdue tomasses. jaccpot’sFastMultipoleMethod.compute_accelerations(positions, masses)satisfies it throughmimirax.adapters.jaccpot; an analytic potential satisfies it directly;mimirax.testing.DirectSumGravityis the double.Differentiable in
positionsandmasses. A tree or FMM backend is differentiable at fixed topology only; where its topology is rebuilt is the backend’s business and must be documented on the adapter.- accelerations(positions, masses, *, args=None)[source]¶
Return the acceleration at every particle.
- Parameters:
positions (Vec3) –
(n, 3)particle positions.masses (PerParticle) –
(n,)particle masses.args (object) – Caller-owned channel for backend options; the core never sets it.
- Returns:
(n, 3)accelerations.- Return type:
Vec3
- class mimirax.protocols.ForwardModel(*args, **kwargs)[source]¶
Bases:
ProtocolParameters in, a physical state (or prediction) out.
This is the seam between inference and physics: everything above it (observables, likelihoods, priors, samplers) is mimirax’s; everything below it (forces, integrators) is a solver’s. A forward model may be a static map (positions -> field at tracers, as in Paper I section 7), a rollout (initial conditions -> final state, as in ODISSEO), or the identity.
Must be differentiable in
params.
- class mimirax.protocols.Likelihood(*args, **kwargs)[source]¶
Bases:
ProtocolCompares a prediction with an observation and returns a log-probability.
A misfit is the negative of this; the convention is fixed to log-densities so that likelihoods and priors add. Registered in
mimirax.likelihoods.LIKELIHOODS. Must be differentiable inpredicted.- log_prob(predicted, observed)[source]¶
Return the log-likelihood of
observedgivenpredicted.- Parameters:
predicted (Array) – What an
Observableproduced from the model.observed (Array) – The data, same shape as
predicted.
- Returns:
The log-likelihood; additive constants may be dropped, and the implementation’s docstring must say if they are.
- Return type:
Scalar
- class mimirax.protocols.Observable(*args, **kwargs)[source]¶
Bases:
ProtocolAn observation operator: a state in, a data-space array out.
Kinematic maps, projections, binned densities and stream-track coordinates are all observables. Registered in
mimirax.observables.OBSERVABLES. Must be differentiable instate.
- class mimirax.protocols.Optimizer(*args, **kwargs)[source]¶
Bases:
ProtocolMinimizes a scalar objective over a parameter pytree.
Registered in
mimirax.inference.METHODS.- minimize(objective, params)[source]¶
Minimize
objectivestarting fromparams.- Parameters:
objective (Callable[[PyTree], Scalar]) – A differentiable scalar function of the parameters.
params (PyTree) – The starting point; its structure is preserved in the result.
- Returns:
Final parameters and the per-step objective trace.
- Return type:
- class mimirax.protocols.Prior(*args, **kwargs)[source]¶
Bases:
ProtocolA log-density over parameters. Regularizers are priors that say so.
Registered in
mimirax.priors.PRIORS. Must be differentiable inparams.
- class mimirax.protocols.Reparameterization(*args, **kwargs)[source]¶
Bases:
ProtocolA bijection between an unconstrained and a constrained parameter space.
Optimizers and samplers work in the unconstrained space; the forward model sees the constrained one. Implementations live in
mimirax.parameters.- forward(unconstrained)[source]¶
Map unconstrained values to the constrained space.
- Parameters:
unconstrained (Array) – Values in the unconstrained space.
- Returns:
The constrained values, same shape.
- Return type:
Array
- class mimirax.protocols.Sampler(*args, **kwargs)[source]¶
Bases:
ProtocolDraws parameter samples from an unnormalized log-density.
Registered in
mimirax.inference.METHODS.- sample(log_density, params, *, key, num_samples)[source]¶
Draw
num_samplessamples oflog_densitystarting fromparams.- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target density.
params (PyTree) – The initial state of the chain.
key (Array) – A
jax.randomkey.num_samples (int) – How many samples to return. Static.
- Returns:
The samples and their log-densities.
- Return type:
Result containers shared by the inference protocols and their implementations.
Kept in their own module (as yggdrax/types.py is) so that
mimirax.protocols can reference them without importing any
implementation, and so that an adapter satisfying a protocol structurally can
return them without importing mimirax.inference.
- class mimirax.types.FitResult(params, objective_trace)[source]¶
Bases:
NamedTupleWhat an optimizer returns.
- Variables:
params (PyTree) – The final parameters, in the same structure as the initial ones.
objective_trace (Array) –
(k,)objective value at every step, so convergence can be judged from the result alone.
- Parameters:
params (Any)
objective_trace (Array)
- params: Any¶
Alias for field number 0
- objective_trace: Array¶
Alias for field number 1
- class mimirax.types.SampleResult(samples, log_density)[source]¶
Bases:
NamedTupleWhat a sampler returns.
- Variables:
samples (PyTree) – Parameter samples with a leading
kaxis on every leaf.log_density (Array) –
(k,)log target density at each sample.
- Parameters:
samples (Any)
log_density (Array)
- samples: Any¶
Alias for field number 0
- log_density: Array¶
Alias for field number 1
Parameters¶
Parameter pytrees, reparameterizations, gauge fixing and constraints.
The free parameters of an inference problem are whatever pytree the caller
chose – a bare (n, 3) array of positions, a dict of potential parameters,
a nested structure. Nothing here assumes a structure; every function works
through jax.tree_util or through a flat vector obtained with ravel().
Three concerns live here because they are about the parameter space, not about any model:
Reparameterizations – bijections between an unconstrained space, where optimizers and samplers move freely, and the constrained space a forward model expects (positive masses, bounded angles). Each is a
Reparameterizationand is registered inREPARAMETERIZATIONS; ODISSEO’sPlummer_sphere_reparamis the ecosystem’s existing instance of the pattern.Gauge fixing – removing directions the data cannot see. A self-gravitating system’s centre of mass and net momentum are the standing examples: a fit over particle positions that leaves them free wanders along a flat direction of the objective.
Constraints – equalities expressed as a defect that is zero when the constraint holds (nornax’s multiple-shooting
shooting_defectis one), and the quadratic penalty that turns a defect into an objective term.
Everything is differentiable in its array arguments; nothing here has a
static-argument requirement under jit except the transforms mapping,
whose keys select code paths.
- class mimirax.parameters.AffineTransform(scale=1.0, shift=0.0)[source]¶
Bases:
objectA fixed rescaling and shift:
constrained = scale * u + shift.Used to put parameters of very different magnitude on one footing before an optimizer sees them.
- Variables:
scale (float) – Multiplicative factor; must be non-zero.
shift (float) – Additive offset.
- Parameters:
scale (float)
shift (float)
- scale: float = 1.0¶
- shift: float = 0.0¶
- forward(unconstrained)[source]¶
Apply the affine map.
- Parameters:
unconstrained (Array) – Real values.
- Returns:
scale * unconstrained + shift.- Return type:
Array
- class mimirax.parameters.FunctionConstraint(defect_fn)[source]¶
Bases:
objectA
Constraintwrapping a plain defect function.- Variables:
defect_fn (Callable[[PyTree], Array]) – Maps parameters to a defect array that is zero when the constraint holds.
- Parameters:
defect_fn (Callable[[Any], Array])
- defect_fn: Callable[[Any], Array]¶
- class mimirax.parameters.IdentityTransform[source]¶
Bases:
objectThe trivial reparameterization: constrained equals unconstrained.
- forward(unconstrained)[source]¶
Return
unconstrainedunchanged.- Parameters:
unconstrained (Array) – Values in the unconstrained space.
- Returns:
The same values.
- Return type:
Array
- class mimirax.parameters.LogTransform[source]¶
Bases:
objectPositive constrained values:
constrained = exp(unconstrained).The natural choice for masses, scale radii and noise levels.
- forward(unconstrained)[source]¶
Exponentiate.
- Parameters:
unconstrained (Array) – Real values.
- Returns:
Strictly positive values, same shape.
- Return type:
Array
- class mimirax.parameters.SoftplusTransform[source]¶
Bases:
objectPositive constrained values with a linear tail:
softplus(u).Unlike
LogTransformthis does not blow up for largeu, which makes it the safer choice under aggressive optimizer steps.- forward(unconstrained)[source]¶
Apply
log(1 + exp(u)).- Parameters:
unconstrained (Array) – Real values.
- Returns:
Strictly positive values, same shape.
- Return type:
Array
- class mimirax.parameters.TotalMassConstraint(total)[source]¶
Bases:
objectFix the sum of a mass vector – the made-to-measure normalization.
- Variables:
total (float) – The required total mass.
- Parameters:
total (float)
- total: float¶
- mimirax.parameters.centre_of_mass(positions, masses)[source]¶
Return the mass-weighted mean position.
- Parameters:
positions (Vec3) –
(n, 3)positions.masses (PerParticle) –
(n,)masses.
- Returns:
(3,)centre of mass.- Return type:
Array
- mimirax.parameters.constrain(unconstrained, transforms)[source]¶
Map a dict of unconstrained parameters to the constrained space.
- Parameters:
unconstrained (Mapping[str, Array]) – Parameters keyed by name.
transforms (Mapping[str, Reparameterization]) – Which reparameterization applies to which key. Keys absent from
transformspass through unchanged.
- Returns:
The constrained parameters, same keys.
- Return type:
dict[str, Array]
- mimirax.parameters.constraint_violation(constraint, params)[source]¶
Return the largest absolute defect component, a convergence diagnostic.
- Parameters:
constraint (Constraint) – The constraint.
params (PyTree) – Where to evaluate it.
- Returns:
max |defect|.- Return type:
Scalar
- mimirax.parameters.log_abs_det_jacobian(unconstrained, transforms)[source]¶
Sum the log-Jacobians of every transform applied by
constrain().- Parameters:
unconstrained (Mapping[str, Array]) – Parameters keyed by name, in the unconstrained space.
transforms (Mapping[str, Reparameterization]) – Which reparameterization applies to which key.
- Returns:
The total, which a density over the unconstrained space adds to the constrained-space log-density.
- Return type:
Scalar
- mimirax.parameters.net_momentum(velocities, masses)[source]¶
Return the total linear momentum.
- Parameters:
velocities (Vec3) –
(n, 3)velocities.masses (PerParticle) –
(n,)masses.
- Returns:
(3,)momentum.- Return type:
Array
- mimirax.parameters.num_parameters(params)[source]¶
Count the scalar degrees of freedom in a parameter pytree.
- Parameters:
params (PyTree) – Any pytree of arrays.
- Returns:
The total number of array elements across all leaves.
- Return type:
int
- mimirax.parameters.quadratic_penalty(constraint, params, weight=1.0)[source]¶
Turn a constraint defect into an objective term,
weight * sum(defect^2).- Parameters:
constraint (Constraint) – The constraint.
params (PyTree) – Where to evaluate it.
weight (float) – The penalty strength.
- Returns:
The penalty; add it to a negative log-posterior.
- Return type:
Scalar
- mimirax.parameters.ravel(params)[source]¶
Flatten a parameter pytree into one vector, with its inverse.
- Parameters:
params (PyTree) – Any pytree of arrays.
- Returns:
The concatenated leaves as a 1-d array, and a function that rebuilds the original structure from a vector of the same length.
- Return type:
tuple[Array, Callable[[Array], PyTree]]
- mimirax.parameters.remove_centre_of_mass(positions, masses)[source]¶
Translate
positionsso their centre of mass is at the origin.- Parameters:
positions (Vec3) –
(n, 3)positions.masses (PerParticle) –
(n,)masses.
- Returns:
The recentred positions.
- Return type:
Vec3
- mimirax.parameters.remove_net_momentum(velocities, masses)[source]¶
Boost
velocitiesso the total linear momentum vanishes.- Parameters:
velocities (Vec3) –
(n, 3)velocities.masses (PerParticle) –
(n,)masses.
- Returns:
The velocities in the zero-momentum frame.
- Return type:
Vec3
- mimirax.parameters.unconstrain(constrained, transforms)[source]¶
Map a dict of constrained parameters back to the unconstrained space.
- Parameters:
constrained (Mapping[str, Array]) – Parameters keyed by name.
transforms (Mapping[str, Reparameterization]) – Which reparameterization applies to which key. Keys absent from
transformspass through unchanged.
- Returns:
The unconstrained parameters, same keys.
- Return type:
dict[str, Array]
Observables, likelihoods and priors¶
Observation operators and their registry.
An observable maps a forward model’s state to data space; see
mimirax.protocols.Observable. New observables are registered here with
OBSERVABLES.register(name, cls) – no core module changes.
- class mimirax.observables.GaussianRadialBins(centres, width, moments=('mass', 'v2'))[source]¶
Bases:
objectA reference kernel: soft radial bins, and their mass-weighted
v^2moment.The doubles’ counterpart on the observable side. Real made-to-measure kernels – line-of-sight velocity distributions, IFU pixels, stream tracks – belong to the science that uses them (D-027 keeps domain observables in the domain packages); this exists so the method has something to run on today, the way
ProjectedPositionsdoes.The bins are Gaussian rather than top-hat because a top-hat’s derivative with respect to a position is zero almost everywhere and a delta on a measure-zero set: a fit that also moves the particles would see no gradient from it. The Gaussians overlap, which makes the recovered weights genuinely degenerate in a controlled, reportable way rather than accidentally well-conditioned – read the degeneracy off
mimirax.diagnostics.fisher_information(), do not tune it away.Each requested moment contributes
len(centres)columns, in the ordermomentslists them, so the kernel is(n, len(centres) * len(moments)).- Variables:
centres (Array) –
(b,)bin centres in radius.width (float) – Gaussian bin width, in the same units as
centres.moments (tuple[str, ...]) – Which moments to stack:
"mass"for the binned weight itself and"v2"for the binned weight times|v|^2(a mass-weighted kinetic moment, linear in the weights as the method requires – a ratio such as a velocity dispersion is not).
- Raises:
ValueError – If
momentsis empty or names a moment that is not supported.- Parameters:
centres (Array)
width (float)
moments (tuple[str, ...])
- centres: Array¶
- width: float¶
- moments: tuple[str, ...] = ('mass', 'v2')¶
- class mimirax.observables.IdentityObservable[source]¶
Bases:
objectThe state itself, as one array: the data are the model output.
- class mimirax.observables.ProjectedPositions(axes=(0, 1))[source]¶
Bases:
objectPositions projected onto a subset of axes and flattened.
The simplest mock observable: an
(n, 3)state seen in projection, as an imaging survey sees a stellar system. Differentiable instate.- Variables:
axes (tuple[int, ...]) – Which coordinate axes survive the projection. Static.
- Parameters:
axes (tuple[int, ...])
- axes: tuple[int, ...] = (0, 1)¶
- class mimirax.observables.TimeAverage(inner, decay_steps=None)[source]¶
Bases:
objectAverage an inner observable over a recorded trajectory’s leading axis.
stateis a pytree whose every array leaf has a leadingtaxis – the stacked records a rollout returns. The inner observable isvmap-ed over that axis and the results are averaged, so the inner observable is written for a single snapshot and never has to know it is being time-averaged.Two averages are available. The default is the plain mean, which is the right thing when the recorded window is what the fit is meant to average over. With
decay_stepsset, the weights are exponential in the step index and normalized to sum to one,w_t propto exp(-(t_last - t) / decay_steps): the discrete counterpart of Syer & Tremaine’s temporal smoothingd y~ / dt = alpha (y - y~), withalpha dt = 1 / decay_steps. It is in units of recorded steps, not of time, so the observable stays independent of the integrator’s step size; a driver that wants a physical timescale converts once, at the call site.Neither choice makes a claim about whether the window is long enough for the average to represent a stationary state. That is measured – vary the number of recorded steps and watch the fitted weights – not assumed.
Both averages fold, so this also satisfies
FoldableObservable: a caller can either stack the trajectory and average it (__call__()) or accumulate it one snapshot at a time inside the integration (value()/initial()/fold()/result()), and the two give the same answer to round-off. The second path costsO(m)memory where the first costsO(t * n), which is whatmimirax.adapters.nornax.FoldedRolloutexists to exploit. The mean folds by summing; the exponential average folds through the recursionstate <- exp(-1 / decay_steps) * state + value, whose value aftertsnapshots issum_s a^(t - 1 - s) value_s– exactly the unnormalized weightsweights()returns – soresult()divides by(1 - a^t) / (1 - a).- Variables:
inner (Callable[[PyTree], Array]) – The per-snapshot observable, an
Observable.decay_steps (float | None) – Exponential-smoothing timescale in recorded steps;
None(the default) gives the plain mean.
- Raises:
ValueError – If
decay_stepsis given and is not strictly positive.- Parameters:
inner (Callable[[Any], Array])
decay_steps (float | None)
- inner: Callable[[Any], Array]¶
- decay_steps: float | None = None¶
- step_values(state)[source]¶
Return the inner observable at every recorded step, unaveraged.
- Parameters:
state (PyTree) – A pytree whose array leaves all carry a leading
taxis.- Returns:
(t, m)per-step predictions.- Return type:
Array
- weights(num_steps)[source]¶
Return the
(t,)averaging weights, which sum to one.- Parameters:
num_steps (int) – How many recorded steps to weight.
- Returns:
Uniform
1 / tweights by default, exponential weights ending at the last step whendecay_stepsis set.- Return type:
Array
- value(snapshot)[source]¶
Evaluate the inner observable at one snapshot.
- Parameters:
snapshot (PyTree) – A single time slice, with no leading time axis.
- Returns:
(m,)inner observable at that snapshot.- Return type:
Array
- initial(snapshot)[source]¶
Return the zero accumulator, shaped by tracing the inner observable.
Uses
jax.eval_shape(), so the inner observable is never evaluated and the accumulator carries no gradient fromsnapshot.- Parameters:
snapshot (PyTree) – A template time slice.
- Returns:
(m,)zeros of the inner observable’s dtype.- Return type:
Array
- fold(state, value)[source]¶
Accumulate one snapshot’s value.
- Parameters:
state (Array) – The running accumulator.
value (Array) –
(m,)value at the next snapshot.
- Returns:
state + valuefor the plain mean; the decayed recursionexp(-1 / decay_steps) * state + valuefor exponential smoothing.- Return type:
Array
- result(state, num_steps)[source]¶
Normalize the accumulator into the averaged observable.
- Parameters:
state (Array) – The final accumulator.
num_steps (int) – How many snapshots were folded. Static.
- Returns:
(m,)time-averaged observable, equal to__call__()on the stacked trajectory.- Return type:
Array
- class mimirax.observables.WeightedKernelSum(kernel, weights_key='weights')[source]¶
Bases:
objectThe made-to-measure observable
y_j = sum_i w_i K_j(z_i).Differentiable in the weights and in whatever the kernel reads, so a fit over the weights alone and a fit that also moves the initial conditions use the same observable.
The state is a mapping:
weights_keyselects the weight leaf and the kernel is handed the whole state, so a kernel is free to use positions, velocities or anything else the forward model recorded.- Variables:
kernel (Callable[[PyTree], Array]) –
state -> (n, m): particlei’s contribution to observablej. Must be differentiable in the state’s array leaves.weights_key (str) – Which leaf of the state holds the
(n,)weights.
- Parameters:
kernel (Callable[[Any], Array])
weights_key (str)
- kernel: Callable[[Any], Array]¶
- weights_key: str = 'weights'¶
- kernel_values(state)[source]¶
Return the raw
(n, m)kernel, unweighted.The classic force-of-change update needs this, and so does any hand-written check of the residual gradient
sum_j Delta_j K_ji / sigma_j^2against the one autodiff produces.- Parameters:
state (PyTree) – A state the kernel accepts.
- Returns:
(n, m)kernel values.- Return type:
Array
- mimirax.observables.make_observable(name, **kwargs)[source]¶
Instantiate a registered observable by name.
- Parameters:
name (str) – A key of
OBSERVABLES.**kwargs (object) – Constructor arguments for the registered class.
- Returns:
The instance.
- Return type:
Reference observation operators: the trivial ones every pipeline test needs.
Real observables – kinematic maps, binned densities, stream-track
coordinates – are the work of the science papers and are added to
mimirax.observables.OBSERVABLES as they are written. These two exist so
that every core numerical path has an observable to run through today.
- class mimirax.observables.reference.IdentityObservable[source]¶
Bases:
objectThe state itself, as one array: the data are the model output.
- class mimirax.observables.reference.ProjectedPositions(axes=(0, 1))[source]¶
Bases:
objectPositions projected onto a subset of axes and flattened.
The simplest mock observable: an
(n, 3)state seen in projection, as an imaging survey sees a stellar system. Differentiable instate.- Variables:
axes (tuple[int, ...]) – Which coordinate axes survive the projection. Static.
- Parameters:
axes (tuple[int, ...])
- axes: tuple[int, ...] = (0, 1)¶
The made-to-measure observable, a reference kernel for it, and time averaging.
Made-to-measure is a statement about one shape of observable. Every quantity the method fits is linear in the particle weights,
with z_i particle i’s phase-space coordinates and K_j the kernel of
observable j – a binned density, a mass-weighted velocity moment, a
surface-brightness pixel. WeightedKernelSum is that form, with the
kernel supplied by the caller, and it is the seam the classic algorithm needs:
Syer & Tremaine’s force of change is driven by K_j(z_i) per particle, not by
y_j alone, so the kernel has to be reachable and not buried inside a
closure. WeightedKernelSum.kernel_values() is that access.
TimeAverage is the second half of the method. A made-to-measure fit
compares the data not with an instantaneous model value but with one averaged
along the orbits, which is what turns a snapshot of N particles into a
statement about a stationary distribution function. It wraps any inner
observable and averages it over the leading axis of a recorded trajectory –
mimirax.adapters.nornax.NornaxRollout’s output, or any pytree whose
leaves all carry a time axis.
What is not claimed here: that a given number of recorded steps is a long
enough average, or that a given kernel makes the weights identifiable. Both are
measurements, and mimirax.diagnostics is where they are made.
- class mimirax.observables.m2m.GaussianRadialBins(centres, width, moments=('mass', 'v2'))[source]¶
Bases:
objectA reference kernel: soft radial bins, and their mass-weighted
v^2moment.The doubles’ counterpart on the observable side. Real made-to-measure kernels – line-of-sight velocity distributions, IFU pixels, stream tracks – belong to the science that uses them (D-027 keeps domain observables in the domain packages); this exists so the method has something to run on today, the way
ProjectedPositionsdoes.The bins are Gaussian rather than top-hat because a top-hat’s derivative with respect to a position is zero almost everywhere and a delta on a measure-zero set: a fit that also moves the particles would see no gradient from it. The Gaussians overlap, which makes the recovered weights genuinely degenerate in a controlled, reportable way rather than accidentally well-conditioned – read the degeneracy off
mimirax.diagnostics.fisher_information(), do not tune it away.Each requested moment contributes
len(centres)columns, in the ordermomentslists them, so the kernel is(n, len(centres) * len(moments)).- Variables:
centres (Array) –
(b,)bin centres in radius.width (float) – Gaussian bin width, in the same units as
centres.moments (tuple[str, ...]) – Which moments to stack:
"mass"for the binned weight itself and"v2"for the binned weight times|v|^2(a mass-weighted kinetic moment, linear in the weights as the method requires – a ratio such as a velocity dispersion is not).
- Raises:
ValueError – If
momentsis empty or names a moment that is not supported.- Parameters:
centres (Array)
width (float)
moments (tuple[str, ...])
- centres: Array¶
- width: float¶
- moments: tuple[str, ...] = ('mass', 'v2')¶
- class mimirax.observables.m2m.TimeAverage(inner, decay_steps=None)[source]¶
Bases:
objectAverage an inner observable over a recorded trajectory’s leading axis.
stateis a pytree whose every array leaf has a leadingtaxis – the stacked records a rollout returns. The inner observable isvmap-ed over that axis and the results are averaged, so the inner observable is written for a single snapshot and never has to know it is being time-averaged.Two averages are available. The default is the plain mean, which is the right thing when the recorded window is what the fit is meant to average over. With
decay_stepsset, the weights are exponential in the step index and normalized to sum to one,w_t propto exp(-(t_last - t) / decay_steps): the discrete counterpart of Syer & Tremaine’s temporal smoothingd y~ / dt = alpha (y - y~), withalpha dt = 1 / decay_steps. It is in units of recorded steps, not of time, so the observable stays independent of the integrator’s step size; a driver that wants a physical timescale converts once, at the call site.Neither choice makes a claim about whether the window is long enough for the average to represent a stationary state. That is measured – vary the number of recorded steps and watch the fitted weights – not assumed.
Both averages fold, so this also satisfies
FoldableObservable: a caller can either stack the trajectory and average it (__call__()) or accumulate it one snapshot at a time inside the integration (value()/initial()/fold()/result()), and the two give the same answer to round-off. The second path costsO(m)memory where the first costsO(t * n), which is whatmimirax.adapters.nornax.FoldedRolloutexists to exploit. The mean folds by summing; the exponential average folds through the recursionstate <- exp(-1 / decay_steps) * state + value, whose value aftertsnapshots issum_s a^(t - 1 - s) value_s– exactly the unnormalized weightsweights()returns – soresult()divides by(1 - a^t) / (1 - a).- Variables:
inner (Callable[[PyTree], Array]) – The per-snapshot observable, an
Observable.decay_steps (float | None) – Exponential-smoothing timescale in recorded steps;
None(the default) gives the plain mean.
- Raises:
ValueError – If
decay_stepsis given and is not strictly positive.- Parameters:
inner (Callable[[Any], Array])
decay_steps (float | None)
- inner: Callable[[Any], Array]¶
- decay_steps: float | None = None¶
- step_values(state)[source]¶
Return the inner observable at every recorded step, unaveraged.
- Parameters:
state (PyTree) – A pytree whose array leaves all carry a leading
taxis.- Returns:
(t, m)per-step predictions.- Return type:
Array
- weights(num_steps)[source]¶
Return the
(t,)averaging weights, which sum to one.- Parameters:
num_steps (int) – How many recorded steps to weight.
- Returns:
Uniform
1 / tweights by default, exponential weights ending at the last step whendecay_stepsis set.- Return type:
Array
- value(snapshot)[source]¶
Evaluate the inner observable at one snapshot.
- Parameters:
snapshot (PyTree) – A single time slice, with no leading time axis.
- Returns:
(m,)inner observable at that snapshot.- Return type:
Array
- initial(snapshot)[source]¶
Return the zero accumulator, shaped by tracing the inner observable.
Uses
jax.eval_shape(), so the inner observable is never evaluated and the accumulator carries no gradient fromsnapshot.- Parameters:
snapshot (PyTree) – A template time slice.
- Returns:
(m,)zeros of the inner observable’s dtype.- Return type:
Array
- fold(state, value)[source]¶
Accumulate one snapshot’s value.
- Parameters:
state (Array) – The running accumulator.
value (Array) –
(m,)value at the next snapshot.
- Returns:
state + valuefor the plain mean; the decayed recursionexp(-1 / decay_steps) * state + valuefor exponential smoothing.- Return type:
Array
- result(state, num_steps)[source]¶
Normalize the accumulator into the averaged observable.
- Parameters:
state (Array) – The final accumulator.
num_steps (int) – How many snapshots were folded. Static.
- Returns:
(m,)time-averaged observable, equal to__call__()on the stacked trajectory.- Return type:
Array
- class mimirax.observables.m2m.WeightedKernelSum(kernel, weights_key='weights')[source]¶
Bases:
objectThe made-to-measure observable
y_j = sum_i w_i K_j(z_i).Differentiable in the weights and in whatever the kernel reads, so a fit over the weights alone and a fit that also moves the initial conditions use the same observable.
The state is a mapping:
weights_keyselects the weight leaf and the kernel is handed the whole state, so a kernel is free to use positions, velocities or anything else the forward model recorded.- Variables:
kernel (Callable[[PyTree], Array]) –
state -> (n, m): particlei’s contribution to observablej. Must be differentiable in the state’s array leaves.weights_key (str) – Which leaf of the state holds the
(n,)weights.
- Parameters:
kernel (Callable[[Any], Array])
weights_key (str)
- kernel: Callable[[Any], Array]¶
- weights_key: str = 'weights'¶
- kernel_values(state)[source]¶
Return the raw
(n, m)kernel, unweighted.The classic force-of-change update needs this, and so does any hand-written check of the residual gradient
sum_j Delta_j K_ji / sigma_j^2against the one autodiff produces.- Parameters:
state (PyTree) – A state the kernel accepts.
- Returns:
(n, m)kernel values.- Return type:
Array
Likelihoods and misfits, and their registry.
A likelihood compares a prediction with an observation and returns a
log-probability; see mimirax.protocols.Likelihood. Register new ones
with LIKELIHOODS.register(name, cls).
- class mimirax.likelihoods.GaussianLikelihood(sigma=1.0)[source]¶
Bases:
objectIndependent Gaussian noise on every data point, of one width or of many.
log_probis-chi_squared / 2. The normalization constant-sum_j log(sigma_j sqrt(2 pi))is dropped: it does not depend on the prediction, so neither optimization nor sampling sees it. It does depend onsigma, so this log-density may not be compared across two differentsigma– which is a statement about model comparison and not about fitting.HETEROSCEDASTIC IS THE USUAL CASE HERE. A scalar
sigmais only meaningful when every entry of the data vector is in the same units and measured equally well. A made-to-measure vector usually is not: stack a binned mass moment and a binned|v|^2moment and their numerical scales differ by whatever|v|^2happens to be, so a scalar width silently weights the fit by unit choice. Pass an array of per-point widths instead, one per entry ofobserved.- Variables:
sigma (SigmaLike) – The noise level. A scalar applies one width to every point; an
(m,)array – or anything broadcasting against the data – applies one width per point. Must be strictly positive; not checked, becausesigmamay be a traced array.- Parameters:
sigma (Array | float)
- sigma: Array | float = 1.0¶
- mimirax.likelihoods.chi_squared(predicted, observed, sigma=1.0)[source]¶
Return
sum(((predicted - observed) / sigma)^2).- Parameters:
predicted (Array) – Model prediction.
observed (Array) – Data, same shape.
sigma (SigmaLike) – Noise level: a scalar for homoscedastic noise, or an array broadcasting against the data for one width per data point.
- Returns:
The chi-squared misfit.
- Return type:
Scalar
The Gaussian likelihood and its chi-squared misfit.
The width may be a scalar or one width per data point. That is not a
convenience: a made-to-measure data vector routinely stacks moments with
incommensurate units – a binned mass and a binned mass-weighted |v|^2 in
the same vector – and a scalar sigma then weights them by accident of
units rather than by how well either is measured. The array form is the
supported way to say what each observable’s uncertainty actually is.
- class mimirax.likelihoods.gaussian.GaussianLikelihood(sigma=1.0)[source]¶
Bases:
objectIndependent Gaussian noise on every data point, of one width or of many.
log_probis-chi_squared / 2. The normalization constant-sum_j log(sigma_j sqrt(2 pi))is dropped: it does not depend on the prediction, so neither optimization nor sampling sees it. It does depend onsigma, so this log-density may not be compared across two differentsigma– which is a statement about model comparison and not about fitting.HETEROSCEDASTIC IS THE USUAL CASE HERE. A scalar
sigmais only meaningful when every entry of the data vector is in the same units and measured equally well. A made-to-measure vector usually is not: stack a binned mass moment and a binned|v|^2moment and their numerical scales differ by whatever|v|^2happens to be, so a scalar width silently weights the fit by unit choice. Pass an array of per-point widths instead, one per entry ofobserved.- Variables:
sigma (SigmaLike) – The noise level. A scalar applies one width to every point; an
(m,)array – or anything broadcasting against the data – applies one width per point. Must be strictly positive; not checked, becausesigmamay be a traced array.- Parameters:
sigma (Array | float)
- sigma: Array | float = 1.0¶
- mimirax.likelihoods.gaussian.chi_squared(predicted, observed, sigma=1.0)[source]¶
Return
sum(((predicted - observed) / sigma)^2).- Parameters:
predicted (Array) – Model prediction.
observed (Array) – Data, same shape.
sigma (SigmaLike) – Noise level: a scalar for homoscedastic noise, or an array broadcasting against the data for one width per data point.
- Returns:
The chi-squared misfit.
- Return type:
Scalar
Priors and regularizers, and their registry.
A prior is a log-density over parameters; a regularizer is a prior that says
so. See mimirax.protocols.Prior. Register new ones with
PRIORS.register(name, cls).
- class mimirax.priors.EntropyPrior(mu=1.0, reference=None, key='weights', include_linear_term=True)[source]¶
Bases:
objectThe made-to-measure entropy prior over a positive weight vector.
Syer & Tremaine’s (1996) regularizer keeps the particle weights
wnear a referencew0. Two spellings of it are in the literature and they do not have the same mode, so both are here and the default is stated:include_linear_term=True(the default, Dehnen 2009’s form):S = -sum_i w_i * (log(w_i / w0_i) - 1)
whose gradient is
dS/dw_i = -log(w_i / w0_i). It vanishes atw = w0, so the prior’s mode is the reference unconditionally.include_linear_term=False(Syer & Tremaine’s ownS = -sum w log(w / w0)), whose gradient is-(log(w_i / w0_i) + 1)and therefore vanishes atw = w0 / e. Its mode is the reference only on the constant-total-weight surface thatmimirax.parameters.TotalMassConstraintfixes, where the Lagrange multiplier absorbs the constant.
Both are strictly concave on
w > 0– the Hessian is the negative diagonal-mu / w_ifor either – so the term can only ever pull a fit toward the reference, never create a second optimum.log_probreturnsmu * S, the log density, so it adds to a likelihood the way every otherPriorhere does; themu Sterm of the made-to-measure objective is exactly this, andmulives here and nowhere else (MadeToMeasuredeliberately has no second copy of it).Domain. The entropy is defined on
w > 0and this returnsnanoutside it, deliberately: the package’s rule is that positivity is enforced by a reparameterization (mimirax.parameters.LogTransform), not by clipping inside a density, and a silentwhere-guard here would hide a parameterization mistake instead of failing on it.- Variables:
mu (float) – Regularization strength; scales the whole term linearly.
reference (Array | float | None) –
w0. A scalar or an(n,)array;Nonemeans the uniform reference1, so the mode is the all-ones weight vector. A system of total weightMonnparticles wantsreference=M / n.key (str | None) – Which leaf of a mapping
paramsholds the weights. WithNonethe prior is applied to every leaf, asGaussianPrioris; a non-Nonekey (the default"weights") is what keeps the prior off the position and velocity leaves of a made-to-measure parameter dict, where a logarithm would be a domain error rather than a regularizer.include_linear_term (bool) – Whether to use the
-1shifted form whose mode is the reference; see above.
- Parameters:
mu (float)
reference (Array | float | None)
key (str | None)
include_linear_term (bool)
- mu: float = 1.0¶
- reference: Array | float | None = None¶
- key: str | None = 'weights'¶
- include_linear_term: bool = True¶
- log_prob(params)[source]¶
Return
mu * Satparams, the entropy prior’s log density.- Parameters:
params (PyTree) – The particle weights: a mapping carrying
key, or any pytree whose leaves are all weights whenkeyisNone.- Returns:
-mu * sum(w * (log(w / w0) - 1))with the defaultinclude_linear_term, and-mu * sum(w * log(w / w0))without it.nanwhere any weight is non-positive.- Return type:
Scalar
- Raises:
KeyError – If
paramsis a mapping andkeyis not one of its keys. The message lists the keys that are there, because the usual cause is a parameter dict that spells the weights differently.
- divergence(params)[source]¶
Return the prior’s norm: the KL divergence from the reference.
sum_i [w_i log(w_i / w0_i) - w_i + w0_i], which is>= 0and zero exactly atw = w0. Notemudoes not appear: this is the functionalmuweights, not the weighted term.WHY THIS EXISTS AND
-log_probWILL NOT DO. Choosingmuby an L-curve means plotting the misfit against the regularizer’s norm, and-log_prob / mu = -S = sum_i w_i (log(w_i/w0_i) - 1)is not a norm: at the prior’s own modew = w0it equals-sum_i w0_i, which is negative, and its logarithm – which is what an L-curve is drawn in – does not exist. The two differ by the constantsum_i w0_i, so they have the same minimiser and a different L-curve. An L-curve drawn against-Sis a curve of the wrong quantity; this is the right one.With
include_linear_term=Falsethe log density is a different functional, but this divergence is unchanged – it is the natural norm for either spelling, and it is whatmimirax.diagnostics.l_curve_corner()expects.- Parameters:
params (PyTree) – The particle weights, read the same way
log_prob()reads them.- Returns:
The KL divergence from the reference.
nanwhere any weight is non-positive, for the reasonlog_prob()gives.- Return type:
Scalar
- Raises:
KeyError – If
paramsis a mapping andkeyis not one of its keys.
- class mimirax.priors.GaussianPrior(mean=0.0, sigma=1.0)[source]¶
Bases:
objectIndependent Gaussian prior of one mean and width on every parameter.
The normalization constant is dropped, as in
GaussianLikelihood.- Variables:
mean (float) – Prior mean.
sigma (float) – Prior width.
- Parameters:
mean (float)
sigma (float)
- mean: float = 0.0¶
- sigma: float = 1.0¶
- class mimirax.priors.L2Regularizer(weight=1.0)[source]¶
Bases:
objectRidge penalty as a prior:
log_prob = -weight * sum(params^2).A Gaussian prior of zero mean and
sigma = 1 / sqrt(2 weight), spelled the way an optimizer user thinks of it.- Variables:
weight (float) – Penalty strength.
- Parameters:
weight (float)
- weight: float = 1.0¶
Priors and regularizers: a Gaussian, an L2 penalty, and the M2M entropy prior.
WHERE THE JACOBIAN SITS. Every log_prob here is a density over the space it
is handed, and none of them adds the log-Jacobian of a reparameterization.
That is the scaffold’s convention for GaussianPrior and it is kept for
EntropyPrior: a caller who moves in an unconstrained space and wants a
density over that space adds
mimirax.parameters.log_abs_det_jacobian() itself. The alternative –
folding the Jacobian into the prior – would make the same prior mean two
different things depending on which optimizer called it. This is friction 1 of
the EDDA programme’s D-027 and the choice is made here, once, explicitly:
priors are densities over the constrained parameters, and
mimirax.inference.MadeToMeasure.minimize() evaluates its objective on
constrained parameters for exactly that reason.
- class mimirax.priors.reference.EntropyPrior(mu=1.0, reference=None, key='weights', include_linear_term=True)[source]¶
Bases:
objectThe made-to-measure entropy prior over a positive weight vector.
Syer & Tremaine’s (1996) regularizer keeps the particle weights
wnear a referencew0. Two spellings of it are in the literature and they do not have the same mode, so both are here and the default is stated:include_linear_term=True(the default, Dehnen 2009’s form):S = -sum_i w_i * (log(w_i / w0_i) - 1)
whose gradient is
dS/dw_i = -log(w_i / w0_i). It vanishes atw = w0, so the prior’s mode is the reference unconditionally.include_linear_term=False(Syer & Tremaine’s ownS = -sum w log(w / w0)), whose gradient is-(log(w_i / w0_i) + 1)and therefore vanishes atw = w0 / e. Its mode is the reference only on the constant-total-weight surface thatmimirax.parameters.TotalMassConstraintfixes, where the Lagrange multiplier absorbs the constant.
Both are strictly concave on
w > 0– the Hessian is the negative diagonal-mu / w_ifor either – so the term can only ever pull a fit toward the reference, never create a second optimum.log_probreturnsmu * S, the log density, so it adds to a likelihood the way every otherPriorhere does; themu Sterm of the made-to-measure objective is exactly this, andmulives here and nowhere else (MadeToMeasuredeliberately has no second copy of it).Domain. The entropy is defined on
w > 0and this returnsnanoutside it, deliberately: the package’s rule is that positivity is enforced by a reparameterization (mimirax.parameters.LogTransform), not by clipping inside a density, and a silentwhere-guard here would hide a parameterization mistake instead of failing on it.- Variables:
mu (float) – Regularization strength; scales the whole term linearly.
reference (Array | float | None) –
w0. A scalar or an(n,)array;Nonemeans the uniform reference1, so the mode is the all-ones weight vector. A system of total weightMonnparticles wantsreference=M / n.key (str | None) – Which leaf of a mapping
paramsholds the weights. WithNonethe prior is applied to every leaf, asGaussianPrioris; a non-Nonekey (the default"weights") is what keeps the prior off the position and velocity leaves of a made-to-measure parameter dict, where a logarithm would be a domain error rather than a regularizer.include_linear_term (bool) – Whether to use the
-1shifted form whose mode is the reference; see above.
- Parameters:
mu (float)
reference (Array | float | None)
key (str | None)
include_linear_term (bool)
- mu: float = 1.0¶
- reference: Array | float | None = None¶
- key: str | None = 'weights'¶
- include_linear_term: bool = True¶
- log_prob(params)[source]¶
Return
mu * Satparams, the entropy prior’s log density.- Parameters:
params (PyTree) – The particle weights: a mapping carrying
key, or any pytree whose leaves are all weights whenkeyisNone.- Returns:
-mu * sum(w * (log(w / w0) - 1))with the defaultinclude_linear_term, and-mu * sum(w * log(w / w0))without it.nanwhere any weight is non-positive.- Return type:
Scalar
- Raises:
KeyError – If
paramsis a mapping andkeyis not one of its keys. The message lists the keys that are there, because the usual cause is a parameter dict that spells the weights differently.
- divergence(params)[source]¶
Return the prior’s norm: the KL divergence from the reference.
sum_i [w_i log(w_i / w0_i) - w_i + w0_i], which is>= 0and zero exactly atw = w0. Notemudoes not appear: this is the functionalmuweights, not the weighted term.WHY THIS EXISTS AND
-log_probWILL NOT DO. Choosingmuby an L-curve means plotting the misfit against the regularizer’s norm, and-log_prob / mu = -S = sum_i w_i (log(w_i/w0_i) - 1)is not a norm: at the prior’s own modew = w0it equals-sum_i w0_i, which is negative, and its logarithm – which is what an L-curve is drawn in – does not exist. The two differ by the constantsum_i w0_i, so they have the same minimiser and a different L-curve. An L-curve drawn against-Sis a curve of the wrong quantity; this is the right one.With
include_linear_term=Falsethe log density is a different functional, but this divergence is unchanged – it is the natural norm for either spelling, and it is whatmimirax.diagnostics.l_curve_corner()expects.- Parameters:
params (PyTree) – The particle weights, read the same way
log_prob()reads them.- Returns:
The KL divergence from the reference.
nanwhere any weight is non-positive, for the reasonlog_prob()gives.- Return type:
Scalar
- Raises:
KeyError – If
paramsis a mapping andkeyis not one of its keys.
- class mimirax.priors.reference.GaussianPrior(mean=0.0, sigma=1.0)[source]¶
Bases:
objectIndependent Gaussian prior of one mean and width on every parameter.
The normalization constant is dropped, as in
GaussianLikelihood.- Variables:
mean (float) – Prior mean.
sigma (float) – Prior width.
- Parameters:
mean (float)
sigma (float)
- mean: float = 0.0¶
- sigma: float = 1.0¶
- class mimirax.priors.reference.L2Regularizer(weight=1.0)[source]¶
Bases:
objectRidge penalty as a prior:
log_prob = -weight * sum(params^2).A Gaussian prior of zero mean and
sigma = 1 / sqrt(2 weight), spelled the way an optimizer user thinks of it.- Variables:
weight (float) – Penalty strength.
- Parameters:
weight (float)
- weight: float = 1.0¶
Inference¶
Inference methods – optimizers and samplers – and their registry.
Every entry satisfies Optimizer or
Sampler. OptaxOptimizer and
MadeToMeasure are implemented, as is DataResampling for
uncertainties on made-to-measure weights; HMC, NUTS,
MeanFieldVI and NelderMead are stubs that fix names and
signatures. Register new methods with METHODS.register(name, cls).
- class mimirax.inference.HMC(step_size=0.1, num_leapfrog_steps=10)[source]¶
Bases:
objectHamiltonian Monte Carlo with a fixed leapfrog trajectory. Stub.
- Variables:
step_size (float) – Leapfrog step.
num_leapfrog_steps (int) – Steps per proposal.
- Parameters:
step_size (float)
num_leapfrog_steps (int)
- step_size: float = 0.1¶
- num_leapfrog_steps: int = 10¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – The initial state.
key (Array) – A
jax.randomkey.num_samples (int) – Samples to draw.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
- class mimirax.inference.NUTS(step_size=0.1, max_tree_depth=10)[source]¶
Bases:
objectThe No-U-Turn Sampler. Stub.
- Variables:
step_size (float) – Initial leapfrog step.
max_tree_depth (int) – Doubling limit.
- Parameters:
step_size (float)
max_tree_depth (int)
- step_size: float = 0.1¶
- max_tree_depth: int = 10¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – The initial state.
key (Array) – A
jax.randomkey.num_samples (int) – Samples to draw.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
- class mimirax.inference.DataResampling(optimizer, sequential=True)[source]¶
Bases:
objectSample the weight posterior by refitting perturbed data. Algorithm 1.
Deliberately not a
Sampler. That protocol takes a scalarlog_density, and this method cannot work from one: it has to reach inside the posterior to find the data and perturb it. Claiming the protocol would mean accepting alog_densityand ignoring it, sosample()takes theInferenceProbleminstead and the type system says what the method needs.- Variables:
optimizer (Optimizer) – What to run per sample. A
MadeToMeasureis the intended choice; itsnum_stepsmultiplies the sample count, so this is where the cost lives.sequential (bool) – Whether to draw the samples one at a time (
jax.lax.map(), the default) or all at once (jax.vmap()). Each sample is an independent optimization, sovmapis embarrassingly parallel andnum_samplestimes the memory – which for a fit that differentiates through an N-body rollout is the wrong trade by default. Turn it on for cheap forward models.
- Parameters:
optimizer (Optimizer)
sequential (bool)
- sequential: bool = True¶
- sample(problem, params, *, key, num_samples)[source]¶
Draw
num_samplesweight vectors by refitting resampled data.Each sample perturbs the observations by their own uncertainty,
Y~ ~ N(Y, S), refits fromparams, and keeps the fitted parameters. The returnedlog_densityis the original problem’s log posterior at each sample – not the perturbed one’s – so it can be compared across samples and used to weight them.The starting point is
paramsfor every sample, not the previous sample: the draws are independent by construction and chaining them would introduce a correlation the derivation does not have.- Parameters:
problem (InferenceProblem) – The posterior to sample. Its
observedis perturbed and itslikelihoodmust carry asigma, since that is theSthe perturbation is drawn from.params (PyTree) – The starting point for every refit – normally the best fit.
key (Array) – A
jax.randomkey.num_samples (int) – How many samples to draw. Static.
- Returns:
sampleswith a leadingnum_samplesaxis on every leaf, andlog_densitythe original posterior at each.- Return type:
- Raises:
TypeError – If the likelihood has no
sigma. The algorithm is defined for Gaussian uncertainties and there is nothing to perturb by otherwise; a caller with a general likelihood needs a general sampler.
- covariance(samples, key='weights')[source]¶
Return the sample covariance of one leaf, the estimate of
V.- Parameters:
samples (PyTree) – The
samplesfield of aSampleResult.key (str | None) – Which leaf to take;
Nonetreatssamplesas the array itself.
- Returns:
(d, d)covariance over thenum_samplesaxis, with thenum_samples - 1denominator. Compare it with(K^T S^-1 K)^-1where the model is linear and no prior is active; expect it to be too narrow otherwise, and by how much the prior contributes.- Return type:
Array
- class mimirax.inference.InferenceProblem(forward, observable, likelihood, observed, priors=())[source]¶
Bases:
objectA posterior over parameters, assembled from protocol-satisfying parts.
log_posterior(params) = likelihood.log_prob(observable(forward(params)), observed) + sum(prior.log_prob(params)). Every method is differentiable inparamsprovided the parts are, which the protocols require.- Variables:
forward (ForwardModel) – Parameters to state.
observable (Observable) – State to data space.
likelihood (Likelihood) – Data-space comparison.
observed (Array) – The data.
priors (tuple[Prior, ...]) – Zero or more priors and regularizers, summed.
- Parameters:
forward (ForwardModel)
observable (Observable)
likelihood (Likelihood)
observed (Array)
priors (tuple[Prior, ...])
- forward: ForwardModel¶
- observable: Observable¶
- likelihood: Likelihood¶
- observed: Array¶
- predict(params)[source]¶
Run the forward model and the observable.
- Parameters:
params (PyTree) – The free parameters.
- Returns:
The prediction in data space.
- Return type:
Array
- log_likelihood(params)[source]¶
Return the log-likelihood of the data at
params.- Parameters:
params (PyTree) – The free parameters.
- Returns:
likelihood.log_prob(predict(params), observed).- Return type:
Scalar
- log_prior(params)[source]¶
Return the summed log prior density at
params.- Parameters:
params (PyTree) – The free parameters.
- Returns:
Zero when there are no priors.
- Return type:
Scalar
- class mimirax.inference.MadeToMeasure(optimizer=None, num_steps=100, epsilon=0.1, transform=<factory>, keys=('weights', ), also_fit=(), refine=None, refine_steps=0)[source]¶
Bases:
objectThe made-to-measure weight-adjustment method, in both of its variants.
Satisfies
Optimizerthroughminimize();force_of_change()is the second, classic iteration on the same objective and is deliberately not the protocol method, so a caller can never get the oracle by accident.Which leaves are weights is
keys(for a mappingparams); every other leaf is carried through untouched byforce_of_change()and optimized in its own space byminimize(). A bare-arrayparamsis taken to be the weights themselves.- Variables:
optimizer (optax.GradientTransformation | None) – The update rule
minimize()descends with, in the unconstrained space;made_to_measure()builds an Adam one.Noneis the default and meansminimize()is unavailable – no step size is guessed on the caller’s behalf – which leavesforce_of_change()usable on its own withepsilon.num_steps (int) – Weight-update steps, for either iteration. Static.
epsilon (float) – The classic force of change’s step size, Syer & Tremaine’s
epsilon. Unused byminimize().transform (Reparameterization) – The positivity reparameterization applied to the weight leaves.
LogTransformby default, because gradient descent inln wis the differentiable counterpart of the classic multiplicative update – see the module docstring.keys (tuple[str, ...]) – Which leaves of a mapping
paramshold weights.also_fit (tuple[str, ...]) –
Further leaves of
paramsto treat as free parameters. Empty by default, and that default is the method’s definition rather than a convenience: made-to-measure fits weights, at initial conditions the caller fixed. Every other leaf ofparams– the positions and velocities a rollout needs – is held constant and returned unchanged.The default was not always this. An earlier version optimized every leaf, so a fit handed
{"positions", "velocities", "weights"}for 64 particles silently had 448 free parameters against ten observables instead of 64, and moved the positions by 17 % and the velocities by 47 % while its docstring claimed to be recovering weights. Fitting the initial conditions too is a legitimate thing to want – Syer & Tremaine’s method extends to it – but it is a different inference problem, and it now has to be asked for by name.refine (optax.GradientTransformation | None) – An optional second stage, run from where the first one stopped:
optax.lbfgs()is the intended use, and the reason the stage exists is that a quasi-Newton rule needs a starting point in the right basin more than it needs many steps.None(the default) runs one stage. Whether two stages beat one is a property of the problem and is measured, not assumed – on both of this module’s test problems the best single-stage fit beats the two-stage one; seereports/M2M_module.md.refine_steps (int) – Steps for the second stage. Static. Ignored when
refineisNone; a quasi-Newton stage wants tens, not thousands.
- Parameters:
optimizer (GradientTransformation | None)
num_steps (int)
epsilon (float)
transform (Reparameterization)
keys (tuple[str, ...])
also_fit (tuple[str, ...])
refine (GradientTransformation | None)
refine_steps (int)
- optimizer: GradientTransformation | None = None¶
- num_steps: int = 100¶
- epsilon: float = 0.1¶
- transform: Reparameterization¶
- keys: tuple[str, ...] = ('weights',)¶
- also_fit: tuple[str, ...] = ()¶
- refine: GradientTransformation | None = None¶
- refine_steps: int = 0¶
- weight_keys(params)[source]¶
Return which leaves of
paramsare weights.- Parameters:
params (PyTree) – The parameters.
- Returns:
The subset of
keysthatparamshas, or()whenparamsis not a mapping and is therefore the weights itself.- Return type:
tuple[str, …]
- Raises:
KeyError – If
paramsis a mapping and holds none ofkeys. Silently optimizing nothing is the one outcome worse than failing here.
- free_parameters(params)[source]¶
Split
paramsinto the free leaves and a function that puts them back.The frozen leaves never enter the optimizer’s state, so they cannot be moved by it, cannot pick up an update from a stale moment estimate, and cost nothing to carry. The rebuild preserves the caller’s key order, so
FitResultcomes back in the structure it went in.- Parameters:
params (PyTree) – The parameters as the caller supplied them.
- Returns:
The free subtree, and a function mapping a free subtree back into a full parameter pytree. For a bare array the subtree is the array and the rebuild is the identity.
- Return type:
tuple[PyTree, Callable[[PyTree], PyTree]]
- to_unconstrained(params)[source]¶
Map the weight leaves into the unconstrained space.
- Parameters:
params (PyTree) – Parameters with positive weight leaves.
- Returns:
The same structure with the weight leaves transformed; a bare array is transformed whole.
- Return type:
PyTree
- to_constrained(unconstrained)[source]¶
Map the weight leaves back into the constrained space.
- Parameters:
unconstrained (PyTree) – Parameters in the unconstrained space.
- Returns:
The same structure with the weight leaves transformed back; a bare array is transformed whole.
- Return type:
PyTree
- minimize(objective, params)[source]¶
Descend
objectivein the unconstrained space; the differentiable variant.The optimizer moves in
u, the objective is always evaluated on the constrained parameters, and the result comes back constrained and in the caller’s own structure. The reparameterization’s log-Jacobian is not added: priors here are densities over the constrained parameters (seemimirax.priors.reference), so a caller who wants a density overu– for a sampler, say – addsmimirax.parameters.log_abs_det_jacobian()to the objective itself. Optimizing without it finds the maximum of the constrained posterior, which is what a made-to-measure fit is asking for.Each stage is one
lax.scan, asOptaxOptimizer’s loop is, so a fit that differentiates through an N-body rollout traces once per stage. The trace records the objective before each update, soobjective_trace[0]is the starting value; with arefinestage the two traces are concatenated and the trace isnum_steps + refine_stepslong.Any optax rule works, including a learning-rate schedule (
optax.adam(optax.cosine_decay_schedule(...))) and the quasi-Newton and line-search rules whoseupdateneeds the objective’s value and the objective itself – see_descend(). Which rule to use is a property of the problem and is measured rather than asserted; the module’s report carries the comparison, and its short version is that no rule wins on both of the method’s two constructions.Only the weight leaves move, plus anything named in
also_fit. Everything else inparamsis a fixed input to the forward model – the initial conditions a rollout integrates from – and comes back unchanged. That is what made-to-measure means, and getting it wrong turns “recover 64 weights from ten observables” into a fit with 448 free parameters; seealso_fit.- Parameters:
objective (Callable[[PyTree], Scalar]) – The time-averaged negative log posterior, a function of the constrained parameters.
params (PyTree) – The starting point, with positive weight leaves. The frozen leaves are still required here: the objective needs them.
- Returns:
Constrained final parameters in the input’s structure, with the frozen leaves untouched, and the objective trace.
- Return type:
- Raises:
ValueError – If
optimizerisNone; the step size is the caller’s choice and is not guessed here.
- force_of_change(objective, params)[source]¶
Run the classic Syer & Tremaine force-of-change iteration.
w <- w * exp(-epsilon * dF/dw), the weight leaves only; every other leaf is returned exactly as it came in. This is the oracleminimize()is compared against, not a method to prefer: it is first-order in one hand-tuned step size, it descends in a fixed metric, and – the substantive point – it is only the right gradient when the orbits do not depend on the weights. Use it on the tracer construction (mimirax.adapters.nornax.NornaxRollout.tracer()). On the self-consistent constructiondF/dwhere still includes the orbits’ dependence on the weights, becausejax.grad()cannot be told to forget it, so the iteration is not the classic algorithm there and the classic algorithm is not defined there either.Its fixed point is
minimize()’s: both stop exactly wheredF/dw = 0. The multiplicative step is what enforces positivity – the package’s rule is reparameterization, not clipping – and it is identical to a unit-step gradient descent inln wscaled byepsilon, which is the algebraic reason the two variants share their stationary points.- Parameters:
objective (Callable[[PyTree], Scalar]) – The time-averaged negative log posterior, a function of the constrained parameters.
params (PyTree) – The starting point, with positive weight leaves.
- Returns:
Final parameters in the input’s structure, and the
(num_steps,)objective trace, recorded before each update.- Return type:
- class mimirax.inference.MeanFieldVI(num_steps=1000, learning_rate=0.01)[source]¶
Bases:
objectMean-field Gaussian variational inference by gradient ascent on the ELBO. Stub.
- Variables:
num_steps (int) – Optimization steps for the variational parameters.
learning_rate (float) – Step size.
- Parameters:
num_steps (int)
learning_rate (float)
- num_steps: int = 1000¶
- learning_rate: float = 0.01¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – Initial mean of the variational family.
key (Array) – A
jax.randomkey.num_samples (int) – Draws to return from the fitted family.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
- class mimirax.inference.NelderMead(num_steps=500)[source]¶
Bases:
objectThe Nelder-Mead simplex method. Stub.
- Variables:
num_steps (int) – Simplex iterations.
- Parameters:
num_steps (int)
- num_steps: int = 500¶
- class mimirax.inference.OptaxOptimizer(optimizer, num_steps=100)[source]¶
Bases:
objectMinimize an objective with any
optax.GradientTransformation.The loop is a
lax.scanover a static number of steps, so the whole fit traces once and runs on device. The trace records the objective before each update, soobjective_trace[0]is the value at the starting point.- Variables:
optimizer (optax.GradientTransformation) – The update rule.
num_steps (int) – How many updates to take. Static.
- Parameters:
optimizer (GradientTransformation)
num_steps (int)
- optimizer: GradientTransformation¶
- num_steps: int = 100¶
- mimirax.inference.adam(learning_rate=0.01, num_steps=200)[source]¶
Build an Adam optimizer with the given settings.
- Parameters:
learning_rate (float) – Adam’s step size.
num_steps (int) – Number of updates.
- Returns:
Ready to
minimize.- Return type:
- mimirax.inference.made_to_measure(learning_rate=0.01, num_steps=200, *, epsilon=0.1, keys=('weights',), also_fit=(), refine=None, refine_steps=0)[source]¶
Build a made-to-measure method with an Adam rule, as
adam()does.Adam is the default because it is the default everywhere, not because it is the best rule for this objective – on the module’s tracer problem plain
optax.sgd(1e-6)reaches a gradient norm an order of magnitude smaller, andoptax.lbfgs()reaches a lower objective in fifty steps than Adam does in ten thousand. Neither transfers to the self-consistent problem, where a fixed-step SGD diverges outright. Passoptimizer=toMadeToMeasuredirectly for anything other than Adam, and read the measured comparison inreports/M2M_module.mdbefore choosing.- Parameters:
learning_rate (optax.ScalarOrSchedule) – Adam’s step size in the log-weight space, or an
optaxschedule – the annotation is optax’s own alias rather than a hand-writtenfloat | Callable, because a schedule takeschex.Numericand notArray, and CI’s pyright is stricter than the local one about the difference (a difference the scaffold report already records). –optax.exponential_decay(0.05, 2000, 0.3)measured better on the tracer problem than any constant rate tried.num_steps (int) – Number of weight updates.
epsilon (float) – The classic force of change’s step size.
keys (tuple[str, ...]) – Which leaves of a mapping
paramshold weights.also_fit (tuple[str, ...]) – Further leaves to treat as free parameters; empty means weights only.
refine (optax.GradientTransformation | None) – An optional second-stage rule, e.g.
optax.lbfgs().refine_steps (int) – Steps for the second stage.
- Returns:
Ready to
minimize()or toforce_of_change().- Return type:
Composing a forward model, an observable, a likelihood and priors into one objective.
This is the one place the pieces meet. Everything else in the package is either a pluggable implementation of one protocol or a method that consumes the scalar functions defined here.
- class mimirax.inference.objective.InferenceProblem(forward, observable, likelihood, observed, priors=())[source]¶
Bases:
objectA posterior over parameters, assembled from protocol-satisfying parts.
log_posterior(params) = likelihood.log_prob(observable(forward(params)), observed) + sum(prior.log_prob(params)). Every method is differentiable inparamsprovided the parts are, which the protocols require.- Variables:
forward (ForwardModel) – Parameters to state.
observable (Observable) – State to data space.
likelihood (Likelihood) – Data-space comparison.
observed (Array) – The data.
priors (tuple[Prior, ...]) – Zero or more priors and regularizers, summed.
- Parameters:
forward (ForwardModel)
observable (Observable)
likelihood (Likelihood)
observed (Array)
priors (tuple[Prior, ...])
- forward: ForwardModel¶
- observable: Observable¶
- likelihood: Likelihood¶
- observed: Array¶
- predict(params)[source]¶
Run the forward model and the observable.
- Parameters:
params (PyTree) – The free parameters.
- Returns:
The prediction in data space.
- Return type:
Array
- log_likelihood(params)[source]¶
Return the log-likelihood of the data at
params.- Parameters:
params (PyTree) – The free parameters.
- Returns:
likelihood.log_prob(predict(params), observed).- Return type:
Scalar
- log_prior(params)[source]¶
Return the summed log prior density at
params.- Parameters:
params (PyTree) – The free parameters.
- Returns:
Zero when there are no priors.
- Return type:
Scalar
Gradient-based optimization through optax.
The one inference method the scaffold implements rather than stubs, because every other method’s tests need a working baseline to compare against and because it is the method Paper I section 7’s fit uses.
- class mimirax.inference.optimizers.OptaxOptimizer(optimizer, num_steps=100)[source]¶
Bases:
objectMinimize an objective with any
optax.GradientTransformation.The loop is a
lax.scanover a static number of steps, so the whole fit traces once and runs on device. The trace records the objective before each update, soobjective_trace[0]is the value at the starting point.- Variables:
optimizer (optax.GradientTransformation) – The update rule.
num_steps (int) – How many updates to take. Static.
- Parameters:
optimizer (GradientTransformation)
num_steps (int)
- optimizer: GradientTransformation¶
- num_steps: int = 100¶
- mimirax.inference.optimizers.adam(learning_rate=0.01, num_steps=200)[source]¶
Build an Adam optimizer with the given settings.
- Parameters:
learning_rate (float) – Adam’s step size.
num_steps (int) – Number of updates.
- Returns:
Ready to
minimize.- Return type:
Hamiltonian Monte Carlo and NUTS. Stubs.
Both are Sampler implementations by signature; the
bodies raise until a method is written against them. Whether that is a
hand-rolled leapfrog integrator or a wrapper over an external library is left
open on purpose – the registry takes either.
- class mimirax.inference.hmc.HMC(step_size=0.1, num_leapfrog_steps=10)[source]¶
Bases:
objectHamiltonian Monte Carlo with a fixed leapfrog trajectory. Stub.
- Variables:
step_size (float) – Leapfrog step.
num_leapfrog_steps (int) – Steps per proposal.
- Parameters:
step_size (float)
num_leapfrog_steps (int)
- step_size: float = 0.1¶
- num_leapfrog_steps: int = 10¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – The initial state.
key (Array) – A
jax.randomkey.num_samples (int) – Samples to draw.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
- class mimirax.inference.hmc.NUTS(step_size=0.1, max_tree_depth=10)[source]¶
Bases:
objectThe No-U-Turn Sampler. Stub.
- Variables:
step_size (float) – Initial leapfrog step.
max_tree_depth (int) – Doubling limit.
- Parameters:
step_size (float)
max_tree_depth (int)
- step_size: float = 0.1¶
- max_tree_depth: int = 10¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – The initial state.
key (Array) – A
jax.randomkey.num_samples (int) – Samples to draw.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
Variational inference. Stub.
A fitted variational family is something one samples from, so the class
satisfies Sampler: sample fits the family to
log_density and returns draws from it.
- class mimirax.inference.vi.MeanFieldVI(num_steps=1000, learning_rate=0.01)[source]¶
Bases:
objectMean-field Gaussian variational inference by gradient ascent on the ELBO. Stub.
- Variables:
num_steps (int) – Optimization steps for the variational parameters.
learning_rate (float) – Step size.
- Parameters:
num_steps (int)
learning_rate (float)
- num_steps: int = 1000¶
- learning_rate: float = 0.01¶
- sample(log_density, params, *, key, num_samples)[source]¶
Not implemented yet.
- Parameters:
log_density (Callable[[PyTree], Scalar]) – The unnormalized log target.
params (PyTree) – Initial mean of the variational family.
key (Array) – A
jax.randomkey.num_samples (int) – Draws to return from the fitted family.
- Returns:
Never returns.
- Return type:
- Raises:
NotImplementedError – Always.
Derivative-free optimization. Stub.
Kept in the method registry from day one because the programme’s own plan (the gradient-reliability study ahead of Kessel Run) expects cases where the gradient through a chaotic rollout is not trustworthy and a derivative-free method is the honest comparison.
- class mimirax.inference.derivative_free.NelderMead(num_steps=500)[source]¶
Bases:
objectThe Nelder-Mead simplex method. Stub.
- Variables:
num_steps (int) – Simplex iterations.
- Parameters:
num_steps (int)
- num_steps: int = 500¶
Made-to-measure: particle weights adjusted against time-averaged observables.
WHERE THIS LIVES, AND WHY. The EDDA programme’s decision D-026 (2026-09-06)
puts the made-to-measure module in mimirax rather than in the integrator
package: its three parts are an orbit integration (nornax’s), a force (any
ForceModel), and an observable-residual-driven
weight update with an entropy prior – and the last is inference. The
integrator enters through mimirax.adapters.nornax, behind the
mimirax[nornax] extra; nothing here imports nornax.
THE OBJECTIVE. Both methods in this module minimize the same scalar,
the chi-squared of the time-averaged observables against the data plus the
entropy prior of EntropyPrior. An
InferenceProblem built from
mimirax.adapters.nornax.NornaxRollout,
mimirax.observables.TimeAverage around
mimirax.observables.WeightedKernelSum,
GaussianLikelihood and an EntropyPrior
is that objective, and problem.negative_log_posterior is what both
methods below are handed. mu lives on the prior and has no second copy
here, so the two methods cannot disagree about the regularization they share.
TWO ITERATIONS, ONE STATIONARY POINT. Syer & Tremaine’s (1996) force of change is
so the bracket the classic algorithm assembles by hand is exactly
dF/dw – which is why this module computes it with jax.grad() and
does not re-derive it (a test pins the two against each other on the tracer
problem). The two methods then differ only in the metric they descend in:
MadeToMeasure.force_of_change()is the classic update,ln w <- ln w - eps dF/dw. That is a preconditioned gradient step inwwith preconditionerdiag(w), and the multiplicative spelling is what keeps the weights positive without a clip – the historical Euler formw <- w (1 - eps dF/dw)can step straight through zero and differs from this atO(eps^2).MadeToMeasure.minimize()is the differentiable variant this package exists for: plain gradient descent (any optax rule) inu = ln w, i.e. a step inwwith preconditionerdiag(w^2), on an objective that is differentiated through the orbit integration.
Both are stationary exactly where dF/dw = 0, since w > 0. So they must
agree on the answer and are free to disagree on the path – which is what makes
the classic iteration a usable oracle, and what a measured comparison between
them is testing. When the weights are the masses of a self-gravitating system
the orbits themselves depend on the weights and only the second method’s
gradient is the true one; the first is then not merely slower but wrong about
dF/dw, which is why force_of_change() is documented for the tracer
case.
WHAT IS NOT CLAIMED. Nothing here says a time average over a given window is
long enough, that a chosen mu is right, or that the method converges at FMM
scale. Those are experiments, and the numbers this module was verified at are
in the EDDA programme’s reports/M2M_module.md.
- class mimirax.inference.m2m.MadeToMeasure(optimizer=None, num_steps=100, epsilon=0.1, transform=<factory>, keys=('weights', ), also_fit=(), refine=None, refine_steps=0)[source]¶
Bases:
objectThe made-to-measure weight-adjustment method, in both of its variants.
Satisfies
Optimizerthroughminimize();force_of_change()is the second, classic iteration on the same objective and is deliberately not the protocol method, so a caller can never get the oracle by accident.Which leaves are weights is
keys(for a mappingparams); every other leaf is carried through untouched byforce_of_change()and optimized in its own space byminimize(). A bare-arrayparamsis taken to be the weights themselves.- Variables:
optimizer (optax.GradientTransformation | None) – The update rule
minimize()descends with, in the unconstrained space;made_to_measure()builds an Adam one.Noneis the default and meansminimize()is unavailable – no step size is guessed on the caller’s behalf – which leavesforce_of_change()usable on its own withepsilon.num_steps (int) – Weight-update steps, for either iteration. Static.
epsilon (float) – The classic force of change’s step size, Syer & Tremaine’s
epsilon. Unused byminimize().transform (Reparameterization) – The positivity reparameterization applied to the weight leaves.
LogTransformby default, because gradient descent inln wis the differentiable counterpart of the classic multiplicative update – see the module docstring.keys (tuple[str, ...]) – Which leaves of a mapping
paramshold weights.also_fit (tuple[str, ...]) –
Further leaves of
paramsto treat as free parameters. Empty by default, and that default is the method’s definition rather than a convenience: made-to-measure fits weights, at initial conditions the caller fixed. Every other leaf ofparams– the positions and velocities a rollout needs – is held constant and returned unchanged.The default was not always this. An earlier version optimized every leaf, so a fit handed
{"positions", "velocities", "weights"}for 64 particles silently had 448 free parameters against ten observables instead of 64, and moved the positions by 17 % and the velocities by 47 % while its docstring claimed to be recovering weights. Fitting the initial conditions too is a legitimate thing to want – Syer & Tremaine’s method extends to it – but it is a different inference problem, and it now has to be asked for by name.refine (optax.GradientTransformation | None) – An optional second stage, run from where the first one stopped:
optax.lbfgs()is the intended use, and the reason the stage exists is that a quasi-Newton rule needs a starting point in the right basin more than it needs many steps.None(the default) runs one stage. Whether two stages beat one is a property of the problem and is measured, not assumed – on both of this module’s test problems the best single-stage fit beats the two-stage one; seereports/M2M_module.md.refine_steps (int) – Steps for the second stage. Static. Ignored when
refineisNone; a quasi-Newton stage wants tens, not thousands.
- Parameters:
optimizer (GradientTransformation | None)
num_steps (int)
epsilon (float)
transform (Reparameterization)
keys (tuple[str, ...])
also_fit (tuple[str, ...])
refine (GradientTransformation | None)
refine_steps (int)
- optimizer: GradientTransformation | None = None¶
- num_steps: int = 100¶
- epsilon: float = 0.1¶
- transform: Reparameterization¶
- keys: tuple[str, ...] = ('weights',)¶
- also_fit: tuple[str, ...] = ()¶
- refine: GradientTransformation | None = None¶
- refine_steps: int = 0¶
- weight_keys(params)[source]¶
Return which leaves of
paramsare weights.- Parameters:
params (PyTree) – The parameters.
- Returns:
The subset of
keysthatparamshas, or()whenparamsis not a mapping and is therefore the weights itself.- Return type:
tuple[str, …]
- Raises:
KeyError – If
paramsis a mapping and holds none ofkeys. Silently optimizing nothing is the one outcome worse than failing here.
- free_parameters(params)[source]¶
Split
paramsinto the free leaves and a function that puts them back.The frozen leaves never enter the optimizer’s state, so they cannot be moved by it, cannot pick up an update from a stale moment estimate, and cost nothing to carry. The rebuild preserves the caller’s key order, so
FitResultcomes back in the structure it went in.- Parameters:
params (PyTree) – The parameters as the caller supplied them.
- Returns:
The free subtree, and a function mapping a free subtree back into a full parameter pytree. For a bare array the subtree is the array and the rebuild is the identity.
- Return type:
tuple[PyTree, Callable[[PyTree], PyTree]]
- to_unconstrained(params)[source]¶
Map the weight leaves into the unconstrained space.
- Parameters:
params (PyTree) – Parameters with positive weight leaves.
- Returns:
The same structure with the weight leaves transformed; a bare array is transformed whole.
- Return type:
PyTree
- to_constrained(unconstrained)[source]¶
Map the weight leaves back into the constrained space.
- Parameters:
unconstrained (PyTree) – Parameters in the unconstrained space.
- Returns:
The same structure with the weight leaves transformed back; a bare array is transformed whole.
- Return type:
PyTree
- minimize(objective, params)[source]¶
Descend
objectivein the unconstrained space; the differentiable variant.The optimizer moves in
u, the objective is always evaluated on the constrained parameters, and the result comes back constrained and in the caller’s own structure. The reparameterization’s log-Jacobian is not added: priors here are densities over the constrained parameters (seemimirax.priors.reference), so a caller who wants a density overu– for a sampler, say – addsmimirax.parameters.log_abs_det_jacobian()to the objective itself. Optimizing without it finds the maximum of the constrained posterior, which is what a made-to-measure fit is asking for.Each stage is one
lax.scan, asOptaxOptimizer’s loop is, so a fit that differentiates through an N-body rollout traces once per stage. The trace records the objective before each update, soobjective_trace[0]is the starting value; with arefinestage the two traces are concatenated and the trace isnum_steps + refine_stepslong.Any optax rule works, including a learning-rate schedule (
optax.adam(optax.cosine_decay_schedule(...))) and the quasi-Newton and line-search rules whoseupdateneeds the objective’s value and the objective itself – see_descend(). Which rule to use is a property of the problem and is measured rather than asserted; the module’s report carries the comparison, and its short version is that no rule wins on both of the method’s two constructions.Only the weight leaves move, plus anything named in
also_fit. Everything else inparamsis a fixed input to the forward model – the initial conditions a rollout integrates from – and comes back unchanged. That is what made-to-measure means, and getting it wrong turns “recover 64 weights from ten observables” into a fit with 448 free parameters; seealso_fit.- Parameters:
objective (Callable[[PyTree], Scalar]) – The time-averaged negative log posterior, a function of the constrained parameters.
params (PyTree) – The starting point, with positive weight leaves. The frozen leaves are still required here: the objective needs them.
- Returns:
Constrained final parameters in the input’s structure, with the frozen leaves untouched, and the objective trace.
- Return type:
- Raises:
ValueError – If
optimizerisNone; the step size is the caller’s choice and is not guessed here.
- force_of_change(objective, params)[source]¶
Run the classic Syer & Tremaine force-of-change iteration.
w <- w * exp(-epsilon * dF/dw), the weight leaves only; every other leaf is returned exactly as it came in. This is the oracleminimize()is compared against, not a method to prefer: it is first-order in one hand-tuned step size, it descends in a fixed metric, and – the substantive point – it is only the right gradient when the orbits do not depend on the weights. Use it on the tracer construction (mimirax.adapters.nornax.NornaxRollout.tracer()). On the self-consistent constructiondF/dwhere still includes the orbits’ dependence on the weights, becausejax.grad()cannot be told to forget it, so the iteration is not the classic algorithm there and the classic algorithm is not defined there either.Its fixed point is
minimize()’s: both stop exactly wheredF/dw = 0. The multiplicative step is what enforces positivity – the package’s rule is reparameterization, not clipping – and it is identical to a unit-step gradient descent inln wscaled byepsilon, which is the algebraic reason the two variants share their stationary points.- Parameters:
objective (Callable[[PyTree], Scalar]) – The time-averaged negative log posterior, a function of the constrained parameters.
params (PyTree) – The starting point, with positive weight leaves.
- Returns:
Final parameters in the input’s structure, and the
(num_steps,)objective trace, recorded before each update.- Return type:
- mimirax.inference.m2m.made_to_measure(learning_rate=0.01, num_steps=200, *, epsilon=0.1, keys=('weights',), also_fit=(), refine=None, refine_steps=0)[source]¶
Build a made-to-measure method with an Adam rule, as
adam()does.Adam is the default because it is the default everywhere, not because it is the best rule for this objective – on the module’s tracer problem plain
optax.sgd(1e-6)reaches a gradient norm an order of magnitude smaller, andoptax.lbfgs()reaches a lower objective in fifty steps than Adam does in ten thousand. Neither transfers to the self-consistent problem, where a fixed-step SGD diverges outright. Passoptimizer=toMadeToMeasuredirectly for anything other than Adam, and read the measured comparison inreports/M2M_module.mdbefore choosing.- Parameters:
learning_rate (optax.ScalarOrSchedule) – Adam’s step size in the log-weight space, or an
optaxschedule – the annotation is optax’s own alias rather than a hand-writtenfloat | Callable, because a schedule takeschex.Numericand notArray, and CI’s pyright is stricter than the local one about the difference (a difference the scaffold report already records). –optax.exponential_decay(0.05, 2000, 0.3)measured better on the tracer problem than any constant rate tried.num_steps (int) – Number of weight updates.
epsilon (float) – The classic force of change’s step size.
keys (tuple[str, ...]) – Which leaves of a mapping
paramshold weights.also_fit (tuple[str, ...]) – Further leaves to treat as free parameters; empty means weights only.
refine (optax.GradientTransformation | None) – An optional second-stage rule, e.g.
optax.lbfgs().refine_steps (int) – Steps for the second stage.
- Returns:
Ready to
minimize()or toforce_of_change().- Return type:
Uncertainties on made-to-measure weights, by resampling the data.
THE METHOD, AND WHOSE IT IS. This is Algorithm 1 of Bovy, Kawata & Hunt (2018,
arXiv:1704.03884), the paper that added
uncertainty quantification to made-to-measure. Their observation is that for a
linear-Gaussian model Y = K W + delta with delta ~ N(0, S), the
posterior over W under a uniform prior is Gaussian with mean
V K^T S^-1 Y and covariance V = (K^T S^-1 K)^-1, so instead of
computing that covariance one can sample it:
Y~ ~ N(Y, S)
W~ = argmin chi^2(model(W), Y~)
Each W~ is an exact draw from the posterior, because it is a linear
transform of a Gaussian with the right mean and the right covariance. No
Metropolis acceptance, no chain to converge, no step size – and one
optimization per sample, all of them independent and therefore trivially
parallel.
WHY IT SUITS THIS PROBLEM. Bovy et al. give the reasons and they are the same
reasons a general-purpose sampler is the wrong tool here: the weight space is
high-dimensional (one dimension per particle), the posterior at any given
snapshot is noisy, and the weights’ uncertainties are strongly correlated.
mimirax’s own HMC and
NUTS would meet all three problems; this method
meets none of them, because it never proposes a step in weight space at all.
WHERE IT IS EXACT, AND WHERE IT IS NOT. This matters more here than in the original paper, and the module measures it rather than repeating it:
Exact for a linear model with Gaussian noise and no prior. The made-to-measure observable is linear in the weights (
mimirax.observables.WeightedKernelSum), and in the tracer construction the orbits do not depend on them, so the whole objective is quadratic and this is the posterior. A test compares the sample covariance with(K^T S^-1 K)^-1directly.Approximate with a prior. Bovy et al. say so: the method “does not properly deal with particle weights for which the penalty term … has a significant effect”. Resampling the data does not resample the prior, so the draws are too narrow along every direction the prior holds up – which, on this package’s own tracer problem, is 54 of 64 directions. Read
mimirax.effective_parameters()before trusting a spread from this.Approximate under the positivity constraint, for the same reason: the derivation assumes
Wis unconstrained, and a weight pinned near zero has a one-sided posterior a Gaussian draw cannot represent.Not a posterior at all in the self-consistent construction, where the weights move the orbits and the model is no longer linear in them.
None of that makes it useless – it makes it a method with a stated domain,
which is better than a chain whose convergence nobody checked. The two
approximations are quantified in tests/unit/test_resampling.py and in the
EDDA programme’s reports/M2M_module.md.
- class mimirax.inference.resampling.DataResampling(optimizer, sequential=True)[source]¶
Bases:
objectSample the weight posterior by refitting perturbed data. Algorithm 1.
Deliberately not a
Sampler. That protocol takes a scalarlog_density, and this method cannot work from one: it has to reach inside the posterior to find the data and perturb it. Claiming the protocol would mean accepting alog_densityand ignoring it, sosample()takes theInferenceProbleminstead and the type system says what the method needs.- Variables:
optimizer (Optimizer) – What to run per sample. A
MadeToMeasureis the intended choice; itsnum_stepsmultiplies the sample count, so this is where the cost lives.sequential (bool) – Whether to draw the samples one at a time (
jax.lax.map(), the default) or all at once (jax.vmap()). Each sample is an independent optimization, sovmapis embarrassingly parallel andnum_samplestimes the memory – which for a fit that differentiates through an N-body rollout is the wrong trade by default. Turn it on for cheap forward models.
- Parameters:
optimizer (Optimizer)
sequential (bool)
- sequential: bool = True¶
- sample(problem, params, *, key, num_samples)[source]¶
Draw
num_samplesweight vectors by refitting resampled data.Each sample perturbs the observations by their own uncertainty,
Y~ ~ N(Y, S), refits fromparams, and keeps the fitted parameters. The returnedlog_densityis the original problem’s log posterior at each sample – not the perturbed one’s – so it can be compared across samples and used to weight them.The starting point is
paramsfor every sample, not the previous sample: the draws are independent by construction and chaining them would introduce a correlation the derivation does not have.- Parameters:
problem (InferenceProblem) – The posterior to sample. Its
observedis perturbed and itslikelihoodmust carry asigma, since that is theSthe perturbation is drawn from.params (PyTree) – The starting point for every refit – normally the best fit.
key (Array) – A
jax.randomkey.num_samples (int) – How many samples to draw. Static.
- Returns:
sampleswith a leadingnum_samplesaxis on every leaf, andlog_densitythe original posterior at each.- Return type:
- Raises:
TypeError – If the likelihood has no
sigma. The algorithm is defined for Gaussian uncertainties and there is nothing to perturb by otherwise; a caller with a general likelihood needs a general sampler.
- covariance(samples, key='weights')[source]¶
Return the sample covariance of one leaf, the estimate of
V.- Parameters:
samples (PyTree) – The
samplesfield of aSampleResult.key (str | None) – Which leaf to take;
Nonetreatssamplesas the array itself.
- Returns:
(d, d)covariance over thenum_samplesaxis, with thenum_samples - 1denominator. Compare it with(K^T S^-1 K)^-1where the model is linear and no prior is active; expect it to be too narrow otherwise, and by how much the prior contributes.- Return type:
Array
Diagnostics¶
Residuals between prediction and data.
- mimirax.diagnostics.residuals.normalized_residuals(predicted, observed, sigma=1.0)[source]¶
Return
(predicted - observed) / sigma.- Parameters:
predicted (Array) – Model prediction.
observed (Array) – Data, same shape.
sigma (float) – Noise level.
- Returns:
Residuals in units of the noise.
- Return type:
Array
- mimirax.diagnostics.residuals.residuals(predicted, observed)[source]¶
Return
predicted - observed.- Parameters:
predicted (Array) – Model prediction.
observed (Array) – Data, same shape.
- Returns:
The signed residuals.
- Return type:
Array
- mimirax.diagnostics.residuals.rms_residual(predicted, observed)[source]¶
Return the root-mean-square residual.
- Parameters:
predicted (Array) – Model prediction.
observed (Array) – Data, same shape.
- Returns:
sqrt(mean((predicted - observed)^2)).- Return type:
Scalar
Convergence judgements from an objective trace.
- mimirax.diagnostics.convergence.has_converged(trace, *, rtol=1e-06, window=5)[source]¶
Whether the last
windowrelative changes are all belowrtol.A host-side judgement (returns a Python
bool), meant for after a fit, not inside a traced loop.- Parameters:
trace (Array) –
(k,)objective values,k > window.rtol (float) – Relative-change threshold.
window (int) – How many trailing steps must all be below it.
- Returns:
Truewhen converged by this criterion.- Return type:
bool
- mimirax.diagnostics.convergence.relative_change(trace, eps=1e-300)[source]¶
Return
|x[k] - x[k-1]| / (|x[k-1]| + eps)along a trace.- Parameters:
trace (Array) –
(k,)objective values.eps (float) – Guard against division by zero.
- Returns:
(k - 1,)relative changes.- Return type:
Array
Degeneracy diagnostics: which parameter directions the data do not see.
Recovering discrete source positions from field samples has continuous degeneracies (Paper I section 7 says so in its first paragraph), and the diagnostics that name them are more useful than the ones that hide them. The Fisher information’s null-ish eigenvectors are the first such diagnostic.
- mimirax.diagnostics.degeneracy.degenerate_directions(fisher, *, rtol=1e-08)[source]¶
Return the eigenvalues and the eigenvectors below
rtolof the largest.- Parameters:
fisher (Array) –
(d, d)symmetric matrix fromfisher_information().rtol (float) – Eigenvalues below
rtol * max(eigenvalues)count as degenerate.
- Returns:
All
(d,)eigenvalues in ascending order, and the(d, r)matrix whose columns are the degenerate directions (rmay be zero).- Return type:
tuple[Array, Array]
- mimirax.diagnostics.degeneracy.effective_parameters(data_curvature, prior_curvature)[source]¶
Return how many parameters the data actually determined.
tr(H_data (H_data + H_prior)^-1), the standard effective number of degrees of freedom of a regularized fit. In the joint eigenbasis it issum_i lambda_i / (lambda_i + pi_i): a direction the data constrain tightly (lambda >> pi) contributes 1, a direction only the prior holds up (lambda << pi) contributes 0, and the total interpolates.WHY THIS AND NOT AN EIGENVALUE COUNT.
degenerate_directions()needs anrtoland returns a count, so it reports a rank at a threshold and puts a marginal direction wholly on one side. This needs no threshold and is the quantity that actually answers “is my objective informative enough”: on the made-to-measure tracer problem ofreports/M2M_module.md– 64 weights, 10 observables – it returns 10.000, exactly the number of observables, whiledegenerate_directions(rtol=1e-3)reports 55 of 64 and the fit reaches a chi-squared of 1.4e-08. Ten numbers went in and ten degrees of freedom came out; the other 54 are the prior’s answer, and no optimizer, step size ormuchanges that. More information means more or better observables.Read it against the parameter count, not on its own:
10out of 64 says the fit is prior-dominated, and10out of 10 would say it is saturated.WHERE THIS DENSE ROUTE FAILS. It solves against
H_data + H_prior, and aH_databuilt from a Jacobian asJ^T C^-1 Jalready has the squared condition number ofJ. Its absolute error is abouteps * d_max, so a direction at the prior’s threshold is lost onced_max / thresholdexceeds1 / eps. Measured in the EDDA Fisher probe (reports/FISHER_fd_vs_ad_probe.md, section 5.7), on 32 weights:kept in single precision, it returns values from -314 to +257 on Jacobians taken at the probe’s default steps, and 28.19 on a single-precision AD Jacobian that, analysed in double, gives 13.92;
in double precision it differs from the SVD route by 6.6e-5 to 9.7e-4 on the probe’s three radial sets – more than the effects that probe set out to measure.
The single-precision failure is not a slow loss of accuracy. On the unit test’s version of that set the prior’s
3.2e-6lies below the float32 spacing (8e-3) of every diagonal entry ofJ^T C^-1 J, soH_data + H_priorisH_data, bit for bit, and what comes back istr(F F^-1)of a numerically singularF: rounding, whatevermu.It is also only as much a Fisher matrix as
data_curvatureis: a Hessian taken away from an exact fit of a nonlinear model is not one (seefisher_information()). Given the Jacobian, prefereffective_parameters_from_jacobian(), which has neither problem. This function is kept as it is so that existing results reproduce.- Parameters:
data_curvature (Array) –
(d, d)Hessian of the negative log likelihood alone – the data’s contribution. Get it asfisher_information(problem.negative_log_ likelihood_only, params), or by subtracting the priors’ curvature from the fullfisher_information().prior_curvature (Array) –
(d, d)Hessian of the negative log prior, same basis and ordering.
- Returns:
The effective number of data-determined parameters, between 0 and
d. Not an integer in general.- Return type:
Scalar
- mimirax.diagnostics.degeneracy.effective_parameters_from_jacobian(jacobian, prior_curvature, *, sigma=None)[source]¶
Return
effective_parameters()from the Jacobian, by an SVD.The same quantity,
tr(F (F + H_prior)^-1)withF = J^T C^-1 Jthe Gauss–Newton matrix – the Fisher information of a Gaussian likelihood – computed without ever formingF. Whiten the Jacobian by the noise and by the prior,B = C^(-1/2) J L^-TwithH_prior = L L^T. ThenF + H_prior = L (B^T B + I) L^T, and the trace is exactlysum_k s_k^2 / (s_k^2 + 1)over the singular values
s_kofB. A diagonal priorpmakesL = diag(sqrt(p))and the whitening a column scaling, so no(d, d)matrix is formed at all. Withp_i = mu / w_i– the curvature ofEntropyPrioratw– the sum issum_k d_k / (d_k + mu)over the squared singular valuesd_kofC^(-1/2) J diag(sqrt(w)), the prior-whitened form ofreports/M2M_production_readiness.md(A2); at uniformw = w0it issum_k d_k / (d_k + mu / w0)over those ofC^(-1/2) J. Either waymuis a threshold on a spectrum the observables alone determine.WHY AN SVD. Forming
Fsquares the condition number: its absolute error is abouteps * d_max, and the dense route loses a direction at the threshold onced_max / thresholdexceeds1 / eps. The singular values ofBcarry an absolute error of abouteps * s_maxagainst a threshold ats = 1, so this holds untils_maxreaches1 / eps:d_max / thresholdup to1 / eps^2, the square of the dense route’s range. (Those are norm-wise bounds, i.e. reasoning.) Measured in the EDDA Fisher probe (reports/FISHER_fd_vs_ad_probe.md, section 5.7) on the sensitivity figure’sradial_wideset, 32 weights,d_max / thresholdof 1.6e13, from a single-precision AD Jacobian: this route in single precision gives 13.937 against 13.920 from the same Jacobian analysed in double, whereeffective_parameters()gives 28.19. In double precision, forward- and reverse-mode Jacobians agree through this route to 6e-12, while the dense route differs from it by up to 9.7e-4. The margin does shrink toward1 / eps^2: on the unit test’s circular-orbit version of that set, the single-precision error is 0.003 atd_max / thresholdof 1.6e12 and 0.03 at 1.6e13. The arithmetic is in the precision the inputs promote to, so a float64 prior turns a float32 Jacobian’s analysis into a float64 one.WHY A JACOBIAN AND NOT A HESSIAN.
Fhere isJ^T C^-1 Jat whatever pointJwas taken – positive semi-definite and of rank at mostmin(m, d), so the count cannot exceed the number of observables. The Hessian offisher_information()is that matrix only where the model is linear or the residuals vanish; elsewhere its residual curvature is counted as information (see there).Get
Jby differentiating the prediction –observable(forward(x))– over the flattened free parameters:jax.jacrev()costsmreverse passes and is the cheaper when there are more parameters than data, as in made-to-measure;jax.jacfwd()costsdtangents.- Parameters:
jacobian (Array) –
(m, d)derivative of the prediction with respect to the flattened parameters, rows in data order and columns in the prior’s order. Already whitened,C^(-1/2) J, ifsigmaisNone.prior_curvature (Array) – Hessian of the negative log prior, which must be positive definite. Either
(d,), the diagonal of a diagonal prior – exact, and the form to pass forEntropyPrior, whose curvature ismu / w– or(d, d), a dense one, whitened by its Cholesky factor. A direction with no prior curvature has no defined count, and a zero diagonal entry, or a dense prior that is not positive definite, givesnanorinf.sigma (SigmaLike | None) – Noise width to whiten the rows by: one number, or
(m,), asGaussianLikelihoodtakes it.Nonemeansjacobianis already whitened; for a correlated noise covarianceC = K K^T, passK^-1 JandNone.
- Returns:
The effective number of data-determined parameters, between 0 and
min(m, d). Not an integer in general.- Return type:
Scalar
- Raises:
ValueError – If
jacobianis not two-dimensional, ifprior_curvatureis neither(d,)nor(d, d), or ifsigmais neither one number nor one per row.
- mimirax.diagnostics.degeneracy.fisher_information(negative_log_posterior, params)[source]¶
Return the Hessian of the negative log posterior over the flattened parameters.
At the maximum a posteriori point this is the observed Fisher information; elsewhere it is the local curvature and should be read as such.
WHEN THIS IS A FISHER MATRIX, AND WHEN IT IS NOT. For a Gaussian likelihood with prediction
y(theta), residualr = y - observedand noisesigma, the Hessian of the negative log likelihood isJ^T C^-1 J + sum_j (r_j / sigma_j^2) d^2 y_j / d theta^2with
J = dy / dtheta. The first term is the Fisher information – the Gauss–Newton matrix: positive semi-definite, of rank at mostmin(m, d), and what a Cramer–Rao forecast oreffective_parameters()means by the data’s curvature. The second is residual curvature, and it is neither: it has no sign and is in general of full rank. So this Hessian is the Fisher matrixat any point, when the prediction is linear in the parameters – the tracer made-to-measure weights, whose orbits do not depend on them;
wherever the residuals vanish – at the truth of noise-free mock data, or at an exact fit.
At the maximum a posteriori point of noisy data it is the observed information, which differs from the Fisher matrix by the residual term evaluated there. Anywhere else on a nonlinear model it is local curvature and not a Fisher matrix. Measured in the EDDA Fisher probe (
reports/FISHER_fd_vs_ad_probe.md, section 1.8): in the self-consistent made-to-measure construction, where the weights are the masses and move the orbits, at uniform weights, with ten observables of 32 weights, the residual term fills the null space andeffective_parameters()on this Hessian returns 32.000009 – more than the 32 parameters there are, which only an indefinite curvature can give – whereJ^T C^-1 Jgives 10.00.For the Fisher matrix away from an exact fit, differentiate the prediction rather than the objective and use
effective_parameters_from_jacobian(). And passed the full negative log posterior, this includes the priors’ curvature too; pass the likelihood alone before reading it as the data’s information.- Parameters:
negative_log_posterior (Callable[[PyTree], Scalar]) – The objective, in the parameters’ own pytree structure.
params (PyTree) – Where to evaluate.
- Returns:
(d, d)symmetric matrix over the flattened parameter vector, in the ordermimirax.parameters.ravel()uses.- Return type:
Array
- mimirax.diagnostics.degeneracy.l_curve_corner(regularization, misfit, norm)[source]¶
Pick a regularization weight at the L-curve’s corner.
The standard Tikhonov criterion, and for made-to-measure it answers “what should
mube” – a question the module previously answered with “1e-3, because it is small”. Given a fit repeated over a grid ofmu, the corner of the log-log curve of misfit against regularizer norm is the point of maximum curvature: below it the misfit barely improves while the norm runs away, above it the norm barely improves while the misfit degrades.Curvature is taken with respect to
log(regularization), so an unevenly spaced grid is handled correctly, viakappa = (rho' eta'' - rho'' eta') / (rho'^2 + eta'^2)^(3/2)with
eta = log(norm)andrho = log(misfit). The three points at each end are excluded. Two reasons, and the count is the second one:jnp.gradientfalls back to one-sided differences at the ends, which routinely produce the largest spurious curvature in the array; and taking it twice makes the curvature at indexidepend on the data ati +/- 2, so one bad endpoint reaches two points inward and excluding two would not contain it.MEASURED, against the
muthat actually minimizes the weight error – an oracle a real fit does not have – on three tracer problems over eight noise realizations each (reports/M2M_production_readiness.md, A4):criterion
eff ~ n
eff << n
precise data
this function
1.04x
1.30x
1.02x
generalized cross-validation
1.01x
2.15x
1.01x
discrepancy, chi2 = m - eff
1.72x
2.39x
1.60x
mu = 1e-32.46x
5.30x
3.18x
NNLS, the honest mu -> 0 limit
4.70x
13.97x
6.73x
Read as a multiple of the oracle’s weight error, so 1.00x is the best achievable. GCV – which needs nothing this package does not already have, being
misfit / (m - effective_parameters)^2– beats this function on two of the three and is 2.15x on the degenerate problem, where this one is 1.30x. Neither dominates; both beatmu = 1e-3on all three, and both beat drivingmuto zero, which pins 21 %, 88 % and 51 % of the weights at exactly zero on the three problems.No criterion here is universally reliable, and this one is not either. On synthetic Tikhonov problems chosen to be hostile, all three fail, in different places:
synthetic problem
this one
GCV
discrepancy
hard spectral gap, delta=1e-2
171x
16x
16x
hard spectral gap, delta=1e-4
5.9x
58x
3312x
smooth spectrum, s_i ~ 1/i
99x
1.0x
1.0x
smooth spectrum, s_i ~ 1/i^2
14x
3.5x
3.5x
So: run it per problem, run GCV beside it, and treat a disagreement between the two as a warning rather than picking one on faith. The curvature array is returned for exactly this reason – a curve with no corner has a flat curvature profile and no peak worth trusting, and a caller who only sees the returned
mucannot tell that apart from a sharp corner.WHAT TO PASS AS
norm. The regularizer’s norm, not minus its log density. ForEntropyPriorthat isdivergence():-Sis negative at the prior’s own mode, solog(-S)does not exist and a curve drawn against it is a curve of the wrong quantity.muIS NOT DIMENSIONLESS and the number this returns does not transfer between problems: it carries the ratio of the misfit’s scale to the norm’s, so rescaling the data,sigmaor the total weight rescales it. The criterion transfers; the value does not. Re-run it per problem.- Parameters:
regularization (Array) –
(k,)regularization weights, strictly positive and strictly increasing – themugrid the fit was repeated over. At least nine of them.misfit (Array) –
(k,)misfit at eachmu: the achieved chi-squared, strictly positive.norm (Array) –
(k,)regularizer norm at eachmu, strictly positive.
- Returns:
The
muat the corner, and the(k,)curvature array, whose three end points on each side arenanbecause curvature was not evaluated there. Returning the curvature makes a bad corner visible: a curve with no corner has no clear peak, and a caller that only sees themucannot tell.- Return type:
tuple[Scalar, Array]
- Raises:
ValueError – If the three arrays are not the same one-dimensional shape, if there are fewer than nine points, or if
regularizationis not strictly increasing and positive.
- mimirax.diagnostics.degeneracy.profile_likelihood(negative_log_posterior, params, direction)[source]¶
Profile the objective along a direction, re-optimizing the rest. Stub.
- Parameters:
negative_log_posterior (Callable[[PyTree], Scalar]) – The objective.
params (PyTree) – The starting point.
direction (Array) – A flattened-parameter direction.
- Returns:
Never returns.
- Return type:
Array
- Raises:
NotImplementedError – Always; needs an optimizer in the loop and is left for a method session.
Adapters (optional extras)¶
jaccpot’s FMM as a ForceModel. mimirax[jaccpot].
FastMultipoleMethod.compute_accelerations(positions, masses) already has
the protocol’s shape; this adapter only fixes the keyword contract and the
lazy import. Gradients: compute_accelerations builds its tree on the
host on every call and is not traceable by jax.grad. jaccpot’s
differentiable seam is its differentiable_accelerations at frozen topology
(jaccpot/docs/differentiable_fmm.md); wiring that in, with the
prepare-once / evaluate-many split it needs, is the adapter’s next step and
deliberately not guessed at here.
- class mimirax.adapters.jaccpot.JaccpotForceModel(solver)[source]¶
Bases:
objectThin wrapper exposing a jaccpot solver through the mimirax protocol.
- Variables:
solver (Any) – A
jaccpot.FastMultipoleMethod(or anything with itscompute_accelerationsmethod).- Parameters:
solver (Any)
- solver: Any¶
- accelerations(positions, masses, *, args=None)[source]¶
Forward to
solver.compute_accelerations.- Parameters:
positions (Vec3) –
(n, 3)positions.masses (PerParticle) –
(n,)masses.args (object) – Ignored; jaccpot’s options live on the solver.
- Returns:
(n, 3)accelerations.- Return type:
Vec3
- classmethod from_preset(preset='balanced', **solver_kwargs)[source]¶
Build a
FastMultipoleMethodand wrap it.- Parameters:
preset (str) – A jaccpot preset name.
**solver_kwargs (object) – Forwarded to
FastMultipoleMethod.
- Returns:
- Return type:
Any
- Raises:
ImportError – If jaccpot is not installed; the message names the extra.
A nornax rollout as a ForwardModel. mimirax[nornax].
The integrator half of the made-to-measure module (EDDA decision D-026):
initial conditions and particle weights in, a recorded trajectory out, through
nornax.solvers.leapfrog_kdk.block_kdk_rollout. nornax is imported inside
the call, so this module – and the protocol bridge below it – import and are
testable with no solver installed, and a missing nornax fails at the rollout
with a message naming the extra.
THE PROTOCOL MISMATCH, AND WHAT IT COSTS. nornax’s integrator wants a
MutualForceModel: level_accelerations(positions, masses, *, rung, level),
the acceleration contributed by pairs whose finer endpoint sits on rung
level, applied antisymmetrically. mimirax’s
ForceModel gives a total
accelerations(positions, masses) and knows nothing about rungs. The two meet
in exactly one place: with every particle on rung 0 and k_max = 0, level 0
is the total force, so a mimirax force model can drive a single-rung
rollout exactly. SingleRungMutualForce is that bridge and it refuses
any other level rather than returning something plausible. A multi-rung rollout
needs a real MutualForceModel – nornax’s MutualDirectSumGravity, or
jaccpot’s BlockStepFMM – passed straight through.
WHAT IS DIFFERENTIABLE. params["positions"], params["velocities"] and
params["weights"] are all differentiable leaves of the returned trajectory.
The weights reach the dynamics only in the self-consistent construction
(NornaxRollout.self_consistent()), where they are the masses; in the
tracer construction (NornaxRollout.tracer()) they reach the observable
alone and d(trajectory)/d(weights) is zero on the position and velocity
leaves by construction. What is not differentiable, in either: rung and
the topology, both of which nornax severs from the gradient with
stop_gradient. For a tree or FMM backend that is the substantive statement
of the EDDA programme’s B4 – the gradient is the exact fixed-topology gradient
of the numeric path on each segment between rebuilds, and a rebuild cadence of
rebuild_every base steps is where the segments end.
- class mimirax.adapters.nornax.FoldedRollout(rollout, observable, segments=1)[source]¶
Bases:
objectA rollout with the observable folded in, in checkpointed segments.
paramsin, the reduced observable out – so this is aForwardModelwhose output is already in data space, and theInferenceProblemabove it takesIdentityObservable.WHAT IT BUYS, MEASURED. Two things cost
O(t * n)in a made-to-measure fit, and only one of them is autodiff’s fault:NornaxRolloutstacks the trajectory – positions and velocities at every base step – because a plainObservableneeds the whole thing at once. That is6 t ndoubles in the forward pass. Folding removes it: the per-step record becomes the(m,)observable value instead of the(n, 3)state.lax.scanretains one state per step for the backward pass. Running the rollout assegmentschained sub-rollouts, each wrapped injax.checkpoint(), trades that forO(t / segments + segments)states and one extra forward pass per segment.
Peak scratch for one gradient, from XLA’s own
memory_analysis, atn = 64:t segments=1 (NornaxRollout) segments=sqrt(t) reduction 64 1.4 MB 0.9 MB 1.6x 256 5.6 MB 1.0 MB 5.6x 1024 22.4 MB 1.2 MB 18x
The
segments = 1column is linear int; thesqrt(t)column is flat.segments = sqrt(num_steps)is the optimum oft / segments + segmentsand is whatbalanced()returns.This is a memory schedule, not an approximation. The prediction matches the stacked path to 1.2e-16 and its gradient to 5.9e-15, which is what the fold law of
FoldableObservableguarantees and what a test measures.WHAT IT COSTS. The forward model now knows about the observable, which is exactly the seam
mimirax.protocolsotherwise keeps apart. That is why this is a separate, explicitly-named class rather than a flag onNornaxRollout: the trade is visible at the call site, and a caller who does not need the memory keeps the seam. The licence for chaining segments at all is nornax’sshooting_node, which recomputesaccandrungat each seam so thatsegmentssub-rollouts are one trajectory of the same map – a property this package tests directly.- Variables:
rollout (NornaxRollout) – The integration to run. Its
recordattribute is ignored: this class installs its own record function, which returns the observable’s per-step value.observable (FoldableObservable) – The reduction to fold in.
segments (int) – How many checkpointed segments.
1folds the observable but keeps one scan, which already removes the stacked trajectory.
- Raises:
ValueError – If
segmentsis not positive or does not dividerollout.num_steps. An uneven split would weight the segments unequally in an exponential average, so it is refused rather than silently rounded.- Parameters:
rollout (NornaxRollout)
observable (FoldableObservable)
segments (int)
- rollout: NornaxRollout¶
- observable: FoldableObservable¶
- segments: int = 1¶
- classmethod balanced(rollout, observable)[source]¶
Build one with the memory-optimal segment count near
sqrt(num_steps).Peak storage goes as
num_steps / segments + segments, minimized atsqrt(num_steps). The largest divisor ofnum_stepsat or below that is used, since the split has to be even.- Parameters:
rollout (NornaxRollout) – The integration to run.
observable (FoldableObservable) – The reduction to fold in.
- Returns:
With
segmentsthe best available divisor.- Return type:
- class mimirax.adapters.nornax.NornaxRollout(force, dt, num_steps, k_max=0, weights_are_masses=True, masses=None, eta=0.1, eps=1.0, reassign_rungs=True, checkpoint=True, checkpoint_substeps=False, record=None, rebuild_fn=None, rebuild_every=1, base_index=0)[source]¶
Bases:
objectIntegrate a state forward with nornax and return the recorded trajectory.
paramsis a mapping with"positions"(n, 3),"velocities"(n, 3)and"weights"(n,). The returned trajectory is a mapping whose leaves all carry a leadingnum_stepsaxis, ready formimirax.observables.TimeAverage:"positions","velocities"–(num_steps, n, 3), one row per base step, from nornax’srecord_fn;"weights"–(num_steps, n), the weights broadcast along the time axis. They are constant along a rollout (nothing in the integrator moves a mass), so this materializes nothing the scan had to carry; it is done so that the whole trajectory is one pytree an averaging observable canvmapover, which keepsObservablefrom needing a second argument.
Use
self_consistent()ortracer()rather than the constructor: which of the two a fit is running is the single most consequential thing about a made-to-measure problem, and it should be named at the call site.The initial state is built with nornax’s
shooting_node, not by hand. That constructor exists precisely to recompute the two leaves that must not be free variables –accfrom the positions and masses, andrungfrom thatacc– so a fit that moves the weights cannot move the integration schedule by moving a derived quantity.- Variables:
force (Any) – A nornax
MutualForceModel, or a mimiraxForceModel(bridged bySingleRungMutualForce,k_max = 0only).dt (float) – Base time step, nornax’s
dt_max.num_steps (int) – Base steps to take, nornax’s
n_base. Static.k_max (int) – Highest block-step rung.
0is the single-rung reduced case. Static.weights_are_masses (bool) – Whether
params["weights"]is the dynamical mass vector (self-consistent M2M) or only enters the observable (tracer M2M).masses (Array | None) – The dynamical masses when
weights_are_massesisFalse;Nonemeans unit masses, which is what an external field wants since it ignores them.eta (float) – nornax’s rung-assignment accuracy parameter.
eps (float) – nornax’s rung-assignment softening parameter.
reassign_rungs (bool) –
Whether rungs are recomputed at every base-step boundary (production) or frozen for the whole rollout. Freeze them to make the map globally smooth in the continuous state, which is what a finite-difference gradient check needs.
This is not a free choice, and the difference is measured. With it on, the rollout integrates a schedule that depends on the weights through the acceleration, and nornax severs that dependence with
stop_gradient. The gradient is therefore exact for the schedule the forward pass realised – reverse and forward mode agree on it to 2.6e-16 – but the objective is only piecewise smooth in the weights: a weight change large enough to move a particle across a rung boundary lands on a different map, with a kink between. On a 16-body, three-rung test system the production gradient differs from the frozen-schedule one by 65 % in norm (cosine 0.953), because the two realise different schedules. Neither is wrong; they are gradients of different maps. Freeze the schedule when a fit must descend one smooth objective, and expect the kinks when it must not.checkpoint (bool) – Whether to wrap each base step in
jax.checkpoint, bounding the retained backward-pass state to the base-step boundaries. Leave it on: it does not change the gradient (measured bit-identical, single- and multi-rung) and it is what keeps a long rollout’s memory linear innum_steps.checkpoint_substeps (bool) – Whether to additionally remat each sub-step boundary’s kick.
checkpointbounds memory across base steps but still materializes all2**k_maxsub-step pair tensors while differentiating one base step; this bounds that to one boundary’s worth. nornax’s rollout documents it as needed for deepk_maxgradients, which otherwise run out of memory – so it is the knob a made-to-measure fit at FMM scale and largek_maxwill need, and it is exposed here rather than left reachable only by bypassing the adapter. Off by default, as in nornax; it changes the memory schedule, not the result.record (Callable[[Any], PyTree] | None) – nornax’s
record_fn, which must return a mapping so that__call__()can add the"weights"leaf beside it;Nonerecords positions and velocities.rebuild_fn (Callable[..., Any] | None) – A tree backend’s traceable
(positions, masses) -> topologyrebuild, e.g. jaccpot’sBlockStepFMM.rebuild_state.rebuild_every (int) – Base steps per topology rebuild. Requires
rebuild_fn.base_index (int) – The global base-step index this rollout starts at, which anchors the rebuild cadence when a trajectory is run as chained segments.
- Raises:
ValueError – If a mimirax
ForceModelis paired withk_max > 0, or ifnum_stepsis not positive.- Parameters:
force (Any)
dt (float)
num_steps (int)
k_max (int)
weights_are_masses (bool)
masses (Array | None)
eta (float)
eps (float)
reassign_rungs (bool)
checkpoint (bool)
checkpoint_substeps (bool)
record (Callable[[Any], Any] | None)
rebuild_fn (Callable[[...], Any] | None)
rebuild_every (int)
base_index (int)
- force: Any¶
- dt: float¶
- num_steps: int¶
- k_max: int = 0¶
- weights_are_masses: bool = True¶
- masses: Array | None = None¶
- eta: float = 0.1¶
- eps: float = 1.0¶
- reassign_rungs: bool = True¶
- checkpoint: bool = True¶
- checkpoint_substeps: bool = False¶
- record: Callable[[Any], Any] | None = None¶
- rebuild_fn: Callable[[...], Any] | None = None¶
- rebuild_every: int = 1¶
- base_index: int = 0¶
- classmethod self_consistent(force, dt, num_steps, *, k_max=0, **kwargs)[source]¶
Build the self-consistent construction: the weights are the masses.
The case the differentiable N-body stack was built for. Because the weights are the masses, the orbits themselves depend on the weights, so
d(objective)/d(weights)flows through the force evaluations and through every step of the integration.That orbit-response term is not unprecedented – Bovy, Kawata & Hunt (2018, arXiv:1704.03884) compute it for external-potential parameters by finite differencing the orbit integration, one extra integration per parameter – but they explicitly exclude self-gravity, and finite differencing costs
n + 1rollouts when the parameters are thenparticle masses. Reverse mode costs one, whatevernis, which is what makes this construction tractable rather than merely expressible.forcemust be a real nornaxMutualForceModel; a mimiraxForceModelis rejected abovek_max = 0.- Parameters:
force (Any) – A nornax
MutualForceModel.dt (float) – Base time step.
num_steps (int) – Base steps to take.
k_max (int) – Highest block-step rung.
**kwargs (Any) – Any other attribute of
NornaxRollout.
- Returns:
Configured with
weights_are_masses=True.- Return type:
- classmethod tracer(force, dt, num_steps, *, masses=None, **kwargs)[source]¶
Build the classic tracer construction: the weights stay out of the dynamics.
The orbits are those of test particles in a potential the weights do not set, so the trajectory can be computed once and the weight fit runs on the recorded kernel – which is what makes the classic force-of-change iteration well defined (see
mimirax.inference.MadeToMeasure.force_of_change()). Single-rung by construction: an external field is not antisymmetric, so the block schedule’s momentum-conserving level split does not apply to it.- Parameters:
force (Any) – A mimirax
ForceModel– an analytic potential;mimirax.testing.SoftenedPointMassFieldis the double – or a nornaxMutualForceModel.dt (float) – Base time step.
num_steps (int) – Base steps to take.
masses (Array | None) – The dynamical masses.
Nonemeans unit masses; an external field ignores them, so that is the right default there and the wrong one for a self-gravitating system whose masses are not the weights.**kwargs (Any) – Any other attribute of
NornaxRollout.
- Returns:
Configured with
weights_are_masses=Falseandk_max=0.- Return type:
- mutual_force()[source]¶
Return the force in the form nornax’s integrator consumes.
- Returns:
forceitself when it already haslevel_accelerations, andSingleRungMutualForcearound it otherwise.- Return type:
Any
- dynamical_masses(params)[source]¶
Return the mass vector the integrator advances the state with.
- Parameters:
params (PyTree) – The parameter mapping.
- Returns:
params["weights"]in the self-consistent construction, andmasses(or ones) in the tracer construction.- Return type:
PerParticle
- rollout(params)[source]¶
Run the rollout and return nornax’s own
(final_state, records).The escape hatch for anything that needs the integrator’s state rather than an observable’s view of it – a shooting defect between chained segments, an energy diagnostic, a topology’s pair counts.
- Parameters:
params (PyTree) – A mapping with
"positions","velocities"and"weights".- Returns:
The final
BlockStepStateand the stacked per-base-step records.- Return type:
tuple[Any, PyTree]
- final_state(params)[source]¶
Return only the final
BlockStepState.- Parameters:
params (PyTree) – A mapping with
"positions","velocities"and"weights".- Returns:
nornax’s
BlockStepStateat the end of the rollout.- Return type:
Any
- folding(observable, *, segments=1)[source]¶
Return a
ForwardModelthat foldsobservableinto the rollout.The memory escape hatch. See
FoldedRollout.- Parameters:
observable (FoldableObservable) – The reduction to accumulate;
mimirax.observables.TimeAverageis one.segments (int) – How many checkpointed segments to split the rollout into.
- Returns:
Parameters in, the reduced observable out.
- Return type:
- class mimirax.adapters.nornax.SingleRungMutualForce(force)[source]¶
Bases:
objectA mimirax
ForceModelbehind nornax’s per-level interface.Valid at
k_max = 0only, and it says so by raising. With every particle on rung 0 every pair sits on level 0, so level 0’s antisymmetric contribution is the whole force and the wrapped model’s total is exactly right. At any other level the honest answer does not exist: a mimiraxForceModelhas no notion of which pairs belong to which level, and both plausible guesses are wrong – returning the total at level 0 and zero above would kick the whole force with level 0’s weight at every sub-step boundary (the wrong equations, silently), and returning zero at level 0 would lose the force entirely.What conformance can and cannot say about this bridge. nornax’s
check_mutual_force_modelatk_max = 0checks shape, finiteness, the level partition (trivial with one level) and the momentum residual – and the last one is a property of the wrapped model, not of the bridge. A mutual model (mimirax.testing.DirectSumGravity) passes it to round-off; an external field (mimirax.testing.SoftenedPointMassField, the tracer construction’s double) does not and must not, because an external potential genuinely does not conserve the particles’ momentum. That is why the tracer construction is single-rung: the antisymmetry the block schedule relies on is exactly what an external field lacks. An external field could be split bylevel = rung_iinstead ofmax(rung_i, rung_j)and driven multi-rung, but that is a different bridge for a different force, and guessing which one a caller meant from a model that cannot be asked is how the wrong equations get integrated.- Variables:
force (ForceModel) – The mimirax acceleration provider being bridged.
- Parameters:
force (ForceModel)
- force: ForceModel¶
- level_accelerations(positions, masses, *, rung, level, args=None, topology=None)[source]¶
Return the wrapped model’s total acceleration, as level 0.
- Parameters:
positions (Vec3) –
(n, 3)positions.masses (PerParticle) –
(n,)masses.rung (Array) – Ignored: at
k_max = 0every particle is on rung 0, so the rung partition carries no information the total does not already have.level (int) – Must be
0.args (object) – Passed through to the wrapped model’s caller-owned channel.
topology (object) – Accepted and ignored: a mimirax
ForceModelhas no frozen interaction structure to evaluate against.
- Returns:
(n, 3)accelerations.- Return type:
Vec3
- Raises:
ValueError – If
levelis not0; see the class docstring for why there is no defensible answer above it.
An ODISSEO integration as a ForwardModel. mimirax[odisseo]. Stub.
ODISSEO’s odisseo.integrate(...) and its differentiable FMM lane
(odisseo/differentiable.py) are the obvious body. Left as a stub because
whether ODISSEO stays on the routing path between jaccpot and nornax is an open
decision (O-4 in the EDDA programme), and an adapter written before that is
settled would be written twice.
- class mimirax.adapters.odisseo.OdisseoForwardModel(config, params)[source]¶
Bases:
objectRun an ODISSEO simulation from parameters. Stub.
- Variables:
config (Any) – An
odisseo.option_classes.SimulationConfig.params (Any) – An
odisseo.option_classes.SimulationParamstemplate whose fields the free parameters overwrite.
- Parameters:
config (Any)
params (Any)
- config: Any¶
- params: Any¶
Test doubles¶
Analytic test doubles with closed-form gradients.
WHY THESE EXIST. Every core numerical path in mimirax has to be exercised on
CPU with no solver installed, or the suite will not run often enough to be
worth having. These doubles satisfy the protocols in mimirax.protocols
and, unlike a solver, come with their derivatives written out by hand – so a
test can check jax.grad against an exact expression rather than against
finite differences whose tolerance has to be argued for.
LinearForwardModel–y = A x + b; the gradient of a sum-of-squares misfit is2 A^T (A x + b - y).SoftenedPointMassField– the softened Newtonian field of fixed sources at test-particle positions, with its tidal tensord a / d x = -G sum_j m_j [ I / r^3 - 3 d d^T / r^5 ]in closed form.DirectSumGravity– self-gravity ofnbodies, the honestO(n^2)ForceModelany FMM is checked against.
All are float64-exact to round-off; tests compare at atol = 1e-12.
- class mimirax.testing.doubles.DirectSumGravity(softening=0.0, gravitational_constant=1.0)[source]¶
Bases:
objectSoftened self-gravity of
nbodies by direct summation.The
O(n^2)reference every hierarchical solver is measured against, and the double for aForceModelthat actually usesmasses. Differentiable in positions and masses; the self-term is masked withwhereon both the distance and the weight so the gradient carries no0 * inf.- Variables:
softening (float) – Plummer softening
eps.gravitational_constant (float) –
G.
- Parameters:
softening (float)
gravitational_constant (float)
- softening: float = 0.0¶
- gravitational_constant: float = 1.0¶
- class mimirax.testing.doubles.LinearForwardModel(matrix, offset)[source]¶
Bases:
objecty = A x + b: aForwardModelwhose Jacobian isA.- Variables:
matrix (Array) –
(m, n)design matrixA.offset (Array) –
(m,)offsetb.
- Parameters:
matrix (Array)
offset (Array)
- matrix: Array¶
- offset: Array¶
- class mimirax.testing.doubles.SoftenedPointMassField(sources, source_masses, softening=0.0, gravitational_constant=1.0)[source]¶
Bases:
objectSoftened Newtonian field of fixed sources, evaluated at test particles.
a_i = -G sum_j m_j d_ij / (|d_ij|^2 + eps^2)^{3/2}withd_ij = x_i - s_j. AForceModelwhosemassesargument is ignored (the targets are test particles), and the analytic stand-in for “accelerations at tracers” in Paper I section 7.- Variables:
sources (Array) –
(s, 3)source positions.source_masses (Array) –
(s,)source masses.softening (float) – Plummer softening
eps.gravitational_constant (float) –
G.
- Parameters:
sources (Array)
source_masses (Array)
softening (float)
gravitational_constant (float)
- sources: Array¶
- source_masses: Array¶
- softening: float = 0.0¶
- gravitational_constant: float = 1.0¶
- mimirax.testing.doubles.make_linear_problem(key, *, n_params=4, n_data=12, noise=0.0, sigma=1.0)[source]¶
Build a well-posed linear-Gaussian problem with a known truth.
- Parameters:
key (Array) – A
jax.randomkey.n_params (int) – Number of free parameters. Static.
n_data (int) – Number of data points; must exceed
n_paramsfor a unique optimum. Static.noise (float) – Standard deviation of Gaussian noise added to the synthetic data.
sigma (float) – The likelihood’s assumed noise level.
- Returns:
The problem (linear forward model, identity observable, Gaussian likelihood, no priors) and the
(n_params,)true parameters.- Return type:
tuple[InferenceProblem, Array]
Registry¶
The registry idiom every pluggable family in mimirax uses.
Observables, likelihoods, priors, reparameterizations and inference methods are
all registries: a module-level mapping from a name to an implementation, a
register call that refuses a silent overwrite, and an available query.
This is yggdrax’s register_tree_builder / available_tree_types pattern
(yggdrax/tree.py) made generic, so a reader of one package reads the other.
WHY A REGISTRY AND NOT A BRANCH. Extensibility is the package’s primary design
axis: adding an observable or a sampler must not require editing a core module.
A registry gives a new implementation one call to make instead of one elif
to add, and it gives the error message for a typo the full list of what exists.
- class mimirax._registry.Registry(kind)[source]¶
Bases:
Generic[T]A name -> implementation mapping with explicit registration.
- Parameters:
kind (str) – What the registry holds, used in error messages –
"observable","likelihood", …
- property kind: str¶
Return the kind of implementation this registry holds.
- Returns:
The
kindgiven at construction.- Return type:
str
- register(name, item, *, overwrite=False)[source]¶
Register
itemundername.- Parameters:
name (str) – Lookup key. Leading and trailing whitespace is stripped.
item (T) – The implementation to register.
overwrite (bool) – Whether an existing entry under
namemay be replaced. Off by default so two plugins cannot silently shadow each other.
- Returns:
item, unchanged, so the call can wrap a definition.- Return type:
T
- Raises:
ValueError – If
nameis empty, or already registered andoverwriteisFalse.
- decorate(name, *, overwrite=False)[source]¶
Return a decorator that registers the decorated object under
name.- Parameters:
name (str) – Lookup key.
overwrite (bool) – Passed through to
register().
- Returns:
A decorator returning its argument unchanged.
- Return type:
Callable[[T], T]