Skip to content

Commit bee93fa

Browse files
committed
refactor(distribution): now there can be several analytical calculations for one characteristic
1 parent fe62ce9 commit bee93fa

10 files changed

Lines changed: 290 additions & 89 deletions

File tree

src/pysatl_core/distributions/distribution.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,28 @@
1212
__license__ = "SPDX-License-Identifier: MIT"
1313

1414
from abc import ABC, abstractmethod
15-
from collections.abc import Iterable, Mapping
15+
from collections.abc import Mapping
1616
from copy import deepcopy
1717
from typing import TYPE_CHECKING, Self, cast
1818

1919
from pysatl_core.distributions.strategies import (
2020
ComputationStrategy,
2121
SamplingStrategy,
2222
)
23-
from pysatl_core.types import NumericArray
23+
from pysatl_core.types import DEFAULT_ANALYTICAL_COMPUTATION_LABEL, NumericArray
2424

2525
_KEEP: object = object()
2626

2727

2828
if TYPE_CHECKING:
2929
from typing import Any
3030

31-
from pysatl_core.distributions.computation import AnalyticalComputation, Method
31+
from pysatl_core.distributions.computation import AnalyticalComputation
3232
from pysatl_core.distributions.support import Support
3333
from pysatl_core.types import (
3434
DistributionType,
3535
GenericCharacteristicName,
36+
LabelName,
3637
Method,
3738
)
3839

