fimx 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fimx/__init__.py +22 -0
- fimx/cli.py +97 -0
- fimx/combination.py +67 -0
- fimx/construction.py +100 -0
- fimx/datasets.py +72 -0
- fimx/inversion.py +78 -0
- fimx/io.py +24 -0
- fimx/parameters.py +204 -0
- fimx/plotting/__init__.py +88 -0
- fimx/plotting/base.py +98 -0
- fimx/plotting/getdist.py +68 -0
- fimx/py.typed +0 -0
- fimx-0.1.0.dist-info/METADATA +203 -0
- fimx-0.1.0.dist-info/RECORD +17 -0
- fimx-0.1.0.dist-info/WHEEL +4 -0
- fimx-0.1.0.dist-info/entry_points.txt +3 -0
- fimx-0.1.0.dist-info/licenses/LICENSE +21 -0
fimx/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Functions for Fisher information on labeled xarray matrices."""
|
|
2
|
+
|
|
3
|
+
from .combination import combine, gaussian_prior
|
|
4
|
+
from .construction import matrix
|
|
5
|
+
from .datasets import dataset
|
|
6
|
+
from .inversion import errors, inv
|
|
7
|
+
from .parameters import expand, fix, marginalize, transform
|
|
8
|
+
from .plotting import plot
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"combine",
|
|
12
|
+
"dataset",
|
|
13
|
+
"errors",
|
|
14
|
+
"expand",
|
|
15
|
+
"fix",
|
|
16
|
+
"gaussian_prior",
|
|
17
|
+
"inv",
|
|
18
|
+
"marginalize",
|
|
19
|
+
"matrix",
|
|
20
|
+
"plot",
|
|
21
|
+
"transform",
|
|
22
|
+
]
|
fimx/cli.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Command-line interface for plotting Fisher forecasts."""
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser, BooleanOptionalAction
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from json import loads
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .io import load_dataset
|
|
9
|
+
from .plotting import plot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _parser() -> ArgumentParser:
|
|
13
|
+
"""Build the command-line argument parser."""
|
|
14
|
+
parser = ArgumentParser(
|
|
15
|
+
description="Overlay Fisher forecast Datasets in a corner plot."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--file",
|
|
19
|
+
dest="files",
|
|
20
|
+
action="append",
|
|
21
|
+
required=True,
|
|
22
|
+
type=Path,
|
|
23
|
+
metavar="PATH",
|
|
24
|
+
help="NetCDF Dataset to plot; repeat to overlay multiple forecasts.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--figure-file",
|
|
28
|
+
type=Path,
|
|
29
|
+
default=Path("plot.png"),
|
|
30
|
+
metavar="PATH",
|
|
31
|
+
help="Output figure path (default: plot.png).",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--figure-dpi",
|
|
35
|
+
type=int,
|
|
36
|
+
default=150,
|
|
37
|
+
metavar="DPI",
|
|
38
|
+
help="Output resolution in dots per inch (default: 150).",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--parameters",
|
|
42
|
+
nargs="+",
|
|
43
|
+
metavar="NAME",
|
|
44
|
+
help="Parameters to include in the plot, in the requested order.",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--filled",
|
|
48
|
+
action=BooleanOptionalAction,
|
|
49
|
+
default=True,
|
|
50
|
+
help="Fill two-dimensional contours (default; use --no-filled for lines).",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--backend",
|
|
54
|
+
default="getdist",
|
|
55
|
+
metavar="NAME",
|
|
56
|
+
help="Plotting backend (default: getdist).",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--backend-kwargs",
|
|
60
|
+
type=loads,
|
|
61
|
+
metavar="JSON",
|
|
62
|
+
help="JSON object of backend-specific plotting options.",
|
|
63
|
+
)
|
|
64
|
+
return parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
68
|
+
"""Load Fisher forecast files, plot them, and save the figure.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
argv : sequence of str, optional
|
|
73
|
+
Arguments to parse. Defaults to the process command line.
|
|
74
|
+
"""
|
|
75
|
+
parser = _parser()
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
datasets = {}
|
|
78
|
+
for path in args.files:
|
|
79
|
+
label = path.stem
|
|
80
|
+
if label in datasets:
|
|
81
|
+
parser.error(
|
|
82
|
+
f"Input filenames must have unique stems; duplicate {label!r}."
|
|
83
|
+
)
|
|
84
|
+
datasets[label] = load_dataset(path)
|
|
85
|
+
|
|
86
|
+
figure = plot(
|
|
87
|
+
datasets,
|
|
88
|
+
parameters=args.parameters,
|
|
89
|
+
filled=args.filled,
|
|
90
|
+
backend=args.backend,
|
|
91
|
+
backend_kwargs=args.backend_kwargs,
|
|
92
|
+
)
|
|
93
|
+
figure.savefig(args.figure_file, dpi=args.figure_dpi)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
fimx/combination.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Combination of independent information and Gaussian priors."""
|
|
2
|
+
|
|
3
|
+
import operator
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import xarray as xr
|
|
9
|
+
|
|
10
|
+
from .construction import _labels, _new_matrix, _real_values, _validate_matrix
|
|
11
|
+
from .parameters import expand
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def combine(*matrices: xr.DataArray) -> xr.DataArray:
|
|
15
|
+
"""Add independent information over the union of parameter labels.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
*matrices : xarray.DataArray
|
|
20
|
+
One or more matrices satisfying the canonical matrix contract.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
xarray.DataArray
|
|
25
|
+
Fresh sum. Keep the first input order and append newly encountered
|
|
26
|
+
labels in subsequent input order; absent entries contribute zero.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
ValueError
|
|
31
|
+
If no matrices are supplied or any matrix is malformed.
|
|
32
|
+
"""
|
|
33
|
+
if not matrices:
|
|
34
|
+
raise ValueError("At least one matrix is required.")
|
|
35
|
+
labels = [list(_validate_matrix(F)[1]) for F in matrices]
|
|
36
|
+
union = list(dict.fromkeys(label for group in labels for label in group))
|
|
37
|
+
total = reduce(operator.add, (expand(F, union) for F in matrices))
|
|
38
|
+
return _new_matrix(total.values, union)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def gaussian_prior(sigmas: Mapping[str, float]) -> xr.DataArray:
|
|
42
|
+
"""Construct independent Gaussian prior information from standard deviations.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
sigmas : mapping of str to float
|
|
47
|
+
Nonempty mapping of unique parameter names to finite, positive
|
|
48
|
+
standard deviations. Mapping order determines matrix order.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
xarray.DataArray
|
|
53
|
+
Fresh diagonal matrix with entries ``1 / sigma**2``.
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
ValueError
|
|
58
|
+
If names or standard deviations are invalid, or information cannot
|
|
59
|
+
be represented as finite float64 values.
|
|
60
|
+
"""
|
|
61
|
+
labels = _labels(list(sigmas), name="sigmas")
|
|
62
|
+
deviations = _real_values(list(sigmas.values()))
|
|
63
|
+
if np.any(deviations <= 0):
|
|
64
|
+
raise ValueError("Standard deviations must be positive.")
|
|
65
|
+
with np.errstate(over="ignore", divide="ignore", invalid="ignore"):
|
|
66
|
+
information = np.square(1 / deviations)
|
|
67
|
+
return _new_matrix(np.diag(information), labels)
|
fimx/construction.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Construction and shared validation of labeled dense matrices."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
from numpy.typing import ArrayLike, NDArray
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _labels(parameters: Sequence[str], *, name: str) -> list[str]:
|
|
11
|
+
"""Validate a nonempty sequence of unique string labels."""
|
|
12
|
+
if isinstance(parameters, str):
|
|
13
|
+
# A bare string is an invalid label sequence, even for one parameter.
|
|
14
|
+
raise ValueError(f"{name} must be a sequence of parameter names.") # noqa: TRY004
|
|
15
|
+
labels = list(parameters)
|
|
16
|
+
if not labels or any(not isinstance(label, str) for label in labels):
|
|
17
|
+
raise ValueError(f"{name} must be a nonempty sequence of unique string labels.")
|
|
18
|
+
if len(set(labels)) != len(labels):
|
|
19
|
+
raise ValueError(f"{name} must contain unique string labels.")
|
|
20
|
+
return labels
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _real_values(values: ArrayLike) -> NDArray[np.float64]:
|
|
24
|
+
"""Convert finite, real numeric data to float64."""
|
|
25
|
+
array = np.asarray(values)
|
|
26
|
+
if array.dtype.kind not in "iuf":
|
|
27
|
+
raise ValueError("Values must be real numeric data.")
|
|
28
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
29
|
+
result = np.asarray(array, dtype=np.float64)
|
|
30
|
+
if not np.all(np.isfinite(result)):
|
|
31
|
+
raise ValueError("Values must be finite.")
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _coordinate_labels(array: xr.DataArray, dimension: str) -> list[str]:
|
|
36
|
+
"""Read an explicit one-dimensional parameter coordinate."""
|
|
37
|
+
if dimension not in array.coords or array.coords[dimension].dims != (dimension,):
|
|
38
|
+
raise ValueError(f"An explicit coordinate for {dimension!r} is required.")
|
|
39
|
+
return _labels(array.coords[dimension].values.tolist(), name=dimension)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _validate_matrix(F: xr.DataArray) -> tuple[xr.DataArray, list[str]]:
|
|
43
|
+
"""Validate a canonical matrix without checking positive definiteness."""
|
|
44
|
+
if not isinstance(F, xr.DataArray):
|
|
45
|
+
raise TypeError("Matrix inputs must be xarray.DataArray objects.")
|
|
46
|
+
if F.dims != ("row", "col"):
|
|
47
|
+
raise ValueError("Matrix dimensions must be exactly ('row', 'col').")
|
|
48
|
+
if F.shape[0] == 0 or F.shape[0] != F.shape[1]:
|
|
49
|
+
raise ValueError("Matrices must be nonempty and square.")
|
|
50
|
+
parameters = _coordinate_labels(F, "row")
|
|
51
|
+
if parameters != _coordinate_labels(F, "col"):
|
|
52
|
+
raise ValueError("Row and column parameter coordinates must match in order.")
|
|
53
|
+
values = _real_values(F.values)
|
|
54
|
+
if not np.allclose(values, values.T, rtol=1e-10, atol=1e-12):
|
|
55
|
+
raise ValueError("Matrices must be symmetric.")
|
|
56
|
+
return _new_matrix(values, parameters), parameters
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _new_matrix(values: ArrayLike, parameters: Sequence[str]) -> xr.DataArray:
|
|
60
|
+
"""Create a fresh matrix with only canonical coordinates."""
|
|
61
|
+
labels = list(parameters)
|
|
62
|
+
return xr.DataArray(
|
|
63
|
+
_real_values(values).copy(),
|
|
64
|
+
dims=("row", "col"),
|
|
65
|
+
coords={"row": labels, "col": labels.copy()},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def matrix(values: ArrayLike, parameters: Sequence[str]) -> xr.DataArray:
|
|
70
|
+
"""Construct a labeled, symmetric dense matrix.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
values : array_like
|
|
75
|
+
Nonempty square array of finite, real numeric values.
|
|
76
|
+
parameters : sequence of str
|
|
77
|
+
Unique parameter names, in matrix order.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
xarray.DataArray
|
|
82
|
+
Fresh float64 matrix with dimensions ``('row', 'col')``.
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
ValueError
|
|
87
|
+
If the values or parameter names violate the matrix contract.
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
-----
|
|
91
|
+
Symmetry uses ``rtol=1e-10`` and ``atol=1e-12``. Positive definiteness
|
|
92
|
+
is checked only by operations requiring a Cholesky solve.
|
|
93
|
+
"""
|
|
94
|
+
labels = _labels(parameters, name="parameters")
|
|
95
|
+
array = _real_values(values)
|
|
96
|
+
if array.ndim != 2 or array.shape != (len(labels), len(labels)):
|
|
97
|
+
raise ValueError("Values must be square and match the parameter count.")
|
|
98
|
+
result = _new_matrix(array, labels)
|
|
99
|
+
_validate_matrix(result)
|
|
100
|
+
return result
|
fimx/datasets.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Construction of labeled Fisher matrix Datasets with parameter metadata."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable, Mapping
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
from .construction import _validate_matrix
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def dataset(
|
|
13
|
+
F: xr.DataArray, arrays: Mapping[Hashable, ArrayLike | xr.DataArray]
|
|
14
|
+
) -> xr.Dataset:
|
|
15
|
+
"""Combine a Fisher matrix and arrays on shared parameter coordinates.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
F : xarray.DataArray
|
|
20
|
+
Canonical matrix with dimensions ``('row', 'col')``.
|
|
21
|
+
arrays : mapping of hashable to array_like or xarray.DataArray
|
|
22
|
+
Additional variables. One-dimensional arrays use ``row``; two-
|
|
23
|
+
dimensional arrays use ``('row', 'col')``. Plain arrays follow matrix
|
|
24
|
+
order. DataArray dimensions are renamed by position and indexed axes
|
|
25
|
+
are aligned by label. Unindexed axes follow matrix order. Names
|
|
26
|
+
``fisher``, ``row``, and ``col`` are reserved.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
xarray.Dataset
|
|
31
|
+
Independent variables with shared matrix coordinates. Additional
|
|
32
|
+
arrays retain their dtypes; positive definiteness is not required.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
TypeError
|
|
37
|
+
If F is not a DataArray or arrays is not a mapping.
|
|
38
|
+
ValueError
|
|
39
|
+
If the matrix is invalid, names are reserved, arrays are not one- or
|
|
40
|
+
two-dimensional, or xarray rejects incompatible sizes or coordinates.
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
Shape, coordinate, and merge compatibility are checked by xarray.
|
|
45
|
+
Metadata need not be numeric or finite. Plotting validates fiducials.
|
|
46
|
+
"""
|
|
47
|
+
fisher, _ = _validate_matrix(F)
|
|
48
|
+
if not isinstance(arrays, Mapping):
|
|
49
|
+
raise TypeError("Arrays must be a mapping of names to arrays.")
|
|
50
|
+
variables: dict[Hashable, xr.DataArray] = {"fisher": fisher}
|
|
51
|
+
dimensions = {1: ("row",), 2: ("row", "col")}
|
|
52
|
+
ordered = fisher.sortby(["row", "col"])
|
|
53
|
+
for name, array in arrays.items():
|
|
54
|
+
if name in {"fisher", "row", "col"}:
|
|
55
|
+
raise ValueError(f"Variable name {name!r} is reserved.")
|
|
56
|
+
variable = (
|
|
57
|
+
array
|
|
58
|
+
if isinstance(array, xr.DataArray)
|
|
59
|
+
else xr.DataArray(np.asarray(array))
|
|
60
|
+
)
|
|
61
|
+
if variable.ndim not in dimensions:
|
|
62
|
+
raise ValueError("Additional arrays must be one- or two-dimensional.")
|
|
63
|
+
dims = dimensions[variable.ndim]
|
|
64
|
+
variable = variable.rename(dict(zip(variable.dims, dims, strict=True)))
|
|
65
|
+
variable = variable.assign_coords(
|
|
66
|
+
{dim: fisher.coords[dim] for dim in dims if dim not in variable.indexes}
|
|
67
|
+
)
|
|
68
|
+
# Exact alignment on sorted coordinates permits reordered labels while
|
|
69
|
+
# letting xarray reject missing, extra, or duplicate labels and sizes.
|
|
70
|
+
xr.align(ordered, variable.sortby(list(dims)), join="exact")
|
|
71
|
+
variables[name] = variable.sel({dim: fisher.coords[dim] for dim in dims})
|
|
72
|
+
return xr.Dataset(variables).copy(deep=True)
|
fimx/inversion.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Cholesky inversion and marginalized constraints."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import xarray as xr
|
|
5
|
+
from xarray_einstats import linalg
|
|
6
|
+
|
|
7
|
+
from .construction import _new_matrix, _validate_matrix
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _solve(values: xr.DataArray, rhs: xr.DataArray) -> xr.DataArray:
|
|
11
|
+
"""Solve a positive definite system using labeled Cholesky operations."""
|
|
12
|
+
chol = linalg.cholesky(values, dims=("row", "col"))
|
|
13
|
+
intermediate = linalg.solve(chol, rhs, dims=("row", "col", "rhs_col"))
|
|
14
|
+
return linalg.solve(
|
|
15
|
+
chol.transpose("col", "row"),
|
|
16
|
+
intermediate,
|
|
17
|
+
dims=("col", "row", "rhs_col"),
|
|
18
|
+
).rename(rhs_col="col")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def inv(F: xr.DataArray) -> xr.DataArray:
|
|
22
|
+
"""Return the labeled inverse, or covariance of a Fisher matrix.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
F : xarray.DataArray
|
|
27
|
+
Matrix satisfying the canonical matrix contract.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
xarray.DataArray
|
|
32
|
+
Fresh symmetric inverse with the input parameter order.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
numpy.linalg.LinAlgError
|
|
37
|
+
If the matrix is singular or not positive definite.
|
|
38
|
+
ValueError
|
|
39
|
+
If the matrix contract is violated.
|
|
40
|
+
"""
|
|
41
|
+
values, parameters = _validate_matrix(F)
|
|
42
|
+
rhs = xr.DataArray(
|
|
43
|
+
np.eye(len(parameters)),
|
|
44
|
+
dims=("row", "rhs_col"),
|
|
45
|
+
coords={"row": parameters, "rhs_col": parameters},
|
|
46
|
+
)
|
|
47
|
+
covariance = _solve(values, rhs)
|
|
48
|
+
raw = covariance.values
|
|
49
|
+
return _new_matrix(raw / 2 + raw.T / 2, parameters)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def errors(F: xr.DataArray) -> xr.DataArray:
|
|
53
|
+
"""Return marginalized standard deviations from a Fisher matrix.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
F : xarray.DataArray
|
|
58
|
+
Matrix satisfying the canonical matrix contract.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
xarray.DataArray
|
|
63
|
+
Square roots of the covariance diagonal on dimension ``parameter``.
|
|
64
|
+
|
|
65
|
+
Raises
|
|
66
|
+
------
|
|
67
|
+
numpy.linalg.LinAlgError
|
|
68
|
+
If the matrix is singular or not positive definite.
|
|
69
|
+
ValueError
|
|
70
|
+
If the matrix contract is violated.
|
|
71
|
+
"""
|
|
72
|
+
covariance = inv(F)
|
|
73
|
+
diagonal = linalg.diagonal(covariance, dims=("row", "col"))
|
|
74
|
+
return xr.DataArray(
|
|
75
|
+
np.sqrt(diagonal.values),
|
|
76
|
+
dims=("parameter",),
|
|
77
|
+
coords={"parameter": covariance.coords["row"].values.copy()},
|
|
78
|
+
)
|
fimx/io.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""NetCDF loading helpers."""
|
|
2
|
+
|
|
3
|
+
from os import PathLike
|
|
4
|
+
|
|
5
|
+
import xarray as xr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_dataset(path: str | PathLike[str]) -> xr.Dataset:
|
|
9
|
+
"""Load a NetCDF Dataset using the optional h5netcdf engine.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
path : str or os.PathLike
|
|
14
|
+
Path to a NetCDF file.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
xarray.Dataset
|
|
19
|
+
Loaded Dataset.
|
|
20
|
+
"""
|
|
21
|
+
return xr.load_dataset(path, engine="h5netcdf")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["load_dataset"]
|
fimx/parameters.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Parameter addition, removal and changes of variables."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import xarray as xr
|
|
6
|
+
from xarray_einstats import linalg
|
|
7
|
+
|
|
8
|
+
from .construction import (
|
|
9
|
+
_coordinate_labels,
|
|
10
|
+
_labels,
|
|
11
|
+
_new_matrix,
|
|
12
|
+
_real_values,
|
|
13
|
+
_validate_matrix,
|
|
14
|
+
)
|
|
15
|
+
from .inversion import _solve
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _selection(
|
|
19
|
+
labels: list[str], parameters: str | Sequence[str]
|
|
20
|
+
) -> tuple[list[int], list[int]]:
|
|
21
|
+
"""Resolve parameter removal while preserving retained input order."""
|
|
22
|
+
selected = [parameters] if isinstance(parameters, str) else list(parameters)
|
|
23
|
+
if any(not isinstance(name, str) for name in selected):
|
|
24
|
+
raise ValueError("Selected parameters must be strings.")
|
|
25
|
+
if len(set(selected)) != len(selected):
|
|
26
|
+
raise ValueError("Selected parameters must be unique.")
|
|
27
|
+
for name in selected:
|
|
28
|
+
if name not in labels:
|
|
29
|
+
raise KeyError(name)
|
|
30
|
+
if len(selected) == len(labels):
|
|
31
|
+
raise ValueError("Cannot remove all parameters.")
|
|
32
|
+
removed = set(selected)
|
|
33
|
+
return (
|
|
34
|
+
[i for i, name in enumerate(labels) if name not in removed],
|
|
35
|
+
[i for i, name in enumerate(labels) if name in removed],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def expand(F: xr.DataArray, parameters: Sequence[str]) -> xr.DataArray:
|
|
40
|
+
"""Embed a matrix in a larger parameter set, filling new entries with zeros.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
F : xarray.DataArray
|
|
45
|
+
Matrix satisfying the canonical matrix contract.
|
|
46
|
+
parameters : sequence of str
|
|
47
|
+
Unique target names, in the desired output order. Must contain every
|
|
48
|
+
name in ``F``; the order may differ from that of ``F``.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
xarray.DataArray
|
|
53
|
+
Fresh matrix over ``parameters``. Entries involving names absent from
|
|
54
|
+
``F`` are zero, so matrices expanded to the same ``parameters`` can be
|
|
55
|
+
added directly with ``+``.
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
ValueError
|
|
60
|
+
If input is malformed, ``parameters`` is invalid, or a name in ``F``
|
|
61
|
+
is missing from ``parameters``.
|
|
62
|
+
"""
|
|
63
|
+
values, labels = _validate_matrix(F)
|
|
64
|
+
target = _labels(parameters, name="parameters")
|
|
65
|
+
missing = [name for name in labels if name not in target]
|
|
66
|
+
if missing:
|
|
67
|
+
raise ValueError(f"parameters is missing names present in F: {missing}.")
|
|
68
|
+
filled = values.reindex(row=target, col=target, fill_value=0)
|
|
69
|
+
return _new_matrix(filled.values, target)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def fix(F: xr.DataArray, parameters: str | Sequence[str]) -> xr.DataArray:
|
|
73
|
+
"""Fix selected parameters by keeping the remaining principal submatrix.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
F : xarray.DataArray
|
|
78
|
+
Matrix satisfying the canonical matrix contract.
|
|
79
|
+
parameters : str or sequence of str
|
|
80
|
+
Names to remove. An empty sequence returns a fresh copy.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
xarray.DataArray
|
|
85
|
+
Fresh principal submatrix in retained input order.
|
|
86
|
+
|
|
87
|
+
Raises
|
|
88
|
+
------
|
|
89
|
+
KeyError
|
|
90
|
+
If a selected name is unknown.
|
|
91
|
+
ValueError
|
|
92
|
+
If input is malformed, names are duplicated, or all are removed.
|
|
93
|
+
"""
|
|
94
|
+
values, labels = _validate_matrix(F)
|
|
95
|
+
keep, _ = _selection(labels, parameters)
|
|
96
|
+
retained = [labels[i] for i in keep]
|
|
97
|
+
selected = values.sel(row=retained, col=retained)
|
|
98
|
+
return _new_matrix(selected.values, retained)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def marginalize(F: xr.DataArray, parameters: str | Sequence[str]) -> xr.DataArray:
|
|
102
|
+
"""Remove nuisance parameters through the Schur complement.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
F : xarray.DataArray
|
|
107
|
+
Matrix satisfying the canonical matrix contract.
|
|
108
|
+
parameters : str or sequence of str
|
|
109
|
+
Names to remove. An empty sequence returns a fresh copy.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
xarray.DataArray
|
|
114
|
+
Fresh marginalized matrix in retained input order.
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
KeyError
|
|
119
|
+
If a selected name is unknown.
|
|
120
|
+
ValueError
|
|
121
|
+
If input is malformed, names are duplicated, or all are removed.
|
|
122
|
+
numpy.linalg.LinAlgError
|
|
123
|
+
If the removed block is singular or not positive definite.
|
|
124
|
+
|
|
125
|
+
Notes
|
|
126
|
+
-----
|
|
127
|
+
For retained block A, cross block B and removed block D, compute
|
|
128
|
+
``A - B @ solve(D, B.T)``. Only D must be positive definite.
|
|
129
|
+
"""
|
|
130
|
+
values, labels = _validate_matrix(F)
|
|
131
|
+
keep, drop = _selection(labels, parameters)
|
|
132
|
+
retained = [labels[i] for i in keep]
|
|
133
|
+
removed = [labels[i] for i in drop]
|
|
134
|
+
result = values.sel(row=retained, col=retained)
|
|
135
|
+
if drop:
|
|
136
|
+
cross = values.sel(row=retained, col=removed)
|
|
137
|
+
rhs = cross.transpose("col", "row").rename(row="rhs_col").rename(col="row")
|
|
138
|
+
solved = _solve(values.sel(row=removed, col=removed), rhs).rename(
|
|
139
|
+
row="drop_parameter", col="rhs_col"
|
|
140
|
+
)
|
|
141
|
+
product = linalg.matmul(
|
|
142
|
+
cross,
|
|
143
|
+
solved,
|
|
144
|
+
dims=(("row", "col"), ("drop_parameter", "rhs_col")),
|
|
145
|
+
).rename(rhs_col="col")
|
|
146
|
+
result = result - product
|
|
147
|
+
raw = result.values
|
|
148
|
+
return _new_matrix(raw / 2 + raw.T / 2, retained)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def transform(F: xr.DataArray, jacobian: xr.DataArray) -> xr.DataArray:
|
|
152
|
+
"""Change variables using a labeled Jacobian of old with respect to new.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
F : xarray.DataArray
|
|
157
|
+
Matrix satisfying the canonical matrix contract.
|
|
158
|
+
jacobian : xarray.DataArray
|
|
159
|
+
Finite real array with dimensions ``('old', 'new')``
|
|
160
|
+
and unique string coordinates. Entries are
|
|
161
|
+
``J[i, j] = d theta_i / d phi_j``. Old labels must match all F labels;
|
|
162
|
+
rows are reordered to F. New labels must be nonempty.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
xarray.DataArray
|
|
167
|
+
Fresh symmetric matrix ``J.T @ F @ J`` in new-parameter order.
|
|
168
|
+
|
|
169
|
+
Raises
|
|
170
|
+
------
|
|
171
|
+
ValueError
|
|
172
|
+
If the matrix or Jacobian contract is violated.
|
|
173
|
+
TypeError
|
|
174
|
+
If the Jacobian is not a DataArray.
|
|
175
|
+
"""
|
|
176
|
+
values, labels = _validate_matrix(F)
|
|
177
|
+
if not isinstance(jacobian, xr.DataArray):
|
|
178
|
+
raise TypeError("The Jacobian must be an xarray.DataArray.")
|
|
179
|
+
if jacobian.dims != ("old", "new"):
|
|
180
|
+
raise ValueError("Jacobian dimensions must be ('old', 'new').")
|
|
181
|
+
old = _coordinate_labels(jacobian, "old")
|
|
182
|
+
new = _coordinate_labels(jacobian, "new")
|
|
183
|
+
if set(old) != set(labels):
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"Jacobian old labels must match the complete matrix parameters."
|
|
186
|
+
)
|
|
187
|
+
J = xr.DataArray(
|
|
188
|
+
_real_values(jacobian.sel(old=labels).values),
|
|
189
|
+
dims=("old", "new"),
|
|
190
|
+
coords={"old": labels, "new": new},
|
|
191
|
+
).rename(old="col")
|
|
192
|
+
right = linalg.matmul(values, J, dims=("row", "col", "new"))
|
|
193
|
+
left = J.transpose("new", "col")
|
|
194
|
+
right = right.rename(row="old", new="new_right")
|
|
195
|
+
result = linalg.matmul(
|
|
196
|
+
left,
|
|
197
|
+
right,
|
|
198
|
+
dims=(
|
|
199
|
+
("new", "col"),
|
|
200
|
+
("old", "new_right"),
|
|
201
|
+
),
|
|
202
|
+
).rename(new="row", new_right="col")
|
|
203
|
+
raw = result.values
|
|
204
|
+
return _new_matrix(raw / 2 + raw.T / 2, new)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Backend selection for Fisher forecast plots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import xarray as xr
|
|
9
|
+
|
|
10
|
+
from .base import PlotBackend
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from matplotlib.figure import Figure
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _getdist() -> PlotBackend:
|
|
17
|
+
"""Load the optional GetDist implementation."""
|
|
18
|
+
try:
|
|
19
|
+
from .getdist import plot
|
|
20
|
+
except ModuleNotFoundError as exc:
|
|
21
|
+
if exc.name not in {"getdist", "matplotlib"}:
|
|
22
|
+
raise
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"The GetDist backend requires plotting dependencies. "
|
|
25
|
+
"Install them with uv add 'fimx[plotting]'."
|
|
26
|
+
) from exc
|
|
27
|
+
return plot
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_BACKENDS: dict[str, Callable[[], PlotBackend]] = {"getdist": _getdist}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def plot(
|
|
34
|
+
datasets: Mapping[str, xr.Dataset],
|
|
35
|
+
*,
|
|
36
|
+
parameters: Sequence[str] | None = None,
|
|
37
|
+
filled: bool = True,
|
|
38
|
+
backend: str = "getdist",
|
|
39
|
+
backend_kwargs: Mapping[str, Any] | None = None,
|
|
40
|
+
) -> Figure:
|
|
41
|
+
"""Plot overlaid marginalized constraints from labeled Fisher forecasts.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
datasets : mapping of str to xarray.Dataset
|
|
46
|
+
Nonempty forecast mapping. Keys provide legend labels and overlay
|
|
47
|
+
order. Each Dataset requires ``fisher`` on ``('row', 'col')`` and
|
|
48
|
+
finite ``fiducials`` on ``row`` with matching parameter coordinates.
|
|
49
|
+
parameters : sequence of str, optional
|
|
50
|
+
Unique parameters present in every forecast, in plotting order.
|
|
51
|
+
Defaults to their intersection in the first forecast's order.
|
|
52
|
+
Omitted parameters are marginalized over.
|
|
53
|
+
filled : bool
|
|
54
|
+
Whether to fill two-dimensional contours. Default is True.
|
|
55
|
+
backend : str
|
|
56
|
+
Backend name. Currently only ``'getdist'`` is supported.
|
|
57
|
+
backend_kwargs : mapping, optional
|
|
58
|
+
GetDist ``triangle_plot`` options excluding ``roots``, ``params``,
|
|
59
|
+
``legend_labels``, and ``filled``. Native styles and confidence
|
|
60
|
+
levels apply unless customized.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
matplotlib.figure.Figure
|
|
65
|
+
Figure for editing or saving; it is not automatically shown or saved.
|
|
66
|
+
|
|
67
|
+
Raises
|
|
68
|
+
------
|
|
69
|
+
TypeError
|
|
70
|
+
If containers have incorrect types or backend options are reserved.
|
|
71
|
+
ValueError
|
|
72
|
+
If data are invalid, no parameters are shared, or backend is unknown.
|
|
73
|
+
KeyError
|
|
74
|
+
If a requested parameter is unavailable in any forecast.
|
|
75
|
+
numpy.linalg.LinAlgError
|
|
76
|
+
If any complete Fisher matrix is singular or not positive definite.
|
|
77
|
+
ImportError
|
|
78
|
+
If the selected backend's optional dependencies are unavailable.
|
|
79
|
+
"""
|
|
80
|
+
if backend not in _BACKENDS:
|
|
81
|
+
raise ValueError(f"Unknown plotting backend {backend!r}. Available: getdist.")
|
|
82
|
+
implementation = _BACKENDS[backend]()
|
|
83
|
+
return implementation(
|
|
84
|
+
datasets, parameters=parameters, filled=filled, backend_kwargs=backend_kwargs
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["plot"]
|
fimx/plotting/base.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Backend protocol and shared preparation for marginalized Gaussian plots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import xarray as xr
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
from ..construction import _labels, _real_values, _validate_matrix
|
|
14
|
+
from ..inversion import inv
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from matplotlib.figure import Figure
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlotBackend(Protocol):
|
|
21
|
+
"""Callable interface implemented by each plotting backend."""
|
|
22
|
+
|
|
23
|
+
def __call__(
|
|
24
|
+
self,
|
|
25
|
+
datasets: Mapping[str, xr.Dataset],
|
|
26
|
+
*,
|
|
27
|
+
parameters: Sequence[str] | None = None,
|
|
28
|
+
filled: bool = True,
|
|
29
|
+
backend_kwargs: Mapping[str, Any] | None = None,
|
|
30
|
+
) -> Figure:
|
|
31
|
+
"""Return a Figure overlaying the supplied forecasts."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _Gaussian:
|
|
37
|
+
"""Prepared marginal distribution for one forecast."""
|
|
38
|
+
|
|
39
|
+
label: str
|
|
40
|
+
mean: NDArray[np.float64]
|
|
41
|
+
covariance: NDArray[np.float64]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_dataset(forecast: xr.Dataset) -> xr.Dataset:
|
|
45
|
+
"""Validate the Fisher matrix and numeric reference values for plotting."""
|
|
46
|
+
if not isinstance(forecast, xr.Dataset):
|
|
47
|
+
raise TypeError("Plot inputs must be xarray.Dataset objects.")
|
|
48
|
+
if not {"fisher", "fiducials"}.issubset(forecast.data_vars):
|
|
49
|
+
raise ValueError("Datasets must contain 'fisher' and 'fiducials' variables.")
|
|
50
|
+
fisher, labels = _validate_matrix(forecast["fisher"])
|
|
51
|
+
reference = forecast["fiducials"]
|
|
52
|
+
if reference.dims != ("row",):
|
|
53
|
+
raise ValueError("Fiducial dimensions must be exactly ('row',).")
|
|
54
|
+
return xr.Dataset(
|
|
55
|
+
{
|
|
56
|
+
"fisher": fisher,
|
|
57
|
+
"fiducials": xr.DataArray(
|
|
58
|
+
_real_values(reference.values).copy(),
|
|
59
|
+
dims="row",
|
|
60
|
+
coords={"row": labels},
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _prepare(
|
|
67
|
+
datasets: Mapping[str, xr.Dataset], parameters: Sequence[str] | None
|
|
68
|
+
) -> tuple[list[str], list[_Gaussian]]:
|
|
69
|
+
"""Validate forecasts and select marginalized Gaussian distributions."""
|
|
70
|
+
if not isinstance(datasets, Mapping):
|
|
71
|
+
raise TypeError("Plot inputs must be a mapping of labels to Datasets.")
|
|
72
|
+
if not datasets:
|
|
73
|
+
raise ValueError("At least one Dataset is required.")
|
|
74
|
+
if any(not isinstance(label, str) for label in datasets):
|
|
75
|
+
raise ValueError("Plot labels must be strings.")
|
|
76
|
+
forecasts = {label: _validate_dataset(ds) for label, ds in datasets.items()}
|
|
77
|
+
first = next(iter(forecasts.values()))
|
|
78
|
+
if parameters is None:
|
|
79
|
+
names = [
|
|
80
|
+
name
|
|
81
|
+
for name in first.row.values.tolist()
|
|
82
|
+
if all(name in ds.row.values for ds in forecasts.values())
|
|
83
|
+
]
|
|
84
|
+
if not names:
|
|
85
|
+
raise ValueError("Datasets have no shared parameters to plot.")
|
|
86
|
+
else:
|
|
87
|
+
names = _labels(parameters, name="parameters")
|
|
88
|
+
for label, ds in forecasts.items():
|
|
89
|
+
for name in names:
|
|
90
|
+
if name not in ds.row.values:
|
|
91
|
+
raise KeyError(f"Parameter {name!r} is absent from {label!r}.")
|
|
92
|
+
distributions = []
|
|
93
|
+
for label, ds in forecasts.items():
|
|
94
|
+
covariance = inv(ds["fisher"]).sel(row=names, col=names)
|
|
95
|
+
distributions.append(
|
|
96
|
+
_Gaussian(label, ds["fiducials"].sel(row=names).values, covariance.values)
|
|
97
|
+
)
|
|
98
|
+
return names, distributions
|
fimx/plotting/getdist.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Analytic Gaussian corner plots using GetDist."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import xarray as xr
|
|
7
|
+
from getdist import plots
|
|
8
|
+
from getdist.gaussian_mixtures import GaussianND
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
|
|
11
|
+
from .base import PlotBackend, _prepare
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot(
|
|
15
|
+
datasets: Mapping[str, xr.Dataset],
|
|
16
|
+
*,
|
|
17
|
+
parameters: Sequence[str] | None = None,
|
|
18
|
+
filled: bool = True,
|
|
19
|
+
backend_kwargs: Mapping[str, Any] | None = None,
|
|
20
|
+
) -> Figure:
|
|
21
|
+
"""Overlay marginalized Gaussian forecasts with GetDist.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
datasets : mapping of str to xarray.Dataset
|
|
26
|
+
Forecasts containing ``fisher`` and ``fiducials`` variables.
|
|
27
|
+
parameters : sequence of str, optional
|
|
28
|
+
Ordered parameter selection; otherwise use the common parameters.
|
|
29
|
+
filled : bool
|
|
30
|
+
Whether to fill the two-dimensional contours.
|
|
31
|
+
backend_kwargs : mapping, optional
|
|
32
|
+
Additional arguments to GetDist's ``triangle_plot``.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
matplotlib.figure.Figure
|
|
37
|
+
Figure containing the overlaid forecasts.
|
|
38
|
+
|
|
39
|
+
Raises
|
|
40
|
+
------
|
|
41
|
+
TypeError
|
|
42
|
+
If backend options override arguments controlled by this function.
|
|
43
|
+
"""
|
|
44
|
+
options = dict(backend_kwargs or {})
|
|
45
|
+
reserved = {"roots", "params", "legend_labels", "filled"}.intersection(options)
|
|
46
|
+
if reserved:
|
|
47
|
+
raise TypeError(
|
|
48
|
+
f"Backend options cannot override: {', '.join(sorted(reserved))}."
|
|
49
|
+
)
|
|
50
|
+
names, distributions = _prepare(datasets, parameters)
|
|
51
|
+
roots = [
|
|
52
|
+
GaussianND(item.mean, item.covariance, names=names, label=item.label)
|
|
53
|
+
for item in distributions
|
|
54
|
+
]
|
|
55
|
+
plotter = plots.get_subplot_plotter()
|
|
56
|
+
plotter.triangle_plot(
|
|
57
|
+
roots,
|
|
58
|
+
params=names,
|
|
59
|
+
legend_labels=[item.label for item in distributions],
|
|
60
|
+
filled=filled,
|
|
61
|
+
**options,
|
|
62
|
+
)
|
|
63
|
+
figure = plotter.fig
|
|
64
|
+
assert figure is not None
|
|
65
|
+
return figure
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_backend: PlotBackend = plot
|
fimx/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fimx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dense Fisher matrix functions on labeled xarray arrays
|
|
5
|
+
Keywords: fisher-matrix,forecasting,cosmology,xarray
|
|
6
|
+
Author: binado
|
|
7
|
+
Author-email: binado <bernardopveronese@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: xarray
|
|
17
|
+
Requires-Dist: xarray-einstats>=0.10.0
|
|
18
|
+
Requires-Dist: h5netcdf[h5py] ; extra == 'io'
|
|
19
|
+
Requires-Dist: getdist ; extra == 'plotting'
|
|
20
|
+
Requires-Dist: matplotlib ; extra == 'plotting'
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Project-URL: Homepage, https://github.com/binado/fimx
|
|
23
|
+
Project-URL: Repository, https://github.com/binado/fimx
|
|
24
|
+
Project-URL: Issues, https://github.com/binado/fimx/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/binado/fimx/blob/main/CHANGELOG.md
|
|
26
|
+
Provides-Extra: io
|
|
27
|
+
Provides-Extra: plotting
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# fimx
|
|
31
|
+
|
|
32
|
+
`fimx` provides functions for dense Fisher information matrices stored
|
|
33
|
+
as ordinary `xarray.DataArray` objects. NumPy handles the computation; xarray
|
|
34
|
+
handles labels, metadata containers, and persistence.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv add fimx
|
|
38
|
+
# Optional NetCDF backend:
|
|
39
|
+
uv add 'fimx[io]'
|
|
40
|
+
# Optional Gaussian plotting backend:
|
|
41
|
+
uv add 'fimx[plotting]'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from fimx import combine, errors, fix, gaussian_prior, inv, marginalize, matrix
|
|
48
|
+
|
|
49
|
+
F = matrix([[4, 1], [1, 2]], ["a", "b"])
|
|
50
|
+
fixed = fix(F, "b") # [[4.0]]: b is held fixed
|
|
51
|
+
reduced = marginalize(F, "b") # [[3.5]]: uncertainty in b is integrated out
|
|
52
|
+
C = inv(F) # covariance, with the same row/col labels
|
|
53
|
+
sigma = errors(F) # sqrt(diag(C)), dimension "parameter"
|
|
54
|
+
|
|
55
|
+
prior = gaussian_prior({"a": 0.5}) # standard deviation 0.5 -> information 4
|
|
56
|
+
posterior = combine(F, prior)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Function | Behavior |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `matrix(values, parameters)` | Construct and validate a canonical matrix. |
|
|
62
|
+
| `expand(F, parameters)` | Embed in a larger or reordered parameter set, filling with zeros. |
|
|
63
|
+
| `fix(F, parameters)` | Remove names through a principal submatrix. |
|
|
64
|
+
| `marginalize(F, parameters)` | Remove names through the Schur complement. |
|
|
65
|
+
| `inv(F)` | Return the inverse using Cholesky. |
|
|
66
|
+
| `errors(F)` | Return marginalized standard deviations. |
|
|
67
|
+
| `transform(F, jacobian)` | Change variables using `J.T @ F @ J`. |
|
|
68
|
+
| `combine(*matrices)` | Sum independent information over the parameter union. |
|
|
69
|
+
| `gaussian_prior(sigmas)` | Construct diagonal information `1 / sigma**2`. |
|
|
70
|
+
|
|
71
|
+
## Array contract
|
|
72
|
+
|
|
73
|
+
- Matrices have dimensions exactly `("row", "col")`, a nonempty square shape,
|
|
74
|
+
and explicit, identical, ordered coordinates of unique string names.
|
|
75
|
+
Batches, inferred dimensions, and coordinate repair are unsupported.
|
|
76
|
+
- Values must be real, finite, and symmetric. They are converted to float64.
|
|
77
|
+
- Positive semidefiniteness is not checked, so singular matrices can be built,
|
|
78
|
+
fixed, transformed, or combined. `inv` and `errors` require positive
|
|
79
|
+
definite input; `marginalize` only requires the removed block to be.
|
|
80
|
+
- Functions return fresh DataArrays and never mutate their inputs. Attributes,
|
|
81
|
+
names, and auxiliary coordinates are not preserved.
|
|
82
|
+
- Arrays built directly with xarray work if they meet the contract.
|
|
83
|
+
|
|
84
|
+
Behavior worth knowing:
|
|
85
|
+
|
|
86
|
+
- `fix` and `marginalize` accept one name or a sequence and keep the remaining
|
|
87
|
+
order. Unknown names raise `KeyError`; duplicates or removing every
|
|
88
|
+
parameter raise `ValueError`.
|
|
89
|
+
- `expand` embeds a matrix in a larger or reordered set of names, filling new
|
|
90
|
+
entries with zeros. The target must contain every existing name. Matrices
|
|
91
|
+
expanded to the same names add with plain `+`, whereas `+` on mismatched
|
|
92
|
+
names silently keeps only the overlap.
|
|
93
|
+
- `combine` keeps the first matrix's order and appends new parameters as they
|
|
94
|
+
appear. Missing entries contribute zero.
|
|
95
|
+
- `gaussian_prior` takes **standard deviations**, not information values.
|
|
96
|
+
- Failures are explicit: `ValueError` for malformed or non-finite data,
|
|
97
|
+
`TypeError` for non-DataArray inputs, and `numpy.linalg.LinAlgError` for
|
|
98
|
+
singular or indefinite matrices. There is no pseudoinverse or regularization.
|
|
99
|
+
|
|
100
|
+
### Changes of variables
|
|
101
|
+
|
|
102
|
+
The Jacobian has dimensions `("old", "new")` and orientation
|
|
103
|
+
**`J[i, j] = d theta_i / d phi_j`**, where theta are the old parameters and phi
|
|
104
|
+
the new ones. Every old parameter must appear exactly once (rows are reordered
|
|
105
|
+
to match `F`), and the order of the new parameters sets the output order.
|
|
106
|
+
Rectangular Jacobians are supported.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import xarray as xr
|
|
110
|
+
from fimx import transform
|
|
111
|
+
|
|
112
|
+
# theta_a = 2 * phi_x, theta_b = phi_x
|
|
113
|
+
# Rows deliberately appear in reverse order to F.
|
|
114
|
+
J = xr.DataArray(
|
|
115
|
+
[[1.0], [2.0]],
|
|
116
|
+
dims=("old", "new"),
|
|
117
|
+
coords={"old": ["b", "a"], "new": ["x"]},
|
|
118
|
+
)
|
|
119
|
+
G = transform(F, J) # [[22.0]]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Plotting
|
|
123
|
+
|
|
124
|
+
Plotting needs the `plotting` extra (GetDist and Matplotlib, loaded lazily).
|
|
125
|
+
|
|
126
|
+
### Forecast Datasets
|
|
127
|
+
|
|
128
|
+
Keep fiducials separate from the Fisher matrix and bundle them with
|
|
129
|
+
`dataset()`. In the mapping of extra variables, 1D arrays use `row` and 2D
|
|
130
|
+
arrays use `(row, col)`; plain arrays follow matrix order and indexed axes are
|
|
131
|
+
aligned by label. Strings, units, and nonfinite metadata are allowed. The names
|
|
132
|
+
`fisher`, `row`, and `col` are reserved.
|
|
133
|
+
|
|
134
|
+
### Corner plots
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from fimx import dataset, plot
|
|
138
|
+
|
|
139
|
+
fiducials = xr.DataArray([1.0, 2.0], dims="row", coords={"row": ["a", "b"]})
|
|
140
|
+
survey_a = dataset(F, {"fiducials": fiducials, "units": ["km", "s"]})
|
|
141
|
+
survey_b = dataset(2 * F, {"fiducials": fiducials})
|
|
142
|
+
|
|
143
|
+
fig = plot({"Survey A": survey_a, "Survey B": survey_b})
|
|
144
|
+
fig.savefig("constraints.pdf")
|
|
145
|
+
|
|
146
|
+
# Explicit order and GetDist customization:
|
|
147
|
+
fig = plot(
|
|
148
|
+
{"Survey A": survey_a, "Survey B": survey_b},
|
|
149
|
+
parameters=["b", "a"],
|
|
150
|
+
backend="getdist",
|
|
151
|
+
filled=False,
|
|
152
|
+
backend_kwargs={"contour_colors": ["C0", "C1"]},
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `plot()` takes a nonempty mapping of labels to Datasets with `fisher` and
|
|
157
|
+
finite, real `fiducials`. Each forecast is centered on its own fiducials;
|
|
158
|
+
mapping order sets the overlay order and keys become legend labels.
|
|
159
|
+
- By default, the plot uses the parameters shared by all forecasts, in the
|
|
160
|
+
first forecast's order. An explicit `parameters` list must be unique and
|
|
161
|
+
present in every forecast.
|
|
162
|
+
- Each full Fisher matrix is inverted before selecting parameters, so omitted
|
|
163
|
+
parameters are marginalized and every complete matrix (nuisance blocks
|
|
164
|
+
included) must be positive definite.
|
|
165
|
+
- The GetDist backend draws analytic Gaussians, with no sampling, and returns
|
|
166
|
+
a Matplotlib `Figure` without showing or saving it. Filled contours are the
|
|
167
|
+
default. `backend_kwargs` is forwarded to `triangle_plot`, except `roots`,
|
|
168
|
+
`params`, `legend_labels`, and `filled`, which `plot()` controls.
|
|
169
|
+
- Additional backends implement `fimx.plotting.base.PlotBackend`.
|
|
170
|
+
|
|
171
|
+
### Command line
|
|
172
|
+
|
|
173
|
+
With both extras installed, `fimx-plot` draws a corner plot from NetCDF
|
|
174
|
+
Datasets that contain `fisher` and `fiducials`:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
uv add 'fimx[io,plotting]'
|
|
178
|
+
fimx-plot --file survey-a.nc --file survey-b.nc \
|
|
179
|
+
--figure-file constraints.png --figure-dpi 200 \
|
|
180
|
+
--parameters a b --no-filled --backend getdist \
|
|
181
|
+
--backend-kwargs '{"contour_colors": ["C0", "C1"]}'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each file stem becomes a legend label, so stems must be unique. The output
|
|
185
|
+
defaults to `plot.png` at 150 dpi. `--parameters` selects and orders
|
|
186
|
+
parameters, `--no-filled` draws line contours, `--backend` picks a backend, and
|
|
187
|
+
`--backend-kwargs` takes a JSON object. `fimx.io.load_dataset(path)` loads the
|
|
188
|
+
same files from Python.
|
|
189
|
+
|
|
190
|
+
## Storage
|
|
191
|
+
|
|
192
|
+
There is no `fimx` file format; use xarray. The `io` extra supplies `h5netcdf`.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
F.to_netcdf("fisher.nc", engine="h5netcdf")
|
|
196
|
+
restored = xr.load_dataarray("fisher.nc", engine="h5netcdf")
|
|
197
|
+
C_restored = inv(restored)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## More
|
|
201
|
+
|
|
202
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. `fimx` is released under
|
|
203
|
+
the MIT license; see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
fimx/__init__.py,sha256=KiZtoATZAuidXk1VQQAPacV3nNdHALf1uWBn8aGMsiI,481
|
|
2
|
+
fimx/cli.py,sha256=W2reMC1rGOlYEGV5MS5T279b90BNmIBb2tsF-r4s7PU,2648
|
|
3
|
+
fimx/combination.py,sha256=7WLjJDR7VnY877vlHTtNR_NSrkpCKHAIvaBzGyf6vFA,2210
|
|
4
|
+
fimx/construction.py,sha256=cHxPac74894ZX9VnGJhh0mdFK590ZhHVZlhbDtxrPTU,3956
|
|
5
|
+
fimx/datasets.py,sha256=cZv-Dtw4MWA0ws16OtJuVHNq9aghcta0Pb0RojyNL7Q,2942
|
|
6
|
+
fimx/inversion.py,sha256=vYXKBtRUxywVmJf800UAerwhJTO-RryPn4ZD3gbl-Nk,2238
|
|
7
|
+
fimx/io.py,sha256=HFQa7B51nvtQ13ajL5MneH3w7G_BeKgUxV4jYmq9jdU,451
|
|
8
|
+
fimx/parameters.py,sha256=hSK5OEqHE01zXisBWkwL_IoEbdmG_6hIuGzxN8C3K7c,6860
|
|
9
|
+
fimx/plotting/__init__.py,sha256=qvb06tjlnwHCeFaRmn0pRhvgNOrJeaZqR1zzgRvo36M,2948
|
|
10
|
+
fimx/plotting/base.py,sha256=FsxKHihDh8qYp9PhbISgGiLf3sgAcioAAjv_68YKd9I,3443
|
|
11
|
+
fimx/plotting/getdist.py,sha256=SOIPWVpYw4yYcBak-3HVYOhs_As1AnSGnvsD_TaV_MU,1979
|
|
12
|
+
fimx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
fimx-0.1.0.dist-info/licenses/LICENSE,sha256=wn8ZVTnTDUzM-LNP9CaVo-XYYopba86GYhu3wsSvLy0,1080
|
|
14
|
+
fimx-0.1.0.dist-info/WHEEL,sha256=rp-k_VZaX8oABFEtLwG_Df95p8LSKt9-uPyQ4jKH0P4,81
|
|
15
|
+
fimx-0.1.0.dist-info/entry_points.txt,sha256=vFYzACDdtb0n25NcPbzCxSuKN8hob9zREVyXRf2qEHg,45
|
|
16
|
+
fimx-0.1.0.dist-info/METADATA,sha256=-wwmugUGyFfM3uYZtH6lGQrfg0ciIdn4s3SNtUipBnM,7983
|
|
17
|
+
fimx-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bernardo Porto Veronese
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|