distparams 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.
- distparams/__init__.py +72 -0
- distparams/_converter.py +557 -0
- distparams/_decorator.py +385 -0
- distparams/_distributions/__init__.py +159 -0
- distparams/_distributions/asymmetric_laplace.py +42 -0
- distparams/_distributions/bernoulli.py +39 -0
- distparams/_distributions/beta.py +125 -0
- distparams/_distributions/beta_binomial.py +21 -0
- distparams/_distributions/beta_negative_binomial.py +27 -0
- distparams/_distributions/binomial.py +24 -0
- distparams/_distributions/burr12.py +30 -0
- distparams/_distributions/categorical.py +21 -0
- distparams/_distributions/cauchy.py +24 -0
- distparams/_distributions/chi.py +21 -0
- distparams/_distributions/chi_squared.py +17 -0
- distparams/_distributions/dirichlet.py +17 -0
- distparams/_distributions/dirichlet_multinomial.py +19 -0
- distparams/_distributions/discrete_laplace.py +30 -0
- distparams/_distributions/discrete_uniform.py +22 -0
- distparams/_distributions/discrete_weibull.py +24 -0
- distparams/_distributions/exgaussian.py +50 -0
- distparams/_distributions/exponential.py +22 -0
- distparams/_distributions/f.py +19 -0
- distparams/_distributions/gamma.py +74 -0
- distparams/_distributions/generalized_extreme.py +35 -0
- distparams/_distributions/generalized_normal.py +32 -0
- distparams/_distributions/generalized_pareto.py +29 -0
- distparams/_distributions/generalized_poisson.py +25 -0
- distparams/_distributions/geometric.py +21 -0
- distparams/_distributions/gompertz.py +32 -0
- distparams/_distributions/gumbel.py +23 -0
- distparams/_distributions/half_cauchy.py +21 -0
- distparams/_distributions/half_generalized_normal.py +29 -0
- distparams/_distributions/half_normal.py +23 -0
- distparams/_distributions/half_student_t.py +28 -0
- distparams/_distributions/hypergeometric.py +21 -0
- distparams/_distributions/inverse_gamma.py +25 -0
- distparams/_distributions/johnson_su.py +35 -0
- distparams/_distributions/kumaraswamy.py +19 -0
- distparams/_distributions/laplace.py +23 -0
- distparams/_distributions/levy.py +23 -0
- distparams/_distributions/lkj.py +17 -0
- distparams/_distributions/log_logistic.py +30 -0
- distparams/_distributions/log_normal.py +26 -0
- distparams/_distributions/log_uniform.py +30 -0
- distparams/_distributions/logistic.py +24 -0
- distparams/_distributions/logit_normal.py +26 -0
- distparams/_distributions/lomax.py +29 -0
- distparams/_distributions/matrix_normal.py +21 -0
- distparams/_distributions/maxwell.py +25 -0
- distparams/_distributions/moyal.py +19 -0
- distparams/_distributions/multinomial.py +24 -0
- distparams/_distributions/multivariate_normal.py +19 -0
- distparams/_distributions/negative_binomial.py +44 -0
- distparams/_distributions/normal.py +25 -0
- distparams/_distributions/pareto.py +30 -0
- distparams/_distributions/poisson.py +23 -0
- distparams/_distributions/rayleigh.py +21 -0
- distparams/_distributions/rice.py +28 -0
- distparams/_distributions/skellam.py +17 -0
- distparams/_distributions/skew_normal.py +33 -0
- distparams/_distributions/skew_student_t.py +33 -0
- distparams/_distributions/student_t.py +24 -0
- distparams/_distributions/trapezoid.py +55 -0
- distparams/_distributions/triangular.py +24 -0
- distparams/_distributions/truncated_normal.py +72 -0
- distparams/_distributions/uniform.py +44 -0
- distparams/_distributions/von_mises.py +30 -0
- distparams/_distributions/wald.py +19 -0
- distparams/_distributions/weibull.py +27 -0
- distparams/_distributions/wishart.py +19 -0
- distparams/_distributions/zipf.py +25 -0
- distparams/_ops.py +82 -0
- distparams/_plugins.py +156 -0
- distparams/_registry.py +46 -0
- distparams/_types.py +387 -0
- distparams/_vocabularies.py +193 -0
- distparams/integrations/__init__.py +12 -0
- distparams/integrations/scipy.py +158 -0
- distparams/plugins.py +36 -0
- distparams/py.typed +0 -0
- distparams/random.py +212 -0
- distparams/stats.py +93 -0
- distparams-0.1.0.dist-info/METADATA +496 -0
- distparams-0.1.0.dist-info/RECORD +86 -0
- distparams-0.1.0.dist-info/WHEEL +4 -0
distparams/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""distparams — single source of truth for distribution parameterizations."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
from distparams._types import (
|
|
6
|
+
Bijection,
|
|
7
|
+
CanonicalParametrization,
|
|
8
|
+
Distribution,
|
|
9
|
+
MatrixSupport,
|
|
10
|
+
NamingConvention,
|
|
11
|
+
Parameter,
|
|
12
|
+
ParameterSet,
|
|
13
|
+
Support,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Bijection",
|
|
18
|
+
"CanonicalParametrization",
|
|
19
|
+
"Distribution",
|
|
20
|
+
"MatrixSupport",
|
|
21
|
+
"NamingConvention",
|
|
22
|
+
"Parameter",
|
|
23
|
+
"ParameterSet",
|
|
24
|
+
"Support",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
from distparams import (
|
|
28
|
+
_distributions, # noqa: F401 (registers built-ins)
|
|
29
|
+
random,
|
|
30
|
+
stats,
|
|
31
|
+
)
|
|
32
|
+
from distparams._converter import (
|
|
33
|
+
UnknownParameterError,
|
|
34
|
+
convert,
|
|
35
|
+
supported_ecosystems,
|
|
36
|
+
)
|
|
37
|
+
from distparams._decorator import distribution, resolve_parameters
|
|
38
|
+
from distparams._plugins import (
|
|
39
|
+
register_distribution_ops,
|
|
40
|
+
register_ecosystem_mapping,
|
|
41
|
+
register_ecosystem_vocabulary,
|
|
42
|
+
register_parameter_alias,
|
|
43
|
+
register_parameter_set,
|
|
44
|
+
)
|
|
45
|
+
from distparams._registry import get_distribution, list_distributions, register_distribution
|
|
46
|
+
from distparams.random import Random
|
|
47
|
+
|
|
48
|
+
__all__ += [
|
|
49
|
+
"Random",
|
|
50
|
+
"UnknownParameterError",
|
|
51
|
+
"convert",
|
|
52
|
+
"distribution",
|
|
53
|
+
"get_distribution",
|
|
54
|
+
"list_distributions",
|
|
55
|
+
"random",
|
|
56
|
+
"register_distribution",
|
|
57
|
+
"register_distribution_ops",
|
|
58
|
+
"register_ecosystem_mapping",
|
|
59
|
+
"register_ecosystem_vocabulary",
|
|
60
|
+
"register_parameter_alias",
|
|
61
|
+
"register_parameter_set",
|
|
62
|
+
"resolve_parameters",
|
|
63
|
+
"stats",
|
|
64
|
+
"supported_ecosystems",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
from importlib.metadata import version
|
|
69
|
+
|
|
70
|
+
__version__ = version("distparams")
|
|
71
|
+
except PackageNotFoundError: # pragma: no cover — running from source without install
|
|
72
|
+
__version__ = "0.0.0"
|
distparams/_converter.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Cross-ecosystem parameter conversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from distparams._registry import get_distribution
|
|
9
|
+
from distparams._vocabularies import rows_for_parametrization, vocabulary_for
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnknownParameterError(ValueError):
|
|
13
|
+
"""Raised when a parameter name is not known to the distribution."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParameterConflictError(ValueError):
|
|
17
|
+
"""Raised when two parameter names map to the same canonical parameter."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert(
|
|
21
|
+
distribution_name: str,
|
|
22
|
+
source_ecosystem: str,
|
|
23
|
+
target_ecosystem: str,
|
|
24
|
+
*,
|
|
25
|
+
partial: bool = False,
|
|
26
|
+
**params: Any,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Convert parameters from one ecosystem's naming to another.
|
|
29
|
+
|
|
30
|
+
Conversions flow *source params → canonical parameters → target params*.
|
|
31
|
+
Source param names map to canonical parameters via the source ecosystem's
|
|
32
|
+
naming convention; values are normalized to the canonical value (through
|
|
33
|
+
parameter bijections or set-level transforms) and then emitted under
|
|
34
|
+
the target ecosystem's names.
|
|
35
|
+
|
|
36
|
+
With ``partial=True``, source params resolve independently — no complete
|
|
37
|
+
parameterization is required, and only supplied parameters are emitted. Used
|
|
38
|
+
by the decorator so callables can fill the rest from their own defaults.
|
|
39
|
+
Derived parameterization targets (``"group:..."``) are not supported in
|
|
40
|
+
partial mode, since their values depend on multiple canonical parameters.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
distribution_name: name of the distribution (e.g. "gamma")
|
|
44
|
+
source_ecosystem: source ecosystem (e.g. "scipy")
|
|
45
|
+
target_ecosystem: target ecosystem (e.g. "pymc")
|
|
46
|
+
partial: resolve supplied params only (defaults: False)
|
|
47
|
+
**params: parameters in source ecosystem's naming
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Parameters in target ecosystem's naming.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> convert("gamma", "scipy", "pymc", a=2, scale=3)
|
|
54
|
+
{"alpha": 2, "beta": 0.333}
|
|
55
|
+
"""
|
|
56
|
+
if partial and target_ecosystem.startswith("group:"):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Partial conversion does not support derived parameterization targets "
|
|
59
|
+
f"('{target_ecosystem}'): their values depend on multiple canonical "
|
|
60
|
+
"parameters."
|
|
61
|
+
)
|
|
62
|
+
dist = get_distribution(distribution_name)
|
|
63
|
+
|
|
64
|
+
# Source: param names → canonical parameters via the source ecosystem.
|
|
65
|
+
source_map = _find_source_convention(dist, source_ecosystem, set(params.keys()))
|
|
66
|
+
source_transforms: dict[str, str] = {}
|
|
67
|
+
|
|
68
|
+
if source_map is not None:
|
|
69
|
+
source_map, source_transforms = _resolve_convention(dist, source_map)
|
|
70
|
+
|
|
71
|
+
if source_map is None:
|
|
72
|
+
# No naming convention — try to find a matching parameter set with transforms.
|
|
73
|
+
source_set = _find_set_with_transforms(dist, set(params.keys()))
|
|
74
|
+
if source_set is not None:
|
|
75
|
+
source_map = {p: p for p in params}
|
|
76
|
+
else:
|
|
77
|
+
# Try alias fallback (registered parameter aliases).
|
|
78
|
+
source_map = _alias_fallback(dist, set(params.keys()))
|
|
79
|
+
if source_map is None and source_ecosystem == "any":
|
|
80
|
+
# "any" source: also try matching against all conventions.
|
|
81
|
+
source_map = _try_all_conventions(dist, set(params.keys()))
|
|
82
|
+
if source_map is not None:
|
|
83
|
+
source_map, source_transforms = _resolve_convention(dist, source_map)
|
|
84
|
+
if source_map is None and partial:
|
|
85
|
+
# Partial mode: resolve each name independently — no
|
|
86
|
+
# completeness requirement (defaults fill the rest).
|
|
87
|
+
source_map = _per_name_resolution(dist, set(params.keys()))
|
|
88
|
+
if source_map is None:
|
|
89
|
+
incomplete = _incomplete_set_message(dist, set(params.keys()))
|
|
90
|
+
if incomplete is not None:
|
|
91
|
+
raise ValueError(incomplete)
|
|
92
|
+
if source_map is None:
|
|
93
|
+
raise _source_error(dist, source_ecosystem, set(params.keys()))
|
|
94
|
+
|
|
95
|
+
# Check for conflicts (multiple params mapping to the same canonical parameter).
|
|
96
|
+
seen: dict[str, str] = {}
|
|
97
|
+
for param_name, canonical_name in source_map.items():
|
|
98
|
+
if canonical_name in seen:
|
|
99
|
+
raise ParameterConflictError(
|
|
100
|
+
f"Conflict: '{param_name}' and '{seen[canonical_name]}' "
|
|
101
|
+
f"both map to parameter '{canonical_name}'"
|
|
102
|
+
)
|
|
103
|
+
seen[canonical_name] = param_name
|
|
104
|
+
|
|
105
|
+
# Check for set-level transforms on the source side. A convention may
|
|
106
|
+
# target a derived parameterization (e.g. R's size/mu for negative
|
|
107
|
+
# binomial): its names map *into* the set's transform keys, so the set
|
|
108
|
+
# can also be found through the mapped canonical names.
|
|
109
|
+
source_set = _find_set_with_transforms(dist, set(source_map.keys()))
|
|
110
|
+
if source_set is None:
|
|
111
|
+
source_set = _find_set_with_transforms(dist, set(source_map.values()))
|
|
112
|
+
|
|
113
|
+
if source_set is not None and source_set.param_transforms is not None:
|
|
114
|
+
# Use set-level transforms (cross-parameter derived parameterization).
|
|
115
|
+
# Values are keyed by transform key, whether the raw names *are* the
|
|
116
|
+
# keys (pymc mu/alpha) or map onto them (R size/mu).
|
|
117
|
+
source_values = {canonical: params[raw] for raw, canonical in source_map.items()}
|
|
118
|
+
canonical: dict[str, Any] = {}
|
|
119
|
+
for name, (to_fn, _from_fn, target_param) in source_set.param_transforms.items():
|
|
120
|
+
if name in source_values:
|
|
121
|
+
canonical[target_param] = to_fn(source_values)
|
|
122
|
+
# Add parameters without set transforms.
|
|
123
|
+
for name in source_set.parameters:
|
|
124
|
+
if name not in canonical:
|
|
125
|
+
param_name = _source_name_for_param(name, source_map)
|
|
126
|
+
if param_name is not None:
|
|
127
|
+
canonical[name] = _apply_to_canonical(dist, name, param_name, params[param_name])
|
|
128
|
+
else:
|
|
129
|
+
# Standard path: parameter-level bijections.
|
|
130
|
+
canonical = {}
|
|
131
|
+
for param_name, value in params.items():
|
|
132
|
+
canonical_name = source_map[param_name]
|
|
133
|
+
transform = source_transforms.get(param_name, param_name)
|
|
134
|
+
canonical[canonical_name] = _apply_to_canonical(dist, canonical_name, transform, value)
|
|
135
|
+
|
|
136
|
+
# Target: canonical parameters → param names via the target ecosystem.
|
|
137
|
+
# Special targets: "canonical" emits canonical names directly;
|
|
138
|
+
# "canonical:<names>" emits the listed canonical names;
|
|
139
|
+
# "group:<names>" uses set-level transforms to emit the set's param names;
|
|
140
|
+
# "params:<names>" emits under known parameter names via their bijections.
|
|
141
|
+
target_map: dict[str, tuple[str, str]] | None = None
|
|
142
|
+
target_set = None
|
|
143
|
+
|
|
144
|
+
if target_ecosystem == "canonical":
|
|
145
|
+
# Emit canonical parameter names directly.
|
|
146
|
+
target_map = {r: (r, r) for r in canonical}
|
|
147
|
+
elif target_ecosystem.startswith("canonical:"):
|
|
148
|
+
# "canonical:mu,sigma" — emit the listed canonical names in order.
|
|
149
|
+
requested = target_ecosystem.split(":", 1)[1].split(",")
|
|
150
|
+
target_map = {r: (r, r) for r in requested if r in canonical}
|
|
151
|
+
elif target_ecosystem.startswith("params:"):
|
|
152
|
+
# "params:loc,scale" — emit under these known parameter names,
|
|
153
|
+
# applying each name's own bijection (e.g. tau = sigma**-2).
|
|
154
|
+
requested = target_ecosystem.split(":", 1)[1].split(",")
|
|
155
|
+
full = _full_lookup(dist)
|
|
156
|
+
target_map = {}
|
|
157
|
+
for name in requested:
|
|
158
|
+
canonical_param = full.get(name)
|
|
159
|
+
if canonical_param is not None:
|
|
160
|
+
target_map[canonical_param] = (name, name)
|
|
161
|
+
elif target_ecosystem.startswith("group:"):
|
|
162
|
+
# "group:mu,sigma" — use set-level transforms.
|
|
163
|
+
requested = target_ecosystem.split(":", 1)[1].split(",")
|
|
164
|
+
target_set = _find_set_with_transforms(dist, set(requested))
|
|
165
|
+
if target_set is not None:
|
|
166
|
+
target_map = {}
|
|
167
|
+
for name, (_to_fn, _from_fn, target_param) in target_set.param_transforms.items():
|
|
168
|
+
if name in requested:
|
|
169
|
+
target_map[target_param] = (name, name)
|
|
170
|
+
else:
|
|
171
|
+
target_convention = _find_target_convention(dist, target_ecosystem, set(canonical.keys()))
|
|
172
|
+
if target_convention is not None:
|
|
173
|
+
roles, transforms = _resolve_convention(dist, target_convention)
|
|
174
|
+
target_map = {role: (p, transforms[p]) for p, role in roles.items()}
|
|
175
|
+
else:
|
|
176
|
+
# No naming convention — try a matching parameter set with transforms.
|
|
177
|
+
target_set = _find_set_with_transforms_for_params(dist, set(canonical.keys()))
|
|
178
|
+
if target_set is not None:
|
|
179
|
+
target_map = {}
|
|
180
|
+
for pn, (_to_fn, _from_fn, target_param) in target_set.param_transforms.items():
|
|
181
|
+
target_map[target_param] = (pn, pn)
|
|
182
|
+
|
|
183
|
+
if target_map is None:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"No naming convention found for ecosystem '{target_ecosystem}' with "
|
|
186
|
+
f"params {sorted(canonical.keys())}. "
|
|
187
|
+
f"{_available_conventions(dist, target_ecosystem)}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
result: dict[str, Any] = {}
|
|
191
|
+
skipped: set[str] = set()
|
|
192
|
+
if target_set is not None and target_set.param_transforms is not None:
|
|
193
|
+
# Use set-level transforms for target.
|
|
194
|
+
for canonical_name, value in canonical.items():
|
|
195
|
+
emitted = target_map.get(canonical_name)
|
|
196
|
+
if emitted is None:
|
|
197
|
+
skipped.add(canonical_name)
|
|
198
|
+
continue
|
|
199
|
+
param_name, transform = emitted
|
|
200
|
+
if param_name in target_set.param_transforms:
|
|
201
|
+
_to_fn, from_fn, _target_param = target_set.param_transforms[param_name]
|
|
202
|
+
result[param_name] = from_fn(canonical)
|
|
203
|
+
else:
|
|
204
|
+
result[param_name] = _apply_from_canonical(dist, canonical_name, transform, value)
|
|
205
|
+
else:
|
|
206
|
+
# Standard path: parameter-level bijections.
|
|
207
|
+
for canonical_name, value in canonical.items():
|
|
208
|
+
emitted = target_map.get(canonical_name)
|
|
209
|
+
if emitted is None:
|
|
210
|
+
skipped.add(canonical_name)
|
|
211
|
+
continue
|
|
212
|
+
param_name, transform = emitted
|
|
213
|
+
result[param_name] = _apply_from_canonical(dist, canonical_name, transform, value)
|
|
214
|
+
|
|
215
|
+
if partial and skipped:
|
|
216
|
+
# Partial mode must never silently drop a supplied parameter.
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Resolved parameter(s) {sorted(skipped)} cannot be passed to target '{target_ecosystem}'"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def supported_ecosystems(dist) -> set[str]:
|
|
225
|
+
"""Ecosystems with naming data for this distribution.
|
|
226
|
+
|
|
227
|
+
Union of the per-distribution convention ecosystems and every vocabulary
|
|
228
|
+
row registered for the distribution's canonical parametrization.
|
|
229
|
+
"""
|
|
230
|
+
eco = {nc.ecosystem for nc in dist.naming_conventions}
|
|
231
|
+
eco.update(ecosystem for ecosystem, _row in rows_for_parametrization(dist.canonical.name))
|
|
232
|
+
return eco
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _ecosystem_mappings(dist, ecosystem: str) -> list[tuple[str, dict[str, str]]]:
|
|
236
|
+
"""All ``(parametrization, mapping)`` pairs for an ecosystem.
|
|
237
|
+
|
|
238
|
+
Per-distribution :class:`NamingConvention` overrides come first and take
|
|
239
|
+
precedence; the ecosystem's vocabulary row for the distribution's
|
|
240
|
+
canonical parametrization (if any) follows.
|
|
241
|
+
"""
|
|
242
|
+
mappings = [
|
|
243
|
+
(
|
|
244
|
+
nc.parametrization or dist.canonical.name,
|
|
245
|
+
{p: r for p, r in nc.mapping.items() if r is not None},
|
|
246
|
+
)
|
|
247
|
+
for nc in dist.naming_conventions
|
|
248
|
+
if nc.ecosystem == ecosystem
|
|
249
|
+
]
|
|
250
|
+
row = vocabulary_for(ecosystem, dist.canonical.name)
|
|
251
|
+
if row is not None:
|
|
252
|
+
mappings.append((dist.canonical.name, {p: r for p, r in row.items() if r is not None}))
|
|
253
|
+
return mappings
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _find_source_convention(dist, ecosystem: str, param_names: set[str]):
|
|
257
|
+
"""Find an ecosystem mapping whose param names match exactly."""
|
|
258
|
+
for _pz, mapped in _ecosystem_mappings(dist, ecosystem):
|
|
259
|
+
if set(mapped) == param_names:
|
|
260
|
+
return mapped
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _find_target_convention(dist, ecosystem: str, canonical_names: set[str]):
|
|
265
|
+
"""Find an ecosystem mapping that covers every canonical parameter to emit."""
|
|
266
|
+
for _pz, mapped in _ecosystem_mappings(dist, ecosystem):
|
|
267
|
+
roles, _transforms = _resolve_convention(dist, mapped)
|
|
268
|
+
if canonical_names <= set(roles.values()):
|
|
269
|
+
return mapped
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _find_set_with_transforms(dist, param_names: set[str]):
|
|
274
|
+
"""Find a parameter set whose param names match and which has transforms."""
|
|
275
|
+
for ps in dist.canonical.parameter_sets:
|
|
276
|
+
if ps.param_transforms is None:
|
|
277
|
+
continue
|
|
278
|
+
if param_names == set(ps.param_transforms.keys()):
|
|
279
|
+
return ps
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _find_set_with_transforms_for_params(dist, canonical_names: set[str]):
|
|
284
|
+
"""Find a parameter set with transforms whose target parameters match."""
|
|
285
|
+
for ps in dist.canonical.parameter_sets:
|
|
286
|
+
if ps.param_transforms is None:
|
|
287
|
+
continue
|
|
288
|
+
target_params = {r for _, _, r in ps.param_transforms.values()}
|
|
289
|
+
if canonical_names <= target_params:
|
|
290
|
+
return ps
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _known_param_names(dist) -> set[str]:
|
|
295
|
+
"""All parameter names the registry knows for this distribution.
|
|
296
|
+
|
|
297
|
+
Includes canonical parameters, their bijections, naming-convention parameter
|
|
298
|
+
names, vocabulary-row parameter names, and set-transform keys (e.g.
|
|
299
|
+
"mean" or "mu" for beta). Convention entries mapped to None are
|
|
300
|
+
deliberately not distribution parameters and are excluded, so they fall
|
|
301
|
+
through to context-parameter handling.
|
|
302
|
+
"""
|
|
303
|
+
names: set[str] = set()
|
|
304
|
+
for name, parameter in dist.canonical.parameters.items():
|
|
305
|
+
names.add(name)
|
|
306
|
+
names.update(parameter.bijections)
|
|
307
|
+
for nc in dist.naming_conventions:
|
|
308
|
+
names.update(p for p, r in nc.mapping.items() if r is not None)
|
|
309
|
+
for _eco, row in rows_for_parametrization(dist.canonical.name):
|
|
310
|
+
names.update(p for p, r in row.items() if r is not None)
|
|
311
|
+
for ps in dist.canonical.parameter_sets:
|
|
312
|
+
if ps.param_transforms is not None:
|
|
313
|
+
names.update(ps.param_transforms)
|
|
314
|
+
return names
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _known_params_grouped(dist) -> list[list[str]]:
|
|
318
|
+
"""Group every known parameter name by the canonical parameter it resolves to.
|
|
319
|
+
|
|
320
|
+
Each group reads as ``(canonical or alias or convention-name ...)``: the
|
|
321
|
+
canonical name first, then its bijections, then convention/vocabulary names
|
|
322
|
+
mapping to it. Set-transform keys (derived parameterizations such as
|
|
323
|
+
gamma's ``mu``/``sigma``) resolve as a set rather than per parameter, so
|
|
324
|
+
they form a trailing group.
|
|
325
|
+
"""
|
|
326
|
+
groups: list[list[str]] = []
|
|
327
|
+
shown: set[str] = set()
|
|
328
|
+
for name, parameter in dist.canonical.parameters.items():
|
|
329
|
+
group = [name]
|
|
330
|
+
shown.add(name)
|
|
331
|
+
for alias in parameter.bijections:
|
|
332
|
+
if alias not in shown:
|
|
333
|
+
group.append(alias)
|
|
334
|
+
shown.add(alias)
|
|
335
|
+
for nc in dist.naming_conventions:
|
|
336
|
+
for param, r in nc.mapping.items():
|
|
337
|
+
if r == name and param not in shown:
|
|
338
|
+
group.append(param)
|
|
339
|
+
shown.add(param)
|
|
340
|
+
for _eco, row in rows_for_parametrization(dist.canonical.name):
|
|
341
|
+
for param, r in row.items():
|
|
342
|
+
if r == name and param not in shown:
|
|
343
|
+
group.append(param)
|
|
344
|
+
shown.add(param)
|
|
345
|
+
groups.append(group)
|
|
346
|
+
extras: list[str] = []
|
|
347
|
+
for ps in dist.canonical.parameter_sets:
|
|
348
|
+
if ps.param_transforms is not None:
|
|
349
|
+
for key in ps.param_transforms:
|
|
350
|
+
if key not in shown:
|
|
351
|
+
extras.append(key)
|
|
352
|
+
shown.add(key)
|
|
353
|
+
if extras:
|
|
354
|
+
groups.append(extras)
|
|
355
|
+
return groups
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _unknown_parameter_error(dist, param_names: set[str]) -> UnknownParameterError:
|
|
359
|
+
"""Build a helpful unknown-parameter error.
|
|
360
|
+
|
|
361
|
+
Names the distribution, shows every known parameter grouped by canonical
|
|
362
|
+
parameter as ``(canonical or alias ...)`` clauses, and suggests the
|
|
363
|
+
closest known name when one is sufficiently similar.
|
|
364
|
+
"""
|
|
365
|
+
unknown = sorted(param_names)
|
|
366
|
+
groups = _known_params_grouped(dist)
|
|
367
|
+
msg = f"Unknown parameter '{unknown[0]}' for distribution '{dist.name}'."
|
|
368
|
+
if len(unknown) > 1:
|
|
369
|
+
msg += f" Also unknown: {', '.join(repr(u) for u in unknown[1:])}."
|
|
370
|
+
if groups:
|
|
371
|
+
known = " and ".join("(" + " or ".join(group) + ")" for group in groups)
|
|
372
|
+
msg += f" Known parameters: {known}."
|
|
373
|
+
matches = difflib.get_close_matches(unknown[0], [name for group in groups for name in group])
|
|
374
|
+
if matches:
|
|
375
|
+
msg += f" Did you mean '{matches[0]}'?"
|
|
376
|
+
return UnknownParameterError(msg)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _alias_fallback(dist, param_names: set[str]) -> dict[str, str] | None:
|
|
380
|
+
"""Resolve param names through parameter bijections when no convention matches.
|
|
381
|
+
|
|
382
|
+
Only accepts parameter sets whose resolved names form a complete valid
|
|
383
|
+
parameter set, so partial parameterizations are rejected rather than
|
|
384
|
+
silently converted.
|
|
385
|
+
"""
|
|
386
|
+
lookup = _alias_lookup(dist)
|
|
387
|
+
if not param_names <= set(lookup):
|
|
388
|
+
return None
|
|
389
|
+
resolved = {lookup[p] for p in param_names}
|
|
390
|
+
valid_sets = {ps.parameters for ps in dist.canonical.parameter_sets}
|
|
391
|
+
if resolved not in valid_sets:
|
|
392
|
+
return None
|
|
393
|
+
return {p: lookup[p] for p in param_names}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _source_name_for_param(canonical_name: str, mapping: dict[str, str]) -> str | None:
|
|
397
|
+
"""Find the source param name that maps to a given canonical parameter."""
|
|
398
|
+
for param, name in mapping.items():
|
|
399
|
+
if name == canonical_name:
|
|
400
|
+
return param
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _resolve_convention(dist, mapped: dict[str, str]) -> tuple[dict[str, str], dict[str, str]]:
|
|
405
|
+
"""Resolve a convention mapping to canonical roles and transform names.
|
|
406
|
+
|
|
407
|
+
A mapping value may name a *bijection* rather than the canonical
|
|
408
|
+
parameter (e.g. the stdlib random module's gamma ``beta`` is a scale):
|
|
409
|
+
the role then resolves through that bijection and the value transform
|
|
410
|
+
uses it. Otherwise the role is the value itself and the transform comes
|
|
411
|
+
from the ecosystem name's own bijection (identity when none registered),
|
|
412
|
+
which is how every pre-existing mapping behaves.
|
|
413
|
+
|
|
414
|
+
Returns ``(roles, transforms)`` keyed by the ecosystem's parameter name.
|
|
415
|
+
"""
|
|
416
|
+
full = _full_lookup(dist)
|
|
417
|
+
roles: dict[str, str] = {}
|
|
418
|
+
transforms: dict[str, str] = {}
|
|
419
|
+
for eco_name, value in mapped.items():
|
|
420
|
+
param = value if value in dist.canonical.parameters else full.get(value)
|
|
421
|
+
if (
|
|
422
|
+
param is not None
|
|
423
|
+
and param != value
|
|
424
|
+
and param in dist.canonical.parameters
|
|
425
|
+
and dist.canonical.parameters[param].bijections.get(value) is not None
|
|
426
|
+
):
|
|
427
|
+
roles[eco_name] = param
|
|
428
|
+
transforms[eco_name] = value
|
|
429
|
+
else:
|
|
430
|
+
roles[eco_name] = value
|
|
431
|
+
transforms[eco_name] = eco_name
|
|
432
|
+
return roles, transforms
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _apply_to_canonical(dist, canonical_name: str, param_name: str, value: Any) -> Any:
|
|
436
|
+
"""Convert a source ecosystem's value to the canonical parameter value."""
|
|
437
|
+
parameter = dist.canonical.parameters[canonical_name]
|
|
438
|
+
bijection = parameter.bijections.get(param_name)
|
|
439
|
+
if bijection is not None and bijection.to_canonical is not None:
|
|
440
|
+
return bijection.to_canonical(value)
|
|
441
|
+
return value
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _apply_from_canonical(dist, canonical_name: str, param_name: str, value: Any) -> Any:
|
|
445
|
+
"""Convert a canonical parameter value to a target ecosystem's value."""
|
|
446
|
+
parameter = dist.canonical.parameters[canonical_name]
|
|
447
|
+
bijection = parameter.bijections.get(param_name)
|
|
448
|
+
if bijection is not None and bijection.from_canonical is not None:
|
|
449
|
+
return bijection.from_canonical(value)
|
|
450
|
+
return value
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _available_conventions(dist, ecosystem: str) -> str:
|
|
454
|
+
mappings = _ecosystem_mappings(dist, ecosystem)
|
|
455
|
+
if not mappings:
|
|
456
|
+
return f"No conventions for ecosystem '{ecosystem}'."
|
|
457
|
+
details = []
|
|
458
|
+
for pz, mapped in mappings:
|
|
459
|
+
details.append(f" {pz}: {sorted(mapped)}")
|
|
460
|
+
return f"Available for '{ecosystem}':\n" + "\n".join(details)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _try_all_conventions(dist, param_names: set[str]) -> dict[str, str] | None:
|
|
464
|
+
"""For 'any' source: try matching param names against all ecosystems.
|
|
465
|
+
|
|
466
|
+
Considers per-distribution overrides first, then vocabulary rows for the
|
|
467
|
+
distribution's parametrization across all ecosystems.
|
|
468
|
+
"""
|
|
469
|
+
for ecosystem in dict.fromkeys(nc.ecosystem for nc in dist.naming_conventions):
|
|
470
|
+
for _pz, mapped in _ecosystem_mappings(dist, ecosystem):
|
|
471
|
+
if set(mapped) == param_names:
|
|
472
|
+
return mapped
|
|
473
|
+
for _eco, row in rows_for_parametrization(dist.canonical.name):
|
|
474
|
+
mapped = {p: r for p, r in row.items() if r is not None}
|
|
475
|
+
if set(mapped) == param_names:
|
|
476
|
+
return mapped
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _alias_lookup(dist) -> dict[str, str]:
|
|
481
|
+
"""Param name → canonical parameter via canonical names and bijections.
|
|
482
|
+
|
|
483
|
+
Canonical names are registered first so a parameter always resolves to
|
|
484
|
+
itself, even when another parameter lists that name as a bijection (e.g.
|
|
485
|
+
hypergeometric's ``K`` alias ``n`` vs the ``n`` parameter).
|
|
486
|
+
"""
|
|
487
|
+
lookup: dict[str, str] = {}
|
|
488
|
+
for name in dist.canonical.parameters:
|
|
489
|
+
lookup.setdefault(name, name)
|
|
490
|
+
for name, parameter in dist.canonical.parameters.items():
|
|
491
|
+
for alias in parameter.bijections:
|
|
492
|
+
lookup.setdefault(alias, name)
|
|
493
|
+
return lookup
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _full_lookup(dist) -> dict[str, str]:
|
|
497
|
+
"""``_alias_lookup`` extended with convention and vocabulary names."""
|
|
498
|
+
lookup = _alias_lookup(dist)
|
|
499
|
+
for nc in dist.naming_conventions:
|
|
500
|
+
for p, r in nc.mapping.items():
|
|
501
|
+
if r is not None:
|
|
502
|
+
lookup.setdefault(p, r)
|
|
503
|
+
for _eco, row in rows_for_parametrization(dist.canonical.name):
|
|
504
|
+
for p, r in row.items():
|
|
505
|
+
if r is not None:
|
|
506
|
+
lookup.setdefault(p, r)
|
|
507
|
+
return lookup
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _per_name_resolution(dist, param_names: set[str]) -> dict[str, str] | None:
|
|
511
|
+
"""Resolve each param name independently to a canonical parameter (partial mode).
|
|
512
|
+
|
|
513
|
+
Looks across parameters, bijections, and all naming conventions. Set-transform
|
|
514
|
+
keys are deliberately excluded — their values depend on multiple canonical
|
|
515
|
+
parameters, so they only resolve through a complete set match.
|
|
516
|
+
"""
|
|
517
|
+
lookup = _full_lookup(dist)
|
|
518
|
+
if not param_names <= set(lookup):
|
|
519
|
+
return None
|
|
520
|
+
return {p: lookup[p] for p in param_names}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _incomplete_set_message(dist, param_names: set[str]) -> str | None:
|
|
524
|
+
"""Helpful error when params are a strict subset of a set's transform keys.
|
|
525
|
+
|
|
526
|
+
Surfaces the set's ``name``/``description`` when present, so the error
|
|
527
|
+
explains the parameterization instead of only listing its members.
|
|
528
|
+
"""
|
|
529
|
+
for ps in dist.canonical.parameter_sets:
|
|
530
|
+
if ps.param_transforms is None:
|
|
531
|
+
continue
|
|
532
|
+
keys = set(ps.param_transforms.keys())
|
|
533
|
+
if param_names and param_names < keys:
|
|
534
|
+
msg = (
|
|
535
|
+
f"Parameter(s) {sorted(param_names)} belong to the "
|
|
536
|
+
f"({', '.join(sorted(keys))}) parameterization — supply all of "
|
|
537
|
+
f"{sorted(keys)} for it to resolve."
|
|
538
|
+
)
|
|
539
|
+
if ps.description:
|
|
540
|
+
prefix = f"{ps.name}: " if ps.name else ""
|
|
541
|
+
msg += f" {prefix}{ps.description}"
|
|
542
|
+
return msg
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _source_error(dist, ecosystem: str, param_names: set[str]) -> Exception:
|
|
547
|
+
"""Build a helpful error for source resolution failure."""
|
|
548
|
+
# Check which params are unknown (not in any alias/convention/vocab).
|
|
549
|
+
lookup = _full_lookup(dist)
|
|
550
|
+
unknown = param_names - set(lookup)
|
|
551
|
+
if unknown:
|
|
552
|
+
return _unknown_parameter_error(dist, unknown)
|
|
553
|
+
return ValueError(
|
|
554
|
+
f"No naming convention found for ecosystem '{ecosystem}' with "
|
|
555
|
+
f"params {sorted(param_names)}. "
|
|
556
|
+
f"{_available_conventions(dist, ecosystem)}"
|
|
557
|
+
)
|