Hyperiso 1.0.3
Modular flavour-physics calculations, Wilson coefficients and statistical inference
Loading...
Searching...
No Matches
JointDistribution.py
Go to the documentation of this file.
1"""Joint probability distributions built from marginals and a copula.
2
3The C++ backend represents a joint law as a set of one-dimensional marginal
4distributions plus a copula. This Python module exposes wrappers and factory
5helpers to build such distributions from Python configuration objects.
6"""
7
8from __future__ import annotations
9
10from typing import List, Optional, Sequence, Union
11
12from pyhyperiso.phyperiso.pyhyperiso import statistic as st
13from pyhyperiso.core.Statistic.Copula import CopulaKind
14from pyhyperiso.core.Statistic.CopulaConfig import (
15 GaussianCopulaConfigPy as GaussianCopulaConfig,
16 StudentTCopulaConfigPy as StudentTCopulaConfig,
17)
18from pyhyperiso.core.Statistic.MarginalConfig import (
19 FlatMarginalConfig,
20 GaussianMarginalConfig,
21 LikelihoodMarginalConfig,
22 MarginalKind,
23 SplitGaussianMarginalConfig,
24)
25
26
27MarginalConfig = Union[
28 FlatMarginalConfig,
29 GaussianMarginalConfig,
30 SplitGaussianMarginalConfig,
31 LikelihoodMarginalConfig,
32]
33CopulaConfig = Union[GaussianCopulaConfig, StudentTCopulaConfig]
34
35
36def _require(value, typ, name: str):
37 """Validate the type of a user-facing argument.
38
39 Args:
40 value: Object to validate.
41 typ: Expected Python type.
42 name: Argument name used in the error message.
43
44 Returns:
45 The original value.
46
47 Raises:
48 TypeError: If ``value`` is not an instance of ``typ``.
49 """
50 if not isinstance(value, typ):
51 raise TypeError(f"{name} must be {typ.__name__}, received {type(value)!r}.")
52 return value
53
54
55def _cpp_marginal_kind(kind: MarginalKind):
56 """Convert a Python marginal kind to the C++ enum value."""
57 return _require(kind, MarginalKind, "MarginalKind").to_cpp()
58
59
60def _cpp_copula_kind(kind: CopulaKind):
61 """Convert a Python copula kind to the C++ enum value."""
62 return _require(kind, CopulaKind, "CopulaKind").to_cpp()
63
64
65def _cpp_marginal_config(cfg: MarginalConfig):
66 """Convert a Python marginal configuration to its C++ representation.
67
68 Raises:
69 TypeError: If the configuration family is not supported.
70 """
71 if not isinstance(
72 cfg,
73 (
74 FlatMarginalConfig,
75 GaussianMarginalConfig,
76 SplitGaussianMarginalConfig,
77 LikelihoodMarginalConfig,
78 ),
79 ):
80 raise TypeError(f"unsupported marginal configuration: {type(cfg)!r}.")
81 return cfg.to_cpp()
82
83
84def _cpp_copula_config(cfg: CopulaConfig):
85 """Convert a Python copula configuration to its C++ representation.
86
87 Raises:
88 TypeError: If the configuration family is not supported.
89 """
90 if not isinstance(cfg, (GaussianCopulaConfig, StudentTCopulaConfig)):
91 raise TypeError(f"unsupported copula configuration: {type(cfg)!r}.")
92 return cfg.to_cpp()
93
94
96 """Python wrapper around a C++ joint distribution.
97
98 A joint distribution combines marginal distributions with a dependence
99 structure encoded by a copula. It is used by the statistic layer for nuisance
100 parameters and experimental observables.
101
102 Args:
103 cpp_obj: Bound C++ ``JointDistribution`` instance.
104 """
105
106 __slots__ = ("_cpp_obj",)
107
108 def __init__(self, cpp_obj) -> None:
109 """Store the bound C++ joint distribution object."""
110 self._cpp_obj = cpp_obj
111
112 @classmethod
113 def from_cpp(cls, cpp_obj) -> "JointDistribution":
114 """Wrap a bound C++ joint distribution.
115
116 Args:
117 cpp_obj: Bound C++ ``JointDistribution`` instance.
118
119 Returns:
120 A Python wrapper retaining the C++ object.
121 """
122 return cls(cpp_obj)
123
124 def _to_cpp(self):
125 """Return the underlying C++ object for internal binding calls."""
126 return self._cpp_obj
127
128 def sample(self, n: Optional[int] = None) -> Union[List[float], List[List[float]]]:
129 """Draw one or more samples from the joint distribution.
130
131 Args:
132 n: Optional number of samples. When omitted, a single sample vector is
133 returned.
134
135 Returns:
136 A single sample ``list[float]`` when ``n`` is ``None``; otherwise a
137 list of sample vectors with shape ``n x dim``.
138
139 Raises:
140 ValueError: If ``n`` is negative.
141 """
142 if n is None:
143 return [float(v) for v in self._cpp_obj.sample()]
144 if int(n) < 0:
145 raise ValueError("n must be >= 0.")
146 return [[float(v) for v in row] for row in self._cpp_obj.sample(int(n))]
147
148 def logpdf(self, x: Sequence[float]) -> float:
149 """Evaluate the joint log-density at ``x``.
150
151 Args:
152 x: Point in physical variable space. Its length must match the joint
153 distribution dimension.
154
155 Returns:
156 The scalar log-density ``log f(x)``.
157 """
158 return float(self._cpp_obj.logpdf([float(v) for v in x]))
159
160 def dim(self) -> int:
161 """Return the distribution dimension."""
162 return int(self._cpp_obj.dim())
163
164 @property
165 def ndim(self) -> int:
166 """Alias for :meth:`dim`, following NumPy naming conventions."""
167 return self.dim()
168
169 def __repr__(self) -> str:
170 """Return a compact representation useful in notebooks and logs."""
171 return f"JointDistribution(dim={self.dim()})"
172
173
175 """Factory helpers for C++ joint distributions.
176
177 The factory receives Python marginal and copula configurations, converts
178 them to C++ objects and returns a :class:`JointDistribution` wrapper.
179 """
180
181 @staticmethod
183 marginal_types: Sequence[MarginalKind],
184 marginal_configs: Sequence[MarginalConfig],
185 copula_type: CopulaKind,
186 copula_config: CopulaConfig,
187 *,
188 seed: Optional[int] = None,
189 ) -> JointDistribution:
190 """Create a joint distribution with a shared optional seed.
191
192 Args:
193 marginal_types: Marginal distribution family for each dimension.
194 marginal_configs: Configuration object for each marginal.
195 copula_type: Copula family encoding dependence.
196 copula_config: Copula configuration, typically a correlation matrix.
197 seed: Optional seed passed to the C++ factory.
198
199 Returns:
200 A ``JointDistribution`` wrapper.
201
202 Raises:
203 ValueError: If marginal type/config lengths differ or no marginal is
204 provided.
205
206 Examples:
207 >>> jd = JointDistributionFactory.create(
208 ... [MarginalKind.GAUSSIAN, MarginalKind.FLAT],
209 ... [GaussianMarginalConfig(0.0, 1.0), FlatMarginalConfig(-1.0, 1.0)],
210 ... CopulaKind.GAUSSIAN,
211 ... GaussianCopulaConfig(R=[[1.0, 0.2], [0.2, 1.0]]),
212 ... seed=123,
213 ... )
214 >>> jd.ndim
215 2
216 """
217 if len(marginal_types) != len(marginal_configs):
218 raise ValueError("marginal_types et marginal_configs must have the same size.")
219 if not marginal_types:
220 raise ValueError("Au moins une marginale est requise.")
221
222 cpp = st.JointDistribution.create(
223 [_cpp_marginal_kind(t) for t in marginal_types],
224 [_cpp_marginal_config(c) for c in marginal_configs],
225 _cpp_copula_kind(copula_type),
226 _cpp_copula_config(copula_config),
227 seed if seed is not None else None,
228 )
229 return JointDistribution.from_cpp(cpp)
230
231 @staticmethod
233 marginal_types: Sequence[MarginalKind],
234 marginal_configs: Sequence[MarginalConfig],
235 marginal_seeds: Sequence[int],
236 copula_type: CopulaKind,
237 copula_config: CopulaConfig,
238 *,
239 copula_seed: int,
240 ) -> JointDistribution:
241 """Create a joint distribution with explicit marginal and copula seeds.
242
243 Args:
244 marginal_types: Marginal distribution family for each dimension.
245 marginal_configs: Configuration object for each marginal.
246 marginal_seeds: Seed used for each marginal RNG.
247 copula_type: Copula family encoding dependence.
248 copula_config: Copula configuration.
249 copula_seed: Seed used for the copula RNG.
250
251 Returns:
252 A ``JointDistribution`` wrapper.
253
254 Raises:
255 ValueError: If input sequence lengths differ or no marginal is
256 provided.
257 """
258 if len(marginal_types) != len(marginal_configs) or len(marginal_types) != len(
259 marginal_seeds
260 ):
261 raise ValueError(
262 "marginal_types, marginal_configs et marginal_seeds must have the same size."
263 )
264 if not marginal_types:
265 raise ValueError("Au moins une marginale est requise.")
266
267 cpp = st.JointDistribution.create_with_seeds(
268 [_cpp_marginal_kind(t) for t in marginal_types],
269 [_cpp_marginal_config(c) for c in marginal_configs],
270 [int(s) for s in marginal_seeds],
271 _cpp_copula_kind(copula_type),
272 _cpp_copula_config(copula_config),
273 int(copula_seed),
274 )
275 return JointDistribution.from_cpp(cpp)
276
277
278__all__ = ["JointDistribution", "JointDistributionFactory"]
JointDistribution create(Sequence[MarginalKind] marginal_types, Sequence[MarginalConfig] marginal_configs, CopulaKind copula_type, CopulaConfig copula_config, *Optional[int] seed=None)
JointDistribution create_with_seeds(Sequence[MarginalKind] marginal_types, Sequence[MarginalConfig] marginal_configs, Sequence[int] marginal_seeds, CopulaKind copula_type, CopulaConfig copula_config, *int copula_seed)
Union[List[float], List[List[float]]] sample(self, Optional[int] n=None)
"JointDistribution" from_cpp(cls, cpp_obj)
float logpdf(self, Sequence[float] x)
_require(value, typ, str name)
_cpp_copula_kind(CopulaKind kind)
_cpp_marginal_config(MarginalConfig cfg)
_cpp_copula_config(CopulaConfig cfg)
_cpp_marginal_kind(MarginalKind kind)