Gaussian Likelihoods

This module provides the base classes for Gaussian likelihoods in SOLikeT, including support for combining multiple likelihoods with cross-covariances.

GaussianLikelihood

The base class for all Gaussian likelihoods. Subclasses should implement the _get_theory() method to compute the theory prediction.

class soliket.gaussian.GaussianLikelihood(info: Mapping[str, Any] = mappingproxy({}), name: str | None = None, timing: bool | None = None, packages_path: str | None = None, initialize=True, standalone=True)[source]

Bases: Likelihood

Base class for Gaussian likelihoods in SOLikeT.

This class provides the infrastructure for computing Gaussian log-likelihoods from SACC data files. Subclasses must implement the _get_theory() method to compute the theory prediction for the data vector.

Parameters

namestr

Name identifier for the likelihood (default: “Gaussian”)

datapathstr

Path to the SACC file containing data and covariance

use_spectrastr or list

Which spectra to use. Either “all” or a list of tracer pairs like [("tracer1", "tracer2")]

ncovsimsint, optional

Number of simulations used to estimate covariance. If provided, applies the Hartlap correction factor to the inverse covariance.

Attributes

dataGaussianData

The assembled Gaussian data object with covariance

sacc_datasacc.Sacc

The loaded SACC data object

xnp.ndarray

The bin centers (ell values)

ynp.ndarray

The data vector

covnp.ndarray

The covariance matrix

Examples

To create a custom Gaussian likelihood:

class MyLikelihood(GaussianLikelihood):
    name = "my_likelihood"
    _allowable_tracers = ("cmb_temperature", "cmb_polarization")

    def _get_theory(self, **params):
        # Compute theory prediction
        return theory_vector
_get_theory(**params_values) ndarray[source]

Bin the unbinned theory per tracer combination via get_binning().

_get_unbinned_theory(**kwargs) list[ndarray][source]

Unbinned theory spectra on the fine multipole grid, one per tracer combination.

Subclasses that compute a finely-sampled theory and then bin it (e.g. the Limber cross-correlations) override this and inherit binning from the default _get_theory(). Subclasses that produce binned theory directly override _get_theory() instead.

_reorder_to_combo_major(sacc_data: Sacc) None[source]

Reorder sacc_data in place so its data points are grouped by tracer combination, matching the order in which the theory vector is built. sacc.reorder permutes the data and covariance together, so the data vector and covariance can never desynchronise.

get_binning(tracer_comb: tuple) tuple[ndarray, ndarray][source]

Bandpower support multipoles and window matrix for a tracer pair.

The result depends only on the (fixed) SACC file, so it is memoised per tracer combination: each likelihood evaluation would otherwise re-derive it twice – once in _get_unbinned_theory() for the theory’s ell support and once in _get_theory() to bin – each time hitting the SACC index and bandpower-window lookups.

The window is looked up by tracers alone (not by data type: the shear cross-correlations are cl_0e/cl_e0, not cl_00), so a pair carrying several data types would silently yield a window spanning all of them. Rejected here, in the shared lookup, rather than in each caller – _get_unbinned_theory() reads the ell support from here too, and would otherwise spend a Limber calculation on the doubled grid before _get_theory() noticed.

get_unbinned_theory(**params_values) list[ndarray][source]

The unbinned theory spectra, one array per tracer combination.

Public accessor used by cross-covariance computations that need the theory before bandpower binning.

logp(**params_values) float[source]

Computes and returns the log likelihood value. Takes as keyword arguments the parameter values. To get the derived parameters, pass a _derived keyword with an empty dictionary.

Alternatively you can just implement calculate() and save the log likelihood into state[‘logp’]; this may be more convenient if you also need to also calculate other quantities.

MultiGaussianLikelihood

A likelihood that combines multiple Gaussian likelihoods, optionally accounting for cross-covariances between them.

class soliket.gaussian.MultiGaussianLikelihood(info=mappingproxy({}), **kwargs)[source]

Bases: GaussianLikelihood

A likelihood combining multiple Gaussian likelihoods with cross-covariances.

This class enables joint analysis of multiple datasets by combining their data vectors and covariance matrices. Cross-covariances between datasets can be specified via a CrossCov object stored in SACC format.

Parameters

componentslist of str

List of likelihood class names to combine, e.g., ["soliket.mflike.MFLike", "soliket.lensing.LensingLikelihood"]

optionslist of dict

Configuration options for each component likelihood. Each dict should contain at minimum datapath and any other required parameters.

cross_cov_pathstr, optional

Path to a SACC file containing cross-covariances between components. If not provided, components are assumed independent (zero cross-covariance).

Attributes

likelihoodslist of Likelihood

