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 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
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.22
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ fimx-plot = fimx.cli:main
3
+
@@ -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.