@@ -49,7 +50,13 @@ class Distribution(ABC):
4950
----------
5051
distribution_type : DistributionType
5152
Type information about the distribution (kind, dimension, etc.).
52-
analytical_computations : Mapping[str, AnalyticalComputation]
53+
analytical_computations : Mapping[
54+
GenericCharacteristicName,
55+
(
56+
AnalyticalComputation[Any, Any]
57+
| Mapping[LabelName, AnalyticalComputation[Any, Any]]
58+
),
59+
]
5360
Direct analytical computations provided by the distribution.
5461
sampling_strategy : SamplingStrategy
5562
Strategy for generating random samples.
@@ -62,10 +69,10 @@ class Distribution(ABC):
6269
def __init__(
6370
self,
6471
distribution_type: DistributionType,
65-
analytical_computations: (
66-
Iterable[AnalyticalComputation[Any, Any]]
67-
| Mapping[GenericCharacteristicName, AnalyticalComputation[Any, Any]]
68-
),
72+
analytical_computations: Mapping[
73+
GenericCharacteristicName,
74+
(AnalyticalComputation[Any, Any] | Mapping[LabelName, AnalyticalComputation[Any, Any]]),
75+
],
6976
support: Support | None = None,
7077
sampling_strategy: SamplingStrategy | None = None,
7178
computation_strategy: ComputationStrategy | None = None,
@@ -78,7 +85,13 @@ def __init__(
7885
distribution_type : DistributionType
7986
Type information about the distribution (kind, dimension, etc.).
8087
analytical_computations :
81-
Iterable[AnalyticalComputation] | Mapping[str, AnalyticalComputation]
88+
Mapping[
89+
GenericCharacteristicName,
90+
(
91+
AnalyticalComputation[Any, Any]
92+
| Mapping[LabelName, AnalyticalComputation[Any, Any]]
93+
),
94+
]
8295
Analytical computations provided by the distribution.
8396
support : Support or None, default=None
8497
Support of the distribution.
@@ -93,14 +106,27 @@ def __init__(
93106
)
94107

95108
self._distribution_type = distribution_type
96-
if isinstance(analytical_computations, Mapping):
97-
normalized_analytical = dict(analytical_computations)
98-
else:
99-
normalized_analytical = {ac.target: ac for ac in analytical_computations}
109+
normalized_analytical: dict[
110+
GenericCharacteristicName, dict[LabelName, AnalyticalComputation[Any, Any]]
111+
] = {}
112+
for characteristic_name, methods in analytical_computations.items():
113+
if isinstance(methods, Mapping):
114+
normalized_analytical[characteristic_name] = dict(methods)
115+
else:
116+
normalized_analytical[characteristic_name] = {
117+
DEFAULT_ANALYTICAL_COMPUTATION_LABEL: methods
118+
}
100119

101120
if not normalized_analytical:
102121
raise ValueError("Distribution requires at least one analytical computation.")
103122

123+
for characteristic_name, labeled_methods in normalized_analytical.items():
124+
if not labeled_methods:
125+
raise ValueError(
126+
f"Characteristic '{characteristic_name}' must provide at least one "
127+
"analytical computation."
128+
)
129+
104130
self._analytical_computations = normalized_analytical
105131
self._support = support
106132
self._sampling_strategy = sampling_strategy or DefaultSamplingUnivariateStrategy()
@@ -114,7 +140,7 @@ def distribution_type(self) -> DistributionType:
114140
@property
115141
def analytical_computations(
116142
self,
117-
) -> Mapping[GenericCharacteristicName, AnalyticalComputation[Any, Any]]:
143+
) -> Mapping[GenericCharacteristicName, Mapping[LabelName, AnalyticalComputation[Any, Any]]]:
118144
"""Return analytical computations provided directly by this distribution."""
119145
return self._analytical_computations
120146

src/pysatl_core/distributions/strategies.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
from pysatl_core.types import CharacteristicName, Method, NumericArray
2020

2121
if TYPE_CHECKING:
22+
from collections.abc import Mapping
2223
from typing import Any
2324

24-
from pysatl_core.distributions.computation import FittedComputationMethod
25+
from pysatl_core.distributions.computation import (
26+
AnalyticalComputation,
27+
FittedComputationMethod,
28+
Method,
29+
)
2530
from pysatl_core.distributions.distribution import Distribution
26-
from pysatl_core.types import GenericCharacteristicName
31+
from pysatl_core.types import GenericCharacteristicName, LabelName
2732

2833

2934
class ComputationStrategy(Protocol):
@@ -101,6 +106,26 @@ def _pop_guard(self, distr: Distribution, state: GenericCharacteristicName) -> N
101106
if not seen:
102107
self._resolving.pop(key, None)
103108

109+
@staticmethod
110+
def _pick_analytical_method(
111+
state: GenericCharacteristicName,
112+
methods: Mapping[LabelName, AnalyticalComputation[Any, Any]],
113+
) -> AnalyticalComputation[Any, Any]:
114+
"""
115+
Pick the first available analytical method for a characteristic.
116+
117+
Raises
118+
------
119+
RuntimeError
120+
If no labeled analytical methods are available for the characteristic.
121+
"""
122+
try:
123+
return next(iter(methods.values()))
124+
except StopIteration as exc:
125+
raise RuntimeError(
126+
f"Characteristic '{state}' provides no labeled analytical computations."
127+
) from exc
128+
104129
def query_method(
105130
self, state: GenericCharacteristicName, distr: Distribution, **options: Any
106131
) -> Method[Any, Any]:
@@ -134,7 +159,7 @@ def query_method(
134159
"""
135160
# 1. Check for analytical implementation
136161
if state in distr.analytical_computations:
137-
return distr.analytical_computations[state]
162+
return self._pick_analytical_method(state, distr.analytical_computations[state])
138163

139164
# 2. Check cache if enabled
140165
if self._enable_caching:
@@ -156,7 +181,7 @@ def query_method(
156181
# 5. Try each analytical characteristic as a source
157182
for src in distr.analytical_computations:
158183
if src == state:
159-
return distr.analytical_computations[src]
184+
return self._pick_analytical_method(src, distr.analytical_computations[src])
160185

161186
# Find conversion path in the graph
162187
path = reg.find_path(src, state)

src/pysatl_core/families/distribution.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from pysatl_core.types import (
3636
DistributionType,
3737
GenericCharacteristicName,
38+
LabelName,
3839
ParametrizationName,
3940
)
4041

@@ -69,7 +70,8 @@ def __init__(
6970
family_name: str,
7071
distribution_type: DistributionType,
7172
analytical_computations: Mapping[
72-
GenericCharacteristicName, AnalyticalComputation[Any, Any]
73+
GenericCharacteristicName,
74+
Mapping[LabelName, AnalyticalComputation[Any, Any]],
7375
],
7476
parametrization: Parametrization,
7577
support: Support | None,

src/pysatl_core/families/parametric_family.py

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919

2020
from pysatl_core.distributions.computation import AnalyticalComputation
2121
from pysatl_core.families.distribution import ParametricFamilyDistribution
22-
from pysatl_core.types import ComputationFunc, DistributionType
22+
from pysatl_core.types import (
23+
DEFAULT_ANALYTICAL_COMPUTATION_LABEL,
24+
ComputationFunc,
25+
DistributionType,
26+
)
2327

2428
if TYPE_CHECKING:
2529
from collections.abc import Callable
@@ -31,18 +35,23 @@
3135
)
3236
from pysatl_core.types import (
3337
GenericCharacteristicName,
38+
LabelName,
3439
ParametrizationName,
3540
)
3641

3742
type SupportArg = Callable[[Parametrization], Support | None] | None
3843
type SupportResolver = Callable[[Parametrization], Support | None]
44+
type LabeledCharacteristicProvider = (
45+
Mapping[LabelName, ParametricFamilyCharacteristic[Any, Any]]
46+
| ParametricFamilyCharacteristic[Any, Any]
47+
)
3948
type CharacteristicProvider = (
40-
Mapping[ParametrizationName, CharacteristicFunction[Any, Any]]
41-
| CharacteristicFunction[Any, Any]
49+
Mapping[ParametrizationName, LabeledCharacteristicProvider]
50+
| ParametricFamilyCharacteristic[Any, Any]
4251
)
4352
type CharacteristicsMap = Mapping[GenericCharacteristicName, CharacteristicProvider]
4453
type NonParametrizedCharacteristic[In, Out] = Callable[[], Out]
45-
type CharacteristicFunction[In, Out] = (
54+
type ParametricFamilyCharacteristic[In, Out] = (
4655
NonParametrizedCharacteristic[In, Out] | ParametrizedCharacteristic[In, Out]
4756
)
4857

@@ -78,7 +87,9 @@ class ParametricFamily:
7887
- nullary characteristics (e.g., mean, var): provider(params, **kwargs) -> Any
7988
- pointwise characteristics (e.g., pdf, cdf, ppf): provider(params, x, **kwargs) -> Any
8089
81-
If a single callable is provided, it is treated as defined for the base parametrization.
90+
Providers are grouped by parametrization and may define multiple labeled methods.
91+
If a single callable is provided, it is treated as the base-parametrization method
92+
under ``DEFAULT_ANALYTICAL_COMPUTATION_LABEL``.
8293
support_by_parametrization : Callable or None, optional
8394
Function that returns support for given parameters.
8495
"""
@@ -109,31 +120,63 @@ def __init__(
109120
# Runtime registry of parametrization classes
110121
self._parametrizations: dict[ParametrizationName, type[Parametrization]] = {}
111122

112-
def _normalize_characteristic(
113-
value: Mapping[ParametrizationName, CharacteristicFunction[Any, Any]]
114-
| CharacteristicFunction[Any, Any],
115-
) -> dict[ParametrizationName, CharacteristicFunction[Any, Any]]:
116-
return (
117-
dict(value)
118-
if isinstance(value, Mapping)
119-
else {self.base_parametrization_name: value}
123+
def _normalize_labeled_provider(
124+
characteristic_name: GenericCharacteristicName,
125+
parametrization_name: ParametrizationName,
126+
provider: LabeledCharacteristicProvider,
127+
) -> dict[LabelName, ParametricFamilyCharacteristic[Any, Any]]:
128+
normalized = (
129+
dict(provider)
130+
if isinstance(provider, Mapping)
131+
else {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: provider}
120132
)
133+
if not normalized:
134+
raise ValueError(
135+
f"Characteristic '{characteristic_name}' has no labeled providers for "
136+
f"parametrization '{parametrization_name}'."
137+
)
138+
return normalized
139+
140+
def _normalize_characteristic(
141+
characteristic_name: GenericCharacteristicName,
142+
value: CharacteristicProvider,
143+
) -> dict[ParametrizationName, dict[LabelName, ParametricFamilyCharacteristic[Any, Any]]]:
144+
if not isinstance(value, Mapping):
145+
base_name = self.base_parametrization_name
146+
return {
147+
base_name: _normalize_labeled_provider(characteristic_name, base_name, value)
148+
}
149+
150+
normalized_by_parametrization: dict[
151+
ParametrizationName, dict[LabelName, ParametricFamilyCharacteristic[Any, Any]]
152+
] = {}
153+
for parametrization_name, provider in value.items():
154+
normalized_by_parametrization[parametrization_name] = _normalize_labeled_provider(
155+
characteristic_name,
156+
parametrization_name,
157+
provider,
158+
)
159+
return normalized_by_parametrization
121160

122161
self.distr_characteristics: dict[
123-
GenericCharacteristicName, dict[ParametrizationName, CharacteristicFunction[Any, Any]]
124-
] = {k: _normalize_characteristic(v) for k, v in distr_characteristics.items()}
162+
GenericCharacteristicName,
163+
dict[ParametrizationName, dict[LabelName, ParametricFamilyCharacteristic[Any, Any]]],
164+
] = {
165+
characteristic_name: _normalize_characteristic(characteristic_name, provider)
166+
for characteristic_name, provider in distr_characteristics.items()
167+
}
125168

126169
# Validate characteristic providers
127170
valid_names = set(self.parametrization_names)
128171
for char_name, forms in self.distr_characteristics.items():
172+
if not forms:
173+
raise ValueError(f"Characteristic '{char_name}' has no providers.")
129174
unknown = set(forms) - valid_names
130175
if unknown:
131176
raise ValueError(
132177
f"Characteristic '{char_name}' has providers for unknown parametrizations: "
133178
f"{sorted(unknown)}."
134179
)
135-
if self.base_parametrization_name not in forms and len(forms) == 0:
136-
raise ValueError(f"Characteristic '{char_name}' has no providers.")
137180

138181
# Precompute analytical plan: for each parametrization pick provider (self or base)
139182
self._analytical_plan: dict[
@@ -236,7 +279,7 @@ def to_base(self, parameters: Parametrization) -> Parametrization:
236279

237280
@staticmethod
238281
def _bind_parametrization[In, Out](
239-
func: CharacteristicFunction[In, Out], params_obj: Parametrization
282+
func: ParametricFamilyCharacteristic[In, Out], params_obj: Parametrization
240283
) -> ComputationFunc[In, Out]:
241284
"""Bind ``params_obj`` to ``func`` only when ``func`` can accept positional arguments.
242285
@@ -265,14 +308,16 @@ def _bind_parametrization[In, Out](
265308

266309
def _build_analytical_computations(
267310
self, parameters: Parametrization
268-
) -> dict[GenericCharacteristicName, AnalyticalComputation[Any, Any]]:
311+
) -> dict[GenericCharacteristicName, dict[LabelName, AnalyticalComputation[Any, Any]]]:
269312
"""
270313
Build analytical computations for given parameters.
271314
272315
Uses precomputed provider plan for efficient computation.
273316
"""
274317
plan = self._analytical_plan.get(parameters.name, {})
275-
result: dict[GenericCharacteristicName, AnalyticalComputation[Any, Any]] = {}
318+
result: dict[
319+
GenericCharacteristicName, dict[LabelName, AnalyticalComputation[Any, Any]]
320+
] = {}
276321
base_params: Parametrization | None = None
277322

278323
for characteristic, provider_name in plan.items():
@@ -282,11 +327,14 @@ def _build_analytical_computations(
282327
base_params = base_params or self.to_base(parameters)
283328
params_obj = base_params
284329

285-
func_factory = self.distr_characteristics[characteristic][provider_name]
286-
result[characteristic] = AnalyticalComputation(
287-
target=characteristic,
288-
func=self._bind_parametrization(func_factory, params_obj),
289-
)
330+
labeled_providers = self.distr_characteristics[characteristic][provider_name]
331+
result[characteristic] = {
332+
label_name: AnalyticalComputation(
333+
target=characteristic,
334+
func=self._bind_parametrization(func_factory, params_obj),
335+
)
336+
for label_name, func_factory in labeled_providers.items()
337+
}
290338

291339
return result
292340

src/pysatl_core/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ def shape(self) -> ContinuousSupportShape1D:
243243
type GenericCharacteristicName = str
244244
"""Type alias for characteristic names (e.g., 'pdf', 'cdf')."""
245245

246+
type LabelName = str
247+
"""Type alias for labels that distinguish computation variants."""
248+
249+
DEFAULT_ANALYTICAL_COMPUTATION_LABEL: LabelName = "PySATL_default_analytical_computation"
250+
"""Default label for analytical methods when a label is not explicitly provided."""
251+
246252
type ParametrizationName = str
247253
"""Type alias for parametrization names."""
248254

@@ -302,6 +308,8 @@ class FamilyName(StrEnum):
302308
"UnivariateContinuous",
303309
"UnivariateDiscrete",
304310
"GenericCharacteristicName",
311+
"LabelName",
312+
"DEFAULT_ANALYTICAL_COMPUTATION_LABEL",
305313
"ParametrizationName",
306314
"ComputationFunc",
307315
"DistributionType",

0 commit comments

Comments
 (0)