The instantiated component likelihoods

cross_covCrossCov or None

The loaded cross-covariance container

dataMultiGaussianData

The combined data object with joint covariance

Examples

YAML configuration:

likelihood:
  soliket.MultiGaussianLikelihood:
    components:
      - soliket.mflike.MFLike
      - soliket.lensing.LensingLikelihood
    options:
      - datapath: /path/to/mflike.fits
        use_spectra: all
      - datapath: /path/to/lensing.fits
    cross_cov_path: /path/to/cross_cov.fits

Python usage:

from soliket import MultiGaussianLikelihood

info = {
    "components": ["soliket.mflike.MFLike", "soliket.lensing.LensingLikelihood"],
    "options": [
        {"datapath": "mflike.fits", "use_spectra": "all"},
        {"datapath": "lensing.fits"},
    ],
    "cross_cov_path": "cross_cov.fits",
}
like = MultiGaussianLikelihood(info)
_get_theory(**kwargs) ndarray[source]

Bin the unbinned theory per tracer combination via get_binning().

classmethod get_defaults(return_yaml=False, yaml_expand_defaults=True, input_options=mappingproxy({}))[source]

Return defaults for this component_or_class, with syntax:

option: value
[...]

params:
  [...]  # if required

prior:
  [...]  # if required

If keyword return_yaml is set to True, it returns literally that, whereas if False (default), it returns the corresponding Python dict.

Note that in external components installed as zip_safe=True packages files cannot be accessed directly. In this case using !default .yaml includes currently does not work.

Also note that if you return a dictionary it may be modified (return a deep copy if you want to keep it).

if yaml_expand_defaults then !default: file includes will be expanded

input_options may be a dictionary of input options, e.g. in case default params are dynamically dependent on an input variable

get_helper_theories() dict[str, Theory][source]

Return dictionary of optional names and helper Theory instances that should be used in conjunction with this component. The helpers can be created here as only called once, and before any other use of helpers.

Returns:

dictionary of names and Theory instances

classmethod get_modified_defaults(defaults, input_options=mappingproxy({}))[source]

After defaults dictionary is loaded, you can dynamically modify them here as needed,e.g. to add or remove defaults[‘params’]. Use this when you don’t want the inheritance-recursive nature of get_defaults() or don’t only want to affect class attributes (like get_class_options() does0.

get_requirements()[source]

Get a dictionary of requirements (or a list of requirement name, option tuples) that are always needed (e.g. must be calculated by another component or provided as input parameters).

Returns:

dictionary or list of tuples of requirement names and options (or iterable of requirement names if no optional parameters are needed)

initialize_with_provider(provider: Provider)[source]

Final initialization after parameters, provider and assigned requirements set. The provider is used to get the requirements of this theory using provider.get_X() and provider.get_param(‘Y’).

Parameters:

provider – the theory.Provider instance that should be used by this component to get computed requirements

Usage Example

To combine multiple likelihoods (e.g., CMB TT and lensing) with cross-covariances:

likelihood:
  soliket.MultiGaussianLikelihood:
    components:
      - soliket.mflike.MFLike
      - soliket.lensing.LensingLikelihood
    options:
      - datapath: /path/to/mflike_data.fits
        use_spectra: all
      - datapath: /path/to/lensing_data.fits
    cross_cov_path: /path/to/cross_covariance.fits

The cross_cov_path parameter is optional. If not provided, the likelihoods are assumed to be independent (zero cross-covariance).

Component likelihoods may apply different scale cuts, so their data vectors need not share the same range: the cross-covariance is automatically trimmed to each probe’s retained bandpowers. Blocks are also aligned to the data by identity (per-bandpower keys) rather than by position, so the order in which the cross-covariance was built need not match the order produced at run time. See Identity alignment below.

CrossCov

A container for storing cross-covariances between likelihood components. Supports saving and loading in SACC format.

class soliket.gaussian.CrossCov(*args, **kwargs)[source]

Bases: dict

Labelled-block covariance store for multi-component Gaussian likelihoods.

A CrossCov is a labelled-block store: a dict whose keys are pairs of component names (e.g. ("mflike", "lensing")) and whose values are the corresponding covariance blocks. Diagonal keys (name, name) hold a component’s auto-covariance; off-diagonal keys (name1, name2) hold a cross-covariance. add_component and add_cross_covariance are the same underlying store operation — they only differ in whether the block lands on the diagonal or off it.

Each block may carry per-axis bandpower identities (ids), recorded in _block_ids_map keyed by the block’s (row, col) tuple. These labels let a block be aligned to a target order regardless of how it was stored: blocks may be supplied in any order, on a full (un-cut, shuffled) range, with no reliance on positional or borrowed ids. MultiGaussianData canonicalises the store to the data order at assembly time via to_canonical(), which fuses realignment and scale-cut trimming into one identity gather per axis. Supports saving and loading in SACC format for persistence.

See .claude/plans/2026-06-05-crosscov-labelled-blocks-design.md for the design rationale.

Examples

Auto blocks via add_component and cross blocks via add_cross_covariance, optionally labelled with per-axis ids:

cross_cov = CrossCov()
cross_cov.add_component("mflike", mflike_cov, ids=mflike_ids)
cross_cov.add_component("lensing", lensing_cov, ids=lensing_ids)
cross_cov.add_cross_covariance(
    "mflike", "lensing", cross_block, ids1=mflike_ids, ids2=lensing_ids
)
cross_cov.save("cross_cov.fits")

Auto-covariances may be omitted; assembly then falls back to each likelihood’s own cov:

cross_cov = CrossCov()
cross_cov.add_cross_covariance("mflike", "lensing", cross_block)
cross_cov.save("cross_cov.fits")

Loading:

cross_cov = CrossCov.load("cross_cov.fits")
block = cross_cov[("mflike", "lensing")]
add_component(name, cov, ids=None)[source]

Register a component’s auto-covariance (diagonal block).

add_cross_covariance(name1, name2, cross_cov, ids1=None, ids2=None)[source]

Register the cross term between two components (off-diagonal block).

component_ids(name)[source]

Derived per-component ids: the diagonal block’s, else any block’s.

property component_names: list[str]

Get ordered list of component names.

classmethod from_cmb_lensing(mflike, lensing, **overrides)[source]

Compute the CMB-primary x CMB-lensing cross-covariance, labelled by id.

Convenience entry point: pulls fsky, fiducial cosmology/accuracy, the CMB bandpower windows and the kappa binning from the two evaluated likelihoods mflike and lensing (e.g. resolve_aliases(model).mflike and .lensing – the concrete handles, not a Session), runs CAMB and the cross-covariance kernel, and returns a single labelled cross block ready to save(). overrides (fsky/cosmo/accuracy/lmax) are forwarded to the low-level soliket.cross_covariance.cmb_lensing_crosscov().

Every block (the cross term and both auto-covariances) carries its bandpower identity, so the order in which any of them is built is irrelevant: to_canonical() realigns each to the data by identity at assembly. The CMB rows use mflike’s spec_meta vocabulary (pol, hasYX_xsp, (t1, t2), leff) – exactly what soliket.gaussian.gaussian.bandpower_ids() reconstructs for mflike – and the kappa columns reuse the lensing data’s own ids. The full-kappa columns are trimmed to the bins the lensing likelihood keeps, so the cross block and the lensing auto share one column identity.

The component auto-covariances are carried alongside the cross block (keyed by the components’ real GaussianData names) so the saved file is a self-contained joint covariance that drops straight into a MultiGaussianLikelihood. Both likelihoods’ model must already be evaluated, and mflike must expose spec_meta.

to_canonical(order: dict) ndarray[source]

Assemble the full joint covariance in the given per-component order.

order maps component name -> target ids (canonical; already scale-cut for a trimmed matrix) OR an int size (positional, when the data has no ids). Missing blocks are left as zeros. Realign and trim fuse into one gather per axis.

Usage Modes

Mode 1: Full covariance specification

Use add_component() to register each component with its auto-covariance, then add_cross_covariance() for off-diagonal blocks:

from soliket.gaussian import CrossCov

cross_cov = CrossCov()

# Add auto-covariances
cross_cov.add_component("mflike", mflike_cov)
cross_cov.add_component("lensing", lensing_cov)

# Add cross-covariance
cross_cov.add_cross_covariance("mflike", "lensing", mflike_lensing_cov)

# Save to SACC format
cross_cov.save("cross_covariance.fits")

Mode 2: Cross-covariance only

If you only want to specify the cross-covariance (using auto-covariances from individual likelihoods), just use add_cross_covariance():

cross_cov = CrossCov()
cross_cov.add_cross_covariance("mflike", "lensing", mflike_lensing_cov)
cross_cov.save("cross_covariance.fits")

When loaded by MultiGaussianLikelihood, the auto-covariances will be taken from each individual likelihood’s SACC file.

Loading CrossCov

To load a previously saved cross-covariance:

from soliket.gaussian import CrossCov

cross_cov = CrossCov.load("cross_covariance.fits")

# Access blocks
mflike_lensing_block = cross_cov[("mflike", "lensing")]

Identity alignment

Each component may carry per-bandpower identity keys – a unique label for every data point (e.g. (field, channel pair, ell)). When present, MultiGaussianData aligns each covariance block to the data by matching these keys instead of relying on position, so a cross-covariance keeps working even if the data is reordered or scale-cut differently at run time. Missing or duplicate keys raise a clear error rather than silently corrupting the joint covariance.

Identity keys are optional, supplied via ids and persisted by save() / load():

cross_cov.add_component("mflike", mflike_cov, ids=mflike_ids)
cross_cov.add_component("lensing", lensing_cov, ids=lensing_ids)
cross_cov.add_cross_covariance("mflike", "lensing", mflike_lensing_cov)

SOLikeT GaussianLikelihood subclasses populate these keys automatically from their SACC file, and MultiGaussianLikelihood also sources them for external components such as mflike (via soliket.gaussian.gaussian.bandpower_ids). If keys are omitted, blocks fall back to positional alignment.

GaussianData

Low-level data container for named multivariate Gaussian data.

class soliket.gaussian.GaussianData(name, x: Sequence, y: Sequence[float], cov: ndarray, ncovsims: int | None = None, indices: ndarray | None = None, ids: Sequence | None = None)[source]

Bases: object

Container for named multivariate Gaussian data.

Stores a data vector with its covariance matrix and provides methods for computing the Gaussian log-likelihood.

Parameters

namestr

Name identifier for the data

xSequence

Labels or coordinates for each data point (e.g., ell values)

ySequence[float]

The data vector values

covnp.ndarray

Covariance matrix with shape (n, n) where n = len(x)

ncovsimsint, optional

Number of simulations used to estimate covariance. If provided, applies the Hartlap correction factor to the inverse covariance.

indicesnp.ndarray, optional

Boolean array for trimming cross-covariances when scale cuts are applied

idssequence, optional

Per-bandpower identity keys over the FULL (pre-cut) range, i.e. one key per element of indices. Lets MultiGaussianData align cross- covariance blocks to the data by identity instead of by position.

Attributes

inv_covnp.ndarray

Inverse covariance matrix (with Hartlap correction if applicable)

norm_constfloat

Normalization constant for the Gaussian likelihood

Raises

ValueError

If dimensions of x, y, and cov are incompatible If covariance matrix has non-positive determinant

loglike(theory: ndarray) float[source]

Compute the Gaussian log-likelihood.

Parameters

theorynp.ndarray

Theory prediction vector with same length as data

Returns

float

Log-likelihood value including normalization constant

MultiGaussianData

Assembles multiple GaussianData objects into a joint data vector with combined covariance matrix.

class soliket.gaussian.MultiGaussianData(data_list, cross_covs=None)[source]

Bases: GaussianData

Combined Gaussian data from multiple components with cross-covariances.

Assembles multiple GaussianData objects into a single joint data vector with a combined covariance matrix that includes both auto-covariances and cross-covariances between components.

A thin caller over the CrossCov store: the data_list is the single source of truth for order. Each component’s target order is its data vector’s own ids (already scale-cut), and the cross-covariance blocks are aligned to it by identity via CrossCov.to_canonical(). Alignment is decided per axis: an identity gather when both the target and the block carry ids; positional alignment only when neither does; and a raise when the data carries ids but a block does not (it refuses to guess, since the data is reordered to canonical order). Missing auto blocks fall back to each likelihood’s own cov, which is already in canonical data order.

See .claude/plans/2026-06-05-crosscov-labelled-blocks-design.md for the design rationale.

Parameters

data_listlist of GaussianData

Individual data objects to combine

cross_covsCrossCov, optional

Labelled-block cross-covariance store. If None, components are assumed independent. Auto-covariances can come from either the CrossCov or the individual GaussianData objects (individual data is used whenever the CrossCov doesn’t contain an auto-covariance for a component).

Attributes

data_listlist of GaussianData

The original individual data objects

nameslist of str

Names of all components

lengthslist of int

Data vector lengths for each component

labelslist of str

Component name for each element in the combined data vector

Examples

Combining two datasets with cross-covariance:

data1 = GaussianData("mflike", x1, y1, cov1)
data2 = GaussianData("lensing", x2, y2, cov2)

cross_cov = CrossCov()
cross_cov.add_cross_covariance("mflike", "lensing", cross_block)

multi_data = MultiGaussianData([data1, data2], cross_cov)

# Access combined properties
print(multi_data.cov.shape)  # (n1 + n2, n1 + n2)
loglike = multi_data.loglike(theory_vector)
loglike(theory: ndarray) float[source]

Compute the Gaussian log-likelihood.

Parameters

theorynp.ndarray

Theory prediction vector with same length as data

Returns

float

Log-likelihood value including normalization constant