From 4afae304969effd4d0fac9570169529a30387b4d Mon Sep 17 00:00:00 2001 From: LeonidElkin Date: Sun, 22 Mar 2026 21:21:00 +0300 Subject: [PATCH 1/4] feat(transformations): transformations base primitives --- src/pysatl_core/__init__.py | 4 + src/pysatl_core/distributions/distribution.py | 47 +- .../distributions/registry/__init__.py | 2 + .../distributions/registry/graph.py | 28 +- .../registry/graph_primitives.py | 17 + src/pysatl_core/distributions/strategies.py | 19 +- src/pysatl_core/families/distribution.py | 3 +- src/pysatl_core/transformations/__init__.py | 47 + .../approximations/__init__.py | 25 + .../approximations/approximation.py | 53 + .../linear_interpolations/__init__.py | 19 + .../linear_interpolations/_common.py | 119 +++ .../linear_interpolations/cdf.py | 184 ++++ .../linear_interpolations/pdf.py | 121 +++ .../linear_interpolations/ppf.py | 116 +++ .../transformations/distribution.py | 291 ++++++ .../lightweight_distribution.py | 212 ++++ .../transformations/operations/__init__.py | 27 + .../transformations/operations/affine.py | 626 ++++++++++++ .../operations/binary/__init__.py | 28 + .../transformations/operations/binary/base.py | 935 ++++++++++++++++++ .../operations/binary/division.py | 389 ++++++++ .../operations/binary/linear.py | 344 +++++++ .../operations/binary/multiplication.py | 334 +++++++ .../transformations/operations/mixture.py | 771 +++++++++++++++ .../transformations/operators_mixin.py | 144 +++ .../transformations/transformation_method.py | 168 ++++ src/pysatl_core/types.py | 54 + tests/unit/transformations/__init__.py | 12 + .../approximations/__init__.py | 12 + .../test_linear_interpolation_approximator.py | 170 ++++ .../transformations/operations/__init__.py | 12 + .../transformations/operations/test_affine.py | 94 ++ .../transformations/operations/test_binary.py | 367 +++++++ .../operations/test_mixture.py | 332 +++++++ .../unit/transformations/test_distribution.py | 65 ++ .../test_lightweight_distribution.py | 72 ++ .../transformations/test_operators_mixin.py | 68 ++ .../test_transformation_method.py | 86 ++ 39 files changed, 6400 insertions(+), 17 deletions(-) create mode 100644 src/pysatl_core/transformations/__init__.py create mode 100644 src/pysatl_core/transformations/approximations/__init__.py create mode 100644 src/pysatl_core/transformations/approximations/approximation.py create mode 100644 src/pysatl_core/transformations/approximations/linear_interpolations/__init__.py create mode 100644 src/pysatl_core/transformations/approximations/linear_interpolations/_common.py create mode 100644 src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py create mode 100644 src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py create mode 100644 src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py create mode 100644 src/pysatl_core/transformations/distribution.py create mode 100644 src/pysatl_core/transformations/lightweight_distribution.py create mode 100644 src/pysatl_core/transformations/operations/__init__.py create mode 100644 src/pysatl_core/transformations/operations/affine.py create mode 100644 src/pysatl_core/transformations/operations/binary/__init__.py create mode 100644 src/pysatl_core/transformations/operations/binary/base.py create mode 100644 src/pysatl_core/transformations/operations/binary/division.py create mode 100644 src/pysatl_core/transformations/operations/binary/linear.py create mode 100644 src/pysatl_core/transformations/operations/binary/multiplication.py create mode 100644 src/pysatl_core/transformations/operations/mixture.py create mode 100644 src/pysatl_core/transformations/operators_mixin.py create mode 100644 src/pysatl_core/transformations/transformation_method.py create mode 100644 tests/unit/transformations/__init__.py create mode 100644 tests/unit/transformations/approximations/__init__.py create mode 100644 tests/unit/transformations/approximations/test_linear_interpolation_approximator.py create mode 100644 tests/unit/transformations/operations/__init__.py create mode 100644 tests/unit/transformations/operations/test_affine.py create mode 100644 tests/unit/transformations/operations/test_binary.py create mode 100644 tests/unit/transformations/operations/test_mixture.py create mode 100644 tests/unit/transformations/test_distribution.py create mode 100644 tests/unit/transformations/test_lightweight_distribution.py create mode 100644 tests/unit/transformations/test_operators_mixin.py create mode 100644 tests/unit/transformations/test_transformation_method.py diff --git a/src/pysatl_core/__init__.py b/src/pysatl_core/__init__.py index da0efa9..80ed151 100644 --- a/src/pysatl_core/__init__.py +++ b/src/pysatl_core/__init__.py @@ -16,6 +16,8 @@ from .families import __all__ as _family_all from .sampling import * from .sampling import __all__ as _sampling_all +from .transformations import * +from .transformations import __all__ as _transformations_all from .types import * from .types import __all__ as _types_all @@ -26,9 +28,11 @@ *_family_all, *_types_all, *_sampling_all, + *_transformations_all, ] del _distr_all del _family_all del _types_all +del _transformations_all del _sampling_all diff --git a/src/pysatl_core/distributions/distribution.py b/src/pysatl_core/distributions/distribution.py index 40ebbb4..8431851 100644 --- a/src/pysatl_core/distributions/distribution.py +++ b/src/pysatl_core/distributions/distribution.py @@ -57,7 +57,10 @@ class Distribution(ABC): | Mapping[LabelName, AnalyticalComputation[Any, Any]] ), ] - Direct analytical computations provided by the distribution. + Distribution-provided characteristic methods. + For non-transformed distributions every method in this mapping is + fully analytical, so this mapping matches the set of loops with + ``is_analytical=True`` in the graph view. sampling_strategy : SamplingStrategy Strategy for generating random samples. computation_strategy : ComputationStrategy @@ -92,7 +95,9 @@ def __init__( | Mapping[LabelName, AnalyticalComputation[Any, Any]] ), ] - Analytical computations provided by the distribution. + Distribution-provided characteristic methods. + For non-transformed distributions these methods are fully + analytical. support : Support or None, default=None Support of the distribution. sampling_strategy : SamplingStrategy or None, default=None @@ -141,9 +146,45 @@ def distribution_type(self) -> DistributionType: def analytical_computations( self, ) -> Mapping[GenericCharacteristicName, Mapping[LabelName, AnalyticalComputation[Any, Any]]]: - """Return analytical computations provided directly by this distribution.""" + """ + Return distribution-provided characteristic methods. + + For non-transformed distributions this mapping coincides with + graph loops marked as ``is_analytical=True``. + """ return self._analytical_computations + def loop_is_analytical( + self, + characteristic_name: GenericCharacteristicName, + label_name: LabelName, + ) -> bool: + """ + Tell whether a self-loop method is fully analytical in the graph. + + Parameters + ---------- + characteristic_name : GenericCharacteristicName + Characteristic name of the self-loop. + label_name : LabelName + Label of the analytical computation variant. + + Returns + ------- + bool + ``True`` when every required predecessor in the transformation + chain is analytical. + + Notes + ----- + Presence in ``analytical_computations`` means that a characteristic + has at least one analytical ancestor in its derivation chain. + For non-transformed distributions these notions coincide, therefore + this method always returns ``True``. + """ + _ = characteristic_name, label_name + return True + @property def sampling_strategy(self) -> SamplingStrategy: """Return the currently attached sampling strategy.""" diff --git a/src/pysatl_core/distributions/registry/__init__.py b/src/pysatl_core/distributions/registry/__init__.py index 9aa13bd..33d144b 100644 --- a/src/pysatl_core/distributions/registry/__init__.py +++ b/src/pysatl_core/distributions/registry/__init__.py @@ -29,6 +29,7 @@ ComputationEdgeMeta, EdgeMeta, GraphInvariantError, + TransformationLoopEdgeMeta, ) __all__ = [ @@ -37,6 +38,7 @@ "EdgeMeta", "ComputationEdgeMeta", "AnalyticalLoopEdgeMeta", + "TransformationLoopEdgeMeta", "GraphInvariantError", # Constraint types "Constraint", diff --git a/src/pysatl_core/distributions/registry/graph.py b/src/pysatl_core/distributions/registry/graph.py index 485db67..64bb3ff 100644 --- a/src/pysatl_core/distributions/registry/graph.py +++ b/src/pysatl_core/distributions/registry/graph.py @@ -33,6 +33,7 @@ ComputationEdgeMeta, EdgeMeta, GraphInvariantError, + TransformationLoopEdgeMeta, ) if TYPE_CHECKING: @@ -304,6 +305,15 @@ def _compute_definitive_nodes(self, distr: Distribution) -> set[GenericCharacter definitive.add(name) return definitive + @staticmethod + def _loop_is_analytical( + distr: Distribution, + characteristic_name: GenericCharacteristicName, + label_name: LabelName, + ) -> bool: + """Resolve loop analytical flag for a distribution-provided method.""" + return distr.loop_is_analytical(characteristic_name, label_name) + @staticmethod def _attach_analytical_loops( adj: dict[ @@ -314,12 +324,14 @@ def _attach_analytical_loops( present_nodes: set[GenericCharacteristicName], ) -> None: """ - Attach analytical self-loops for distribution-provided computations. + Attach distribution-provided self-loops to the view graph. Notes ----- - Analytical loops are only added for characteristics present in this view. - Each labeled analytical computation becomes one loop edge ``char -> char``. + Loops are only added for characteristics present in this view. + Each labeled computation in ``analytical_computations`` becomes one + loop edge ``char -> char``. The loop class is selected via + ``distr.loop_is_analytical(...)``. """ for characteristic_name, labeled_methods in distr.analytical_computations.items(): if characteristic_name not in present_nodes: @@ -329,7 +341,15 @@ def _attach_analytical_loops( characteristic_name, {} ) for label_name, analytical_method in labeled_methods.items(): - loop_variants[label_name] = AnalyticalLoopEdgeMeta(method=analytical_method) + loop_variants[label_name] = ( + AnalyticalLoopEdgeMeta(method=analytical_method) + if CharacteristicRegistry._loop_is_analytical( + distr, + characteristic_name, + label_name, + ) + else TransformationLoopEdgeMeta(method=analytical_method) + ) def view(self, distr: Distribution) -> RegistryView: """ diff --git a/src/pysatl_core/distributions/registry/graph_primitives.py b/src/pysatl_core/distributions/registry/graph_primitives.py index 6a2c741..74226db 100644 --- a/src/pysatl_core/distributions/registry/graph_primitives.py +++ b/src/pysatl_core/distributions/registry/graph_primitives.py @@ -77,6 +77,23 @@ def edge_kind(self) -> str: return "analytical_loop" +@dataclass(frozen=True, slots=True) +class TransformationLoopEdgeMeta(EdgeMeta): + """ + Edge metadata for transformation-provided self-loops. + + Such loops are attached from ``analytical_computations`` as regular + stopping points for the strategy, but they are not considered fully + analytical by the graph semantics. + """ + + method: AnalyticalComputation[Any, Any] + is_analytical: bool = field(default=False) + + def edge_kind(self) -> str: + return "transformation_loop" + + class GraphInvariantError(RuntimeError): """ Raised when characteristic graph invariants are violated. diff --git a/src/pysatl_core/distributions/strategies.py b/src/pysatl_core/distributions/strategies.py index b7ca0b1..8ab8111 100644 --- a/src/pysatl_core/distributions/strategies.py +++ b/src/pysatl_core/distributions/strategies.py @@ -126,14 +126,14 @@ def _pick_analytical_method( ) from exc @staticmethod - def _pick_analytical_loop_method( + def _pick_loop_method( state: GenericCharacteristicName, view: RegistryView, ) -> Method[Any, Any] | None: """ - Pick the first analytical self-loop method for a characteristic in a view. + Pick the first available self-loop method for a characteristic in a view. """ - loops = view.analytical_variants(state) + loops = view.variants(state, state) if not loops: return None return cast(Method[Any, Any], next(iter(loops.values())).method) @@ -147,8 +147,8 @@ def query_method( Resolution order: 1. Cached fitted method (if caching enabled) 2. Analytical implementation for non-registry characteristics - 3. Analytical self-loop from the registry view - 4. Conversion path from analytical-loop characteristics via the graph + 3. First self-loop from the registry view + 4. Conversion path from loop characteristics via the graph Parameters ---------- @@ -199,13 +199,13 @@ def query_method( self._push_guard(distr, state) try: - loop_method = self._pick_analytical_loop_method(state, view) + loop_method = self._pick_loop_method(state, view) if loop_method is not None: return loop_method - # 5. Try each analytical-loop characteristic as a source + # 5. Try each loop characteristic as a source for src in distr.analytical_computations: - if not view.analytical_variants(src): + if not view.variants(src, src): continue # Find conversion path in the graph @@ -226,7 +226,8 @@ def query_method( return last_fitted raise RuntimeError( - f"No conversion path from any analytical characteristic to '{state}'." + "No conversion path from any characteristic in " + f"analytical_computations to '{state}'." ) finally: self._pop_guard(distr, state) diff --git a/src/pysatl_core/families/distribution.py b/src/pysatl_core/families/distribution.py index 166b98c..a83e79b 100644 --- a/src/pysatl_core/families/distribution.py +++ b/src/pysatl_core/families/distribution.py @@ -15,6 +15,7 @@ from pysatl_core.distributions.distribution import _KEEP, Distribution from pysatl_core.families.registry import ParametricFamilyRegister +from pysatl_core.transformations.operators_mixin import TransformationOperatorsMixin from pysatl_core.types import NumericArray if TYPE_CHECKING: @@ -40,7 +41,7 @@ ) -class ParametricFamilyDistribution(Distribution): +class ParametricFamilyDistribution(TransformationOperatorsMixin, Distribution): """ A specific distribution instance from a parametric family. diff --git a/src/pysatl_core/transformations/__init__.py b/src/pysatl_core/transformations/__init__.py new file mode 100644 index 0000000..1d6995e --- /dev/null +++ b/src/pysatl_core/transformations/__init__.py @@ -0,0 +1,47 @@ +""" +Transformations framework for derived probability distributions. + +This package provides the base primitives for constructing distributions +obtained from other distributions, together with approximation interfaces +and concrete transformation implementations. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from .approximations import * +from .approximations import __all__ as _approximations_all +from .distribution import * +from .distribution import __all__ as _distribution_all +from .lightweight_distribution import * +from .lightweight_distribution import __all__ as _lightweight_all +from .operations import * +from .operations import __all__ as _operations_all +from .operators_mixin import * +from .operators_mixin import __all__ as _operators_mixin_all +from .transformation_method import * +from .transformation_method import __all__ as _methods_all + +__all__ = [ + *_approximations_all, + *_distribution_all, + *_lightweight_all, + *_operations_all, + *_operators_mixin_all, + *_methods_all, +] + +del _approximations_all + +del _distribution_all + +del _lightweight_all + +del _operations_all + +del _operators_mixin_all + +del _methods_all diff --git a/src/pysatl_core/transformations/approximations/__init__.py b/src/pysatl_core/transformations/approximations/__init__.py new file mode 100644 index 0000000..bfcce56 --- /dev/null +++ b/src/pysatl_core/transformations/approximations/__init__.py @@ -0,0 +1,25 @@ +""" +Approximation utilities for transformed distributions. + +This subpackage contains approximation interfaces and concrete +approximators that can materialize analytical characteristics for +complex transformation trees. +""" + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from .approximation import CharacteristicApproximationMethod +from .linear_interpolations import ( + CDFMonotoneSplineApproximation, + PDFLinearInterpolationApproximation, + PPFMonotoneSplineApproximation, +) + +__all__ = [ + "CharacteristicApproximationMethod", + "PDFLinearInterpolationApproximation", + "CDFMonotoneSplineApproximation", + "PPFMonotoneSplineApproximation", +] diff --git a/src/pysatl_core/transformations/approximations/approximation.py b/src/pysatl_core/transformations/approximations/approximation.py new file mode 100644 index 0000000..dc9be1f --- /dev/null +++ b/src/pysatl_core/transformations/approximations/approximation.py @@ -0,0 +1,53 @@ +""" +Protocols for characteristic-level approximation methods. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, Protocol + +if TYPE_CHECKING: + from pysatl_core.distributions.computation import AnalyticalComputation + from pysatl_core.transformations.distribution import DerivedDistribution + + +class CharacteristicApproximationMethod(Protocol): + """ + Protocol for a single characteristic approximation method. + + Implementations are responsible only for one characteristic and can + use any numeric approximation strategy (interpolation, splines, + tabulation, etc.). + """ + + def approximate( + self, + distribution: DerivedDistribution, + **options: Any, + ) -> AnalyticalComputation[Any, Any]: + """ + Build an analytical computation for a target characteristic. + + Parameters + ---------- + distribution : DerivedDistribution + Distribution to approximate. + **options : Any + Extra approximation options. + + Returns + ------- + AnalyticalComputation[Any, Any] + Approximate analytical computation for the target + characteristic. + """ + ... + + +__all__ = [ + "CharacteristicApproximationMethod", +] diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/__init__.py b/src/pysatl_core/transformations/approximations/linear_interpolations/__init__.py new file mode 100644 index 0000000..4e240c6 --- /dev/null +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/__init__.py @@ -0,0 +1,19 @@ +""" +Interpolation-based approximation methods for specific characteristics. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from .cdf import CDFMonotoneSplineApproximation +from .pdf import PDFLinearInterpolationApproximation +from .ppf import PPFMonotoneSplineApproximation + +__all__ = [ + "PDFLinearInterpolationApproximation", + "CDFMonotoneSplineApproximation", + "PPFMonotoneSplineApproximation", +] diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/_common.py b/src/pysatl_core/transformations/approximations/linear_interpolations/_common.py new file mode 100644 index 0000000..6c9c1fd --- /dev/null +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/_common.py @@ -0,0 +1,119 @@ +""" +Shared utilities for interpolation-based characteristic approximation. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.types import Kind, NumericArray + +if TYPE_CHECKING: + from pysatl_core.transformations.distribution import DerivedDistribution + from pysatl_core.types import GenericCharacteristicName + + +def get_analytical_method( + distribution: DerivedDistribution, + characteristic_name: GenericCharacteristicName, +) -> AnalyticalComputation[Any, Any]: + """ + Get the first analytical method for a characteristic. + + Parameters + ---------- + distribution : DerivedDistribution + Distribution containing analytical methods. + characteristic_name : GenericCharacteristicName + Characteristic name to fetch. + + Returns + ------- + AnalyticalComputation[Any, Any] + First available analytical method for the requested + characteristic. + + Raises + ------ + ValueError + If characteristic is not available analytically. + """ + methods = distribution.analytical_computations.get(characteristic_name) + if methods is None: + raise ValueError( + "Approximation requires an analytical method for " + f"characteristic '{characteristic_name}'." + ) + + try: + return next(iter(methods.values())) + except StopIteration as exc: + raise ValueError( + f"Characteristic '{characteristic_name}' provides no analytical methods." + ) from exc + + +def evaluate_on_grid( + computation: AnalyticalComputation[Any, Any], + grid: NumericArray, + *, + characteristic_name: GenericCharacteristicName, + **options: Any, +) -> NumericArray: + """ + Evaluate a method on a 1D grid with strict array semantics. + + Parameters + ---------- + computation : AnalyticalComputation[Any, Any] + Source analytical computation. + grid : NumericArray + One-dimensional interpolation grid. + characteristic_name : GenericCharacteristicName + Name of the approximated characteristic for error messages. + **options : Any + Extra options forwarded to the method. + + Returns + ------- + NumericArray + Values evaluated on the input grid. + """ + values = np.asarray(computation(grid, **options), dtype=float) + if values.shape != grid.shape: + raise ValueError( + f"Approximation for characteristic '{characteristic_name}' expects " + "array semantics with shape-preserving outputs on interpolation grid." + ) + return cast(NumericArray, values) + + +def validate_univariate_continuous(distribution: DerivedDistribution) -> None: + """ + Validate that a distribution is univariate continuous. + + Parameters + ---------- + distribution : DerivedDistribution + Distribution to validate. + + Raises + ------ + TypeError + If distribution kind or dimension is unsupported. + """ + distribution_type = distribution.distribution_type + kind = getattr(distribution_type, "kind", None) + dimension = getattr(distribution_type, "dimension", None) + if kind != Kind.CONTINUOUS or dimension != 1: + raise TypeError( + "Interpolation approximation currently supports only univariate " + "continuous distributions." + ) diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py new file mode 100644 index 0000000..a09d57e --- /dev/null +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py @@ -0,0 +1,184 @@ +""" +Monotone-spline approximation for CDF. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np +from scipy.interpolate import PchipInterpolator + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.support import ContinuousSupport +from pysatl_core.transformations.approximations.linear_interpolations._common import ( + evaluate_on_grid, + get_analytical_method, + validate_univariate_continuous, +) +from pysatl_core.types import CharacteristicName, ComputationFunc, NumericArray + +if TYPE_CHECKING: + from pysatl_core.transformations.distribution import DerivedDistribution + + +class CDFMonotoneSplineApproximation: + """ + Approximate CDF on a finite domain with monotone cubic splines. + + Parameters + ---------- + n_grid : int, default=513 + Number of interpolation nodes. + lower_limit_prob : float, default=1e-6 + Left-tail probability used to determine finite lower domain + boundary ``x_left`` such that ``CDF(x_left) <= lower_limit_prob``. + upper_limit_prob : float, default=1e-6 + Right-tail probability used to determine finite upper domain + boundary ``x_right`` such that ``CDF(x_right) >= 1 - upper_limit_prob``. + max_search_steps : int, default=80 + Maximum number of geometric expansion steps when searching for + finite domain boundaries. + """ + + def __init__( + self, + *, + n_grid: int = 513, + lower_limit_prob: float = 1e-6, + upper_limit_prob: float = 1e-6, + max_search_steps: int = 80, + ) -> None: + if n_grid < 2: + raise ValueError("n_grid must be at least 2.") + if not (0.0 <= lower_limit_prob < 0.5): + raise ValueError("lower_limit_prob must satisfy 0 <= p < 0.5.") + if not (0.0 <= upper_limit_prob < 0.5): + raise ValueError("upper_limit_prob must satisfy 0 <= p < 0.5.") + if max_search_steps < 1: + raise ValueError("max_search_steps must be positive.") + + self._n_grid = int(n_grid) + self._lower_limit_prob = float(lower_limit_prob) + self._upper_limit_prob = float(upper_limit_prob) + self._max_search_steps = int(max_search_steps) + + def approximate( + self, + distribution: DerivedDistribution, + **options: Any, + ) -> AnalyticalComputation[Any, Any]: + """ + Approximate CDF for a distribution. + """ + validate_univariate_continuous(distribution) + source_method = get_analytical_method(distribution, CharacteristicName.CDF) + lower_limit, upper_limit = self._resolve_limits(distribution, source_method, **options) + + grid = np.linspace(lower_limit, upper_limit, self._n_grid, dtype=float) + values = evaluate_on_grid( + source_method, + grid, + characteristic_name=CharacteristicName.CDF, + **options, + ) + values = np.nan_to_num(values, nan=0.0, posinf=1.0, neginf=0.0) + values = np.clip(values, 0.0, 1.0) + values = np.maximum.accumulate(values) + if float(values[-1] - values[0]) <= 0.0: + raise ValueError("Could not build monotone CDF approximation: degenerate grid values.") + + spline = PchipInterpolator(grid, values, extrapolate=False) + + def _cdf(data: Any, /, **_kwargs: Any) -> Any: + array = np.asarray(data, dtype=float) + result = np.asarray(spline(array), dtype=float) + result = np.where(array < lower_limit, 0.0, result) + result = np.where(array > upper_limit, 1.0, result) + result = np.nan_to_num(result, nan=0.0, posinf=1.0, neginf=0.0) + if np.ndim(array) == 0: + return float(result) + return cast(NumericArray, result) + + return AnalyticalComputation( + target=CharacteristicName.CDF, + func=cast(ComputationFunc[Any, Any], _cdf), + ) + + def _resolve_limits( + self, + distribution: DerivedDistribution, + cdf_method: AnalyticalComputation[Any, Any], + **options: Any, + ) -> tuple[float, float]: + """ + Resolve finite interpolation limits using support and tail probabilities. + """ + support = distribution.support + left: float | None = None + right: float | None = None + + if isinstance(support, ContinuousSupport): + left = float(support.left) if np.isfinite(support.left) else None + right = float(support.right) if np.isfinite(support.right) else None + + if left is None: + left = self._search_lower_limit(cdf_method, **options) + if right is None: + right = self._search_upper_limit(cdf_method, **options) + + if not np.isfinite(left) or not np.isfinite(right) or left >= right: + raise ValueError("Failed to resolve finite CDF interpolation limits.") + + return left, right + + def _search_lower_limit( + self, cdf_method: AnalyticalComputation[Any, Any], **options: Any + ) -> float: + """ + Find finite lower bound satisfying tail-probability constraint. + """ + magnitudes = np.power(2.0, np.arange(self._max_search_steps, dtype=float)) + candidates = -magnitudes + values = evaluate_on_grid( + cdf_method, + cast(NumericArray, candidates), + characteristic_name=CharacteristicName.CDF, + **options, + ) + mask = values <= self._lower_limit_prob + if not np.any(mask): + raise ValueError( + "Failed to infer lower interpolation limit from CDF and lower_limit_prob." + ) + return float(candidates[int(np.argmax(mask))]) + + def _search_upper_limit( + self, cdf_method: AnalyticalComputation[Any, Any], **options: Any + ) -> float: + """ + Find finite upper bound satisfying tail-probability constraint. + """ + target = 1.0 - self._upper_limit_prob + candidates = np.power(2.0, np.arange(self._max_search_steps, dtype=float)) + values = evaluate_on_grid( + cdf_method, + cast(NumericArray, candidates), + characteristic_name=CharacteristicName.CDF, + **options, + ) + mask = values >= target + if not np.any(mask): + raise ValueError( + "Failed to infer upper interpolation limit from CDF and upper_limit_prob." + ) + return float(candidates[int(np.argmax(mask))]) + + +__all__ = [ + "CDFMonotoneSplineApproximation", +] diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py new file mode 100644 index 0000000..8b18b4b --- /dev/null +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py @@ -0,0 +1,121 @@ +""" +Linear interpolation approximation for PDF. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.support import ContinuousSupport +from pysatl_core.transformations.approximations.linear_interpolations._common import ( + evaluate_on_grid, + get_analytical_method, + validate_univariate_continuous, +) +from pysatl_core.types import CharacteristicName, ComputationFunc, NumericArray + +if TYPE_CHECKING: + from pysatl_core.transformations.distribution import DerivedDistribution + + +class PDFLinearInterpolationApproximation: + """ + Approximate PDF with piecewise-linear interpolation on a finite grid. + + Parameters + ---------- + n_grid : int, default=513 + Number of interpolation nodes. + lower_limit : float or None, optional + Left interpolation bound. If ``None``, it is inferred from finite + support when available, otherwise a large negative constant is used. + upper_limit : float or None, optional + Right interpolation bound. If ``None``, it is inferred from finite + support when available, otherwise a large positive constant is used. + """ + + _AUTO_LOWER_LIMIT = -1_000.0 + _AUTO_UPPER_LIMIT = 1_000.0 + + def __init__( + self, + *, + n_grid: int = 513, + lower_limit: float | None = None, + upper_limit: float | None = None, + ) -> None: + if n_grid < 2: + raise ValueError("n_grid must be at least 2.") + self._n_grid = int(n_grid) + self._lower_limit = lower_limit + self._upper_limit = upper_limit + + def approximate( + self, + distribution: DerivedDistribution, + **options: Any, + ) -> AnalyticalComputation[Any, Any]: + """ + Approximate PDF for a distribution. + """ + validate_univariate_continuous(distribution) + + lower_limit, upper_limit = self._resolve_limits(distribution) + grid = np.linspace(lower_limit, upper_limit, self._n_grid, dtype=float) + source_method = get_analytical_method(distribution, CharacteristicName.PDF) + values = evaluate_on_grid( + source_method, + grid, + characteristic_name=CharacteristicName.PDF, + **options, + ) + values = np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0) + values = np.clip(values, 0.0, None) + + def _pdf(data: Any, /, **_kwargs: Any) -> Any: + array = np.asarray(data, dtype=float) + result = np.interp(array, grid, values, left=0.0, right=0.0) + if np.ndim(array) == 0: + return float(result) + return cast(NumericArray, np.asarray(result, dtype=float)) + + return AnalyticalComputation( + target=CharacteristicName.PDF, + func=cast(ComputationFunc[Any, Any], _pdf), + ) + + def _resolve_limits(self, distribution: DerivedDistribution) -> tuple[float, float]: + """ + Resolve interpolation limits. + """ + lower = self._lower_limit + upper = self._upper_limit + + support = distribution.support + if isinstance(support, ContinuousSupport): + if lower is None and np.isfinite(support.left): + lower = float(support.left) + if upper is None and np.isfinite(support.right): + upper = float(support.right) + + lower = self._AUTO_LOWER_LIMIT if lower is None else float(lower) + upper = self._AUTO_UPPER_LIMIT if upper is None else float(upper) + + if not np.isfinite(lower) or not np.isfinite(upper) or lower >= upper: + raise ValueError( + "Interpolation limits must be finite and satisfy lower_limit < upper_limit." + ) + + return lower, upper + + +__all__ = [ + "PDFLinearInterpolationApproximation", +] diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py new file mode 100644 index 0000000..5b7ab25 --- /dev/null +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py @@ -0,0 +1,116 @@ +""" +Monotone-spline approximation for PPF. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np +from scipy.interpolate import PchipInterpolator + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.transformations.approximations.linear_interpolations._common import ( + evaluate_on_grid, + get_analytical_method, + validate_univariate_continuous, +) +from pysatl_core.types import CharacteristicName, ComputationFunc, NumericArray + +if TYPE_CHECKING: + from pysatl_core.transformations.distribution import DerivedDistribution + + +class PPFMonotoneSplineApproximation: + """ + Approximate PPF on a finite probability interval with monotone splines. + + Parameters + ---------- + n_grid : int, default=513 + Number of interpolation nodes. + lower_limit : float, default=0.0 + Left bound of the probability interval used for interpolation. + upper_limit : float, default=1.0 + Right bound of the probability interval used for interpolation. + """ + + def __init__( + self, + *, + n_grid: int = 513, + lower_limit: float = 0.0, + upper_limit: float = 1.0, + ) -> None: + if n_grid < 2: + raise ValueError("n_grid must be at least 2.") + if not (0.0 <= lower_limit < upper_limit <= 1.0): + raise ValueError("PPF limits must satisfy 0 <= lower_limit < upper_limit <= 1.") + + self._n_grid = int(n_grid) + self._lower_limit = float(lower_limit) + self._upper_limit = float(upper_limit) + + def approximate( + self, + distribution: DerivedDistribution, + **options: Any, + ) -> AnalyticalComputation[Any, Any]: + """ + Approximate PPF for a distribution. + """ + validate_univariate_continuous(distribution) + source_method = get_analytical_method(distribution, CharacteristicName.PPF) + + grid = np.linspace(self._lower_limit, self._upper_limit, self._n_grid, dtype=float) + values = evaluate_on_grid( + source_method, + grid, + characteristic_name=CharacteristicName.PPF, + **options, + ) + values = self._regularize_monotone_ppf(values) + spline = PchipInterpolator(grid, values, extrapolate=False) + lower_value = float(values[0]) + upper_value = float(values[-1]) + + def _ppf(data: Any, /, **_kwargs: Any) -> Any: + array = np.asarray(data, dtype=float) + clipped = np.clip(array, self._lower_limit, self._upper_limit) + result = np.asarray(spline(clipped), dtype=float) + result = np.where(array <= self._lower_limit, lower_value, result) + result = np.where(array >= self._upper_limit, upper_value, result) + if np.ndim(array) == 0: + return float(result) + return cast(NumericArray, result) + + return AnalyticalComputation( + target=CharacteristicName.PPF, + func=cast(ComputationFunc[Any, Any], _ppf), + ) + + @staticmethod + def _regularize_monotone_ppf(values: NumericArray) -> NumericArray: + """ + Regularize PPF samples to finite monotone values. + """ + regularized = np.asarray(values, dtype=float) + finite_mask = np.isfinite(regularized) + if not np.any(finite_mask): + raise ValueError("Could not build PPF approximation: all grid values are non-finite.") + + if not np.all(finite_mask): + indices = np.arange(regularized.size, dtype=float) + regularized = np.interp(indices, indices[finite_mask], regularized[finite_mask]) + + regularized = np.maximum.accumulate(regularized) + return cast(NumericArray, regularized) + + +__all__ = [ + "PPFMonotoneSplineApproximation", +] diff --git a/src/pysatl_core/transformations/distribution.py b/src/pysatl_core/transformations/distribution.py new file mode 100644 index 0000000..0af9419 --- /dev/null +++ b/src/pysatl_core/transformations/distribution.py @@ -0,0 +1,291 @@ +""" +Base classes for transformed distributions. + +This module introduces the first architectural layer for derived +probability distributions produced by transformations. The goal is to +keep them fully compatible with the existing :class:`Distribution` +protocol and computation graph while still preserving transformation +metadata. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from abc import abstractmethod +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.distribution import _KEEP, Distribution +from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, +) +from pysatl_core.transformations.lightweight_distribution import LightweightDistribution +from pysatl_core.transformations.operators_mixin import TransformationOperatorsMixin +from pysatl_core.types import ( + DistributionType, + GenericCharacteristicName, + LabelName, + ParentRole, + TransformationName, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.support import Support + from pysatl_core.transformations.approximations.approximation import ( + CharacteristicApproximationMethod, + ) + + +class DerivedDistribution(TransformationOperatorsMixin, Distribution): + """ + Base class for distributions obtained from one or more parents. + + Parameters + ---------- + distribution_type : DistributionType + Type descriptor of the derived distribution. + bases : Mapping[ParentRole, Distribution] + Parent distributions participating in the transformation. + Internally, they are stored as lightweight snapshots to avoid + retaining full parent distribution objects. + analytical_computations : Mapping[ + GenericCharacteristicName, + ( + AnalyticalComputation[Any, Any] + | Mapping[LabelName, AnalyticalComputation[Any, Any]] + ), + ] + Derived characteristic methods exposed by the transformation. + Presence here means that at least one ancestor in the derivation + chain is analytical. + transformation_name : TransformationName + Logical name of the transformation. + support : Support | None, optional + Support of the transformed distribution. + sampling_strategy : SamplingStrategy | None, optional + Strategy used to generate random samples. + computation_strategy : ComputationStrategy | None, optional + Strategy used to resolve characteristics. + loop_analytical_flags : Mapping[ + GenericCharacteristicName, + Mapping[LabelName, bool], + ] | None, optional + Optional graph flags for loop analytical status. + A loop has ``is_analytical=True`` only when all required ancestors + of that characteristic are analytical. + """ + + def __init__( + self, + *, + distribution_type: DistributionType, + bases: Mapping[ParentRole, Distribution], + analytical_computations: Mapping[ + GenericCharacteristicName, + (AnalyticalComputation[Any, Any] | Mapping[LabelName, AnalyticalComputation[Any, Any]]), + ], + transformation_name: TransformationName, + support: Support | None = None, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + loop_analytical_flags: ( + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]] | None + ) = None, + ) -> None: + super().__init__( + distribution_type=distribution_type, + analytical_computations=analytical_computations, + support=support, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + ) + self._bases = { + role: LightweightDistribution.from_distribution(base) for role, base in bases.items() + } + self._transformation_name = transformation_name + self._loop_analytical_flags = { + characteristic_name: dict(flags_by_label) + for characteristic_name, flags_by_label in (loop_analytical_flags or {}).items() + } + + @property + def bases(self) -> Mapping[ParentRole, Distribution]: + """Get parent distributions grouped by their logical roles.""" + return self._bases + + @property + def transformation_name(self) -> TransformationName: + """Get the logical name of the transformation.""" + return self._transformation_name + + def loop_is_analytical( + self, + characteristic_name: GenericCharacteristicName, + label_name: LabelName, + ) -> bool: + """ + Return transformation-aware analytical flag for a loop method. + + The method returns ``True`` only when all required predecessors are + analytical. A method can still be present in + ``analytical_computations`` when this returns ``False``. + """ + return self._loop_analytical_flags.get(characteristic_name, {}).get(label_name, True) + + def approximate( + self, + methods: Mapping[GenericCharacteristicName, CharacteristicApproximationMethod], + **options: Any, + ) -> ApproximatedDistribution: + """ + Approximate selected characteristics of the current derivation. + + Parameters + ---------- + methods : Mapping[GenericCharacteristicName, CharacteristicApproximationMethod] + Mapping from characteristic names to characteristic-level + approximation methods. + **options : Any + Extra options forwarded to each approximation method. + + Returns + ------- + ApproximatedDistribution + Distribution with materialized approximations for selected + characteristics. + """ + if not methods: + raise ValueError("At least one characteristic approximation method must be provided.") + + analytical_computations: dict[ + GenericCharacteristicName, AnalyticalComputation[Any, Any] + ] = {} + for characteristic_name, method in methods.items(): + computation = method.approximate( + self, + **options, + ) + if computation.target != characteristic_name: + raise ValueError( + "Approximation method returned computation for a mismatched " + f"target: expected '{characteristic_name}', got '{computation.target}'." + ) + analytical_computations[characteristic_name] = computation + + return ApproximatedDistribution( + distribution_type=self.distribution_type, + analytical_computations=analytical_computations, + support=self.support, + sampling_strategy=self.sampling_strategy, + computation_strategy=self.computation_strategy, + ) + + @abstractmethod + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> DerivedDistribution: + """ + Return a copy of the derived distribution with updated strategies. + + Concrete subclasses must preserve their own transformation + parameters while applying strategy overrides. + """ + _ = sampling_strategy, computation_strategy + raise NotImplementedError + + +class ApproximatedDistribution(DerivedDistribution): + """ + Derived distribution whose analytical computations were materialized by an + external approximator. + + Parameters + ---------- + distribution_type : DistributionType + Type descriptor of the approximated distribution. + analytical_computations : Mapping[ + GenericCharacteristicName, + ( + AnalyticalComputation[Any, Any] + | Mapping[LabelName, AnalyticalComputation[Any, Any]] + ), + ] + Materialized methods produced by the approximator. + They are exposed in ``analytical_computations`` for strategy + resolution, but are never treated as fully analytical. + support : Support | None, optional + Support of the approximated distribution. + sampling_strategy : SamplingStrategy | None, optional + Sampling strategy to expose. + computation_strategy : ComputationStrategy | None, optional + Characteristic resolution strategy. + """ + + def __init__( + self, + *, + distribution_type: DistributionType, + analytical_computations: Mapping[ + GenericCharacteristicName, + (AnalyticalComputation[Any, Any] | Mapping[LabelName, AnalyticalComputation[Any, Any]]), + ], + support: Support | None = None, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + super().__init__( + distribution_type=distribution_type, + bases={}, + analytical_computations=analytical_computations, + transformation_name=TransformationName.APPROXIMATION, + support=support, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + loop_analytical_flags={}, + ) + self._loop_analytical_flags = self._build_non_analytical_loop_flags( + self.analytical_computations + ) + + @staticmethod + def _build_non_analytical_loop_flags( + analytical_computations: Mapping[ + GenericCharacteristicName, + Mapping[LabelName, AnalyticalComputation[Any, Any]], + ], + ) -> dict[GenericCharacteristicName, dict[LabelName, bool]]: + """Build loop flags where every approximation loop is non-analytical.""" + return { + characteristic_name: dict.fromkeys(labeled_methods, False) + for characteristic_name, labeled_methods in analytical_computations.items() + } + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> ApproximatedDistribution: + """Return a copy of the approximated distribution with updated strategies.""" + return ApproximatedDistribution( + distribution_type=self.distribution_type, + analytical_computations=self.analytical_computations, + support=self.support, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + +__all__ = [ + "ApproximatedDistribution", + "DerivedDistribution", + "LightweightDistribution", +] diff --git a/src/pysatl_core/transformations/lightweight_distribution.py b/src/pysatl_core/transformations/lightweight_distribution.py new file mode 100644 index 0000000..86235e9 --- /dev/null +++ b/src/pysatl_core/transformations/lightweight_distribution.py @@ -0,0 +1,212 @@ +""" +Lightweight distribution snapshot for transformation pipelines. + +The snapshot keeps only metadata required by transformations and +strategies, while avoiding strong references to full parent +distribution objects. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.distribution import _KEEP, Distribution +from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, +) +from pysatl_core.types import ( + DistributionType, + GenericCharacteristicName, + LabelName, + ParentRole, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.support import Support + + +class LightweightDistribution(Distribution): + """ + Lightweight ``Distribution`` implementation for transformation internals. + + Parameters + ---------- + distribution_type : DistributionType + Type descriptor of the distribution. + analytical_computations : Mapping[ + GenericCharacteristicName, + ( + AnalyticalComputation[Any, Any] + | Mapping[LabelName, AnalyticalComputation[Any, Any]] + ), + ] + Labeled characteristic methods exposed by the snapshot. + support : Support | None, optional + Support metadata copied from the source distribution. + sampling_strategy : SamplingStrategy | None, optional + Sampling strategy attached to the snapshot. + computation_strategy : ComputationStrategy | None, optional + Computation strategy attached to the snapshot. + bases : Mapping[ParentRole, LightweightDistribution] | None, optional + Lightweight base snapshots for chained transformations. + loop_analytical_flags : Mapping[ + GenericCharacteristicName, + Mapping[LabelName, bool], + ] | None, optional + Optional analytical flags for loop variants. + """ + + def __init__( + self, + *, + distribution_type: DistributionType, + analytical_computations: Mapping[ + GenericCharacteristicName, + (AnalyticalComputation[Any, Any] | Mapping[LabelName, AnalyticalComputation[Any, Any]]), + ], + support: Support | None = None, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + bases: Mapping[ParentRole, LightweightDistribution] | None = None, + loop_analytical_flags: ( + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]] | None + ) = None, + ) -> None: + super().__init__( + distribution_type=distribution_type, + analytical_computations=analytical_computations, + support=support, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + ) + self._bases = dict(bases or {}) + self._loop_analytical_flags = { + characteristic_name: dict(flags_by_label) + for characteristic_name, flags_by_label in (loop_analytical_flags or {}).items() + } + + @classmethod + def from_distribution(cls, distribution: Distribution) -> LightweightDistribution: + """ + Build a lightweight snapshot from an arbitrary distribution. + + The method copies only strategy-relevant fields and recursively + snapshots known bases when the source distribution exposes them. + + Parameters + ---------- + distribution : Distribution + Source distribution. + + Returns + ------- + LightweightDistribution + Lightweight snapshot compatible with ``Distribution``. + """ + if isinstance(distribution, cls): + return distribution + return cls._from_distribution(distribution, memo={}) + + @classmethod + def _from_distribution( + cls, + distribution: Distribution, + memo: dict[int, LightweightDistribution], + ) -> LightweightDistribution: + """ + Internal recursive snapshot builder with memoization. + """ + if isinstance(distribution, cls): + return distribution + + distribution_id = id(distribution) + cached = memo.get(distribution_id) + if cached is not None: + return cached + + snapshot = cls( + distribution_type=distribution.distribution_type, + analytical_computations=distribution.analytical_computations, + support=distribution.support, + sampling_strategy=distribution.sampling_strategy, + computation_strategy=distribution.computation_strategy, + bases={}, + loop_analytical_flags=cls._collect_loop_flags(distribution), + ) + memo[distribution_id] = snapshot + snapshot._bases = cls._collect_bases(distribution, memo) + return snapshot + + @staticmethod + def _collect_loop_flags( + distribution: Distribution, + ) -> dict[GenericCharacteristicName, dict[LabelName, bool]]: + """Collect loop analytical flags from the source distribution.""" + return { + characteristic_name: { + label_name: distribution.loop_is_analytical(characteristic_name, label_name) + for label_name in labeled_methods + } + for characteristic_name, labeled_methods in distribution.analytical_computations.items() + } + + @classmethod + def _collect_bases( + cls, + distribution: Distribution, + memo: dict[int, LightweightDistribution], + ) -> dict[ParentRole, LightweightDistribution]: + """ + Recursively collect known base distributions for transformation chains. + """ + maybe_bases = getattr(distribution, "bases", None) + if not isinstance(maybe_bases, Mapping): + return {} + + bases: dict[ParentRole, LightweightDistribution] = {} + for role, base in maybe_bases.items(): + if isinstance(base, Distribution): + bases[role] = cls._from_distribution(base, memo) + return bases + + @property + def bases(self) -> Mapping[ParentRole, Distribution]: + """Get lightweight base snapshots grouped by role.""" + return self._bases + + def loop_is_analytical( + self, + characteristic_name: GenericCharacteristicName, + label_name: LabelName, + ) -> bool: + """Return preserved loop analytical flag for the snapshot.""" + return self._loop_analytical_flags.get(characteristic_name, {}).get(label_name, True) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> LightweightDistribution: + """Return a copy of the snapshot with updated strategies.""" + return LightweightDistribution( + distribution_type=self.distribution_type, + analytical_computations=self.analytical_computations, + support=self.support, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + bases=self._bases, + loop_analytical_flags=self._loop_analytical_flags, + ) + + +__all__ = [ + "LightweightDistribution", +] diff --git a/src/pysatl_core/transformations/operations/__init__.py b/src/pysatl_core/transformations/operations/__init__.py new file mode 100644 index 0000000..2130be8 --- /dev/null +++ b/src/pysatl_core/transformations/operations/__init__.py @@ -0,0 +1,27 @@ +""" +Concrete distribution transformation operations. + +This subpackage contains concrete transformed-distribution implementations, +including affine and binary operations. +""" + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from .affine import * +from .affine import __all__ as _affine_all +from .binary import * +from .binary import __all__ as _binary_all +from .mixture import * +from .mixture import __all__ as _mixture_all + +__all__ = [ + *_affine_all, + *_binary_all, + *_mixture_all, +] + +del _affine_all +del _binary_all +del _mixture_all diff --git a/src/pysatl_core/transformations/operations/affine.py b/src/pysatl_core/transformations/operations/affine.py new file mode 100644 index 0000000..6cc77a2 --- /dev/null +++ b/src/pysatl_core/transformations/operations/affine.py @@ -0,0 +1,626 @@ +""" +Affine transformation for probability distributions. + +This module implements the transformation ``Y = aX + b``. The +transformation is represented as a derived distribution with analytical +computations built from parent methods resolved through ``query_method()``. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.computation import Method +from pysatl_core.distributions.distribution import _KEEP, Distribution +from pysatl_core.distributions.support import ( + ContinuousSupport, + ExplicitTableDiscreteSupport, + Support, +) +from pysatl_core.transformations.distribution import DerivedDistribution +from pysatl_core.transformations.lightweight_distribution import LightweightDistribution +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, + TransformationMethod, +) +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + CharacteristicName, + ComplexArray, + ComputationFunc, + DistributionType, + GenericCharacteristicName, + Kind, + LabelName, + NumericArray, + ParentRole, + TransformationName, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + +_BASE_ROLE: ParentRole = "base" + + +class AffineDistribution(DerivedDistribution): + """ + Distribution obtained from the affine transformation ``Y = aX + b``. + + Parameters + ---------- + base_distribution : Distribution + Source distribution being transformed. + scale : float + Multiplicative coefficient ``a``. + shift : float, default=0.0 + Additive coefficient ``b``. + sampling_strategy : SamplingStrategy | None, optional + Sampling strategy exposed by the transformed distribution. + computation_strategy : ComputationStrategy | None, optional + Computation strategy exposed by the transformed distribution. + + Notes + ----- + The current implementation focuses on one-dimensional continuous and + discrete distributions. + """ + + def __init__( + self, + base_distribution: Distribution, + *, + scale: float, + shift: float = 0.0, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + if scale == 0.0: + raise ValueError("scale must be non-zero for an affine transformation.") + + base_snapshot = LightweightDistribution.from_distribution(base_distribution) + self._base_distribution: LightweightDistribution = base_snapshot + self._scale = scale + self._shift = shift + distribution_type = self._validate_distribution_type(base_snapshot.distribution_type) + bases: dict[ParentRole, LightweightDistribution] = {_BASE_ROLE: base_snapshot} + analytical_computations, loop_analytical_flags = self._build_analytical_computations( + distribution_type=distribution_type, + bases=bases, + ) + + super().__init__( + distribution_type=distribution_type, + bases=bases, + analytical_computations=analytical_computations, + transformation_name=TransformationName.AFFINE, + support=self._transform_support(base_snapshot.support), + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + loop_analytical_flags=loop_analytical_flags, + ) + + @property + def base_distribution(self) -> LightweightDistribution: + """Get the lightweight snapshot of the source distribution.""" + return self._base_distribution + + @property + def scale(self) -> float: + """Get the multiplicative coefficient ``a``.""" + return self._scale + + @property + def shift(self) -> float: + """Get the additive coefficient ``b``.""" + return self._shift + + def sample(self, n: int, **options: Any) -> NumericArray: + """ + Generate affine samples from transformed base-distribution samples. + + The method samples ``X`` from the lightweight base distribution using + the currently attached sampling strategy and returns ``aX + b``. + """ + base_samples = self.sampling_strategy.sample(n, distr=self.base_distribution, **options) + transformed_samples = np.asarray(base_samples, dtype=float) * self.scale + self.shift + return cast(NumericArray, transformed_samples) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> AffineDistribution: + """Return a copy of the affine distribution with updated strategies.""" + return AffineDistribution( + base_distribution=self.base_distribution, + scale=self.scale, + shift=self.shift, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + def _validate_distribution_type(self, distribution_type: DistributionType) -> DistributionType: + """ + Validate that the affine transformation can be applied. + + Parameters + ---------- + distribution_type : DistributionType + Distribution type descriptor of the parent distribution. + + Returns + ------- + DistributionType + The validated distribution type. + + Raises + ------ + TypeError + If the distribution is not one-dimensional continuous or discrete. + """ + dimension = getattr(distribution_type, "dimension", None) + kind = getattr(distribution_type, "kind", None) + + if dimension != 1: + raise TypeError( + "AffineDistribution currently supports only one-dimensional distributions." + ) + + if kind not in {Kind.CONTINUOUS, Kind.DISCRETE}: + raise TypeError("Unsupported distribution kind for affine transformation.") + + return distribution_type + + def _build_analytical_computations( + self, + *, + distribution_type: DistributionType, + bases: Mapping[ParentRole, LightweightDistribution], + ) -> tuple[ + Mapping[GenericCharacteristicName, Mapping[LabelName, TransformationMethod[Any, Any]]], + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]], + ]: + """ + Build analytical computations for the affine transformation. + + Parameters + ---------- + distribution_type : DistributionType + Type descriptor of the transformed distribution. + bases : Mapping[ParentRole, LightweightDistribution] + Lightweight parent distributions grouped by logical role. + + Returns + ------- + tuple[ + Mapping[GenericCharacteristicName, Mapping[LabelName, TransformationMethod[Any, Any]]], + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]], + ] + Labeled analytical computations and loop analytical flags. + + Raises + ------ + TypeError + If the distribution kind is not supported. + RuntimeError + If no transformed characteristic is eligible to be attached. + """ + computations: dict[ + GenericCharacteristicName, dict[LabelName, TransformationMethod[Any, Any]] + ] = {} + loop_analytical_flags: dict[GenericCharacteristicName, dict[LabelName, bool]] = {} + kind = getattr(distribution_type, "kind", None) + + def _register( + target: GenericCharacteristicName, + source_requirements: SourceRequirements, + evaluator: TransformationEvaluator[Any, Any], + ) -> None: + method, is_analytical, has_any_present_source = TransformationMethod.try_from_parents( + target=target, + transformation=TransformationName.AFFINE, + bases=bases, + source_requirements=source_requirements, + evaluator=evaluator, + ) + if method is None or not has_any_present_source: + return + computations[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: method} + loop_analytical_flags[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: is_analytical} + + _register( + CharacteristicName.CF, + self._requirements(CharacteristicName.CF), + self._make_cf, + ) + _register( + CharacteristicName.MEAN, + self._requirements(CharacteristicName.MEAN), + self._make_mean, + ) + _register( + CharacteristicName.VAR, + self._requirements(CharacteristicName.VAR), + self._make_var, + ) + _register( + CharacteristicName.SKEW, + self._requirements(CharacteristicName.SKEW), + self._make_skew, + ) + _register( + CharacteristicName.KURT, + self._requirements(CharacteristicName.KURT), + self._make_kurt, + ) + + if kind == Kind.CONTINUOUS: + _register( + CharacteristicName.CDF, + self._requirements(CharacteristicName.CDF), + self._make_continuous_cdf, + ) + _register( + CharacteristicName.PDF, + self._requirements(CharacteristicName.PDF), + self._make_continuous_pdf, + ) + _register( + CharacteristicName.PPF, + self._requirements(CharacteristicName.PPF), + self._make_continuous_ppf, + ) + if computations: + return computations, loop_analytical_flags + raise RuntimeError( + "Affine transformation produced no analytical computations. " + "At least one source characteristic must be present." + ) + + if kind == Kind.DISCRETE: + _register( + CharacteristicName.PMF, + self._requirements(CharacteristicName.PMF), + self._make_discrete_pmf, + ) + _register( + CharacteristicName.CDF, + ( + self._requirements(CharacteristicName.CDF) + if self.scale > 0.0 + else self._requirements(CharacteristicName.CDF, CharacteristicName.PMF) + ), + self._make_discrete_cdf + if self.scale > 0.0 + else self._make_discrete_cdf_negative_scale, + ) + _register( + CharacteristicName.PPF, + self._requirements(CharacteristicName.PPF), + self._make_discrete_ppf, + ) + if computations: + return computations, loop_analytical_flags + raise RuntimeError( + "Affine transformation produced no analytical computations. " + "At least one source characteristic must be present." + ) + + raise TypeError("Unsupported distribution kind for affine transformation.") + + def _requirements( + self, + *characteristics: GenericCharacteristicName, + ) -> SourceRequirements: + """ + Build source requirements for the base distribution. + + Parameters + ---------- + *characteristics : GenericCharacteristicName + Parent characteristics required to evaluate a transformed one. + + Returns + ------- + SourceRequirements + Requirements grouped by the single base role. + """ + return {_BASE_ROLE: tuple(characteristics)} + + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed CDF for a continuous base distribution. + + For ``Y = aX + b`` the formula is + ``F_Y(y) = F_X((y - b) / a)`` for ``a > 0`` and + ``F_Y(y) = 1 - F_X((y - b) / a)`` for ``a < 0``. + """ + base_cdf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.CDF] + ) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + values = base_cdf((data - self.shift) / self.scale, **options) + return 1.0 - values if self.scale < 0.0 else values + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed PDF for a continuous base distribution. + + The affine density transformation is + ``f_Y(y) = f_X((y - b) / a) / |a|``. + """ + base_pdf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.PDF] + ) + + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + return cast( + NumericArray, + base_pdf((data - self.shift) / self.scale, **options) / abs(self.scale), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _pdf) + + def _make_continuous_ppf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed PPF for a continuous base distribution. + + For positive scale the quantiles are transformed directly. For + negative scale the probabilities are mirrored as ``1 - p``. + """ + base_ppf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.PPF] + ) + + def _ppf(data: NumericArray, **options: Any) -> NumericArray: + probabilities = data if self.scale > 0.0 else 1.0 - data + return self.scale * base_ppf(probabilities, **options) + self.shift + + return cast(ComputationFunc[NumericArray, NumericArray], _ppf) + + def _make_discrete_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed CDF for a discrete base distribution with ``a > 0``. + """ + base_cdf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.CDF] + ) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return base_cdf((data - self.shift) / self.scale, **options) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _make_discrete_cdf_negative_scale( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed CDF for a discrete base distribution with ``a < 0``. + + For a decreasing affine map the lower tail of ``Y`` becomes the upper + tail of ``X``. Using both CDF and PMF avoids support-specific logic and + preserves jump values exactly. + """ + base_cdf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.CDF] + ) + base_pmf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.PMF] + ) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + x = (data - self.shift) / self.scale + return np.asarray(1.0 - base_cdf(x, **options) + base_pmf(x, **options)) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _make_discrete_pmf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed PMF for a discrete base distribution. + + Since the affine map is bijective for ``a != 0``, the probability mass + is preserved without any Jacobian factor. + """ + base_pmf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.PMF] + ) + + def _pmf(data: NumericArray, **options: Any) -> NumericArray: + return base_pmf((data - self.shift) / self.scale, **options) + + return cast(ComputationFunc[NumericArray, NumericArray], _pmf) + + def _make_discrete_ppf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """ + Build the transformed PPF for a discrete base distribution. + + For positive scale the quantiles are transformed directly. For + negative scale the lower-tail quantile of ``Y`` corresponds to the + strict upper-tail quantile of ``X``, implemented through + ``nextafter(1 - p, 1)``. + """ + base_ppf = cast( + Method[NumericArray, NumericArray], sources[_BASE_ROLE][CharacteristicName.PPF] + ) + + def _ppf(data: NumericArray, **options: Any) -> NumericArray: + x = data if self.scale > 0.0 else np.nextafter(1.0 - data, 1.0) + return self.scale * base_ppf(x, **options) + self.shift + + return cast(ComputationFunc[NumericArray, NumericArray], _ppf) + + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """ + Build the characteristic function of the affine transform. + + The formula is ``phi_Y(t) = exp(i b t) * phi_X(a t)``. + """ + base_cf = cast( + Method[NumericArray, ComplexArray], sources[_BASE_ROLE][CharacteristicName.CF] + ) + + def _cf(data: NumericArray, **options: Any) -> ComplexArray: + return cast( + ComplexArray, np.exp(1j * self.shift * data) * base_cf(self.scale * data, **options) + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf) + + def _make_mean(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build the transformed mean.""" + base_mean = cast(Method[Any, float], sources[_BASE_ROLE][CharacteristicName.MEAN]) + + def _mean(**options: Any) -> float: + return self.scale * base_mean(**options) + self.shift + + return _mean + + def _make_var(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build the transformed variance.""" + base_var = cast(Method[Any, float], sources[_BASE_ROLE][CharacteristicName.VAR]) + + def _var(**options: Any) -> float: + return self.scale**2 * base_var(**options) + + return _var + + def _make_skew(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """ + Build the transformed skewness. + + Multiplication by a negative constant flips the sign of skewness. + """ + base_skew = cast(Method[Any, float], sources[_BASE_ROLE][CharacteristicName.SKEW]) + + def _skew(**options: Any) -> float: + sign = -1.0 if self.scale < 0.0 else 1.0 + return sign * base_skew(**options) + + return _skew + + def _make_kurt(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """ + Build the transformed kurtosis. + + Kurtosis is invariant under affine transformations with non-zero scale. + """ + base_kurt = cast(Method[Any, float], sources[_BASE_ROLE][CharacteristicName.KURT]) + + def _kurt(**options: Any) -> float: + return base_kurt(**options) + + return _kurt + + def _transform_support(self, support: Support | None) -> Support | None: + """ + Transform the parent support when its structure is known. + + Notes + ----- + Some support types are intentionally left unhandled for now. + In such cases the transformed distribution exposes ``None``. + """ + if support is None: + return None + + if isinstance(support, ContinuousSupport): + return self._transform_continuous_support(support) + + if isinstance(support, ExplicitTableDiscreteSupport): + transformed_points = np.asarray(support.points, dtype=float) * self.scale + self.shift + return ExplicitTableDiscreteSupport(points=transformed_points, assume_sorted=False) + + return None + + def _transform_continuous_support(self, support: ContinuousSupport) -> ContinuousSupport: + """ + Transform a continuous interval support under the affine map. + """ + left = float(self.scale * support.left + self.shift) + right = float(self.scale * support.right + self.shift) + + if self.scale > 0.0: + return ContinuousSupport( + left=left, + right=right, + left_closed=support.left_closed, + right_closed=support.right_closed, + ) + + return ContinuousSupport( + left=right, + right=left, + left_closed=support.right_closed, + right_closed=support.left_closed, + ) + + +def affine( + distribution: Distribution, + *, + scale: float, + shift: float = 0.0, +) -> AffineDistribution: + """ + Apply the affine transformation ``Y = aX + b`` to a distribution. + + Parameters + ---------- + distribution : Distribution + Source distribution. + scale : float + Multiplicative coefficient ``a``. + shift : float, default=0.0 + Additive coefficient ``b``. + + Returns + ------- + AffineDistribution + Derived distribution representing the transformed random variable. + """ + return AffineDistribution(distribution, scale=scale, shift=shift) + + +__all__ = [ + "AffineDistribution", + "affine", +] diff --git a/src/pysatl_core/transformations/operations/binary/__init__.py b/src/pysatl_core/transformations/operations/binary/__init__.py new file mode 100644 index 0000000..29ff318 --- /dev/null +++ b/src/pysatl_core/transformations/operations/binary/__init__.py @@ -0,0 +1,28 @@ +""" +Binary transformations for probability distributions. + +This package provides pairwise transformed distributions such as +``X + Y``, ``X - Y``, ``X * Y``, and ``X / Y``. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from pysatl_core.types import BinaryOperationName + +from .base import BinaryDistribution, binary +from .division import DivisionBinaryDistribution +from .linear import LinearBinaryDistribution +from .multiplication import MultiplicationBinaryDistribution + +__all__ = [ + "BinaryDistribution", + "BinaryOperationName", + "DivisionBinaryDistribution", + "LinearBinaryDistribution", + "MultiplicationBinaryDistribution", + "binary", +] diff --git a/src/pysatl_core/transformations/operations/binary/base.py b/src/pysatl_core/transformations/operations/binary/base.py new file mode 100644 index 0000000..dc0e3f5 --- /dev/null +++ b/src/pysatl_core/transformations/operations/binary/base.py @@ -0,0 +1,935 @@ +""" +Base abstractions for binary transformations over distributions. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from abc import ABC, abstractmethod +from collections.abc import Callable, Mapping +from math import inf, isfinite, sqrt +from typing import TYPE_CHECKING, Any, cast + +import numpy as np +from scipy.integrate import quad +from scipy.optimize import brentq + +from pysatl_core.distributions.computation import Method +from pysatl_core.distributions.distribution import Distribution +from pysatl_core.distributions.registry import characteristic_registry +from pysatl_core.distributions.support import ( + ContinuousSupport, + ExplicitTableDiscreteSupport, + Support, +) +from pysatl_core.transformations.distribution import DerivedDistribution +from pysatl_core.transformations.lightweight_distribution import LightweightDistribution +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, + TransformationMethod, +) +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + BinaryOperationName, + CharacteristicName, + ComplexArray, + ComputationFunc, + DistributionType, + GenericCharacteristicName, + Kind, + LabelName, + NumericArray, + ParentRole, + TransformationName, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + +_LEFT_ROLE: ParentRole = "left" +_RIGHT_ROLE: ParentRole = "right" +_MAX_MOMENT_ORDER = 4 + + +class BinaryDistribution(DerivedDistribution, ABC): + """ + Base class for binary transformations over two parent distributions. + + Parameters + ---------- + left_distribution : Distribution + Left parent distribution. + right_distribution : Distribution + Right parent distribution. + operation : BinaryOperationName + Binary operation identifier. + sampling_strategy : SamplingStrategy | None, optional + Sampling strategy exposed by the transformed distribution. + computation_strategy : ComputationStrategy | None, optional + Computation strategy exposed by the transformed distribution. + """ + + def __init__( + self, + left_distribution: Distribution, + right_distribution: Distribution, + *, + operation: BinaryOperationName, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + self._operation = operation + + left_snapshot = LightweightDistribution.from_distribution(left_distribution) + right_snapshot = LightweightDistribution.from_distribution(right_distribution) + + self._left_distribution = left_snapshot + self._right_distribution = right_snapshot + self._cached_discrete_mass_table: tuple[NumericArray, NumericArray, NumericArray] | None = ( + None + ) + + distribution_type = self._validate_distribution_types( + left_snapshot.distribution_type, + right_snapshot.distribution_type, + ) + bases: dict[ParentRole, LightweightDistribution] = { + _LEFT_ROLE: left_snapshot, + _RIGHT_ROLE: right_snapshot, + } + transformed_support = self._transform_support(left_snapshot.support, right_snapshot.support) + self._precomputed_support = transformed_support + analytical_computations, loop_analytical_flags = self._build_analytical_computations( + distribution_type=distribution_type, + bases=bases, + ) + + super().__init__( + distribution_type=distribution_type, + bases=bases, + analytical_computations=analytical_computations, + transformation_name=TransformationName.BINARY, + support=transformed_support, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + loop_analytical_flags=loop_analytical_flags, + ) + + @property + def left_distribution(self) -> LightweightDistribution: + """Get the lightweight snapshot of the left parent distribution.""" + return self._left_distribution + + @property + def right_distribution(self) -> LightweightDistribution: + """Get the lightweight snapshot of the right parent distribution.""" + return self._right_distribution + + @property + def operation(self) -> BinaryOperationName: + """Get the binary operation name.""" + return self._operation + + def sample(self, n: int, **options: Any) -> NumericArray: + """ + Generate transformed samples from two parent-distribution samples. + + The method samples both parents using the currently attached + sampling strategy and applies the concrete binary operation + element-wise. + """ + left_samples = np.asarray( + self.sampling_strategy.sample(n, distr=self.left_distribution, **options), + dtype=float, + ) + right_samples = np.asarray( + self.sampling_strategy.sample(n, distr=self.right_distribution, **options), + dtype=float, + ) + transformed_samples = self._operation_value_array(left_samples, right_samples) + return cast(NumericArray, np.asarray(transformed_samples, dtype=float)) + + @abstractmethod + def _characteristic_specs( + self, *, kind: Kind | None + ) -> tuple[ + tuple[ + GenericCharacteristicName, + SourceRequirements, + TransformationEvaluator[Any, Any], + ], + ..., + ]: + """Return characteristic registration specs for the concrete operation.""" + + @abstractmethod + def _operation_value( + self, + left: float | NumericArray, + right: float | NumericArray, + ) -> float | NumericArray: + """Apply the concrete binary operation in scalar or array semantics.""" + + def _operation_value_array(self, left: NumericArray, right: NumericArray) -> NumericArray: + """Apply binary operation to broadcast-compatible arrays.""" + return cast(NumericArray, np.asarray(self._operation_value(left, right), dtype=float)) + + @abstractmethod + def _result_raw_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = _MAX_MOMENT_ORDER, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute transformed raw moments up to ``max_order`` (1..4).""" + + @abstractmethod + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """Build transformed characteristic function.""" + + @abstractmethod + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous PDF.""" + + @abstractmethod + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous CDF.""" + + def _build_analytical_computations( + self, + *, + distribution_type: DistributionType, + bases: Mapping[ParentRole, LightweightDistribution], + ) -> tuple[ + Mapping[GenericCharacteristicName, Mapping[LabelName, TransformationMethod[Any, Any]]], + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]], + ]: + """ + Build analytical computations for the concrete binary transformation. + """ + computations: dict[ + GenericCharacteristicName, dict[LabelName, TransformationMethod[Any, Any]] + ] = {} + loop_analytical_flags: dict[GenericCharacteristicName, dict[LabelName, bool]] = {} + kind = cast(Kind | None, getattr(distribution_type, "kind", None)) + declared_registry_characteristics = characteristic_registry().declared_characteristics + + def _register( + target: GenericCharacteristicName, + source_requirements: SourceRequirements, + evaluator: TransformationEvaluator[Any, Any], + ) -> None: + # Non-registry characteristics cannot be resolved via graph fitters. + # For them we require direct parent presence for each required source. + for role, characteristics in source_requirements.items(): + base = bases[role] + for characteristic in characteristics: + if characteristic in declared_registry_characteristics: + continue + if characteristic not in base.analytical_computations: + return + + method, is_analytical, has_any_present_source = TransformationMethod.try_from_parents( + target=target, + transformation=TransformationName.BINARY, + bases=bases, + source_requirements=source_requirements, + evaluator=evaluator, + ) + if method is None or not has_any_present_source: + return + computations[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: method} + loop_analytical_flags[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: is_analytical} + + for target, source_requirements, evaluator in self._characteristic_specs(kind=kind): + _register(target, source_requirements, evaluator) + + if computations: + return computations, loop_analytical_flags + + raise RuntimeError( + "Binary transformation produced no analytical computations. " + "At least one source characteristic must be present." + ) + + @staticmethod + def _validate_distribution_types( + left_type: DistributionType, + right_type: DistributionType, + ) -> DistributionType: + """ + Validate compatibility of parent distribution types. + + Parameters + ---------- + left_type : DistributionType + Type descriptor of the left parent. + right_type : DistributionType + Type descriptor of the right parent. + + Returns + ------- + DistributionType + Type descriptor used by the transformed distribution. + """ + left_dimension = getattr(left_type, "dimension", None) + right_dimension = getattr(right_type, "dimension", None) + left_kind = getattr(left_type, "kind", None) + right_kind = getattr(right_type, "kind", None) + + if left_dimension != 1 or right_dimension != 1: + raise TypeError( + "BinaryDistribution currently supports only one-dimensional distributions." + ) + if left_kind not in {Kind.CONTINUOUS, Kind.DISCRETE}: + raise TypeError("Unsupported distribution kind for binary transformation.") + if left_kind != right_kind: + raise TypeError( + "BinaryDistribution currently requires both parents to have the same kind." + ) + return left_type + + @staticmethod + def _requirements_both( + *characteristics: GenericCharacteristicName, + ) -> SourceRequirements: + """Build source requirements for both parent roles.""" + return { + _LEFT_ROLE: tuple(characteristics), + _RIGHT_ROLE: tuple(characteristics), + } + + @staticmethod + def _density_characteristic(kind: Kind | None) -> GenericCharacteristicName: + """Resolve density characteristic for the given kind.""" + return CharacteristicName.PDF if kind == Kind.CONTINUOUS else CharacteristicName.PMF + + @staticmethod + def _validate_moment_order(max_order: int) -> int: + """Validate and normalize required moment order.""" + if not 1 <= max_order <= _MAX_MOMENT_ORDER: + raise ValueError(f"Moment order must be in [1, {_MAX_MOMENT_ORDER}], got {max_order}.") + return max_order + + @classmethod + def _statistics_for_moment_order( + cls, + max_order: int, + ) -> tuple[GenericCharacteristicName, ...]: + """Return parent statistics needed to recover raw moments up to ``max_order``.""" + order = cls._validate_moment_order(max_order) + if order == 1: + return (CharacteristicName.MEAN,) + if order == 2: + return CharacteristicName.MEAN, CharacteristicName.VAR + if order == 3: + return ( + CharacteristicName.MEAN, + CharacteristicName.VAR, + CharacteristicName.SKEW, + ) + return ( + CharacteristicName.MEAN, + CharacteristicName.VAR, + CharacteristicName.SKEW, + CharacteristicName.KURT, + ) + + @staticmethod + def _eval_method_scalar( + method: Method[Any, Any], + argument: float, + **options: Any, + ) -> float: + """Evaluate a scalar-valued method at one point and cast to ``float``.""" + return float(np.asarray(method(argument, **options), dtype=float)) + + @staticmethod + def _eval_method_scalar_complex( + method: Method[Any, Any], + argument: float, + **options: Any, + ) -> complex: + """Evaluate a complex-valued method at one point and cast to ``complex``.""" + return complex(np.asarray(method(argument, **options), dtype=complex)) + + @staticmethod + def _eval_nullary_scalar( + method: Method[Any, Any], + **options: Any, + ) -> float: + """Evaluate a nullary method and cast to ``float``.""" + return float(np.asarray(method(**options), dtype=float)) + + @staticmethod + def _map_scalar_real( + data: NumericArray, + scalar_func: Callable[[float], float], + ) -> NumericArray: + """Apply a scalar real function to scalar or vector input.""" + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + mapped = np.fromiter((scalar_func(float(x)) for x in flat), dtype=float, count=flat.size) + return cast(NumericArray, mapped.reshape(array.shape)) + + @staticmethod + def _map_scalar_complex( + data: NumericArray, + scalar_func: Callable[[float], complex], + ) -> ComplexArray: + """Apply a scalar complex function to scalar or vector input.""" + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + mapped = np.fromiter((scalar_func(float(x)) for x in flat), dtype=complex, count=flat.size) + return cast(ComplexArray, mapped.reshape(array.shape)) + + @staticmethod + def _quad_real( + integrand: Callable[[float], float], + left: float, + right: float, + ) -> float: + """Integrate a real-valued function with SciPy ``quad``.""" + value, _ = quad(integrand, left, right, limit=300) + return float(value) + + @classmethod + def _integrate_real( + cls, + integrand: Callable[[float], float], + left: float, + right: float, + *, + split_at_zero: bool = False, + ) -> float: + """Integrate a real function and optionally split around zero.""" + if left >= right: + return 0.0 + + if split_at_zero and left < 0.0 < right: + eps_pos = float(np.nextafter(0.0, 1.0)) + eps_neg = float(np.nextafter(0.0, -1.0)) + return cls._quad_real(integrand, left, eps_neg) + cls._quad_real( + integrand, eps_pos, right + ) + + return cls._quad_real(integrand, left, right) + + @classmethod + def _integrate_complex( + cls, + integrand: Callable[[float], complex], + left: float, + right: float, + *, + split_at_zero: bool = False, + ) -> complex: + """Integrate a complex function by integrating real and imaginary parts.""" + real_part = cls._integrate_real( + lambda x: float(np.real(integrand(x))), + left, + right, + split_at_zero=split_at_zero, + ) + imag_part = cls._integrate_real( + lambda x: float(np.imag(integrand(x))), + left, + right, + split_at_zero=split_at_zero, + ) + return complex(real_part, imag_part) + + def _continuous_bounds_for_role(self, role: ParentRole) -> tuple[float, float]: + """Get integration bounds from continuous support or fallback to real line.""" + support = ( + self.left_distribution.support + if role == _LEFT_ROLE + else self.right_distribution.support + ) + if isinstance(support, ContinuousSupport): + return float(support.left), float(support.right) + return -inf, inf + + def _discrete_points_for_role(self, role: ParentRole) -> NumericArray: + """Get explicit discrete support points for one parent role.""" + support = ( + self.left_distribution.support + if role == _LEFT_ROLE + else self.right_distribution.support + ) + if not isinstance(support, ExplicitTableDiscreteSupport): + raise RuntimeError( + "Binary discrete computations require ExplicitTableDiscreteSupport " + "for both parents." + ) + return cast(NumericArray, np.asarray(support.points, dtype=float)) + + @staticmethod + def _raw_moments_from_statistics( + mean: float, + variance: float, + skewness: float, + raw_kurtosis: float, + ) -> tuple[float, float, float, float]: + """Convert mean/variance/skewness/kurtosis to raw moments up to order 4.""" + variance_safe = max(variance, 0.0) + std = sqrt(variance_safe) + mu3 = skewness * std**3 + mu4 = raw_kurtosis * variance_safe**2 + + m1 = mean + m2 = variance_safe + mean**2 + m3 = mu3 + 3.0 * mean * variance_safe + mean**3 + m4 = mu4 + 4.0 * mean * mu3 + 6.0 * mean**2 * variance_safe + mean**4 + return m1, m2, m3, m4 + + @staticmethod + def _central_moments_from_raw( + m1: float, + m2: float, + m3: float, + m4: float, + ) -> tuple[float, float, float]: + """Convert raw moments to central moments ``(var, mu3, mu4)``.""" + variance = max(m2 - m1**2, 0.0) + mu3 = m3 - 3.0 * m1 * m2 + 2.0 * m1**3 + mu4 = m4 - 4.0 * m1 * m3 + 6.0 * m1**2 * m2 - 3.0 * m1**4 + return variance, mu3, mu4 + + @staticmethod + def _kurt_raw_from_method( + method: Method[Any, Any], + **options: Any, + ) -> float: + """Evaluate raw kurtosis from a method with optional ``excess`` support.""" + try: + value = method(excess=False, **options) + except TypeError: + value = method(**options) + return float(np.asarray(value, dtype=float)) + + def _parent_raw_moments( + self, + sources: ResolvedSourceMethods, + role: ParentRole, + *, + max_order: int = _MAX_MOMENT_ORDER, + **options: Any, + ) -> tuple[float, float, float, float]: + """Get parent raw moments up to ``max_order`` from statistical characteristics.""" + order = self._validate_moment_order(max_order) + mean_method = sources[role][CharacteristicName.MEAN] + mean = self._eval_nullary_scalar(mean_method, **options) + if order == 1: + return mean, 0.0, 0.0, 0.0 + + var_method = sources[role][CharacteristicName.VAR] + variance = self._eval_nullary_scalar(var_method, **options) + m1 = mean + m2 = max(variance, 0.0) + mean**2 + if order == 2: + return m1, m2, 0.0, 0.0 + + skew_method = sources[role][CharacteristicName.SKEW] + skewness = self._eval_nullary_scalar(skew_method, **options) + _, _, m3, _ = self._raw_moments_from_statistics(mean, variance, skewness, 3.0) + if order == 3: + return m1, m2, m3, 0.0 + + kurt_method = sources[role][CharacteristicName.KURT] + raw_kurtosis = self._kurt_raw_from_method(kurt_method, **options) + return self._raw_moments_from_statistics(mean, variance, skewness, raw_kurtosis) + + def _make_mean( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[Any, float]: + """Build transformed mean.""" + + def _mean(**options: Any) -> float: + m1, _, _, _ = self._result_raw_moments(sources, max_order=1, **options) + return m1 + + return _mean + + def _make_var( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[Any, float]: + """Build transformed variance.""" + + def _var(**options: Any) -> float: + m1, m2, _, _ = self._result_raw_moments(sources, max_order=2, **options) + return max(m2 - m1**2, 0.0) + + return _var + + def _make_skew( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[Any, float]: + """Build transformed skewness.""" + + def _skew(**options: Any) -> float: + m1, m2, m3, _ = self._result_raw_moments(sources, max_order=3, **options) + variance, mu3, _ = self._central_moments_from_raw(m1, m2, m3, 0.0) + if variance <= 0.0: + return 0.0 + return float(mu3 / variance**1.5) + + return _skew + + def _make_kurt( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[Any, float]: + """Build transformed raw or excess kurtosis.""" + + def _kurt(*, excess: bool = False, **options: Any) -> float: + m1, m2, m3, m4 = self._result_raw_moments(sources, max_order=4, **options) + variance, _, mu4 = self._central_moments_from_raw(m1, m2, m3, m4) + raw = 3.0 if variance <= 0.0 else mu4 / variance**2 + return raw - 3.0 if excess else raw + + return _kurt + + def _make_continuous_ppf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous PPF via numerical inversion of transformed CDF.""" + cdf_method = cast(Method[float, float], self._make_continuous_cdf(sources)) + support = self._precomputed_support + support_left = float(support.left) if isinstance(support, ContinuousSupport) else -inf + support_right = float(support.right) if isinstance(support, ContinuousSupport) else inf + + def _solve_single_quantile(q: float, **options: Any) -> float: + def _f(x: float) -> float: + return float(cdf_method(x, **options) - q) + + def _f_brentq( + x: np.float64, + *_args: Any, + **_kwargs: Any, + ) -> np.float64: + return np.float64(_f(float(x))) + + left = support_left + right = support_right + if not (isfinite(left) and isfinite(right) and left < right): + left = -1.0 + right = 1.0 + for _ in range(80): + fl = _f(left) + fr = _f(right) + if fl <= 0.0 <= fr: + break + left *= 2.0 + right *= 2.0 + else: + raise RuntimeError( + "Could not bracket transformed PPF root for binary continuous operation." + ) + return float(brentq(cast(Any, _f_brentq), left, right, xtol=1e-10, maxiter=200)) + + def _ppf(data: NumericArray, **options: Any) -> NumericArray: + probabilities = np.asarray(data, dtype=float) + flat_probabilities = probabilities.reshape(-1) + + if np.any((flat_probabilities < 0.0) | (flat_probabilities > 1.0)): + raise ValueError("PPF input must be in [0, 1].") + + results = np.empty_like(flat_probabilities, dtype=float) + is_zero = flat_probabilities == 0.0 + is_one = flat_probabilities == 1.0 + is_interior = ~(is_zero | is_one) + + results[is_zero] = support_left + results[is_one] = support_right + + interior_indices = np.nonzero(is_interior)[0] + for idx in interior_indices: + results[idx] = _solve_single_quantile(float(flat_probabilities[idx]), **options) + + return cast(NumericArray, results.reshape(probabilities.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _ppf) + + def _discrete_mass_table( + self, + sources: ResolvedSourceMethods, + **options: Any, + ) -> tuple[NumericArray, NumericArray, NumericArray]: + """Build transformed finite PMF table for discrete parent supports.""" + if not options and self._cached_discrete_mass_table is not None: + return self._cached_discrete_mass_table + + left_pmf = sources[_LEFT_ROLE][CharacteristicName.PMF] + right_pmf = sources[_RIGHT_ROLE][CharacteristicName.PMF] + left_points = self._discrete_points_for_role(_LEFT_ROLE) + right_points = self._discrete_points_for_role(_RIGHT_ROLE) + left_masses = np.asarray( + [self._eval_method_scalar(left_pmf, float(x), **options) for x in left_points], + dtype=float, + ) + right_masses = np.asarray( + [self._eval_method_scalar(right_pmf, float(y), **options) for y in right_points], + dtype=float, + ) + + positive_left = np.nonzero(left_masses > 0.0)[0] + positive_right = np.nonzero(right_masses > 0.0)[0] + left_values = left_points[positive_left] + left_weights = left_masses[positive_left] + right_values = right_points[positive_right] + right_weights = right_masses[positive_right] + + if self.operation == BinaryOperationName.DIV: + nonzero_mask = ~np.isclose(right_values, 0.0, atol=0.0, rtol=0.0) + right_values = right_values[nonzero_mask] + right_weights = right_weights[nonzero_mask] + + if left_values.size == 0 or right_values.size == 0: + raise RuntimeError("Binary discrete transformation produced an empty PMF table.") + + transformed = self._operation_value_array( + left_values[:, None], right_values[None, :] + ).reshape(-1) + pair_weights = (left_weights[:, None] * right_weights[None, :]).reshape(-1) + + rounded_points = np.round(transformed, 12) + unique_points, inverse = np.unique(rounded_points, return_inverse=True) + accumulated_masses = np.zeros_like(unique_points, dtype=float) + np.add.at(accumulated_masses, inverse, pair_weights) + + positive_mass_mask = accumulated_masses > 0.0 + points = cast(NumericArray, unique_points[positive_mass_mask]) + masses = cast(NumericArray, accumulated_masses[positive_mass_mask]) + total = float(np.sum(masses)) + if total <= 0.0: + raise RuntimeError("Binary discrete transformation produced non-positive total mass.") + masses = cast(NumericArray, masses / total) + cdf_values = cast(NumericArray, np.cumsum(masses, dtype=float)) + cdf_values[-1] = 1.0 + output = (points, masses, cdf_values) + if not options: + self._cached_discrete_mass_table = output + return output + + def _make_discrete_pmf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed discrete PMF.""" + + def _pmf(data: NumericArray, **options: Any) -> NumericArray: + points, masses, _ = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + + indices = np.searchsorted(points, flat, side="left") + values = np.zeros_like(flat, dtype=float) + + in_range = indices < points.size + if np.any(in_range): + in_range_indices = indices[in_range] + close = np.isclose(points[in_range_indices], flat[in_range], atol=1e-12, rtol=0.0) + if np.any(close): + values[np.where(in_range)[0][close]] = masses[in_range_indices[close]] + + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _pmf) + + def _make_discrete_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed discrete CDF.""" + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + points, _, cdf_values = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + + indices = np.searchsorted(points, flat, side="right") - 1 + values = np.zeros_like(flat, dtype=float) + + valid = indices >= 0 + if np.any(valid): + clipped = np.minimum(indices[valid], cdf_values.size - 1) + values[valid] = cdf_values[clipped] + + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _make_discrete_ppf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed discrete PPF.""" + + def _ppf(data: NumericArray, **options: Any) -> NumericArray: + points, _, cdf_values = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + if np.any((flat < 0.0) | (flat > 1.0)): + raise ValueError("PPF input must be in [0, 1].") + + indices = np.searchsorted(cdf_values, flat, side="left") + clipped = np.clip(indices, 0, points.size - 1) + values = points[clipped] + + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _ppf) + + def _transform_support( + self, + left_support: Support | None, + right_support: Support | None, + ) -> Support | None: + """Transform support metadata when both parent supports are explicit enough.""" + if isinstance(left_support, ContinuousSupport) and isinstance( + right_support, ContinuousSupport + ): + finite_bounds = all( + isfinite(value) + for value in ( + float(left_support.left), + float(left_support.right), + float(right_support.left), + float(right_support.right), + ) + ) + if not finite_bounds: + return None + + left_bounds = (float(left_support.left), float(left_support.right)) + right_bounds = (float(right_support.left), float(right_support.right)) + if ( + self.operation == BinaryOperationName.DIV + and right_bounds[0] <= 0.0 <= right_bounds[1] + ): + return None + + left_values = np.asarray( + [left_bounds[0], left_bounds[0], left_bounds[1], left_bounds[1]], + dtype=float, + ) + right_values = np.asarray( + [right_bounds[0], right_bounds[1], right_bounds[0], right_bounds[1]], + dtype=float, + ) + values = self._operation_value_array(left_values, right_values) + closed = ( + left_support.left_closed + and left_support.right_closed + and right_support.left_closed + and right_support.right_closed + ) + return ContinuousSupport( + left=float(np.min(values)), + right=float(np.max(values)), + left_closed=closed, + right_closed=closed, + ) + + if isinstance(left_support, ExplicitTableDiscreteSupport) and isinstance( + right_support, ExplicitTableDiscreteSupport + ): + left_points = np.asarray(left_support.points, dtype=float) + right_points = np.asarray(right_support.points, dtype=float) + if self.operation == BinaryOperationName.DIV: + right_points = right_points[~np.isclose(right_points, 0.0, atol=0.0, rtol=0.0)] + + if left_points.size == 0 or right_points.size == 0: + return None + + transformed = self._operation_value_array( + left_points[:, None], + right_points[None, :], + ).reshape(-1) + return ExplicitTableDiscreteSupport(points=transformed.tolist(), assume_sorted=False) + + return None + + +_SUPPORTED_BINARY_OPERATIONS: frozenset[BinaryOperationName] = frozenset(BinaryOperationName) + + +def binary( + left_distribution: Distribution, + right_distribution: Distribution, + *, + operation: BinaryOperationName, +) -> BinaryDistribution: + """ + Apply a binary operation to two distributions. + + Parameters + ---------- + left_distribution : Distribution + Left parent distribution. + right_distribution : Distribution + Right parent distribution. + operation : BinaryOperationName + Binary operation to apply. + + Returns + ------- + BinaryDistribution + Derived distribution representing the binary transformation. + """ + from .division import DivisionBinaryDistribution + from .linear import LinearBinaryDistribution + from .multiplication import MultiplicationBinaryDistribution + + if operation in {BinaryOperationName.ADD, BinaryOperationName.SUB}: + return LinearBinaryDistribution( + left_distribution=left_distribution, + right_distribution=right_distribution, + operation=operation, + ) + if operation == BinaryOperationName.MUL: + return MultiplicationBinaryDistribution( + left_distribution=left_distribution, + right_distribution=right_distribution, + ) + if operation == BinaryOperationName.DIV: + return DivisionBinaryDistribution( + left_distribution=left_distribution, + right_distribution=right_distribution, + ) + raise ValueError( + f"Unsupported binary operation '{operation}'. " + f"Supported operations: {', '.join(sorted(_SUPPORTED_BINARY_OPERATIONS))}." + ) + + +__all__ = [ + "BinaryDistribution", + "BinaryOperationName", + "_LEFT_ROLE", + "_RIGHT_ROLE", + "binary", +] diff --git a/src/pysatl_core/transformations/operations/binary/division.py b/src/pysatl_core/transformations/operations/binary/division.py new file mode 100644 index 0000000..32b1909 --- /dev/null +++ b/src/pysatl_core/transformations/operations/binary/division.py @@ -0,0 +1,389 @@ +""" +Division binary transformation ``X / Y``. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.distribution import _KEEP +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, +) +from pysatl_core.types import ( + BinaryOperationName, + CharacteristicName, + ComplexArray, + ComputationFunc, + GenericCharacteristicName, + Kind, + NumericArray, +) + +from .base import ( + _LEFT_ROLE, + _RIGHT_ROLE, + BinaryDistribution, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + + +class DivisionBinaryDistribution(BinaryDistribution): + """ + Binary distribution for ratio transformation ``X / Y``. + """ + + def __init__( + self, + left_distribution: Distribution, + right_distribution: Distribution, + *, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + super().__init__( + left_distribution=left_distribution, + right_distribution=right_distribution, + operation=BinaryOperationName.DIV, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + ) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> DivisionBinaryDistribution: + """Return a copy of the division binary distribution with updated strategies.""" + return DivisionBinaryDistribution( + left_distribution=self.left_distribution, + right_distribution=self.right_distribution, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + def _characteristic_specs( + self, *, kind: Kind | None + ) -> tuple[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]], + ..., + ]: + """Return characteristic registration specs for division.""" + density = self._density_characteristic(kind) + mean_requirements = { + _LEFT_ROLE: self._statistics_for_moment_order(1), + _RIGHT_ROLE: (density,), + } + var_requirements = { + _LEFT_ROLE: self._statistics_for_moment_order(2), + _RIGHT_ROLE: (density,), + } + skew_requirements = { + _LEFT_ROLE: self._statistics_for_moment_order(3), + _RIGHT_ROLE: (density,), + } + kurt_requirements = { + _LEFT_ROLE: self._statistics_for_moment_order(4), + _RIGHT_ROLE: (density,), + } + + specs: list[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]] + ] = [ + ( + CharacteristicName.CF, + { + _LEFT_ROLE: (CharacteristicName.CF,), + _RIGHT_ROLE: (density,), + }, + self._make_cf, + ), + (CharacteristicName.MEAN, mean_requirements, self._make_mean), + (CharacteristicName.VAR, var_requirements, self._make_var), + (CharacteristicName.SKEW, skew_requirements, self._make_skew), + (CharacteristicName.KURT, kurt_requirements, self._make_kurt), + ] + + if kind == Kind.CONTINUOUS: + specs.extend( + [ + ( + CharacteristicName.CDF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_cdf, + ), + ( + CharacteristicName.PDF, + self._requirements_both(CharacteristicName.PDF), + self._make_continuous_pdf, + ), + ( + CharacteristicName.PPF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_ppf, + ), + ] + ) + else: + discrete_requirements = self._requirements_both(CharacteristicName.PMF) + specs.extend( + [ + (CharacteristicName.PMF, discrete_requirements, self._make_discrete_pmf), + (CharacteristicName.CDF, discrete_requirements, self._make_discrete_cdf), + (CharacteristicName.PPF, discrete_requirements, self._make_discrete_ppf), + ] + ) + + return tuple(specs) + + def _operation_value( + self, + left: float | NumericArray, + right: float | NumericArray, + ) -> float | NumericArray: + """Apply division in scalar or array semantics.""" + left_array = np.asarray(left, dtype=float) + right_array = np.asarray(right, dtype=float) + if np.any(np.isclose(right_array, 0.0, atol=0.0, rtol=0.0)): + raise ZeroDivisionError("Division by zero support point in binary transformation.") + + result = left_array / right_array + if result.ndim == 0: + return float(result) + return cast(NumericArray, result) + + def _right_inverse_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = 4, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute right-parent inverse moments ``E[Y^{-k}]`` up to ``max_order``.""" + order = self._validate_moment_order(max_order) + kind = getattr(self.distribution_type, "kind", None) + + if kind == Kind.CONTINUOUS: + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + left, right = self._continuous_bounds_for_role(_RIGHT_ROLE) + if left <= 0.0 <= right: + raise RuntimeError( + "Division transformation requires denominator support that does not cross zero." + ) + + def _moment(order: int) -> float: + def _integrand(y: float) -> float: + return y ** (-order) * self._eval_method_scalar(right_pdf, y, **options) + + return self._integrate_real(_integrand, left, right) + + output = [0.0, 0.0, 0.0, 0.0] + for current_order in range(1, order + 1): + output[current_order - 1] = _moment(current_order) + return output[0], output[1], output[2], output[3] + + right_pmf = sources[_RIGHT_ROLE][CharacteristicName.PMF] + points = self._discrete_points_for_role(_RIGHT_ROLE) + if np.any(np.isclose(points, 0.0, atol=1e-14, rtol=0.0)): + zero_mass = self._eval_method_scalar(right_pmf, 0.0, **options) + if zero_mass > 0.0: + raise RuntimeError( + "Division transformation is undefined when denominator has " + "positive mass at zero." + ) + + inverse_moments = [0.0, 0.0, 0.0, 0.0] + for y in points: + y_float = float(y) + if y_float == 0.0: + continue + py = self._eval_method_scalar(right_pmf, y_float, **options) + for current_order in range(1, order + 1): + inverse_moments[current_order - 1] += py / y_float**current_order + + return cast(tuple[float, float, float, float], tuple(inverse_moments)) + + def _result_raw_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = 4, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute raw moments up to ``max_order`` for ``X / Y``.""" + order = self._validate_moment_order(max_order) + left_raw = self._parent_raw_moments( + sources, + _LEFT_ROLE, + max_order=order, + **options, + ) + right_inverse = self._right_inverse_moments( + sources, + max_order=order, + **options, + ) + output = [0.0, 0.0, 0.0, 0.0] + for idx in range(order): + output[idx] = left_raw[idx] * right_inverse[idx] + return output[0], output[1], output[2], output[3] + + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """Build transformed characteristic function.""" + left_cf = sources[_LEFT_ROLE][CharacteristicName.CF] + kind = getattr(self.left_distribution.distribution_type, "kind", None) + + if kind == Kind.CONTINUOUS: + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + if right_left <= 0.0 <= right_right: + raise RuntimeError( + "Characteristic function for division requires denominator support " + "that does not cross zero." + ) + + def _cf_scalar_continuous(t: float, **options: Any) -> complex: + def _integrand(y: float) -> complex: + return self._eval_method_scalar_complex( + left_cf, t / y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + return self._integrate_complex(_integrand, right_left, right_right) + + def _cf_continuous( + data: NumericArray, + **options: Any, + ) -> ComplexArray: + return self._map_scalar_complex( + data, + lambda t: _cf_scalar_continuous(t, **options), + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf_continuous) + + right_pmf = sources[_RIGHT_ROLE][CharacteristicName.PMF] + right_points = self._discrete_points_for_role(_RIGHT_ROLE) + + def _cf_scalar_discrete(t: float, **options: Any) -> complex: + total = 0.0j + for y in right_points: + y_float = float(y) + if y_float == 0.0: + continue + py = self._eval_method_scalar(right_pmf, y_float, **options) + total += self._eval_method_scalar_complex(left_cf, t / y_float, **options) * py + return total + + def _cf_discrete(data: NumericArray, **options: Any) -> ComplexArray: + return self._map_scalar_complex( + data, + lambda t: _cf_scalar_discrete(t, **options), + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf_discrete) + + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous PDF.""" + left_pdf = sources[_LEFT_ROLE][CharacteristicName.PDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + if right_left <= 0.0 <= right_right: + raise RuntimeError( + "Continuous division PDF requires denominator support that does not cross zero." + ) + + def _pdf_scalar(z: float, **options: Any) -> float: + def _integrand(y: float) -> float: + return ( + abs(y) + * self._eval_method_scalar(left_pdf, z * y, **options) + * self._eval_method_scalar(right_pdf, y, **options) + ) + + return self._integrate_real(_integrand, right_left, right_right) + + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _pdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _pdf) + + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous CDF.""" + left_cdf = sources[_LEFT_ROLE][CharacteristicName.CDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _cdf_scalar(z: float, **options: Any) -> float: + negative_left = right_left + negative_right = min(right_right, 0.0) + positive_left = max(right_left, 0.0) + positive_right = right_right + + negative = 0.0 + if negative_left < negative_right: + + def _neg_integrand(y: float) -> float: + return ( + 1.0 - self._eval_method_scalar(left_cdf, z * y, **options) + ) * self._eval_method_scalar(right_pdf, y, **options) + + negative = self._integrate_real(_neg_integrand, negative_left, negative_right) + + positive = 0.0 + if positive_left < positive_right: + + def _pos_integrand(y: float) -> float: + return self._eval_method_scalar( + left_cdf, z * y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + positive = self._integrate_real(_pos_integrand, positive_left, positive_right) + + return float(np.clip(negative + positive, 0.0, 1.0)) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _cdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + +__all__ = [ + "DivisionBinaryDistribution", +] diff --git a/src/pysatl_core/transformations/operations/binary/linear.py b/src/pysatl_core/transformations/operations/binary/linear.py new file mode 100644 index 0000000..f5ed5be --- /dev/null +++ b/src/pysatl_core/transformations/operations/binary/linear.py @@ -0,0 +1,344 @@ +""" +Linear binary transformations: addition and subtraction. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from math import comb +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.distribution import _KEEP +from pysatl_core.distributions.support import ContinuousSupport, Support +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, +) +from pysatl_core.types import ( + BinaryOperationName, + CharacteristicName, + ComplexArray, + ComputationFunc, + GenericCharacteristicName, + Kind, + NumericArray, +) + +from .base import ( + _LEFT_ROLE, + _RIGHT_ROLE, + BinaryDistribution, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + +_SUPPORTED_LINEAR_OPERATIONS: frozenset[BinaryOperationName] = frozenset( + {BinaryOperationName.ADD, BinaryOperationName.SUB} +) + + +class LinearBinaryDistribution(BinaryDistribution): + """ + Binary distribution for linear operations ``X + Y`` and ``X - Y``. + """ + + def __init__( + self, + left_distribution: Distribution, + right_distribution: Distribution, + *, + operation: BinaryOperationName, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + if operation not in _SUPPORTED_LINEAR_OPERATIONS: + raise ValueError( + f"Unsupported linear operation '{operation}'. " + f"Supported operations: {', '.join(sorted(_SUPPORTED_LINEAR_OPERATIONS))}." + ) + super().__init__( + left_distribution=left_distribution, + right_distribution=right_distribution, + operation=operation, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + ) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> LinearBinaryDistribution: + """Return a copy of the linear binary distribution with updated strategies.""" + return LinearBinaryDistribution( + left_distribution=self.left_distribution, + right_distribution=self.right_distribution, + operation=self.operation, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + def _characteristic_specs( + self, *, kind: Kind | None + ) -> tuple[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]], + ..., + ]: + """Return characteristic registration specs for linear transformations.""" + specs: list[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]] + ] = [ + ( + CharacteristicName.CF, + self._requirements_both(CharacteristicName.CF), + self._make_cf, + ), + ( + CharacteristicName.MEAN, + self._requirements_both(*self._statistics_for_moment_order(1)), + self._make_mean, + ), + ( + CharacteristicName.VAR, + self._requirements_both(*self._statistics_for_moment_order(2)), + self._make_var, + ), + ( + CharacteristicName.SKEW, + self._requirements_both(*self._statistics_for_moment_order(3)), + self._make_skew, + ), + ( + CharacteristicName.KURT, + self._requirements_both(*self._statistics_for_moment_order(4)), + self._make_kurt, + ), + ] + + if kind == Kind.CONTINUOUS: + specs.extend( + [ + ( + CharacteristicName.CDF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_cdf, + ), + ( + CharacteristicName.PDF, + self._requirements_both(CharacteristicName.PDF), + self._make_continuous_pdf, + ), + ( + CharacteristicName.PPF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_ppf, + ), + ] + ) + else: + discrete_requirements = self._requirements_both(CharacteristicName.PMF) + specs.extend( + [ + (CharacteristicName.PMF, discrete_requirements, self._make_discrete_pmf), + (CharacteristicName.CDF, discrete_requirements, self._make_discrete_cdf), + (CharacteristicName.PPF, discrete_requirements, self._make_discrete_ppf), + ] + ) + + return tuple(specs) + + def _operation_value( + self, + left: float | NumericArray, + right: float | NumericArray, + ) -> float | NumericArray: + """Apply linear operation in scalar or array semantics.""" + left_array = np.asarray(left, dtype=float) + right_array = np.asarray(right, dtype=float) + if self.operation == BinaryOperationName.ADD: + result = left_array + right_array + else: + result = left_array - right_array + + if result.ndim == 0: + return float(result) + return cast(NumericArray, result) + + def _result_raw_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = 4, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute raw moments up to ``max_order`` for ``X ± Y``.""" + order = self._validate_moment_order(max_order) + left_raw = self._parent_raw_moments( + sources, + _LEFT_ROLE, + max_order=order, + **options, + ) + right_raw = self._parent_raw_moments( + sources, + _RIGHT_ROLE, + max_order=order, + **options, + ) + sign = 1.0 if self.operation == BinaryOperationName.ADD else -1.0 + + left_poly = (1.0, *left_raw) + right_poly = ( + 1.0, + sign * right_raw[0], + right_raw[1], + sign * right_raw[2], + right_raw[3], + ) + + output = [0.0, 0.0, 0.0, 0.0] + for current_order in range(1, order + 1): + moment = 0.0 + for i in range(current_order + 1): + moment += comb(current_order, i) * left_poly[i] * right_poly[current_order - i] + output[current_order - 1] = moment + return output[0], output[1], output[2], output[3] + + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """Build transformed characteristic function.""" + left_cf = sources[_LEFT_ROLE][CharacteristicName.CF] + right_cf = sources[_RIGHT_ROLE][CharacteristicName.CF] + + def _cf_scalar(t: float, **options: Any) -> complex: + left_value = self._eval_method_scalar_complex(left_cf, t, **options) + right_arg = t if self.operation == BinaryOperationName.ADD else -t + right_value = self._eval_method_scalar_complex(right_cf, right_arg, **options) + return left_value * right_value + + def _cf(data: NumericArray, **options: Any) -> ComplexArray: + return self._map_scalar_complex( + data, + lambda t: _cf_scalar(t, **options), + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf) + + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous PDF.""" + left_pdf = sources[_LEFT_ROLE][CharacteristicName.PDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _pdf_scalar(z: float, **options: Any) -> float: + if self.operation == BinaryOperationName.ADD: + + def _integrand_add(y: float) -> float: + return self._eval_method_scalar( + left_pdf, z - y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + return self._integrate_real(_integrand_add, right_left, right_right) + + def _integrand_sub(y: float) -> float: + return self._eval_method_scalar( + left_pdf, z + y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + return self._integrate_real(_integrand_sub, right_left, right_right) + + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _pdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _pdf) + + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous CDF.""" + left_cdf = sources[_LEFT_ROLE][CharacteristicName.CDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _cdf_scalar(z: float, **options: Any) -> float: + if self.operation == BinaryOperationName.ADD: + + def _integrand_add(y: float) -> float: + return self._eval_method_scalar( + left_cdf, z - y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + value = self._integrate_real(_integrand_add, right_left, right_right) + return float(np.clip(value, 0.0, 1.0)) + + def _integrand_sub(y: float) -> float: + return self._eval_method_scalar( + left_cdf, z + y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + value = self._integrate_real(_integrand_sub, right_left, right_right) + return float(np.clip(value, 0.0, 1.0)) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _cdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _transform_support( + self, + left_support: Support | None, + right_support: Support | None, + ) -> Support | None: + """Transform support metadata for linear operations.""" + if isinstance(left_support, ContinuousSupport) and isinstance( + right_support, ContinuousSupport + ): + if self.operation == BinaryOperationName.ADD: + return ContinuousSupport( + left=float(left_support.left + right_support.left), + right=float(left_support.right + right_support.right), + left_closed=left_support.left_closed and right_support.left_closed, + right_closed=left_support.right_closed and right_support.right_closed, + ) + return ContinuousSupport( + left=float(left_support.left - right_support.right), + right=float(left_support.right - right_support.left), + left_closed=left_support.left_closed and right_support.right_closed, + right_closed=left_support.right_closed and right_support.left_closed, + ) + return super()._transform_support(left_support, right_support) + + +__all__ = [ + "LinearBinaryDistribution", +] diff --git a/src/pysatl_core/transformations/operations/binary/multiplication.py b/src/pysatl_core/transformations/operations/binary/multiplication.py new file mode 100644 index 0000000..1666f81 --- /dev/null +++ b/src/pysatl_core/transformations/operations/binary/multiplication.py @@ -0,0 +1,334 @@ +""" +Multiplicative binary transformation ``X * Y``. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.distribution import _KEEP +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, +) +from pysatl_core.types import ( + BinaryOperationName, + CharacteristicName, + ComplexArray, + ComputationFunc, + GenericCharacteristicName, + Kind, + NumericArray, +) + +from .base import ( + _LEFT_ROLE, + _RIGHT_ROLE, + BinaryDistribution, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + + +class MultiplicationBinaryDistribution(BinaryDistribution): + """ + Binary distribution for multiplicative transformation ``X * Y``. + """ + + def __init__( + self, + left_distribution: Distribution, + right_distribution: Distribution, + *, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + super().__init__( + left_distribution=left_distribution, + right_distribution=right_distribution, + operation=BinaryOperationName.MUL, + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + ) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> MultiplicationBinaryDistribution: + """Return a copy of the multiplication binary distribution with updated strategies.""" + return MultiplicationBinaryDistribution( + left_distribution=self.left_distribution, + right_distribution=self.right_distribution, + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + def _characteristic_specs( + self, *, kind: Kind | None + ) -> tuple[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]], + ..., + ]: + """Return characteristic registration specs for multiplication.""" + density = self._density_characteristic(kind) + specs: list[ + tuple[GenericCharacteristicName, SourceRequirements, TransformationEvaluator[Any, Any]] + ] = [ + ( + CharacteristicName.CF, + { + _LEFT_ROLE: (CharacteristicName.CF,), + _RIGHT_ROLE: (density,), + }, + self._make_cf, + ), + ( + CharacteristicName.MEAN, + self._requirements_both(*self._statistics_for_moment_order(1)), + self._make_mean, + ), + ( + CharacteristicName.VAR, + self._requirements_both(*self._statistics_for_moment_order(2)), + self._make_var, + ), + ( + CharacteristicName.SKEW, + self._requirements_both(*self._statistics_for_moment_order(3)), + self._make_skew, + ), + ( + CharacteristicName.KURT, + self._requirements_both(*self._statistics_for_moment_order(4)), + self._make_kurt, + ), + ] + + if kind == Kind.CONTINUOUS: + specs.extend( + [ + ( + CharacteristicName.CDF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_cdf, + ), + ( + CharacteristicName.PDF, + self._requirements_both(CharacteristicName.PDF), + self._make_continuous_pdf, + ), + ( + CharacteristicName.PPF, + { + _LEFT_ROLE: (CharacteristicName.CDF,), + _RIGHT_ROLE: (CharacteristicName.PDF,), + }, + self._make_continuous_ppf, + ), + ] + ) + else: + discrete_requirements = self._requirements_both(CharacteristicName.PMF) + specs.extend( + [ + (CharacteristicName.PMF, discrete_requirements, self._make_discrete_pmf), + (CharacteristicName.CDF, discrete_requirements, self._make_discrete_cdf), + (CharacteristicName.PPF, discrete_requirements, self._make_discrete_ppf), + ] + ) + + return tuple(specs) + + def _operation_value( + self, + left: float | NumericArray, + right: float | NumericArray, + ) -> float | NumericArray: + """Apply multiplication in scalar or array semantics.""" + left_array = np.asarray(left, dtype=float) + right_array = np.asarray(right, dtype=float) + result = left_array * right_array + if result.ndim == 0: + return float(result) + return cast(NumericArray, result) + + def _result_raw_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = 4, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute raw moments up to ``max_order`` for ``X * Y``.""" + order = self._validate_moment_order(max_order) + left_raw = self._parent_raw_moments( + sources, + _LEFT_ROLE, + max_order=order, + **options, + ) + right_raw = self._parent_raw_moments( + sources, + _RIGHT_ROLE, + max_order=order, + **options, + ) + + output = [0.0, 0.0, 0.0, 0.0] + for idx in range(order): + output[idx] = left_raw[idx] * right_raw[idx] + return output[0], output[1], output[2], output[3] + + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """Build transformed characteristic function.""" + left_cf = sources[_LEFT_ROLE][CharacteristicName.CF] + kind = getattr(self.left_distribution.distribution_type, "kind", None) + + if kind == Kind.CONTINUOUS: + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _cf_scalar_continuous(t: float, **options: Any) -> complex: + def _integrand(y: float) -> complex: + return self._eval_method_scalar_complex( + left_cf, t * y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + return self._integrate_complex(_integrand, right_left, right_right) + + def _cf_continuous( + data: NumericArray, + **options: Any, + ) -> ComplexArray: + return self._map_scalar_complex( + data, + lambda t: _cf_scalar_continuous(t, **options), + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf_continuous) + + right_pmf = sources[_RIGHT_ROLE][CharacteristicName.PMF] + right_points = self._discrete_points_for_role(_RIGHT_ROLE) + + def _cf_scalar_discrete(t: float, **options: Any) -> complex: + total = 0.0j + for y in right_points: + y_float = float(y) + py = self._eval_method_scalar(right_pmf, y_float, **options) + total += self._eval_method_scalar_complex(left_cf, t * y_float, **options) * py + return total + + def _cf_discrete(data: NumericArray, **options: Any) -> ComplexArray: + return self._map_scalar_complex( + data, + lambda t: _cf_scalar_discrete(t, **options), + ) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf_discrete) + + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous PDF.""" + left_pdf = sources[_LEFT_ROLE][CharacteristicName.PDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _pdf_scalar(z: float, **options: Any) -> float: + def _integrand(y: float) -> float: + return ( + self._eval_method_scalar(left_pdf, z / y, **options) + * self._eval_method_scalar(right_pdf, y, **options) + / abs(y) + ) + + return self._integrate_real( + _integrand, + right_left, + right_right, + split_at_zero=True, + ) + + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _pdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _pdf) + + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build transformed continuous CDF.""" + left_cdf = sources[_LEFT_ROLE][CharacteristicName.CDF] + right_pdf = sources[_RIGHT_ROLE][CharacteristicName.PDF] + right_left, right_right = self._continuous_bounds_for_role(_RIGHT_ROLE) + + def _cdf_scalar(z: float, **options: Any) -> float: + negative_left = right_left + negative_right = min(right_right, 0.0) + positive_left = max(right_left, 0.0) + positive_right = right_right + + if negative_right == 0.0: + negative_right = float(np.nextafter(0.0, -1.0)) + if positive_left == 0.0: + positive_left = float(np.nextafter(0.0, 1.0)) + + negative = 0.0 + if negative_left < negative_right: + + def _neg_integrand(y: float) -> float: + return ( + 1.0 - self._eval_method_scalar(left_cdf, z / y, **options) + ) * self._eval_method_scalar(right_pdf, y, **options) + + negative = self._integrate_real(_neg_integrand, negative_left, negative_right) + + positive = 0.0 + if positive_left < positive_right: + + def _pos_integrand(y: float) -> float: + return self._eval_method_scalar( + left_cdf, z / y, **options + ) * self._eval_method_scalar(right_pdf, y, **options) + + positive = self._integrate_real(_pos_integrand, positive_left, positive_right) + + return float(np.clip(negative + positive, 0.0, 1.0)) + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return self._map_scalar_real( + data, + lambda z: _cdf_scalar(z, **options), + ) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + +__all__ = [ + "MultiplicationBinaryDistribution", +] diff --git a/src/pysatl_core/transformations/operations/mixture.py b/src/pysatl_core/transformations/operations/mixture.py new file mode 100644 index 0000000..bd17da2 --- /dev/null +++ b/src/pysatl_core/transformations/operations/mixture.py @@ -0,0 +1,771 @@ +""" +Finite weighted mixture transformation for probability distributions. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from collections.abc import Mapping, Sequence +from math import sqrt +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from pysatl_core.distributions.computation import Method +from pysatl_core.distributions.distribution import _KEEP, Distribution +from pysatl_core.distributions.registry import characteristic_registry +from pysatl_core.distributions.support import ( + ContinuousSupport, + ExplicitTableDiscreteSupport, + Support, +) +from pysatl_core.transformations.distribution import DerivedDistribution +from pysatl_core.transformations.lightweight_distribution import LightweightDistribution +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + SourceRequirements, + TransformationEvaluator, + TransformationMethod, +) +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + CharacteristicName, + ComplexArray, + ComputationFunc, + DistributionType, + GenericCharacteristicName, + Kind, + LabelName, + NumericArray, + ParentRole, + TransformationName, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.strategies import ( + ComputationStrategy, + SamplingStrategy, + ) + +_COMPONENT_ROLE_PREFIX = "component_" +_MAX_MOMENT_ORDER = 4 + + +def _component_role(index: int) -> ParentRole: + """Build deterministic parent role for a mixture component.""" + return cast(ParentRole, f"{_COMPONENT_ROLE_PREFIX}{index}") + + +class FiniteMixtureDistribution(DerivedDistribution): + """ + Distribution obtained as a finite weighted mixture of components. + + Parameters + ---------- + weighted_components : Sequence[tuple[float, Distribution]] + Ordered pairs ``(weight, distribution)``. + Weights must be finite, non-negative and sum to one. + sampling_strategy : SamplingStrategy | None, optional + Sampling strategy exposed by the transformed distribution. + computation_strategy : ComputationStrategy | None, optional + Computation strategy exposed by the transformed distribution. + """ + + def __init__( + self, + weighted_components: Sequence[tuple[float, Distribution]], + *, + sampling_strategy: SamplingStrategy | None = None, + computation_strategy: ComputationStrategy | None = None, + ) -> None: + if not weighted_components: + raise ValueError("Finite mixture requires at least one component distribution.") + + components = tuple(component for _, component in weighted_components) + weights = [float(weight) for weight, _ in weighted_components] + + component_snapshots = tuple( + LightweightDistribution.from_distribution(component) for component in components + ) + validated_weights = self._validate_weights(weights) + roles = tuple(_component_role(index) for index in range(len(component_snapshots))) + + self._components = component_snapshots + self._weights = validated_weights + self._roles = roles + self._role_to_index = {role: index for index, role in enumerate(roles)} + self._cached_discrete_mass_table: tuple[NumericArray, NumericArray, NumericArray] | None = ( + None + ) + + distribution_type = self._validate_distribution_types( + [component.distribution_type for component in component_snapshots] + ) + self._discrete_support = self._build_discrete_support() + self._continuous_support = self._build_continuous_support() + + self._discrete_points = ( + cast(NumericArray, np.asarray(self._discrete_support.points, dtype=float)) + if self._discrete_support is not None + else None + ) + + bases: dict[ParentRole, LightweightDistribution] = dict( + zip(roles, component_snapshots, strict=True) + ) + analytical_computations, loop_analytical_flags = self._build_analytical_computations( + distribution_type=distribution_type, + bases=bases, + ) + + super().__init__( + distribution_type=distribution_type, + bases=bases, + analytical_computations=analytical_computations, + transformation_name=TransformationName.FINITE_MIXTURE, + support=self._transform_support(distribution_type), + sampling_strategy=sampling_strategy, + computation_strategy=computation_strategy, + loop_analytical_flags=loop_analytical_flags, + ) + + @property + def components(self) -> tuple[LightweightDistribution, ...]: + """Get the lightweight component snapshots.""" + return self._components + + @property + def weights(self) -> NumericArray: + """Get validated component weights.""" + return cast(NumericArray, np.array(self._weights, copy=True)) + + def sample(self, n: int, **options: Any) -> NumericArray: + """ + Generate mixture samples by sampling selected components. + + A component index is sampled for each draw according to the + validated mixture weights, then each component is sampled only + for the number of selected draws. + """ + rng = np.random.default_rng() + component_indices = np.asarray(rng.choice(len(self._components), size=n, p=self._weights)) + samples = np.empty(component_indices.shape, dtype=float) + + for index, component in enumerate(self.components): + selected_positions = np.nonzero(component_indices == index)[0] + selected_count = int(selected_positions.size) + if selected_count == 0: + continue + + component_samples = np.asarray( + self.sampling_strategy.sample(selected_count, distr=component, **options), + dtype=float, + ).reshape(-1) + if component_samples.size != selected_count: + raise RuntimeError( + "Component sampler returned incompatible sample shape for finite mixture." + ) + samples[selected_positions] = component_samples + + return cast(NumericArray, samples) + + def _clone_with_strategies( + self, + *, + sampling_strategy: SamplingStrategy | None | object = _KEEP, + computation_strategy: ComputationStrategy | None | object = _KEEP, + ) -> FiniteMixtureDistribution: + """Return a copy of the mixture distribution with updated strategies.""" + return FiniteMixtureDistribution( + weighted_components=[ + (float(weight), component) + for weight, component in zip(self._weights, self.components, strict=True) + ], + sampling_strategy=self._new_sampling_strategy(sampling_strategy), + computation_strategy=self._new_computation_strategy(computation_strategy), + ) + + @staticmethod + def _validate_weights(weights: Sequence[float]) -> NumericArray: + """Validate mixture weights.""" + validated = np.asarray(weights, dtype=float) + if validated.ndim != 1: + raise ValueError("Mixture weights must be a one-dimensional sequence.") + if np.any(~np.isfinite(validated)): + raise ValueError("Mixture weights must be finite numbers.") + if np.any(validated < 0.0): + raise ValueError("Mixture weights must be non-negative.") + + total = float(np.sum(validated)) + if not np.isclose(total, 1.0, rtol=1e-12, atol=1e-12): + raise ValueError(f"Sum of mixture weights must be equal to 1.0, got {total}.") + return cast(NumericArray, np.array(validated, copy=True)) + + @staticmethod + def _validate_distribution_types( + distribution_types: Sequence[DistributionType], + ) -> DistributionType: + """Validate component distribution type compatibility.""" + first = distribution_types[0] + first_dimension = getattr(first, "dimension", None) + first_kind = getattr(first, "kind", None) + + if first_dimension != 1: + raise TypeError("Finite mixture currently supports only one-dimensional distributions.") + if first_kind not in {Kind.CONTINUOUS, Kind.DISCRETE}: + raise TypeError("Unsupported distribution kind for finite mixture.") + + for distribution_type in distribution_types[1:]: + dimension = getattr(distribution_type, "dimension", None) + kind = getattr(distribution_type, "kind", None) + if dimension != first_dimension: + raise TypeError("Finite mixture requires components with equal dimension.") + if kind != first_kind: + raise TypeError("Finite mixture requires components of the same distribution kind.") + + return first + + def _build_discrete_support(self) -> ExplicitTableDiscreteSupport | None: + """Build explicit discrete support union, if available.""" + points_blocks: list[NumericArray] = [] + for component in self.components: + support = component.support + if not isinstance(support, ExplicitTableDiscreteSupport): + return None + points_blocks.append(cast(NumericArray, np.asarray(support.points, dtype=float))) + + if not points_blocks: + return None + merged = np.unique(np.concatenate(points_blocks)).tolist() + return ExplicitTableDiscreteSupport(points=merged, assume_sorted=True) + + def _build_continuous_support(self) -> ContinuousSupport | None: + """Build continuous support envelope, if available.""" + supports: list[ContinuousSupport] = [] + for component in self.components: + support = component.support + if not isinstance(support, ContinuousSupport): + return None + supports.append(support) + + left = min(float(support.left) for support in supports) + right = max(float(support.right) for support in supports) + left_closed = any( + float(support.left) == left and support.left_closed for support in supports + ) + right_closed = any( + float(support.right) == right and support.right_closed for support in supports + ) + return ContinuousSupport( + left=left, + right=right, + left_closed=left_closed, + right_closed=right_closed, + ) + + def _transform_support(self, distribution_type: DistributionType) -> Support | None: + """Transform support metadata for the mixture.""" + kind = getattr(distribution_type, "kind", None) + + if kind == Kind.CONTINUOUS: + return self._continuous_support + + if kind == Kind.DISCRETE: + return self._discrete_support + + return None + + def _requirements_for_all( + self, + *characteristics: GenericCharacteristicName, + ) -> SourceRequirements: + """Build source requirements for all component roles.""" + return {role: tuple(characteristics) for role in self._roles} + + @staticmethod + def _validate_moment_order(max_order: int) -> int: + """Validate and normalize required moment order.""" + if not 1 <= max_order <= _MAX_MOMENT_ORDER: + raise ValueError(f"Moment order must be in [1, {_MAX_MOMENT_ORDER}], got {max_order}.") + return max_order + + @classmethod + def _statistics_for_moment_order( + cls, + max_order: int, + ) -> tuple[GenericCharacteristicName, ...]: + """Return component statistics needed to recover raw moments up to ``max_order``.""" + order = cls._validate_moment_order(max_order) + if order == 1: + return (CharacteristicName.MEAN,) + if order == 2: + return CharacteristicName.MEAN, CharacteristicName.VAR + if order == 3: + return ( + CharacteristicName.MEAN, + CharacteristicName.VAR, + CharacteristicName.SKEW, + ) + return ( + CharacteristicName.MEAN, + CharacteristicName.VAR, + CharacteristicName.SKEW, + CharacteristicName.KURT, + ) + + def _build_analytical_computations( + self, + *, + distribution_type: DistributionType, + bases: Mapping[ParentRole, LightweightDistribution], + ) -> tuple[ + Mapping[GenericCharacteristicName, Mapping[LabelName, TransformationMethod[Any, Any]]], + Mapping[GenericCharacteristicName, Mapping[LabelName, bool]], + ]: + """Build analytical computations for finite mixture transformation.""" + computations: dict[ + GenericCharacteristicName, dict[LabelName, TransformationMethod[Any, Any]] + ] = {} + loop_analytical_flags: dict[GenericCharacteristicName, dict[LabelName, bool]] = {} + kind = getattr(distribution_type, "kind", None) + declared_registry_characteristics = characteristic_registry().declared_characteristics + + def _register( + target: GenericCharacteristicName, + source_requirements: SourceRequirements, + evaluator: TransformationEvaluator[Any, Any], + ) -> None: + # Non-registry characteristics cannot be resolved via graph fitters. + # For them we require direct component presence for each required source. + for role, characteristics in source_requirements.items(): + base = bases[role] + for characteristic in characteristics: + if characteristic in declared_registry_characteristics: + continue + if characteristic not in base.analytical_computations: + return + + method, is_analytical, has_any_present_source = TransformationMethod.try_from_parents( + target=target, + transformation=TransformationName.FINITE_MIXTURE, + bases=bases, + source_requirements=source_requirements, + evaluator=evaluator, + ) + if method is None or not has_any_present_source: + return + computations[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: method} + loop_analytical_flags[target] = {DEFAULT_ANALYTICAL_COMPUTATION_LABEL: is_analytical} + + _register( + CharacteristicName.CF, + self._requirements_for_all(CharacteristicName.CF), + self._make_cf, + ) + _register( + CharacteristicName.MEAN, + self._requirements_for_all(*self._statistics_for_moment_order(1)), + self._make_mean, + ) + _register( + CharacteristicName.VAR, + self._requirements_for_all(*self._statistics_for_moment_order(2)), + self._make_var, + ) + _register( + CharacteristicName.SKEW, + self._requirements_for_all(*self._statistics_for_moment_order(3)), + self._make_skew, + ) + _register( + CharacteristicName.KURT, + self._requirements_for_all(*self._statistics_for_moment_order(4)), + self._make_kurt, + ) + + if kind == Kind.CONTINUOUS: + _register( + CharacteristicName.CDF, + self._requirements_for_all(CharacteristicName.CDF), + self._make_continuous_cdf, + ) + _register( + CharacteristicName.PDF, + self._requirements_for_all(CharacteristicName.PDF), + self._make_continuous_pdf, + ) + elif kind == Kind.DISCRETE: + _register( + CharacteristicName.PMF, + self._requirements_for_all(CharacteristicName.PMF), + self._make_discrete_pmf, + ) + _register( + CharacteristicName.CDF, + self._requirements_for_all(CharacteristicName.PMF), + self._make_discrete_cdf, + ) + _register( + CharacteristicName.PPF, + self._requirements_for_all(CharacteristicName.PMF), + self._make_discrete_ppf, + ) + else: + raise TypeError("Unsupported distribution kind for finite mixture.") + + if computations: + return computations, loop_analytical_flags + + raise RuntimeError( + "Finite mixture produced no analytical computations. " + "At least one source characteristic must be present." + ) + + @staticmethod + def _eval_nullary_scalar(method: Method[Any, Any], **options: Any) -> float: + """Evaluate nullary method and cast to scalar float.""" + return float(np.asarray(method(**options), dtype=float)) + + @staticmethod + def _kurt_raw_from_method(method: Method[Any, Any], **options: Any) -> float: + """Evaluate raw kurtosis from a method with optional ``excess`` support.""" + try: + value = method(excess=False, **options) + except TypeError: + value = method(**options) + return float(np.asarray(value, dtype=float)) + + @staticmethod + def _raw_moments_from_statistics( + mean: float, + variance: float, + skewness: float, + raw_kurtosis: float, + ) -> tuple[float, float, float, float]: + """Convert mean/variance/skewness/kurtosis to raw moments up to order four.""" + variance_safe = max(variance, 0.0) + std = sqrt(variance_safe) + mu3 = skewness * std**3 + mu4 = raw_kurtosis * variance_safe**2 + + m1 = mean + m2 = variance_safe + mean**2 + m3 = mu3 + 3.0 * mean * variance_safe + mean**3 + m4 = mu4 + 4.0 * mean * mu3 + 6.0 * mean**2 * variance_safe + mean**4 + return m1, m2, m3, m4 + + @staticmethod + def _central_moments_from_raw( + m1: float, + m2: float, + m3: float, + m4: float, + ) -> tuple[float, float, float]: + """Convert raw moments to central moments ``(var, mu3, mu4)``.""" + variance = max(m2 - m1**2, 0.0) + mu3 = m3 - 3.0 * m1 * m2 + 2.0 * m1**3 + mu4 = m4 - 4.0 * m1 * m3 + 6.0 * m1**2 * m2 - 3.0 * m1**4 + return variance, mu3, mu4 + + def _result_raw_moments( + self, + sources: ResolvedSourceMethods, + *, + max_order: int = _MAX_MOMENT_ORDER, + **options: Any, + ) -> tuple[float, float, float, float]: + """Compute mixture raw moments up to ``max_order``.""" + order = self._validate_moment_order(max_order) + m1_total = 0.0 + m2_total = 0.0 + m3_total = 0.0 + m4_total = 0.0 + + for index, role in enumerate(self._roles): + methods = sources[role] + mean = self._eval_nullary_scalar(methods[CharacteristicName.MEAN], **options) + m1 = mean + m2 = 0.0 + m3 = 0.0 + m4 = 0.0 + + if order >= 2: + variance = self._eval_nullary_scalar(methods[CharacteristicName.VAR], **options) + m2 = max(variance, 0.0) + mean**2 + else: + variance = 0.0 + + skewness = 0.0 + if order >= 3: + skewness = self._eval_nullary_scalar(methods[CharacteristicName.SKEW], **options) + _, _, m3, _ = self._raw_moments_from_statistics(mean, variance, skewness, 3.0) + + if order == 4: + raw_kurtosis = self._kurt_raw_from_method( + methods[CharacteristicName.KURT], + **options, + ) + _, _, _, m4 = self._raw_moments_from_statistics( + mean, + variance, + skewness, + raw_kurtosis, + ) + + weight = float(self._weights[index]) + m1_total += weight * m1 + if order >= 2: + m2_total += weight * m2 + if order >= 3: + m3_total += weight * m3 + if order == 4: + m4_total += weight * m4 + + return m1_total, m2_total, m3_total, m4_total + + def _make_mean(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build mixture mean.""" + + def _mean(**options: Any) -> float: + m1, _, _, _ = self._result_raw_moments(sources, max_order=1, **options) + return m1 + + return _mean + + def _make_var(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build mixture variance.""" + + def _var(**options: Any) -> float: + m1, m2, _, _ = self._result_raw_moments(sources, max_order=2, **options) + return max(m2 - m1**2, 0.0) + + return _var + + def _make_skew(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build mixture skewness.""" + + def _skew(**options: Any) -> float: + m1, m2, m3, _ = self._result_raw_moments(sources, max_order=3, **options) + variance, mu3, _ = self._central_moments_from_raw(m1, m2, m3, 0.0) + if variance <= 0.0: + return 0.0 + return float(mu3 / variance**1.5) + + return _skew + + def _make_kurt(self, sources: ResolvedSourceMethods) -> ComputationFunc[Any, float]: + """Build mixture raw or excess kurtosis.""" + + def _kurt(*, excess: bool = False, **options: Any) -> float: + m1, m2, m3, m4 = self._result_raw_moments(sources, max_order=4, **options) + variance, _, mu4 = self._central_moments_from_raw(m1, m2, m3, m4) + raw = 3.0 if variance <= 0.0 else mu4 / variance**2 + return raw - 3.0 if excess else raw + + return _kurt + + def _make_cf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, ComplexArray]: + """Build mixture characteristic function.""" + methods = [ + cast(Method[NumericArray, ComplexArray], sources[role][CharacteristicName.CF]) + for role in self._roles + ] + + def _cf(data: NumericArray, **options: Any) -> ComplexArray: + array = np.asarray(data, dtype=float) + result = np.zeros(array.shape, dtype=complex) + for weight, method in zip(self._weights, methods, strict=True): + result += float(weight) * np.asarray(method(array, **options), dtype=complex) + return cast(ComplexArray, result) + + return cast(ComputationFunc[NumericArray, ComplexArray], _cf) + + def _make_continuous_pdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build mixture PDF for continuous components.""" + methods = [ + cast(Method[NumericArray, NumericArray], sources[role][CharacteristicName.PDF]) + for role in self._roles + ] + + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + result = np.zeros(array.shape, dtype=float) + for weight, method in zip(self._weights, methods, strict=True): + result += float(weight) * np.asarray(method(array, **options), dtype=float) + return cast(NumericArray, result) + + return cast(ComputationFunc[NumericArray, NumericArray], _pdf) + + def _make_continuous_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build mixture CDF for continuous components.""" + methods = [ + cast(Method[NumericArray, NumericArray], sources[role][CharacteristicName.CDF]) + for role in self._roles + ] + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + result = np.zeros(array.shape, dtype=float) + for weight, method in zip(self._weights, methods, strict=True): + result += float(weight) * np.asarray(method(array, **options), dtype=float) + return cast(NumericArray, np.clip(result, 0.0, 1.0)) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _discrete_mass_table( + self, + sources: ResolvedSourceMethods, + **options: Any, + ) -> tuple[NumericArray, NumericArray, NumericArray]: + """Build normalized finite PMF table for discrete mixture.""" + if not options and self._cached_discrete_mass_table is not None: + return self._cached_discrete_mass_table + + if self._discrete_points is None: + raise RuntimeError( + "Discrete finite mixture requires ExplicitTableDiscreteSupport " + "for every component." + ) + + methods = [ + cast(Method[NumericArray, NumericArray], sources[role][CharacteristicName.PMF]) + for role in self._roles + ] + points = self._discrete_points + masses = np.zeros(points.shape, dtype=float) + for weight, method in zip(self._weights, methods, strict=True): + masses += float(weight) * np.asarray(method(points, **options), dtype=float) + + masses = np.clip(masses, 0.0, None) + total = float(np.sum(masses)) + if total <= 0.0: + raise RuntimeError("Discrete finite mixture produced non-positive total mass.") + + masses = cast(NumericArray, masses / total) + cdf_values = cast(NumericArray, np.cumsum(masses, dtype=float)) + cdf_values[-1] = 1.0 + output = (points, masses, cdf_values) + if not options: + self._cached_discrete_mass_table = output + return output + + def _make_discrete_pmf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build mixture PMF for discrete components.""" + + def _pmf(data: NumericArray, **options: Any) -> NumericArray: + points, masses, _ = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + + indices = np.searchsorted(points, flat, side="left") + values = np.zeros_like(flat, dtype=float) + + in_range = indices < points.size + if np.any(in_range): + in_range_indices = indices[in_range] + close = np.isclose(points[in_range_indices], flat[in_range], atol=1e-12, rtol=0.0) + if np.any(close): + values[np.where(in_range)[0][close]] = masses[in_range_indices[close]] + + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _pmf) + + def _make_discrete_cdf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build mixture CDF for discrete components.""" + + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + points, _, cdf_values = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + + indices = np.searchsorted(points, flat, side="right") - 1 + values = np.zeros_like(flat, dtype=float) + + valid = indices >= 0 + if np.any(valid): + clipped = np.minimum(indices[valid], cdf_values.size - 1) + values[valid] = cdf_values[clipped] + + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _cdf) + + def _make_discrete_ppf( + self, + sources: ResolvedSourceMethods, + ) -> ComputationFunc[NumericArray, NumericArray]: + """Build mixture PPF for discrete components.""" + + def _ppf(data: NumericArray, **options: Any) -> NumericArray: + points, _, cdf_values = self._discrete_mass_table(sources, **options) + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + if np.any((flat < 0.0) | (flat > 1.0)): + raise ValueError("PPF input must be in [0, 1].") + + indices = np.searchsorted(cdf_values, flat, side="left") + clipped = np.clip(indices, 0, points.size - 1) + values = points[clipped] + return cast(NumericArray, values.reshape(array.shape)) + + return cast(ComputationFunc[NumericArray, NumericArray], _ppf) + + +def finite_mixture( + weighted_components: Sequence[tuple[float, Distribution]], +) -> FiniteMixtureDistribution: + """ + Build a finite weighted mixture distribution. + + Parameters + ---------- + weighted_components : Sequence[tuple[float, Distribution]] + Ordered component pairs ``(weight, distribution)``. + + Returns + ------- + FiniteMixtureDistribution + Mixture distribution. + """ + return FiniteMixtureDistribution( + weighted_components=weighted_components, + ) + + +def discrete_mixture( + weighted_components: Sequence[tuple[float, Distribution]], +) -> FiniteMixtureDistribution: + """ + Build a finite mixture with a discrete set of component weights. + + This is an alias of :func:`finite_mixture`. + """ + return finite_mixture( + weighted_components=weighted_components, + ) + + +__all__ = [ + "FiniteMixtureDistribution", + "discrete_mixture", + "finite_mixture", +] diff --git a/src/pysatl_core/transformations/operators_mixin.py b/src/pysatl_core/transformations/operators_mixin.py new file mode 100644 index 0000000..a47902a --- /dev/null +++ b/src/pysatl_core/transformations/operators_mixin.py @@ -0,0 +1,144 @@ +""" +Operator mixin for transformation-enabled distributions. + +This mixin provides arithmetic operators implemented through +transformation primitives. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from numbers import Real +from types import NotImplementedType +from typing import TYPE_CHECKING, cast + +from pysatl_core.types import BinaryOperationName + +if TYPE_CHECKING: + from pysatl_core.distributions.distribution import Distribution + + +class TransformationOperatorsMixin: + """ + Mixin adding affine and binary arithmetic operators to distributions. + """ + + def _affine_transform(self, *, scale: float, shift: float) -> Distribution: + """ + Apply affine transformation ``Y = scale * X + shift``. + """ + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.affine import affine + + return affine(cast(Distribution, self), scale=scale, shift=shift) + + def _binary_transform( + self, + other: Distribution, + *, + operation: BinaryOperationName, + ) -> Distribution: + """ + Apply binary transformation between two distributions. + """ + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.binary import binary + + return binary(cast(Distribution, self), other, operation=operation) + + def __add__(self, other: object) -> Distribution | NotImplementedType: + """Return ``self + other`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + + if isinstance(other, Real): + return self._affine_transform(scale=1.0, shift=float(other)) + if isinstance(other, Distribution): + return self._binary_transform(other, operation=BinaryOperationName.ADD) + return NotImplemented + + def __radd__(self, other: object) -> Distribution | NotImplementedType: + """Return ``other + self`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.binary import binary + + if isinstance(other, Real): + return self._affine_transform(scale=1.0, shift=float(other)) + if isinstance(other, Distribution): + return binary(other, cast(Distribution, self), operation=BinaryOperationName.ADD) + return NotImplemented + + def __sub__(self, other: object) -> Distribution | NotImplementedType: + """Return ``self - other`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + + if isinstance(other, Real): + return self._affine_transform(scale=1.0, shift=-float(other)) + if isinstance(other, Distribution): + return self._binary_transform(other, operation=BinaryOperationName.SUB) + return NotImplemented + + def __rsub__(self, other: object) -> Distribution | NotImplementedType: + """Return ``other - self`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.binary import binary + + if isinstance(other, Real): + return self._affine_transform(scale=-1.0, shift=float(other)) + if isinstance(other, Distribution): + return binary(other, cast(Distribution, self), operation=BinaryOperationName.SUB) + return NotImplemented + + def __mul__(self, other: object) -> Distribution | NotImplementedType: + """Return ``self * other`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + + if isinstance(other, Real): + return self._affine_transform(scale=float(other), shift=0.0) + if isinstance(other, Distribution): + return self._binary_transform(other, operation=BinaryOperationName.MUL) + return NotImplemented + + def __rmul__(self, other: object) -> Distribution | NotImplementedType: + """Return ``other * self`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.binary import binary + + if isinstance(other, Real): + return self._affine_transform(scale=float(other), shift=0.0) + if isinstance(other, Distribution): + return binary(other, cast(Distribution, self), operation=BinaryOperationName.MUL) + return NotImplemented + + def __truediv__(self, other: object) -> Distribution | NotImplementedType: + """Return ``self / other`` for scalar or distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + + if isinstance(other, Real): + divisor = float(other) + if divisor == 0.0: + raise ZeroDivisionError("Cannot divide a distribution by zero.") + return self._affine_transform(scale=1.0 / divisor, shift=0.0) + if isinstance(other, Distribution): + return self._binary_transform(other, operation=BinaryOperationName.DIV) + return NotImplemented + + def __rtruediv__(self, other: object) -> Distribution | NotImplementedType: + """Return ``other / self`` for distribution operands.""" + from pysatl_core.distributions.distribution import Distribution + from pysatl_core.transformations.operations.binary import binary + + if isinstance(other, Distribution): + return binary(other, cast(Distribution, self), operation=BinaryOperationName.DIV) + return NotImplemented + + def __neg__(self) -> Distribution: + """Return ``-self`` as an affine transformation.""" + return self._affine_transform(scale=-1.0, shift=0.0) + + +__all__ = [ + "TransformationOperatorsMixin", +] diff --git a/src/pysatl_core/transformations/transformation_method.py b/src/pysatl_core/transformations/transformation_method.py new file mode 100644 index 0000000..fb571bb --- /dev/null +++ b/src/pysatl_core/transformations/transformation_method.py @@ -0,0 +1,168 @@ +""" +Transformation computation primitives. + +This module defines analytical computations produced by distribution +transformations. A transformation method remains compatible with AnalyticalComputation +so that transformed distributions continue to participate in the +existing characteristic graph and computation strategy. +""" + +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +from pysatl_core.distributions.computation import AnalyticalComputation, Method +from pysatl_core.types import ( + ComputationFunc, + GenericCharacteristicName, + ParentRole, + TransformationName, +) + +if TYPE_CHECKING: + from pysatl_core.distributions.distribution import Distribution + +type SourceRequirements = dict[ParentRole, tuple[GenericCharacteristicName, ...]] +"""Required parent characteristics grouped by logical parent role.""" + +type ResolvedSourceMethods = dict[ + ParentRole, + dict[GenericCharacteristicName, Method[Any, Any]], +] +"""Resolved parent methods grouped by logical parent role and characteristic.""" + +type TransformationEvaluator[In, Out] = Callable[[ResolvedSourceMethods], ComputationFunc[In, Out]] +"""Factory producing a bound computation function from resolved parent methods.""" + + +@dataclass(frozen=True, slots=True) +class TransformationMethod[In, Out](AnalyticalComputation[In, Out]): + """ + Analytical computation originating from a transformation. + + Parameters + ---------- + target : GenericCharacteristicName + Name of the target characteristic produced by the transformation. + func : ComputationFunc[In, Out] + Bound callable implementing the transformed characteristic. + transformation : TransformationName + Logical name of the transformation that created this computation. + source_requirements : SourceRequirements + Required parent characteristics used to build the computation. + """ + + transformation: TransformationName + source_requirements: SourceRequirements = field(default_factory=dict) + + @staticmethod + def _source_status( + base: Distribution, + characteristic: GenericCharacteristicName, + ) -> tuple[bool, bool]: + """ + Resolve presence and analytical status for a parent characteristic. + + Parameters + ---------- + base : Distribution + Parent distribution. + characteristic : GenericCharacteristicName + Required characteristic name. + + Returns + ------- + tuple[bool, bool] + ``(is_present, is_analytical)`` for the first loop variant of + the characteristic. + """ + methods = base.analytical_computations.get(characteristic) + if methods is None: + return False, False + + first_label = next(iter(methods)) + return True, base.loop_is_analytical(characteristic, first_label) + + @classmethod + def try_from_parents( + cls, + *, + target: GenericCharacteristicName, + transformation: TransformationName, + bases: Mapping[ParentRole, Distribution], + source_requirements: SourceRequirements, + evaluator: TransformationEvaluator[In, Out], + ) -> tuple[TransformationMethod[In, Out] | None, bool, bool]: + """ + Build a transformation method with source-semantics metadata. + + Parameters + ---------- + target : GenericCharacteristicName + Target characteristic produced by the method. + transformation : TransformationName + Logical transformation name. + bases : Mapping[ParentRole, Distribution] + Parent distributions grouped by role. + source_requirements : SourceRequirements + Required parent characteristics. + evaluator : TransformationEvaluator[In, Out] + Factory producing the bound transformed computation from resolved + parent methods. + + Returns + ------- + tuple[TransformationMethod[In, Out] | None, bool, bool] + ``(method, is_analytical, has_any_present_source)``: + + - ``method`` is ``None`` when no required source characteristic is + present in ``analytical_computations`` of parents. + - ``is_analytical`` is ``True`` only when all required sources are + present and marked analytical. + - ``has_any_present_source`` indicates whether at least one required + source is present in parent ``analytical_computations``. + """ + has_any_present_source = False + is_analytical = True + + for role, characteristics in source_requirements.items(): + base = bases[role] + for characteristic in characteristics: + is_present, source_is_analytical = cls._source_status(base, characteristic) + has_any_present_source = has_any_present_source or is_present + is_analytical = is_analytical and is_present and source_is_analytical + + if not has_any_present_source: + return None, False, False + + resolved: ResolvedSourceMethods = { + role: { + characteristic: bases[role].query_method(characteristic) + for characteristic in characteristics + } + for role, characteristics in source_requirements.items() + } + return ( + cls( + target=target, + func=evaluator(resolved), + transformation=transformation, + source_requirements=source_requirements, + ), + is_analytical, + True, + ) + + +__all__ = [ + "ResolvedSourceMethods", + "SourceRequirements", + "TransformationEvaluator", + "TransformationMethod", +] diff --git a/src/pysatl_core/types.py b/src/pysatl_core/types.py index 150c9f1..3b0ca02 100644 --- a/src/pysatl_core/types.py +++ b/src/pysatl_core/types.py @@ -172,6 +172,7 @@ def __post_init__(self) -> None: @overload def contains(self, x: Number) -> bool: ... + @overload def contains(self, x: NumericArray) -> BoolArray: ... @@ -264,6 +265,8 @@ def shape(self) -> ContinuousSupportShape1D: implementations may or may not accept them, and wrappers typically forward ``**options`` dynamically. """ +type ParentRole = str +"""Type alias for logical roles of parent distributions in a transformation.""" class CharacteristicName(StrEnum): @@ -296,6 +299,54 @@ class CharacteristicName(StrEnum): STANDARD_MOMENT = "standardized_moment" # unimplemented in graph yet +class TransformationName(StrEnum): + """ + Enumeration of built-in distribution transformations. + + Attributes + ---------- + AFFINE + Affine transformation ``aX + b``. + BINARY + Binary operation on two parent distributions. + FUNCTION + Functional transformation ``f(X)``. + FINITE_MIXTURE + Finite weighted mixture of component distributions. + APPROXIMATION + Materialized approximation of a transformed distribution. + """ + + AFFINE = "affine" + BINARY = "binary" + FUNCTION = "function" + FINITE_MIXTURE = "finite_mixture" + APPROXIMATION = "approximation" + ARRAY = "array" + + +class BinaryOperationName(StrEnum): + """ + Enumeration of supported binary operations for transformed distributions. + + Attributes + ---------- + ADD + Sum ``X + Y``. + SUB + Difference ``X - Y``. + MUL + Product ``X * Y``. + DIV + Ratio ``X / Y``. + """ + + ADD = "add" + SUB = "sub" + MUL = "mul" + DIV = "div" + + class FamilyName(StrEnum): NORMAL = "Normal" CONTINUOUS_UNIFORM = "ContinuousUniform" @@ -312,6 +363,9 @@ class FamilyName(StrEnum): "DEFAULT_ANALYTICAL_COMPUTATION_LABEL", "ParametrizationName", "ComputationFunc", + "TransformationName", + "BinaryOperationName", + "ParentRole", "DistributionType", "Interval1D", "ContinuousSupportShape1D", diff --git a/tests/unit/transformations/__init__.py b/tests/unit/transformations/__init__.py new file mode 100644 index 0000000..e8136dc --- /dev/null +++ b/tests/unit/transformations/__init__.py @@ -0,0 +1,12 @@ +""" +PySATL Core +=========== + +Core framework for probabilistic distributions providing type definitions, +distribution abstractions, characteristic computation graphs, and parametric +family management. +""" + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" diff --git a/tests/unit/transformations/approximations/__init__.py b/tests/unit/transformations/approximations/__init__.py new file mode 100644 index 0000000..e8136dc --- /dev/null +++ b/tests/unit/transformations/approximations/__init__.py @@ -0,0 +1,12 @@ +""" +PySATL Core +=========== + +Core framework for probabilistic distributions providing type definitions, +distribution abstractions, characteristic computation graphs, and parametric +family management. +""" + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" diff --git a/tests/unit/transformations/approximations/test_linear_interpolation_approximator.py b/tests/unit/transformations/approximations/test_linear_interpolation_approximator.py new file mode 100644 index 0000000..ae9884a --- /dev/null +++ b/tests/unit/transformations/approximations/test_linear_interpolation_approximator.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import numpy as np +import pytest + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.support import ContinuousSupport +from pysatl_core.transformations.approximations import ( + CDFMonotoneSplineApproximation, + PDFLinearInterpolationApproximation, + PPFMonotoneSplineApproximation, +) +from pysatl_core.transformations.operations import affine +from pysatl_core.types import CharacteristicName, ComputationFunc, Kind +from tests.unit.distributions.test_basic import DistributionTestBase +from tests.utils.mocks import StandaloneEuclideanUnivariateDistribution + + +class TestInterpolationApproximator(DistributionTestBase): + @staticmethod + def _make_vectorized_uniform_pdf_distribution() -> StandaloneEuclideanUnivariateDistribution: + def _pdf(x: np.ndarray, **_kwargs: Any) -> np.ndarray: + array = np.asarray(x, dtype=float) + return np.where((array >= 0.0) & (array <= 1.0), 1.0, 0.0) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations={ + CharacteristicName.PDF: AnalyticalComputation[np.ndarray, np.ndarray]( + target=CharacteristicName.PDF, + func=cast(ComputationFunc[np.ndarray, np.ndarray], _pdf), + ) + }, + support=ContinuousSupport(0.0, 1.0), + ) + + @staticmethod + def _make_vectorized_logistic_cdf_distribution() -> StandaloneEuclideanUnivariateDistribution: + def _cdf(x: np.ndarray, **_kwargs: Any) -> np.ndarray: + array = np.asarray(x, dtype=float) + clipped = np.clip(array, -700.0, 700.0) + return 1.0 / (1.0 + np.exp(-clipped)) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations={ + CharacteristicName.CDF: AnalyticalComputation[np.ndarray, np.ndarray]( + target=CharacteristicName.CDF, + func=cast(ComputationFunc[np.ndarray, np.ndarray], _cdf), + ) + }, + support=ContinuousSupport(), + ) + + def test_pdf_is_non_negative_and_normalized_after_approximation(self) -> None: + source = self._make_vectorized_uniform_pdf_distribution() + transformed = affine(source, scale=2.0, shift=1.0) + + approximated = transformed.approximate( + methods={ + CharacteristicName.PDF: PDFLinearInterpolationApproximation( + n_grid=513, + lower_limit=1.0, + upper_limit=3.0, + ) + }, + ) + pdf = approximated.query_method(CharacteristicName.PDF) + + grid = np.linspace(1.0, 3.0, 4001, dtype=float) + values = np.asarray(pdf(grid), dtype=float) + + assert float(values.min()) >= -1e-12 + assert np.trapezoid(values, grid) == pytest.approx(1.0, abs=5e-3) + assert float(pdf(0.0)) == pytest.approx(0.0) + assert float(pdf(4.0)) == pytest.approx(0.0) + + def test_cdf_is_monotone_and_bounded(self) -> None: + source = self._make_vectorized_logistic_cdf_distribution() + transformed = affine(source, scale=1.0) + + approximated = transformed.approximate( + methods={ + CharacteristicName.CDF: CDFMonotoneSplineApproximation( + n_grid=513, + lower_limit_prob=1e-6, + upper_limit_prob=1e-6, + ) + }, + ) + cdf = approximated.query_method(CharacteristicName.CDF) + + grid = np.linspace(-10.0, 10.0, 1025, dtype=float) + values = np.asarray(cdf(grid), dtype=float) + + assert np.all(np.diff(values) >= -1e-12) + assert float(values[0]) == pytest.approx(0.0, abs=1e-3) + assert float(values[-1]) == pytest.approx(1.0, abs=1e-3) + assert float(cdf(-100.0)) == pytest.approx(0.0, abs=1e-12) + assert float(cdf(100.0)) == pytest.approx(1.0, abs=1e-12) + + def test_ppf_exists_on_full_unit_interval(self) -> None: + source = self.make_uniform_ppf_distribution() + transformed = affine(source, scale=2.0, shift=1.0) + + approximated = transformed.approximate( + methods={ + CharacteristicName.PPF: PPFMonotoneSplineApproximation( + n_grid=513, + lower_limit=0.0, + upper_limit=1.0, + ) + }, + ) + ppf = approximated.query_method(CharacteristicName.PPF) + + probabilities = np.linspace(0.0, 1.0, 513, dtype=float) + values = np.asarray(ppf(probabilities), dtype=float) + + assert np.all(np.isfinite(values)) + assert np.all(np.diff(values) >= -1e-12) + assert float(ppf(0.0)) == pytest.approx(1.0, abs=1e-8) + assert float(ppf(1.0)) == pytest.approx(3.0, abs=1e-8) + + def test_rejects_empty_methods_mapping(self) -> None: + source = self.make_uniform_ppf_distribution() + transformed = affine(source, scale=1.0) + + with pytest.raises(ValueError, match="At least one characteristic approximation method"): + transformed.approximate(methods={}) + + def test_approximates_only_characteristics_from_mapping(self) -> None: + source = self._make_vectorized_logistic_cdf_distribution() + transformed = affine(source, scale=1.0) + + approximated = transformed.approximate( + methods={ + CharacteristicName.CDF: CDFMonotoneSplineApproximation(), + }, + ) + + assert set(approximated.analytical_computations) == {CharacteristicName.CDF} + + def test_raises_when_distribution_cannot_provide_requested_characteristic(self) -> None: + def _mean(**_kwargs: object) -> float: + return 0.0 + + source = StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations={ + CharacteristicName.MEAN: AnalyticalComputation[object, float]( + target=CharacteristicName.MEAN, + func=_mean, + ) + }, + ) + transformed = affine(source, scale=1.0) + + with pytest.raises(ValueError, match="requires an analytical method"): + transformed.approximate( + methods={ + CharacteristicName.PDF: PDFLinearInterpolationApproximation(), + }, + ) diff --git a/tests/unit/transformations/operations/__init__.py b/tests/unit/transformations/operations/__init__.py new file mode 100644 index 0000000..e8136dc --- /dev/null +++ b/tests/unit/transformations/operations/__init__.py @@ -0,0 +1,12 @@ +""" +PySATL Core +=========== + +Core framework for probabilistic distributions providing type definitions, +distribution abstractions, characteristic computation graphs, and parametric +family management. +""" + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" diff --git a/tests/unit/transformations/operations/test_affine.py b/tests/unit/transformations/operations/test_affine.py new file mode 100644 index 0000000..9e649d0 --- /dev/null +++ b/tests/unit/transformations/operations/test_affine.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +import pytest + +from pysatl_core.distributions.registry import characteristic_registry +from pysatl_core.distributions.strategies import DefaultComputationStrategy +from pysatl_core.transformations.operations import affine +from pysatl_core.types import DEFAULT_ANALYTICAL_COMPUTATION_LABEL, CharacteristicName +from tests.unit.distributions.test_basic import DistributionTestBase +from tests.utils.mocks import MockSamplingStrategy + + +class TestAffineDistribution(DistributionTestBase): + def test_negative_scale_marks_cdf_as_transformation_loop(self) -> None: + base = self.make_discrete_point_pmf_distribution() + transformed = affine(base, scale=-1.0, shift=0.0) + + view = characteristic_registry().view(transformed) + cdf_loop = view.variants(CharacteristicName.CDF, CharacteristicName.CDF)[ + DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ] + pmf_loop = view.variants(CharacteristicName.PMF, CharacteristicName.PMF)[ + DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ] + + assert cdf_loop.edge_kind() == "transformation_loop" + assert not cdf_loop.is_analytical + assert pmf_loop.edge_kind() == "analytical_loop" + assert pmf_loop.is_analytical + assert view.analytical_variants(CharacteristicName.CDF) == {} + + cdf = transformed.query_method(CharacteristicName.CDF) + assert cdf(0.0) == pytest.approx(1.0) + assert cdf(-2.0) == pytest.approx(0.3) + + def test_includes_only_present_base_characteristics(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=2.0, shift=1.0) + + assert set(transformed.analytical_computations) == {CharacteristicName.CDF} + view = characteristic_registry().view(transformed) + cdf_loop = view.variants(CharacteristicName.CDF, CharacteristicName.CDF)[ + DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ] + assert cdf_loop.is_analytical + + def test_with_sampling_strategy_preserves_concrete_type(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=2.0, shift=1.0) + sampling_strategy = MockSamplingStrategy() + + clone = transformed.with_sampling_strategy(sampling_strategy) + + assert type(clone) is type(transformed) + assert clone.sampling_strategy is sampling_strategy + assert clone.scale == pytest.approx(transformed.scale) + assert clone.shift == pytest.approx(transformed.shift) + + def test_with_computation_strategy_preserves_concrete_type(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=2.0, shift=1.0) + computation_strategy = DefaultComputationStrategy(enable_caching=True) + + clone = transformed.with_computation_strategy(computation_strategy) + + assert type(clone) is type(transformed) + assert clone.computation_strategy is computation_strategy + assert clone.scale == pytest.approx(transformed.scale) + assert clone.shift == pytest.approx(transformed.shift) + + def test_sample_uses_base_distribution_and_affine_transform( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + base = self.make_uniform_ppf_distribution() + transformed = affine(base, scale=2.0, shift=-1.0) + captured: dict[str, object] = {} + + def _fake_sample(n: int, distr: object, **options: object) -> object: + captured["n"] = n + captured["distr"] = distr + captured["options"] = options + return [0.0, 0.5, 1.0] + + monkeypatch.setattr(transformed.sampling_strategy, "sample", _fake_sample) + samples = transformed.sample(3, token="test") + + assert captured["n"] == 3 + assert captured["distr"] is transformed.base_distribution + assert captured["options"] == {"token": "test"} + assert samples == pytest.approx([-1.0, 0.0, 1.0]) diff --git a/tests/unit/transformations/operations/test_binary.py b/tests/unit/transformations/operations/test_binary.py new file mode 100644 index 0000000..7f3a2fe --- /dev/null +++ b/tests/unit/transformations/operations/test_binary.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import numpy as np +import pytest + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.support import ContinuousSupport, ExplicitTableDiscreteSupport +from pysatl_core.transformations.distribution import ApproximatedDistribution +from pysatl_core.transformations.operations import binary +from pysatl_core.transformations.operations.binary import ( + DivisionBinaryDistribution, + LinearBinaryDistribution, + MultiplicationBinaryDistribution, +) +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + BinaryOperationName, + CharacteristicName, + ComputationFunc, + Kind, +) +from tests.unit.distributions.test_basic import DistributionTestBase +from tests.utils.mocks import StandaloneEuclideanUnivariateDistribution + + +class TestBinaryDistribution(DistributionTestBase): + @staticmethod + def _make_uniform_full_distribution( + left: float, + right: float, + ) -> StandaloneEuclideanUnivariateDistribution: + width = right - left + + def _pdf(data: float, **_options: Any) -> float: + return 1.0 / width if left <= data <= right else 0.0 + + def _cdf(data: float, **_options: Any) -> float: + if data <= left: + return 0.0 + if data >= right: + return 1.0 + return (data - left) / width + + def _ppf(data: float, **_options: Any) -> float: + if not 0.0 <= data <= 1.0: + raise ValueError("PPF input must be in [0, 1].") + return left + width * data + + def _cf(data: float, **_options: Any) -> complex: + if data == 0.0: + return 1.0 + 0.0j + numerator = np.exp(1j * data * right) - np.exp(1j * data * left) + return cast(complex, numerator / (1j * data * width)) + + def _mean(**_options: Any) -> float: + return 0.5 * (left + right) + + def _var(**_options: Any) -> float: + return width**2 / 12.0 + + def _skew(**_options: Any) -> float: + return 0.0 + + def _kurt(*, excess: bool = False, **_options: Any) -> float: + return -1.2 if excess else 1.8 + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations={ + CharacteristicName.PDF: AnalyticalComputation[float, float]( + target=CharacteristicName.PDF, + func=cast(ComputationFunc[float, float], _pdf), + ), + CharacteristicName.CDF: AnalyticalComputation[float, float]( + target=CharacteristicName.CDF, + func=cast(ComputationFunc[float, float], _cdf), + ), + CharacteristicName.PPF: AnalyticalComputation[float, float]( + target=CharacteristicName.PPF, + func=cast(ComputationFunc[float, float], _ppf), + ), + CharacteristicName.CF: AnalyticalComputation[float, complex]( + target=CharacteristicName.CF, + func=cast(ComputationFunc[float, complex], _cf), + ), + CharacteristicName.MEAN: AnalyticalComputation[Any, float]( + target=CharacteristicName.MEAN, + func=cast(ComputationFunc[Any, float], _mean), + ), + CharacteristicName.VAR: AnalyticalComputation[Any, float]( + target=CharacteristicName.VAR, + func=cast(ComputationFunc[Any, float], _var), + ), + CharacteristicName.SKEW: AnalyticalComputation[Any, float]( + target=CharacteristicName.SKEW, + func=cast(ComputationFunc[Any, float], _skew), + ), + CharacteristicName.KURT: AnalyticalComputation[Any, float]( + target=CharacteristicName.KURT, + func=cast(ComputationFunc[Any, float], _kurt), + ), + }, + support=ContinuousSupport(left, right), + ) + + @staticmethod + def _make_discrete_pmf_distribution( + points: list[float], + masses: list[float], + ) -> StandaloneEuclideanUnivariateDistribution: + table = {float(point): float(mass) for point, mass in zip(points, masses, strict=True)} + + def _pmf(data: float, **_options: Any) -> float: + return table.get(float(data), 0.0) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.DISCRETE, + analytical_computations={ + CharacteristicName.PMF: AnalyticalComputation[float, float]( + target=CharacteristicName.PMF, + func=cast(ComputationFunc[float, float], _pmf), + ) + }, + support=ExplicitTableDiscreteSupport(points=points, assume_sorted=False), + ) + + @staticmethod + def _make_statistics_distribution( + *, + mean: float | None = None, + variance: float | None = None, + skewness: float | None = None, + kurtosis: float | None = None, + ) -> StandaloneEuclideanUnivariateDistribution: + analytical_computations: dict[str, AnalyticalComputation[Any, Any]] = {} + + if mean is not None: + analytical_computations[CharacteristicName.MEAN] = AnalyticalComputation[Any, float]( + target=CharacteristicName.MEAN, + func=cast(ComputationFunc[Any, float], lambda **_options: mean), + ) + if variance is not None: + analytical_computations[CharacteristicName.VAR] = AnalyticalComputation[Any, float]( + target=CharacteristicName.VAR, + func=cast(ComputationFunc[Any, float], lambda **_options: variance), + ) + if skewness is not None: + analytical_computations[CharacteristicName.SKEW] = AnalyticalComputation[Any, float]( + target=CharacteristicName.SKEW, + func=cast(ComputationFunc[Any, float], lambda **_options: skewness), + ) + if kurtosis is not None: + analytical_computations[CharacteristicName.KURT] = AnalyticalComputation[Any, float]( + target=CharacteristicName.KURT, + func=cast( + ComputationFunc[Any, float], + lambda *, excess=False, **_options: kurtosis - 3.0 if excess else kurtosis, + ), + ) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations=analytical_computations, + ) + + def test_add_continuous_exposes_full_characteristic_set(self) -> None: + left = self._make_uniform_full_distribution(0.0, 1.0) + right = self._make_uniform_full_distribution(1.0, 2.0) + + transformed = binary(left, right, operation=BinaryOperationName.ADD) + assert isinstance(transformed, LinearBinaryDistribution) + assert set(transformed.analytical_computations) == { + CharacteristicName.CF, + CharacteristicName.MEAN, + CharacteristicName.VAR, + CharacteristicName.SKEW, + CharacteristicName.KURT, + CharacteristicName.CDF, + CharacteristicName.PDF, + CharacteristicName.PPF, + } + + mean = transformed.query_method(CharacteristicName.MEAN) + var = transformed.query_method(CharacteristicName.VAR) + pdf = transformed.query_method(CharacteristicName.PDF) + cdf = transformed.query_method(CharacteristicName.CDF) + ppf = transformed.query_method(CharacteristicName.PPF) + cf = transformed.query_method(CharacteristicName.CF) + + assert mean() == pytest.approx(2.0) + assert var() == pytest.approx(1.0 / 6.0, rel=1e-2) + assert pdf(2.0) == pytest.approx(1.0, rel=2e-2) + assert cdf(2.0) == pytest.approx(0.5, rel=2e-2) + assert ppf(0.5) == pytest.approx(2.0, rel=2e-2) + assert cf(0.0) == pytest.approx(1.0 + 0.0j) + + def test_sub_mul_div_continuous_are_queryable(self) -> None: + sub_left = self._make_uniform_full_distribution(0.0, 1.0) + sub_right = self._make_uniform_full_distribution(1.0, 2.0) + sub_transformed = binary(sub_left, sub_right, operation=BinaryOperationName.SUB) + assert isinstance(sub_transformed, LinearBinaryDistribution) + + sub_mean = sub_transformed.query_method(CharacteristicName.MEAN) + sub_var = sub_transformed.query_method(CharacteristicName.VAR) + assert sub_mean() == pytest.approx(-1.0) + assert sub_var() == pytest.approx(1.0 / 6.0, rel=1e-2) + + mul_left = self._make_uniform_full_distribution(1.0, 2.0) + mul_right = self._make_uniform_full_distribution(2.0, 3.0) + mul_transformed = binary(mul_left, mul_right, operation=BinaryOperationName.MUL) + assert isinstance(mul_transformed, MultiplicationBinaryDistribution) + + mul_mean = mul_transformed.query_method(CharacteristicName.MEAN) + mul_var = mul_transformed.query_method(CharacteristicName.VAR) + mul_pdf = mul_transformed.query_method(CharacteristicName.PDF) + mul_cdf = mul_transformed.query_method(CharacteristicName.CDF) + mul_ppf = mul_transformed.query_method(CharacteristicName.PPF) + assert mul_mean() == pytest.approx(3.75, rel=2e-3) + assert mul_var() == pytest.approx(0.7152777778, rel=1e-2) + assert float(mul_pdf(3.0)) >= 0.0 + assert 0.0 <= float(mul_cdf(3.0)) <= 1.0 + assert np.isfinite(float(mul_ppf(0.5))) + + div_left = self._make_uniform_full_distribution(2.0, 3.0) + div_right = self._make_uniform_full_distribution(1.0, 2.0) + div_transformed = binary(div_left, div_right, operation=BinaryOperationName.DIV) + assert isinstance(div_transformed, DivisionBinaryDistribution) + + div_mean = div_transformed.query_method(CharacteristicName.MEAN) + div_var = div_transformed.query_method(CharacteristicName.VAR) + div_pdf = div_transformed.query_method(CharacteristicName.PDF) + div_cdf = div_transformed.query_method(CharacteristicName.CDF) + div_ppf = div_transformed.query_method(CharacteristicName.PPF) + assert div_mean() == pytest.approx(2.5 * np.log(2.0), rel=1e-2) + assert div_var() == pytest.approx(0.163824299, rel=2e-2) + assert float(div_pdf(1.5)) >= 0.0 + assert 0.0 <= float(div_cdf(1.5)) <= 1.0 + assert np.isfinite(float(div_ppf(0.5))) + + def test_linear_mean_requires_only_parent_means(self) -> None: + left = self._make_statistics_distribution(mean=1.5) + right = self._make_statistics_distribution(mean=-0.25) + + transformed = binary(left, right, operation=BinaryOperationName.ADD) + assert isinstance(transformed, LinearBinaryDistribution) + assert set(transformed.analytical_computations) == {CharacteristicName.MEAN} + assert transformed.query_method(CharacteristicName.MEAN)() == pytest.approx(1.25) + + def test_linear_var_requires_only_mean_and_variance(self) -> None: + left = self._make_statistics_distribution(mean=2.0, variance=1.0) + right = self._make_statistics_distribution(mean=-1.0, variance=0.25) + + transformed = binary(left, right, operation=BinaryOperationName.ADD) + assert isinstance(transformed, LinearBinaryDistribution) + assert set(transformed.analytical_computations) == { + CharacteristicName.MEAN, + CharacteristicName.VAR, + } + assert transformed.query_method(CharacteristicName.MEAN)() == pytest.approx(1.0) + assert transformed.query_method(CharacteristicName.VAR)() == pytest.approx(1.25) + + def test_discrete_operations_expose_pmf_cdf_ppf(self) -> None: + left = self._make_discrete_pmf_distribution([0.0, 1.0, 2.0], [0.2, 0.5, 0.3]) + right = self._make_discrete_pmf_distribution([1.0, 2.0], [0.6, 0.4]) + + for operation in BinaryOperationName: + transformed = binary(left, right, operation=operation) + assert { + CharacteristicName.PMF, + CharacteristicName.CDF, + CharacteristicName.PPF, + }.issubset(set(transformed.analytical_computations)) + + pmf = transformed.query_method(CharacteristicName.PMF) + cdf = transformed.query_method(CharacteristicName.CDF) + ppf = transformed.query_method(CharacteristicName.PPF) + + support = transformed.support + assert isinstance(support, ExplicitTableDiscreteSupport) + points = np.asarray(support.points, dtype=float) + masses = np.asarray([pmf(float(x)) for x in points], dtype=float) + assert float(np.sum(masses)) == pytest.approx(1.0, rel=1e-9) + assert float(cdf(float(points[0] - 1.0))) == pytest.approx(0.0) + assert float(cdf(float(points[-1] + 1.0))) == pytest.approx(1.0) + assert float(ppf(0.0)) == pytest.approx(float(points[0])) + assert float(ppf(1.0)) == pytest.approx(float(points[-1])) + + def test_non_analytical_parent_marks_loop_as_non_analytical(self) -> None: + left_base = self._make_uniform_full_distribution(0.0, 1.0) + right = self._make_uniform_full_distribution(1.0, 2.0) + + left = ApproximatedDistribution( + distribution_type=left_base.distribution_type, + analytical_computations=left_base.analytical_computations, + support=left_base.support, + ) + transformed = binary(left, right, operation=BinaryOperationName.ADD) + + assert not transformed.loop_is_analytical( + CharacteristicName.MEAN, + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + ) + + @pytest.mark.parametrize( + ("operation", "left_samples", "right_samples", "expected"), + [ + ( + BinaryOperationName.ADD, + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + [11.0, 22.0, 33.0], + ), + ( + BinaryOperationName.SUB, + [5.0, 4.0, 3.0], + [1.0, 2.0, 3.0], + [4.0, 2.0, 0.0], + ), + ( + BinaryOperationName.MUL, + [3.0, 4.0, 5.0], + [2.0, 3.0, 4.0], + [6.0, 12.0, 20.0], + ), + ( + BinaryOperationName.DIV, + [2.0, 4.0, 6.0], + [1.0, 2.0, 3.0], + [2.0, 2.0, 2.0], + ), + ], + ) + def test_sample_uses_both_parents_and_applies_operation( + self, + monkeypatch: pytest.MonkeyPatch, + operation: BinaryOperationName, + left_samples: list[float], + right_samples: list[float], + expected: list[float], + ) -> None: + left = self._make_uniform_full_distribution(1.0, 2.0) + right = self._make_uniform_full_distribution(1.0, 2.0) + transformed = binary(left, right, operation=operation) + captured: list[tuple[int, object, dict[str, object]]] = [] + + def _fake_sample(n: int, distr: object, **options: object) -> object: + captured.append((n, distr, dict(options))) + if distr is transformed.left_distribution: + return left_samples + if distr is transformed.right_distribution: + return right_samples + raise AssertionError("Unexpected parent distribution passed to binary sampler.") + + monkeypatch.setattr(transformed.sampling_strategy, "sample", _fake_sample) + samples = transformed.sample(3, token="binary") + + assert captured == [ + (3, transformed.left_distribution, {"token": "binary"}), + (3, transformed.right_distribution, {"token": "binary"}), + ] + assert samples == pytest.approx(expected) diff --git a/tests/unit/transformations/operations/test_mixture.py b/tests/unit/transformations/operations/test_mixture.py new file mode 100644 index 0000000..7454c7e --- /dev/null +++ b/tests/unit/transformations/operations/test_mixture.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import numpy as np +import pytest + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.support import ContinuousSupport, ExplicitTableDiscreteSupport +from pysatl_core.transformations.distribution import ApproximatedDistribution +from pysatl_core.transformations.operations import discrete_mixture, finite_mixture +from pysatl_core.transformations.operations.mixture import FiniteMixtureDistribution +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + CharacteristicName, + ComplexArray, + ComputationFunc, + Kind, + NumericArray, +) +from tests.unit.distributions.test_basic import DistributionTestBase +from tests.utils.mocks import StandaloneEuclideanUnivariateDistribution + + +class TestFiniteMixtureDistribution(DistributionTestBase): + @staticmethod + def _make_uniform_full_distribution( + left: float, + right: float, + ) -> StandaloneEuclideanUnivariateDistribution: + width = right - left + + def _pdf(data: NumericArray, **_options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + values = np.where((left <= array) & (array <= right), 1.0 / width, 0.0) + return cast(NumericArray, values) + + def _cdf(data: NumericArray, **_options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + values = np.clip((array - left) / width, 0.0, 1.0) + return cast(NumericArray, values) + + def _ppf(data: NumericArray, **_options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + if np.any((array < 0.0) | (array > 1.0)): + raise ValueError("PPF input must be in [0, 1].") + values = left + width * array + return cast(NumericArray, values) + + def _cf(data: NumericArray, **_options: Any) -> ComplexArray: + array = np.asarray(data, dtype=float) + numerator = np.exp(1j * array * right) - np.exp(1j * array * left) + denominator = 1j * array * width + values = np.where(np.isclose(array, 0.0), 1.0 + 0.0j, numerator / denominator) + return cast(ComplexArray, np.asarray(values, dtype=complex)) + + def _mean(**_options: Any) -> float: + return 0.5 * (left + right) + + def _var(**_options: Any) -> float: + return width**2 / 12.0 + + def _skew(**_options: Any) -> float: + return 0.0 + + def _kurt(*, excess: bool = False, **_options: Any) -> float: + return -1.2 if excess else 1.8 + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations={ + CharacteristicName.PDF: AnalyticalComputation[NumericArray, NumericArray]( + target=CharacteristicName.PDF, + func=cast(ComputationFunc[NumericArray, NumericArray], _pdf), + ), + CharacteristicName.CDF: AnalyticalComputation[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf), + ), + CharacteristicName.PPF: AnalyticalComputation[NumericArray, NumericArray]( + target=CharacteristicName.PPF, + func=cast(ComputationFunc[NumericArray, NumericArray], _ppf), + ), + CharacteristicName.CF: AnalyticalComputation[NumericArray, ComplexArray]( + target=CharacteristicName.CF, + func=cast(ComputationFunc[NumericArray, ComplexArray], _cf), + ), + CharacteristicName.MEAN: AnalyticalComputation[Any, float]( + target=CharacteristicName.MEAN, + func=cast(ComputationFunc[Any, float], _mean), + ), + CharacteristicName.VAR: AnalyticalComputation[Any, float]( + target=CharacteristicName.VAR, + func=cast(ComputationFunc[Any, float], _var), + ), + CharacteristicName.SKEW: AnalyticalComputation[Any, float]( + target=CharacteristicName.SKEW, + func=cast(ComputationFunc[Any, float], _skew), + ), + CharacteristicName.KURT: AnalyticalComputation[Any, float]( + target=CharacteristicName.KURT, + func=cast(ComputationFunc[Any, float], _kurt), + ), + }, + support=ContinuousSupport(left, right), + ) + + @staticmethod + def _make_discrete_pmf_distribution( + points: list[float], + masses: list[float], + ) -> StandaloneEuclideanUnivariateDistribution: + table = {float(point): float(mass) for point, mass in zip(points, masses, strict=True)} + + def _pmf(data: NumericArray, **_options: Any) -> NumericArray: + array = np.asarray(data, dtype=float) + values = np.zeros(array.shape, dtype=float) + for point, mass in table.items(): + values[np.isclose(array, point, atol=1e-12, rtol=0.0)] = mass + return cast(NumericArray, values) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.DISCRETE, + analytical_computations={ + CharacteristicName.PMF: AnalyticalComputation[NumericArray, NumericArray]( + target=CharacteristicName.PMF, + func=cast(ComputationFunc[NumericArray, NumericArray], _pmf), + ) + }, + support=ExplicitTableDiscreteSupport(points=points, assume_sorted=False), + ) + + @staticmethod + def _make_statistics_distribution( + *, + mean: float | None = None, + variance: float | None = None, + skewness: float | None = None, + kurtosis: float | None = None, + ) -> StandaloneEuclideanUnivariateDistribution: + analytical_computations: dict[str, AnalyticalComputation[Any, Any]] = {} + + if mean is not None: + analytical_computations[CharacteristicName.MEAN] = AnalyticalComputation[Any, float]( + target=CharacteristicName.MEAN, + func=cast(ComputationFunc[Any, float], lambda **_options: mean), + ) + if variance is not None: + analytical_computations[CharacteristicName.VAR] = AnalyticalComputation[Any, float]( + target=CharacteristicName.VAR, + func=cast(ComputationFunc[Any, float], lambda **_options: variance), + ) + if skewness is not None: + analytical_computations[CharacteristicName.SKEW] = AnalyticalComputation[Any, float]( + target=CharacteristicName.SKEW, + func=cast(ComputationFunc[Any, float], lambda **_options: skewness), + ) + if kurtosis is not None: + analytical_computations[CharacteristicName.KURT] = AnalyticalComputation[Any, float]( + target=CharacteristicName.KURT, + func=cast( + ComputationFunc[Any, float], + lambda *, excess=False, **_options: kurtosis - 3.0 if excess else kurtosis, + ), + ) + + return StandaloneEuclideanUnivariateDistribution( + kind=Kind.CONTINUOUS, + analytical_computations=analytical_computations, + ) + + def test_discrete_mixture_with_arbitrary_number_of_components(self) -> None: + first = self._make_discrete_pmf_distribution([0.0, 1.0], [0.7, 0.3]) + second = self._make_discrete_pmf_distribution([1.0, 2.0], [0.2, 0.8]) + third = self._make_discrete_pmf_distribution([2.0], [1.0]) + + mixture = discrete_mixture( + [(0.2, first), (0.5, second), (0.3, third)], + ) + assert isinstance(mixture, FiniteMixtureDistribution) + assert set(mixture.analytical_computations) == { + CharacteristicName.PMF, + CharacteristicName.CDF, + CharacteristicName.PPF, + } + + pmf = mixture.query_method(CharacteristicName.PMF) + cdf = mixture.query_method(CharacteristicName.CDF) + ppf = mixture.query_method(CharacteristicName.PPF) + + assert float(pmf(0.0)) == pytest.approx(0.14) + assert float(pmf(1.0)) == pytest.approx(0.16) + assert float(pmf(2.0)) == pytest.approx(0.70) + assert float(cdf(1.0)) == pytest.approx(0.30) + assert float(cdf(2.0)) == pytest.approx(1.0) + assert float(ppf(0.0)) == pytest.approx(0.0) + assert float(ppf(0.30)) == pytest.approx(1.0) + assert float(ppf(0.31)) == pytest.approx(2.0) + + support = mixture.support + assert isinstance(support, ExplicitTableDiscreteSupport) + assert support.points == pytest.approx([0.0, 1.0, 2.0]) + + def test_continuous_mixture_weighted_characteristics(self) -> None: + left = self._make_uniform_full_distribution(0.0, 1.0) + right = self._make_uniform_full_distribution(1.0, 2.0) + + mixture = finite_mixture([(0.25, left), (0.75, right)]) + assert isinstance(mixture, FiniteMixtureDistribution) + + mean = mixture.query_method(CharacteristicName.MEAN) + pdf = mixture.query_method(CharacteristicName.PDF) + cdf = mixture.query_method(CharacteristicName.CDF) + + assert mean() == pytest.approx(1.25) + assert float(pdf(0.5)) == pytest.approx(0.25) + assert float(pdf(1.5)) == pytest.approx(0.75) + assert float(cdf(0.5)) == pytest.approx(0.125) + assert float(cdf(1.5)) == pytest.approx(0.625) + assert CharacteristicName.PPF not in mixture.analytical_computations + + def test_mixture_mean_requires_only_component_means(self) -> None: + first = self._make_statistics_distribution(mean=-1.0) + second = self._make_statistics_distribution(mean=3.0) + mixture = finite_mixture([(0.25, first), (0.75, second)]) + + assert set(mixture.analytical_computations) == {CharacteristicName.MEAN} + assert mixture.query_method(CharacteristicName.MEAN)() == pytest.approx(2.0) + + def test_mixture_var_requires_only_mean_and_variance(self) -> None: + first = self._make_statistics_distribution(mean=0.0, variance=1.0) + second = self._make_statistics_distribution(mean=2.0, variance=2.0) + mixture = finite_mixture([(0.4, first), (0.6, second)]) + + expected_mean = 0.4 * 0.0 + 0.6 * 2.0 + expected_second_raw = 0.4 * (1.0 + 0.0**2) + 0.6 * (2.0 + 2.0**2) + expected_var = expected_second_raw - expected_mean**2 + + assert set(mixture.analytical_computations) == { + CharacteristicName.MEAN, + CharacteristicName.VAR, + } + assert mixture.query_method(CharacteristicName.MEAN)() == pytest.approx(expected_mean) + assert mixture.query_method(CharacteristicName.VAR)() == pytest.approx(expected_var) + + def test_non_analytical_component_marks_loop_as_non_analytical(self) -> None: + left_base = self._make_uniform_full_distribution(0.0, 1.0) + right = self._make_uniform_full_distribution(1.0, 2.0) + left = ApproximatedDistribution( + distribution_type=left_base.distribution_type, + analytical_computations=left_base.analytical_computations, + support=left_base.support, + ) + + mixture = finite_mixture([(0.5, left), (0.5, right)]) + assert not mixture.loop_is_analytical( + CharacteristicName.MEAN, + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + ) + + def test_component_and_weight_accessors_are_safe(self) -> None: + first = self._make_discrete_pmf_distribution([0.0], [1.0]) + second = self._make_discrete_pmf_distribution([1.0], [1.0]) + mixture = finite_mixture([(0.3, first), (0.7, second)]) + + weights = mixture.weights + assert weights.tolist() == pytest.approx([0.3, 0.7]) + weights[1] = 0.0 + assert mixture.weights.tolist() == pytest.approx([0.3, 0.7]) + + components = mixture.components + assert len(components) == 2 + assert components[0] is not components[1] + assert components[0].distribution_type == first.distribution_type + assert components[1].distribution_type == second.distribution_type + assert components[1].analytical_computations == second.analytical_computations + + def test_validation_rejects_invalid_inputs(self) -> None: + component = self._make_discrete_pmf_distribution([0.0], [1.0]) + continuous = self._make_uniform_full_distribution(0.0, 1.0) + + with pytest.raises(ValueError, match="at least one component"): + finite_mixture([]) + with pytest.raises(ValueError, match="equal to 1.0"): + finite_mixture([(0.6, component)]) + with pytest.raises(ValueError, match="non-negative"): + finite_mixture([(-1.0, component)]) + with pytest.raises(TypeError, match="same distribution kind"): + finite_mixture([(0.5, component), (0.5, continuous)]) + + def test_sample_selects_components_by_weights(self, monkeypatch: pytest.MonkeyPatch) -> None: + first = self._make_discrete_pmf_distribution([0.0], [1.0]) + second = self._make_discrete_pmf_distribution([1.0], [1.0]) + mixture = finite_mixture([(0.3, first), (0.7, second)]) + selected_indices = np.asarray([1, 0, 1, 1, 0], dtype=int) + captured: list[tuple[int, object, dict[str, object]]] = [] + + class _FakeRng: + def choice(self, a: int, *, size: int, p: NumericArray) -> NumericArray: + assert a == 2 + assert size == selected_indices.size + np.testing.assert_allclose( + np.asarray(p, dtype=float), + np.asarray([0.3, 0.7], dtype=float), + ) + return selected_indices + + def _fake_sample(n: int, distr: object, **options: object) -> NumericArray: + sample_size = int(n) + captured.append((sample_size, distr, dict(options))) + if distr is mixture.components[0]: + return cast(NumericArray, np.asarray([100.0, 101.0], dtype=float)) + if distr is mixture.components[1]: + return cast(NumericArray, np.asarray([200.0, 201.0, 202.0], dtype=float)) + raise AssertionError("Unexpected component passed to mixture sampler.") + + monkeypatch.setattr( + "pysatl_core.transformations.operations.mixture.np.random.default_rng", + lambda: _FakeRng(), + ) + monkeypatch.setattr(mixture.sampling_strategy, "sample", _fake_sample) + samples = mixture.sample(5, token="mixture") + + assert captured == [ + (2, mixture.components[0], {"token": "mixture"}), + (3, mixture.components[1], {"token": "mixture"}), + ] + assert samples == pytest.approx([200.0, 100.0, 201.0, 202.0, 101.0]) diff --git a/tests/unit/transformations/test_distribution.py b/tests/unit/transformations/test_distribution.py new file mode 100644 index 0000000..d0df984 --- /dev/null +++ b/tests/unit/transformations/test_distribution.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import pytest + +from pysatl_core.distributions.computation import AnalyticalComputation +from pysatl_core.distributions.registry import characteristic_registry +from pysatl_core.transformations.distribution import ApproximatedDistribution, DerivedDistribution +from pysatl_core.types import ( + DEFAULT_ANALYTICAL_COMPUTATION_LABEL, + CharacteristicName, + ComputationFunc, + Kind, + TransformationName, +) +from tests.unit.distributions.test_basic import DistributionTestBase + + +class TestDerivedDistribution(DistributionTestBase): + def test_derived_distribution_is_abstract(self) -> None: + base = self.make_logistic_cdf_distribution() + derived_distribution_cls: Any = DerivedDistribution + + with pytest.raises(TypeError, match="abstract"): + _ = derived_distribution_cls( + distribution_type=base.distribution_type, + bases={}, + analytical_computations=base.analytical_computations, + transformation_name=TransformationName.AFFINE, + ) + + +class TestApproximatedDistribution(DistributionTestBase): + def test_loops_are_never_analytical(self) -> None: + def _cdf(data: float, **_options: Any) -> float: + return data + + source = self.make_logistic_cdf_distribution() + approx = ApproximatedDistribution( + distribution_type=source.distribution_type, + analytical_computations={ + CharacteristicName.CDF: AnalyticalComputation[float, float]( + target=CharacteristicName.CDF, + func=cast(ComputationFunc[float, float], _cdf), + ) + }, + support=source.support, + ) + + assert getattr(approx.distribution_type, "kind", None) == Kind.CONTINUOUS + assert not approx.loop_is_analytical( + CharacteristicName.CDF, DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ) + + view = characteristic_registry().view(approx) + cdf_loop = view.variants(CharacteristicName.CDF, CharacteristicName.CDF)[ + DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ] + assert cdf_loop.edge_kind() == "transformation_loop" + assert not cdf_loop.is_analytical diff --git a/tests/unit/transformations/test_lightweight_distribution.py b/tests/unit/transformations/test_lightweight_distribution.py new file mode 100644 index 0000000..00ab216 --- /dev/null +++ b/tests/unit/transformations/test_lightweight_distribution.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +import gc +import weakref + +import pytest + +from pysatl_core.distributions.distribution import Distribution +from pysatl_core.distributions.registry import characteristic_registry +from pysatl_core.transformations.lightweight_distribution import LightweightDistribution +from pysatl_core.transformations.operations import affine +from pysatl_core.types import DEFAULT_ANALYTICAL_COMPUTATION_LABEL, CharacteristicName +from tests.unit.distributions.test_basic import DistributionTestBase + +_BASE_ROLE = "base" + + +class TestLightweightBaseStorage(DistributionTestBase): + def test_affine_stores_lightweight_base_distribution(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=2.0, shift=1.0) + + base_snapshot = transformed.bases[_BASE_ROLE] + + assert isinstance(base_snapshot, LightweightDistribution) + assert isinstance(base_snapshot, Distribution) + assert id(base_snapshot) != id(base) + assert base_snapshot.computation_strategy is base.computation_strategy + assert base_snapshot.sampling_strategy is base.sampling_strategy + + cdf = base_snapshot.query_method(CharacteristicName.CDF) + assert cdf(0.0) == pytest.approx(0.5) + + def test_original_base_is_collectible_after_transformation(self) -> None: + base = self.make_logistic_cdf_distribution() + base_reference = weakref.ref(base) + + _ = affine(base, scale=1.5, shift=-0.5) + + del base + gc.collect() + + assert base_reference() is None + + def test_lightweight_preserves_chain_of_bases(self) -> None: + base = self.make_discrete_point_pmf_distribution() + first = affine(base, scale=-1.0, shift=0.0) + second = affine(first, scale=2.0, shift=1.0) + + first_snapshot = second.bases[_BASE_ROLE] + assert isinstance(first_snapshot, LightweightDistribution) + + nested_base = first_snapshot.bases[_BASE_ROLE] + assert isinstance(nested_base, LightweightDistribution) + assert id(nested_base) != id(base) + + def test_loop_analytical_flags_survive_lightweight_snapshot(self) -> None: + base = self.make_discrete_point_pmf_distribution() + first = affine(base, scale=-1.0, shift=0.0) + second = affine(first, scale=2.0, shift=1.0) + + view = characteristic_registry().view(second) + cdf_loop = view.variants(CharacteristicName.CDF, CharacteristicName.CDF)[ + DEFAULT_ANALYTICAL_COMPUTATION_LABEL + ] + + assert cdf_loop.edge_kind() == "transformation_loop" + assert not cdf_loop.is_analytical diff --git a/tests/unit/transformations/test_operators_mixin.py b/tests/unit/transformations/test_operators_mixin.py new file mode 100644 index 0000000..17d40b7 --- /dev/null +++ b/tests/unit/transformations/test_operators_mixin.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import pytest + +from pysatl_core.distributions.distribution import Distribution +from pysatl_core.families.distribution import ParametricFamilyDistribution +from pysatl_core.families.registry import ParametricFamilyRegister +from pysatl_core.transformations import TransformationOperatorsMixin +from pysatl_core.transformations.distribution import DerivedDistribution +from pysatl_core.transformations.operations import affine +from pysatl_core.types import CharacteristicName +from tests.unit.distributions.test_basic import DistributionTestBase +from tests.unit.families.test_basic import TestBaseFamily + + +class TestTransformationOperatorsMixin(DistributionTestBase, TestBaseFamily): + def test_distribution_does_not_inherit_transformation_operator_mixin(self) -> None: + assert not issubclass(Distribution, TransformationOperatorsMixin) + + def test_derived_distribution_inherits_transformation_operator_mixin(self) -> None: + assert issubclass(DerivedDistribution, TransformationOperatorsMixin) + + def test_parametric_family_distribution_inherits_transformation_operator_mixin(self) -> None: + assert issubclass(ParametricFamilyDistribution, TransformationOperatorsMixin) + + def test_base_distribution_without_mixin_rejects_affine_operators(self) -> None: + base = self.make_logistic_cdf_distribution() + with pytest.raises(TypeError): + _ = cast(Any, base) + 2.0 + + def test_parametric_family_distribution_supports_affine_operators(self) -> None: + family = self.make_default_family() + ParametricFamilyRegister.register(family) + distribution = family.distribution("base", value=0.0) + + shifted = distribution + 2.0 + assert isinstance(shifted, DerivedDistribution) + + shifted_cdf = shifted.query_method(CharacteristicName.CDF) + assert shifted_cdf(2.0) == pytest.approx(0.0) + + def test_derived_distribution_supports_affine_operators(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=2.0, shift=1.0) + + scaled = transformed * 3.0 + assert isinstance(scaled, DerivedDistribution) + + scaled_cdf = scaled.query_method(CharacteristicName.CDF) + assert scaled_cdf(3.0) == pytest.approx(0.5) + + def test_unsupported_operand_raises_type_error(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=1.0, shift=0.0) + with pytest.raises(TypeError): + _ = transformed + "bad" + + def test_division_by_zero_raises_error(self) -> None: + base = self.make_logistic_cdf_distribution() + transformed = affine(base, scale=1.0, shift=0.0) + with pytest.raises(ZeroDivisionError, match="Cannot divide a distribution by zero."): + _ = transformed / 0.0 diff --git a/tests/unit/transformations/test_transformation_method.py b/tests/unit/transformations/test_transformation_method.py new file mode 100644 index 0000000..9b31881 --- /dev/null +++ b/tests/unit/transformations/test_transformation_method.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +__author__ = "Leonid Elkin" +__copyright__ = "Copyright (c) 2025 PySATL project" +__license__ = "SPDX-License-Identifier: MIT" + +from typing import Any, cast + +import pytest + +from pysatl_core.transformations.transformation_method import ( + ResolvedSourceMethods, + TransformationMethod, +) +from pysatl_core.types import ( + CharacteristicName, + ComputationFunc, + Method, + TransformationName, +) +from tests.unit.distributions.test_basic import DistributionTestBase + +_BASE_ROLE = "base" + + +class TestTransformationMethod(DistributionTestBase): + def test_try_from_parents_marks_non_analytical_for_mixed_sources(self) -> None: + base = self.make_logistic_cdf_distribution() + + def _evaluator(sources: ResolvedSourceMethods) -> ComputationFunc[float, float]: + base_cdf = cast(Method[float, float], sources[_BASE_ROLE][CharacteristicName.CDF]) + _ = sources[_BASE_ROLE][CharacteristicName.PDF] + + def _cdf(data: float, **options: Any) -> float: + return float(base_cdf(data, **options)) + + return cast(ComputationFunc[float, float], _cdf) + + method: TransformationMethod[float, float] | None + method, is_analytical, has_any_present_source = TransformationMethod.try_from_parents( + target=CharacteristicName.CDF, + transformation=TransformationName.AFFINE, + bases={_BASE_ROLE: base}, + source_requirements={ + _BASE_ROLE: ( + CharacteristicName.CDF, + CharacteristicName.PDF, + ) + }, + evaluator=_evaluator, + ) + + assert method is not None + assert has_any_present_source + assert not is_analytical + assert method(0.0) == pytest.approx(0.5) + + def test_try_from_parents_skips_when_no_sources_present( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + base = self.make_logistic_cdf_distribution() + + def _unexpected_query(*_args: Any, **_kwargs: Any) -> Method[Any, Any]: + raise AssertionError("query_method should not be called when no sources are present.") + + def _unused_evaluator(_sources: ResolvedSourceMethods) -> ComputationFunc[float, float]: + def _zero(data: float, **_options: Any) -> float: + _ = data + return 0.0 + + return cast(ComputationFunc[float, float], _zero) + + monkeypatch.setattr(base, "query_method", _unexpected_query) + + method: TransformationMethod[float, float] | None + method, is_analytical, has_any_present_source = TransformationMethod.try_from_parents( + target=CharacteristicName.PMF, + transformation=TransformationName.AFFINE, + bases={_BASE_ROLE: base}, + source_requirements={_BASE_ROLE: (CharacteristicName.PMF,)}, + evaluator=_unused_evaluator, + ) + + assert method is None + assert not has_any_present_source + assert not is_analytical From ed42b092d95051d84bad5225743344b4a50172c4 Mon Sep 17 00:00:00 2001 From: LeonidElkin Date: Sat, 28 Mar 2026 22:19:35 +0300 Subject: [PATCH 2/4] refactor(fitters): switched to numpy array semantic --- src/pysatl_core/distributions/fitters.py | 282 ++++++++++++------ .../distributions/registry/configuration.py | 18 +- src/pysatl_core/distributions/strategies.py | 10 +- .../linear_interpolations/cdf.py | 2 - .../linear_interpolations/pdf.py | 2 - .../linear_interpolations/ppf.py | 2 - tests/unit/distributions/test_registry.py | 16 +- 7 files changed, 219 insertions(+), 113 deletions(-) diff --git a/src/pysatl_core/distributions/fitters.py b/src/pysatl_core/distributions/fitters.py index ea39faa..9bad929 100644 --- a/src/pysatl_core/distributions/fitters.py +++ b/src/pysatl_core/distributions/fitters.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any, cast import numpy as np -from mypy_extensions import KwArg from scipy import ( integrate as _sp_integrate, optimize as _sp_optimize, @@ -21,20 +20,21 @@ ExplicitTableDiscreteSupport, IntegerLatticeDiscreteSupport, ) -from pysatl_core.types import CharacteristicName +from pysatl_core.types import CharacteristicName, ComputationFunc, Number, NumericArray if TYPE_CHECKING: - from typing import Any - from pysatl_core.distributions.distribution import Distribution from pysatl_core.types import GenericCharacteristicName type ScalarFunc = Callable[[float], float] -def _resolve(distribution: Distribution, name: GenericCharacteristicName) -> ScalarFunc: +type PointwiseMethod = Callable[..., Any] + + +def _resolve(distribution: Distribution, name: GenericCharacteristicName) -> PointwiseMethod: """ - Resolve a scalar characteristic from the distribution. + Resolve a pointwise characteristic method from the distribution. Parameters ---------- @@ -45,8 +45,8 @@ def _resolve(distribution: Distribution, name: GenericCharacteristicName) -> Sca Returns ------- - Callable[[float], float] - Scalar callable for the requested characteristic. + Callable[..., Any] + Pointwise callable for the requested characteristic. Raises ------ @@ -57,13 +57,54 @@ def _resolve(distribution: Distribution, name: GenericCharacteristicName) -> Sca fn = distribution.query_method(name) except AttributeError as e: raise RuntimeError( - "Distribution must provide computation_strategy.querry_method(name, distribution)." + "Distribution must provide computation_strategy.query_method(name, distribution)." ) from e - def _wrap(x: float, **kwargs: Any) -> float: - return float(fn(x, **kwargs)) + return cast(PointwiseMethod, fn) + + +def _eval_method_scalar(method: PointwiseMethod, x: float, **kwargs: Any) -> float: + """ + Evaluate a pointwise method at a scalar and cast the result to ``float``. + + Parameters + ---------- + method : Callable[..., Any] + Method returned by ``distribution.query_method(...)``. + x : float + Evaluation point. + **kwargs : Any + Additional options forwarded to the method. + + Returns + ------- + float + Scalar numeric result. + """ + value = method(np.asarray(x, dtype=float), **kwargs) + return float(np.asarray(value, dtype=float)) + - return _wrap +def _map_scalar_real(data: Number | NumericArray, scalar_func: ScalarFunc) -> NumericArray: + """ + Apply a scalar real function to scalar or array input with NumPy semantics. + + Parameters + ---------- + data : Number or NumericArray + Input values. + scalar_func : Callable[[float], float] + Scalar function to evaluate pointwise. + + Returns + ------- + NumericArray + Result with the same shape as ``data``. + """ + array = np.asarray(data, dtype=float) + flat = array.reshape(-1) + mapped = np.fromiter((scalar_func(float(x)) for x in flat), dtype=float, count=flat.size) + return cast(NumericArray, mapped.reshape(array.shape)) def _ppf_brentq_from_cdf( @@ -224,7 +265,7 @@ def _num_derivative(f: ScalarFunc, x: float, h: float = 1e-5) -> float: def fit_pdf_to_cdf_1C( distribution: Distribution, /, **kwargs: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit ``cdf`` from an analytical or resolvable ``pdf`` via numerical integration. @@ -234,26 +275,34 @@ def fit_pdf_to_cdf_1C( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``pdf -> cdf`` conversion. """ - pdf_func = _resolve(distribution, CharacteristicName.PDF) + _ = kwargs + pdf_method = _resolve(distribution, CharacteristicName.PDF) - def _cdf(x: float, **options: Any) -> float: + def _cdf_scalar(x: float, **options: Any) -> float: val, _ = _sp_integrate.quad( - lambda t: float(pdf_func(t, **options)), float("-inf"), x, limit=200 + lambda t: _eval_method_scalar(pdf_method, t, **options), + float("-inf"), + x, + limit=200, ) return float(np.clip(val, 0.0, 1.0)) - cdf_func = cast(Callable[[float, KwArg(Any)], float], _cdf) - return FittedComputationMethod[float, float]( - target=CharacteristicName.CDF, sources=[CharacteristicName.PDF], func=cdf_func + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _cdf_scalar(x, **options)) + + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + sources=[CharacteristicName.PDF], + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf), ) def fit_cdf_to_pdf_1C( distribution: Distribution, /, **kwargs: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit ``pdf`` as a clipped numerical derivative of ``cdf``. @@ -263,27 +312,32 @@ def fit_cdf_to_pdf_1C( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``cdf -> pdf`` conversion. """ - cdf_func = _resolve(distribution, CharacteristicName.CDF) + _ = kwargs + cdf_method = _resolve(distribution, CharacteristicName.CDF) - def _pdf(x: float, **options: Any) -> float: + def _pdf_scalar(x: float, **options: Any) -> float: def wrapped_cdf(t: float) -> float: - return cdf_func(t, **options) + return _eval_method_scalar(cdf_method, t, **options) d = _num_derivative(wrapped_cdf, x, h=1e-5) return float(max(d, 0.0)) - pdf_func = cast(Callable[[float, KwArg(Any)], float], _pdf) - return FittedComputationMethod[float, float]( - target=CharacteristicName.PDF, sources=[CharacteristicName.CDF], func=pdf_func + def _pdf(data: NumericArray, **options: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _pdf_scalar(x, **options)) + + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.PDF, + sources=[CharacteristicName.CDF], + func=cast(ComputationFunc[NumericArray, NumericArray], _pdf), ) def fit_cdf_to_ppf_1C( distribution: Distribution, /, **options: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit ``ppf`` from a resolvable ``cdf`` using a robust bracketing procedure. @@ -293,28 +347,52 @@ def fit_cdf_to_ppf_1C( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``cdf -> ppf`` conversion. """ - cdf_func = _resolve(distribution, CharacteristicName.CDF) - - def cdf_with_options(x: float) -> float: - return cdf_func(x, **options) - - ppf_func = _ppf_brentq_from_cdf(cdf_with_options, **options) - - def _ppf(q: float, **kwargs: Any) -> float: - return ppf_func(q) + cdf_method = _resolve(distribution, CharacteristicName.CDF) + + solver_option_names = { + "most_left", + "x0", + "init_step", + "expand_factor", + "max_expand", + "x_tol", + "y_tol", + "max_iter", + } + solver_options = {name: value for name, value in options.items() if name in solver_option_names} + cdf_options = { + name: value for name, value in options.items() if name not in solver_option_names + } + + def _cdf_scalar(x: float, **call_options: Any) -> float: + merged = dict(cdf_options) + merged.update(call_options) + return _eval_method_scalar(cdf_method, x, **merged) + + base_ppf_func = _ppf_brentq_from_cdf(lambda x: _cdf_scalar(x), **solver_options) + + def _ppf(data: NumericArray, **kwargs: Any) -> NumericArray: + if kwargs: + dynamic_ppf = _ppf_brentq_from_cdf( + lambda x: _cdf_scalar(x, **kwargs), + **solver_options, + ) + return _map_scalar_real(data, dynamic_ppf) + return _map_scalar_real(data, base_ppf_func) - ppf_cast = cast(Callable[[float, KwArg(Any)], float], _ppf) - return FittedComputationMethod[float, float]( - target=CharacteristicName.PPF, sources=[CharacteristicName.CDF], func=ppf_cast + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.PPF, + sources=[CharacteristicName.CDF], + func=cast(ComputationFunc[NumericArray, NumericArray], _ppf), ) def fit_ppf_to_cdf_1C( distribution: Distribution, /, **_: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit ``cdf`` by numerically inverting a resolvable ``ppf`` with a root solver. @@ -324,17 +402,17 @@ def fit_ppf_to_cdf_1C( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``ppf -> cdf`` conversion. """ - ppf_func = _resolve(distribution, CharacteristicName.PPF) + ppf_method = _resolve(distribution, CharacteristicName.PPF) - def _cdf(x: float, **options: Any) -> float: + def _cdf_scalar(x: float, **options: Any) -> float: if not isfinite(x): return 0.0 if x == float("-inf") else 1.0 def f(q: float) -> float: - return float(ppf_func(q, **options) - x) + return _eval_method_scalar(ppf_method, q, **options) - x lo, hi = 1e-12, 1.0 - 1e-12 flo, fhi = f(lo), f(hi) @@ -345,9 +423,13 @@ def f(q: float) -> float: q = float(_sp_optimize.brentq(f, lo, hi, maxiter=256)) # type: ignore[arg-type] return float(np.clip(q, 0.0, 1.0)) - cdf_func = cast(Callable[[float, KwArg(Any)], float], _cdf) - return FittedComputationMethod[float, float]( - target=CharacteristicName.CDF, sources=[CharacteristicName.PPF], func=cdf_func + def _cdf(data: NumericArray, **options: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _cdf_scalar(x, **options)) + + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + sources=[CharacteristicName.PPF], + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf), ) @@ -356,7 +438,7 @@ def f(q: float) -> float: def fit_pmf_to_cdf_1D( distribution: Distribution, /, **_: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Build Characteristic.CDF from Characteristic.PMF on a discrete support by partial summation. @@ -378,7 +460,7 @@ def fit_pmf_to_cdf_1D( if support is None or not isinstance(support, DiscreteSupport): raise RuntimeError("Discrete support is required for pmf->cdf.") - pmf_func = _resolve(distribution, CharacteristicName.PMF) + pmf_method = _resolve(distribution, CharacteristicName.PMF) # Special case: right-bounded integer lattice if isinstance(support, IntegerLatticeDiscreteSupport): @@ -395,7 +477,7 @@ def fit_pmf_to_cdf_1D( if not support.is_left_bounded and support.max_k is not None: max_k = support.max_k - def _cdf(x: float, **kwargs: Any) -> float: + def _cdf_scalar(x: float, **kwargs: Any) -> float: # Everything to the right of the upper bound has Characteristic.CDF == 1. if x >= max_k: return 1.0 @@ -418,45 +500,50 @@ def _cdf(x: float, **kwargs: Any) -> float: tail = 0.0 cur = k while cur <= max_k: - tail += float(pmf_func(float(cur), **kwargs)) + tail += _eval_method_scalar(pmf_method, float(cur), **kwargs) cur += support.modulus return float(np.clip(1.0 - tail, 0.0, 1.0)) - _cdf_func = cast(Callable[[float, KwArg(Any)], float], _cdf) + def _cdf_lattice(data: NumericArray, **kwargs: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _cdf_scalar(x, **kwargs)) - return FittedComputationMethod[float, float]( - target=CharacteristicName.CDF, sources=[CharacteristicName.PMF], func=_cdf_func + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + sources=[CharacteristicName.PMF], + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf_lattice), ) - def _cdf_prefix(x: float, **kwargs: Any) -> float: + def _cdf_prefix_scalar(x: float, **kwargs: Any) -> float: s = 0.0 for k in support.iter_leq(x): - s += float(pmf_func(float(k), **kwargs)) + s += _eval_method_scalar(pmf_method, float(k), **kwargs) return float(np.clip(s, 0.0, 1.0)) - _cdf_func = cast(Callable[[float, KwArg(Any)], float], _cdf_prefix) + def _cdf(data: NumericArray, **kwargs: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _cdf_prefix_scalar(x, **kwargs)) - return FittedComputationMethod[float, float]( - target=CharacteristicName.CDF, sources=[CharacteristicName.PMF], func=_cdf_func + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + sources=[CharacteristicName.PMF], + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf), ) def fit_cdf_to_pmf_1D( distribution: Distribution, /, **_: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Extract Characteristic.PMF from Characteristic.CDF on a discrete support as jump sizes. Parameters ---------- distribution : Distribution - Distribution exposing a discrete support on ``.support`` and a scalar - ``cdf`` via the computation strategy. + Distribution exposing a discrete support on ``.support``. Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``cdf -> pmf`` conversion. Raises @@ -472,19 +559,22 @@ def fit_cdf_to_pmf_1D( support = distribution.support if support is None or not isinstance(support, DiscreteSupport): raise RuntimeError("Discrete support is required for cdf->pmf.") - cdf_func = _resolve(distribution, CharacteristicName.CDF) + cdf_method = _resolve(distribution, CharacteristicName.CDF) - def _pmf(x: float, **kwargs: Any) -> float: + def _pmf_scalar(x: float, **kwargs: Any) -> float: p = support.prev(x) - left = 0.0 if p is None else float(cdf_func(float(p), **kwargs)) - right = float(cdf_func(x)) + left = 0.0 if p is None else _eval_method_scalar(cdf_method, float(p), **kwargs) + right = _eval_method_scalar(cdf_method, x, **kwargs) mass = max(right - left, 0.0) return float(np.clip(mass, 0.0, 1.0)) - _pmf_func = cast(Callable[[float, KwArg(Any)], float], _pmf) + def _pmf(data: NumericArray, **kwargs: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _pmf_scalar(x, **kwargs)) - return FittedComputationMethod[float, float]( - target=CharacteristicName.PMF, sources=[CharacteristicName.CDF], func=_pmf_func + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.PMF, + sources=[CharacteristicName.CDF], + func=cast(ComputationFunc[NumericArray, NumericArray], _pmf), ) @@ -577,7 +667,7 @@ def _collect_support_values(support: Any) -> np.ndarray: def fit_cdf_to_ppf_1D( distribution: Distribution, /, **options: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit **discrete** Characteristic.PPF from a resolvable Characteristic.CDF and explicit discrete support. @@ -599,24 +689,27 @@ def fit_cdf_to_ppf_1D( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``cdf -> ppf`` conversion for discrete 1D distributions. """ support = distribution.support if support is None or not isinstance(support, DiscreteSupport): raise RuntimeError("Discrete support is required for cdf->ppf.") - cdf_func = _resolve(distribution, CharacteristicName.CDF) + cdf_method = _resolve(distribution, CharacteristicName.CDF) xs = _collect_support_values(support) # sorted float array if xs.size == 0: raise RuntimeError("Discrete support is empty.") # Pre-compute Characteristic.CDF on support and enforce monotonicity (safety against FP noise) - cdf_vals = np.asarray([float(cdf_func(float(x))) for x in xs], dtype=float) + cdf_vals = np.asarray( + [_eval_method_scalar(cdf_method, float(x), **options) for x in xs], + dtype=float, + ) cdf_vals = np.clip(np.maximum.accumulate(cdf_vals), 0.0, 1.0) - def _ppf(q: float, **kwargs: Any) -> float: + def _ppf_scalar(q: float) -> float: if not isfinite(q): return float("nan") q = float(q) @@ -629,16 +722,20 @@ def _ppf(q: float, **kwargs: Any) -> float: idx = xs.size - 1 return float(xs[idx]) - _ppf_func = cast(Callable[[float, KwArg(Any)], float], _ppf) + def _ppf(data: NumericArray, **kwargs: Any) -> NumericArray: + _ = kwargs + return _map_scalar_real(data, _ppf_scalar) - return FittedComputationMethod[float, float]( - target=CharacteristicName.PPF, sources=[CharacteristicName.CDF], func=_ppf_func + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.PPF, + sources=[CharacteristicName.CDF], + func=cast(ComputationFunc[NumericArray, NumericArray], _ppf), ) def fit_ppf_to_cdf_1D( distribution: Distribution, /, **options: Any -) -> FittedComputationMethod[float, float]: +) -> FittedComputationMethod[NumericArray, NumericArray]: """ Fit **discrete** Characteristic.CDF using only a resolvable Characteristic.PPF via bisection on ``q``. @@ -660,24 +757,24 @@ def fit_ppf_to_cdf_1D( Returns ------- - FittedComputationMethod[float, float] + FittedComputationMethod[NumericArray, NumericArray] Fitted ``ppf -> cdf`` conversion for discrete 1D distributions. """ - ppf_func = _resolve(distribution, CharacteristicName.PPF) + ppf_method = _resolve(distribution, CharacteristicName.PPF) q_tol: float = float(options.get("q_tol", 1e-12)) max_iter: int = int(options.get("max_iter", 100)) # Quick edge probes (robust to weird Characteristic.PPF endpoints) try: - p0 = float(ppf_func(0.0)) + p0 = _eval_method_scalar(ppf_method, 0.0) except Exception: p0 = float("-inf") try: - p1 = float(ppf_func(1.0 - 1e-15)) + p1 = _eval_method_scalar(ppf_method, 1.0 - 1e-15) except Exception: p1 = float("inf") - def _cdf(x: float, **kwargs: Any) -> float: + def _cdf_scalar(x: float, **kwargs: Any) -> float: if not isfinite(x): return float("nan") # Hard clamps from endpoint probes @@ -692,7 +789,7 @@ def _cdf(x: float, **kwargs: Any) -> float: it += 1 mid = 0.5 * (lo + hi) try: - y = float(ppf_func(mid, **kwargs)) + y = _eval_method_scalar(ppf_method, mid, **kwargs) except Exception: # If Characteristic.PPF fails at mid, shrink conservatively towards lo hi = mid @@ -703,8 +800,11 @@ def _cdf(x: float, **kwargs: Any) -> float: hi = mid # crossed threshold return float(np.clip(lo, 0.0, 1.0)) - _cdf_func = cast(Callable[[float, KwArg(Any)], float], _cdf) + def _cdf(data: NumericArray, **kwargs: Any) -> NumericArray: + return _map_scalar_real(data, lambda x: _cdf_scalar(x, **kwargs)) - return FittedComputationMethod[float, float]( - target=CharacteristicName.CDF, sources=[CharacteristicName.PPF], func=_cdf_func + return FittedComputationMethod[NumericArray, NumericArray]( + target=CharacteristicName.CDF, + sources=[CharacteristicName.PPF], + func=cast(ComputationFunc[NumericArray, NumericArray], _cdf), ) diff --git a/src/pysatl_core/distributions/registry/configuration.py b/src/pysatl_core/distributions/registry/configuration.py index 61b2bc7..6d7e775 100644 --- a/src/pysatl_core/distributions/registry/configuration.py +++ b/src/pysatl_core/distributions/registry/configuration.py @@ -32,38 +32,38 @@ SetConstraint, ) from pysatl_core.distributions.registry.graph import CharacteristicRegistry -from pysatl_core.types import CharacteristicName, Kind +from pysatl_core.types import CharacteristicName, Kind, NumericArray def _configure(reg: CharacteristicRegistry) -> None: """Default PySATL configuration for characteristic registry.""" - pdf_to_cdf_1C = ComputationMethod[float, float]( + pdf_to_cdf_1C = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.CDF, sources=[CharacteristicName.PDF], fitter=fit_pdf_to_cdf_1C ) - cdf_to_pdf_1C = ComputationMethod[float, float]( + cdf_to_pdf_1C = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.PDF, sources=[CharacteristicName.CDF], fitter=fit_cdf_to_pdf_1C ) - cdf_to_ppf_1C = ComputationMethod[float, float]( + cdf_to_ppf_1C = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.PPF, sources=[CharacteristicName.CDF], fitter=fit_cdf_to_ppf_1C ) - ppf_to_cdf_1C = ComputationMethod[float, float]( + ppf_to_cdf_1C = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.CDF, sources=[CharacteristicName.PPF], fitter=fit_ppf_to_cdf_1C ) - pmf_to_cdf_1D = ComputationMethod[float, float]( + pmf_to_cdf_1D = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.CDF, sources=[CharacteristicName.PMF], fitter=fit_pmf_to_cdf_1D, ) - cdf_to_pmf_1D = ComputationMethod[float, float]( + cdf_to_pmf_1D = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.PMF, sources=[CharacteristicName.CDF], fitter=fit_cdf_to_pmf_1D, ) - cdf_to_ppf_1D = ComputationMethod[float, float]( + cdf_to_ppf_1D = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.PPF, sources=[CharacteristicName.CDF], fitter=fit_cdf_to_ppf_1D ) - ppf_to_cdf_1D = ComputationMethod[float, float]( + ppf_to_cdf_1D = ComputationMethod[NumericArray, NumericArray]( target=CharacteristicName.CDF, sources=[CharacteristicName.PPF], fitter=fit_ppf_to_cdf_1D ) diff --git a/src/pysatl_core/distributions/strategies.py b/src/pysatl_core/distributions/strategies.py index 8ab8111..3542034 100644 --- a/src/pysatl_core/distributions/strategies.py +++ b/src/pysatl_core/distributions/strategies.py @@ -250,7 +250,6 @@ class DefaultSamplingUnivariateStrategy(SamplingStrategy): ----- - Requires the distribution to provide a PPF computation method. - Assumes that the PPF follows NumPy semantics (vectorized evaluation). - - Graph-derived PPFs (scalar-only) are currently not supported. - Returns a NumPy array containing the generated samples. """ @@ -276,8 +275,9 @@ def sample(self, n: int, distr: Distribution, **options: Any) -> NumericArray: ppf = distr.query_method(CharacteristicName.PPF, **options) rng = np.random.default_rng() U = rng.random(n) - # TODO: Now it will be based on the fact that the characteristic - # has NumPy semantics (It is much more faster), that is, - # it will not work with the graph computed characteristics currently. - samples = ppf(U) + samples = np.asarray(ppf(U), dtype=float) + if samples.shape != U.shape: + raise RuntimeError( + "PPF must preserve NumPy input shape for inverse-transform sampling." + ) return cast(NumericArray, samples) diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py index a09d57e..678c40b 100644 --- a/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/cdf.py @@ -100,8 +100,6 @@ def _cdf(data: Any, /, **_kwargs: Any) -> Any: result = np.where(array < lower_limit, 0.0, result) result = np.where(array > upper_limit, 1.0, result) result = np.nan_to_num(result, nan=0.0, posinf=1.0, neginf=0.0) - if np.ndim(array) == 0: - return float(result) return cast(NumericArray, result) return AnalyticalComputation( diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py index 8b18b4b..db5cdb1 100644 --- a/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/pdf.py @@ -82,8 +82,6 @@ def approximate( def _pdf(data: Any, /, **_kwargs: Any) -> Any: array = np.asarray(data, dtype=float) result = np.interp(array, grid, values, left=0.0, right=0.0) - if np.ndim(array) == 0: - return float(result) return cast(NumericArray, np.asarray(result, dtype=float)) return AnalyticalComputation( diff --git a/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py b/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py index 5b7ab25..3d31062 100644 --- a/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py +++ b/src/pysatl_core/transformations/approximations/linear_interpolations/ppf.py @@ -84,8 +84,6 @@ def _ppf(data: Any, /, **_kwargs: Any) -> Any: result = np.asarray(spline(clipped), dtype=float) result = np.where(array <= self._lower_limit, lower_value, result) result = np.where(array >= self._upper_limit, upper_value, result) - if np.ndim(array) == 0: - return float(result) return cast(NumericArray, result) return AnalyticalComputation( diff --git a/tests/unit/distributions/test_registry.py b/tests/unit/distributions/test_registry.py index 3a8e263..03318b9 100644 --- a/tests/unit/distributions/test_registry.py +++ b/tests/unit/distributions/test_registry.py @@ -61,8 +61,10 @@ def test_configuration_continuous_presence_and_connectivity(self) -> None: ppf = strategy.query_method(CharacteristicName.PPF, distr) cdf = strategy.query_method(CharacteristicName.CDF, distr) qs = np.linspace(1e-6, 1.0 - 1e-6, 7) - errs = [abs(float(cdf(float(ppf(float(q))))) - q) for q in qs] - assert max(errs) < 5e-3 + quantiles = np.asarray(ppf(qs), dtype=float) + assert quantiles.shape == qs.shape + roundtrip = np.asarray([float(cdf(float(x))) for x in quantiles], dtype=float) + assert float(np.max(np.abs(roundtrip - qs))) < 5e-3 def test_view_adds_analytical_self_loops_with_labels(self) -> None: def cdf_primary(x: float, **_kwargs: Any) -> float: @@ -156,11 +158,21 @@ def test_configuration_discrete_requires_support_then_ok(self) -> None: assert cdf(0.0) == pytest.approx(0.2, abs=1e-10) assert cdf(1.0) == pytest.approx(0.7, abs=1e-10) assert cdf(2.0) == pytest.approx(1.0, abs=1e-10) + np.testing.assert_allclose( + np.asarray(cdf(np.asarray([0.0, 1.0, 2.0], dtype=float)), dtype=float), + np.asarray([0.2, 0.7, 1.0], dtype=float), + atol=1e-10, + ) ppf = strategy.query_method(CharacteristicName.PPF, distr) assert ppf(0.10) == pytest.approx(0.0, abs=1e-12) assert ppf(0.70) == pytest.approx(1.0, abs=1e-12) assert ppf(0.95) == pytest.approx(2.0, abs=1e-12) + np.testing.assert_allclose( + np.asarray(ppf(np.asarray([0.10, 0.70, 0.95], dtype=float)), dtype=float), + np.asarray([0.0, 1.0, 2.0], dtype=float), + atol=1e-12, + ) def test_edge_dims_constraint_filters_edges(self) -> None: reg = CharacteristicRegistry() From cc46873894705773edc66616a27444bfbf5a2f2c Mon Sep 17 00:00:00 2001 From: LeonidElkin Date: Sat, 28 Mar 2026 22:42:03 +0300 Subject: [PATCH 3/4] example(transformations): add a small walkthrough of the transformations module --- examples/transformations_overview.ipynb | 623 ++++++++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 examples/transformations_overview.ipynb diff --git a/examples/transformations_overview.ipynb b/examples/transformations_overview.ipynb new file mode 100644 index 0000000..c0a4821 --- /dev/null +++ b/examples/transformations_overview.ipynb @@ -0,0 +1,623 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "970bc022e5e0fb04", + "metadata": {}, + "source": [ + "# Transformations Module Walkthrough\n", + "\n", + "This notebook demonstrates the core capabilities of `pysatl_core.transformations`:\n", + "\n", + "1. Affine transformations (`aX + b`).\n", + "2. Binary transformations (`X + Y`, `X - Y`, `X * Y`, `X / Y`).\n", + "3. Finite weighted mixtures.\n", + "4. Characteristic materialization via approximation methods.\n", + "\n", + "All examples use NumPy-array semantics for characteristic evaluation." + ] + }, + { + "cell_type": "markdown", + "id": "468d39002baa63fd", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "64867a50f3f8b2c0", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:15.488037527Z", + "start_time": "2026-03-28T19:39:15.090899497Z" + } + }, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "from typing import Any\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from pysatl_core import (\n", + " BinaryOperationName,\n", + " CharacteristicName,\n", + " FamilyName,\n", + " ParametricFamilyRegister,\n", + " configure_families_register,\n", + ")\n", + "from pysatl_core.transformations.approximations import (\n", + " CDFMonotoneSplineApproximation,\n", + " PDFLinearInterpolationApproximation,\n", + " PPFMonotoneSplineApproximation,\n", + ")\n", + "from pysatl_core.transformations.operations import affine, binary, finite_mixture" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9ab45a12e5cbd58a", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:15.571566841Z", + "start_time": "2026-03-28T19:39:15.491654432Z" + } + }, + "outputs": [], + "source": [ + "# Register built-in families in the global registry.\n", + "configure_families_register()\n", + "\n", + "normal_family = ParametricFamilyRegister.get(FamilyName.NORMAL)\n", + "uniform_family = ParametricFamilyRegister.get(FamilyName.CONTINUOUS_UNIFORM)\n", + "exp_family = ParametricFamilyRegister.get(FamilyName.EXPONENTIAL)\n", + "\n", + "# Base distributions used below.\n", + "normal_left = normal_family.distribution(parametrization_name=\"meanStd\", mu=-0.5, sigma=0.8)\n", + "normal_right = normal_family.distribution(parametrization_name=\"meanStd\", mu=1.0, sigma=0.6)\n", + "\n", + "uniform_a = uniform_family.distribution(\n", + " parametrization_name=\"standard\", lower_bound=0.0, upper_bound=1.0\n", + ")\n", + "uniform_b = uniform_family.distribution(\n", + " parametrization_name=\"standard\", lower_bound=1.0, upper_bound=2.0\n", + ")\n", + "uniform_den = uniform_family.distribution(\n", + " parametrization_name=\"standard\", lower_bound=1.5, upper_bound=2.5\n", + ")\n", + "\n", + "exp_base = exp_family.distribution(parametrization_name=\"rate\", lambda_=1.25)" + ] + }, + { + "cell_type": "markdown", + "id": "7bf219c364e5a075", + "metadata": {}, + "source": [ + "## Plotting Helpers\n", + "\n", + "All helper utilities are grouped here to keep example cells focused on transformations." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "312f82001d564c72", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:15.627082001Z", + "start_time": "2026-03-28T19:39:15.575486627Z" + } + }, + "outputs": [], + "source": [ + "def _method_values(distribution: Any, characteristic: str, grid: np.ndarray) -> np.ndarray:\n", + " \"\"\"Evaluate a characteristic on a grid and cast to float array.\"\"\"\n", + " method = distribution.query_method(characteristic)\n", + " return np.asarray(method(grid), dtype=float)\n", + "\n", + "\n", + "def plot_curve(\n", + " ax: plt.Axes,\n", + " x: np.ndarray,\n", + " y: np.ndarray,\n", + " *,\n", + " label: str,\n", + " title: str,\n", + " xlabel: str = \"x\",\n", + " ylabel: str = \"value\",\n", + ") -> None:\n", + " \"\"\"Draw a single curve on an axis.\"\"\"\n", + " ax.plot(x, y, label=label)\n", + " ax.set_title(title)\n", + " ax.set_xlabel(xlabel)\n", + " ax.set_ylabel(ylabel)\n", + " ax.grid(alpha=0.25)\n", + " ax.legend()\n", + "\n", + "\n", + "def plot_hist_with_optional_pdf(\n", + " ax: plt.Axes,\n", + " samples: np.ndarray,\n", + " *,\n", + " label: str,\n", + " pdf_distribution: Any | None = None,\n", + " x_grid: np.ndarray | None = None,\n", + " bins: int = 80,\n", + ") -> None:\n", + " \"\"\"Draw a normalized histogram and optionally overlay PDF.\"\"\"\n", + " ax.hist(samples, bins=bins, density=True, alpha=0.45, label=label)\n", + " if pdf_distribution is not None and x_grid is not None:\n", + " pdf_values = _method_values(pdf_distribution, CharacteristicName.PDF, x_grid)\n", + " ax.plot(x_grid, pdf_values, color=\"black\", linewidth=1.8, label=\"PDF\")\n", + " ax.grid(alpha=0.25)\n", + " ax.legend()\n", + "\n", + "\n", + "def show_moments(label: str, distribution: Any) -> None:\n", + " \"\"\"Print mean and variance if available.\"\"\"\n", + " mean = distribution.query_method(CharacteristicName.MEAN)\n", + " var = distribution.query_method(CharacteristicName.VAR)\n", + " print(f\"{label:<28} mean={mean(): .6f} var={var(): .6f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f185e5e3fc500d28", + "metadata": {}, + "source": [ + "## 1) Affine Transformation\n", + "\n", + "The `affine(...)` operation builds a `DerivedDistribution` with transformed characteristics and support." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6b170692471b4898", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:15.735195640Z", + "start_time": "2026-03-28T19:39:15.629602858Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transformation name: affine\n", + "Base roles: ('base',)\n", + "Positive-scale support: ContinuousSupport(left=-inf, right=inf, left_closed=False, right_closed=False)\n", + "Negative-scale support: ContinuousSupport(left=-1.0, right=1.0, left_closed=True, right_closed=True)\n" + ] + } + ], + "source": [ + "affine_pos = affine(normal_left, scale=1.8, shift=0.4)\n", + "affine_neg = affine(uniform_a, scale=-2.0, shift=1.0)\n", + "\n", + "print(\"Transformation name:\", affine_pos.transformation_name)\n", + "print(\"Base roles:\", tuple(affine_pos.bases.keys()))\n", + "print(\"Positive-scale support:\", affine_pos.support)\n", + "print(\"Negative-scale support:\", affine_neg.support)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ca2531ab6a1562f6", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:16.038936705Z", + "start_time": "2026-03-28T19:39:15.738073149Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABQoAAAGGCAYAAAAzYLzoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAxFFJREFUeJzs3Wd4VOXWh/F7SnoPaZRA6L1I74JURRQLIiIgKlZs2I7Hgr0cxfbakSaiYBdBKYIoKIj03gk1CQkJ6X3m/TAkEGkp05L8fxe5MrNnz95rVmbCk7WfYrBarVZERERERERERESkWjO6OgARERERERERERFxPRUKRURERERERERERIVCERERERERERERUaFQREREREREREREUKFQREREREREREREUKFQREREREREREREUKFQREREREREREREUKFQREREREREREREUKFQREREREREREREUKFQRKqZWbNm0axZMzw8PAgODi7e/vrrr9OgQQNMJhPt2rUDICYmhltuucUlcYqIiIiIfakdKCJycSoUikiV8cEHH2AwGOjSpcs5H9+5cye33HILDRs2ZMqUKXzyyScALF68mMcee4wePXowffp0Xn75ZWeGXWrPPvssBoOh+MvX15cWLVrw1FNPkZaWVrzfjBkzSuzn7e1NrVq1GDRoEO+++y7p6ekXPfaZXx999JEzX6aIiIhImVX1dmCRjRs3cvPNNxMdHY2XlxehoaH079+f6dOnU1hYWLzfmW05s9lMaGgoHTp04IEHHmD79u1nHTc2Nva8bcGuXbs68yWKiIuZXR2AiIi9zJ49m5iYGNasWcPevXtp1KhRiceXL1+OxWLhnXfeKfHYsmXLMBqNTJ06FU9Pz+Ltu3btwmh0v+spH374If7+/mRkZLB48WJeeuklli1bxp9//onBYCje7/nnn6d+/frk5+cTHx/P8uXLefDBB3nzzTeZN28ebdq0Oe+xz3S+BreIiIiIu6gO7cBPP/2Uu+66i8jISEaPHk3jxo1JT09n6dKl3HbbbcTFxfHf//63eP8BAwYwZswYrFYrqampbNq0iZkzZ/LBBx/w2muvMXHixLPOMXLkSK644ooS28LDwx3+2kTEfahQKCJVwoEDB/jrr7/47rvvuPPOO5k9ezaTJk0qsc/x48cBSgw1Kdru4+NTonEI4OXl5dCYy+v6668nLCwMgLvuuovrrruO7777jtWrV9OtW7fi/S6//HI6duxYfP+JJ55g2bJlXHnllVx11VXs2LEDHx+f8x5bREREpDKoDu3A1atXc9ddd9GtWzd+/vlnAgICih978MEHWbt2LVu3bi3xnCZNmnDzzTeX2Pbqq68ydOhQHn74YZo1a3ZWUbB9+/ZnPUdEqhf3ukQiIlJOs2fPJiQkhCFDhnD99dcze/bsEo/HxMQUNxjDw8MxGAzFw22nT59OZmZm8fCKGTNmFD/nzLlpiob0/vnnn0ycOJHw8HD8/Py45pprSExMPCumX375hV69euHn50dAQABDhgxh27ZtJfbJz89n586dxMXFlfu1X3bZZYCtkVyafZ9++mkOHjzI559/Xu5zioiIiLiL6tAOfO655zAYDMyePbtEkbBIx44dSzWnYo0aNZgzZw5ms5mXXnrpovuLSPWjQqGIVAmzZ8/m2muvxdPTk5EjR7Jnzx7++eef4sfffvttrrnmGsA2vHbWrFlce+21zJo1i169euHl5cWsWbOYNWsWvXv3vuC57rvvPjZt2sSkSZO4++67+emnn5gwYUKJfWbNmsWQIUPw9/fntdde4+mnn2b79u307NmT2NjY4v2OHj1K8+bNeeKJJ8r92vft2wfYGn6lMXr0aMA2J8+/JScnk5SUVPyVkpJS7rhEREREnKGqtwOzsrJYunQpvXv3pm7dumXMztnq1q3LpZdeyurVq0vMc110rjPbgklJSeTn51f4nCJSeWjosYhUeuvWrWPnzp383//9HwA9e/akTp06zJ49m06dOgEwbNgwNm7cyPfff19ieG2bNm349ddfWb9+famHWdSoUYPFixcXzwdosVh49913SU1NJSgoiIyMDO6//35uv/324omyAcaOHUvTpk15+eWXS2wvq+TkZIDiOQo/+OADIiMj6dWrV6meX6dOHYKCgooLjGdq2rRpifv16tUr0aAVERERcSfVoR24d+9e8vPzad26dZmedyGtWrVi6dKlxMbGlpi3etKkSWcN2/7tt9/o06eP3c4tIu5NhUIRqfRmz55NZGQkffv2BWyrvI0YMYLPP/+cyZMnYzKZ7Hq+O+64o8SiIb169eKtt97i4MGDtGnThiVLlnDy5ElGjhxJUlJS8X4mk4kuXbrw22+/FW+LiYnBarWW6fz/Lua1bNmSmTNn4uvrW+pj+Pv7n3P142+//ZbAwMDi+/+ew1BERETEnVSHdmBRr79zDTkur6LF6/7dHrzjjjsYPnx4iW1t27a123lFxP2pUCgilVphYSFz5syhb9++Jebo69KlC5MnT2bp0qUMHDjQruf895CPkJAQgOJhunv27AFOzx34b2cW4sqjqJjn4eFBnTp1aNiwYZmPkZGRQURExFnbe/furcVMREREpFKoLu3Aouec6yJveWVkZABnFx8bN25M//797XYeEal8VCgUkUpt2bJlxMXFMWfOHObMmXPW47Nnz7Z7A/F8V6aLrghbLBbANj9NVFTUWfuZzRX71VvRYt6RI0dITU2lUaNGFYpDRERExJWqSzuwUaNGmM1mtmzZUubnns/WrVsxmUzUr1/fbscUkapBhUIRqdRmz55NREQE77///lmPfffdd3z//fd89NFHTh1CW9TDLyIiwi2vyM6aNQuAQYMGuTgSERERkfKrLu1AX19fLrvsMpYtW8bhw4eJjo6u0PEOHTrE77//Trdu3ew6nFlEqgYVCkWk0srOzua7775j+PDhXH/99Wc9XqtWLb788kvmzZvHiBEjnBbXoEGDCAwM5OWXX6Zv3754eHiUeDwxMZHw8HAA8vPz2bdvH0FBQdSsWdPhsS1btowXXniB+vXrM2rUKIefT0RERMQRqls7cNKkSSxdupTRo0czf/784jkGi6xbt46tW7cyduzYCx4nOTmZkSNHUlhYyJNPPlmOVygiVZ0KhSJSac2bN4/09HSuuuqqcz7etWtXwsPDmT17tlMbiIGBgXz44YeMHj2a9u3bc+ONNxIeHs6hQ4dYsGABPXr04L333gPg6NGjNG/enLFjxzJjxgy7xvHLL7+wc+dOCgoKSEhIYNmyZSxZsoR69eoxb948vL297Xo+EREREWepbu3A7t278/7773PPPffQrFkzRo8eTePGjUlPT2f58uXMmzePF198scRzdu/ezeeff47VaiUtLY1Nmzbx9ddfk5GRwZtvvsngwYMdlQYRqcRUKBSRSmv27Nl4e3szYMCAcz5uNBoZMmQIs2fP5sSJE06N7aabbqJWrVq8+uqrvP766+Tm5lK7dm169erFuHHjnBLDM888A4CnpyehoaG0bt2at99+m3HjxmmYiYiIiFRq1bEdeOedd9KpUycmT57MZ599RmJiIv7+/rRv357p06dz8803l9h/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", + "text/plain": [ + "
" + ] + }, + "jetTransient": { + "display_id": null + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x_aff = np.linspace(-4.0, 4.0, 500)\n", + "\n", + "base_pdf = _method_values(normal_left, CharacteristicName.PDF, x_aff)\n", + "aff_pdf = _method_values(affine_pos, CharacteristicName.PDF, x_aff)\n", + "base_cdf = _method_values(normal_left, CharacteristicName.CDF, x_aff)\n", + "aff_cdf = _method_values(affine_pos, CharacteristicName.CDF, x_aff)\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(13, 4))\n", + "plot_curve(axes[0], x_aff, base_pdf, label=\"Base PDF\", title=\"Affine: PDF\")\n", + "axes[0].plot(x_aff, aff_pdf, linestyle=\"--\", label=\"Affine PDF\")\n", + "axes[0].legend()\n", + "\n", + "plot_curve(axes[1], x_aff, base_cdf, label=\"Base CDF\", title=\"Affine: CDF\")\n", + "axes[1].plot(x_aff, aff_cdf, linestyle=\"--\", label=\"Affine CDF\")\n", + "axes[1].legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "36c9658bc671021d", + "metadata": {}, + "source": [ + "## 2) Binary Transformations and Operator Sugar\n", + "\n", + "Operator overloads (`+`, `-`, `*`, `/`) are thin wrappers around transformation primitives.\n", + "The explicit `binary(...)` API gives the same result with direct operation selection." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6b7d8711ae8df853", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:16.115328701Z", + "start_time": "2026-03-28T19:39:16.062480547Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "normal_left + normal_right mean= 0.500000 var= 1.000000\n", + "normal_left - normal_right mean=-1.500000 var= 1.000000\n", + "uniform_a * uniform_b mean= 0.750000 var= 0.215278\n", + "uniform_b / uniform_den mean= 0.766238 var= 0.035101\n" + ] + } + ], + "source": [ + "sum_d = normal_left + normal_right\n", + "sub_d = normal_left - normal_right\n", + "mul_d = uniform_a * uniform_b\n", + "div_d = binary(uniform_b, uniform_den, operation=BinaryOperationName.DIV)\n", + "\n", + "show_moments(\"normal_left + normal_right\", sum_d)\n", + "show_moments(\"normal_left - normal_right\", sub_d)\n", + "show_moments(\"uniform_a * uniform_b\", mul_d)\n", + "show_moments(\"uniform_b / uniform_den\", div_d)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4f5cba71b3b78cd1", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:17.875244327Z", + "start_time": "2026-03-28T19:39:16.118595951Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAyvdJREFUeJzs3Xd4FFUXx/HvpgMhEEoITUJRkI4gkSI1NAEFpaqUCChdDBZQpAuvNFFEUQRCEWkiIE167yAqSpGuQkILSUggbff9I2YhppCEJLNJfh+efR529s7MmbO72btnZ+41WSwWCyIiIiIiIiIiIpJj2RkdgIiIiIiIiIiIiBhLRUIREREREREREZEcTkVCERERERERERGRHE5FQhERERERERERkRxORUIREREREREREZEcTkVCERERERERERGRHE5FQhERERERERERkRxORUIREREREREREZEcTkVCERERERERERGRHE5FQpFsYvTo0ZhMphS19ff3x2QycfHixVTvZ8eOHZhMJnbs2GFd1rNnT7y8vFK9rUeVWCyStMmTJ1OmTBns7e2pXr260eGkG70ORERE0i41fcj/MplMjB49OlXrGNVvzIoCAwPp0KEDBQsWxGQyMX36dKNDSjd6HYjYJhUJRTJBXFHOZDKxZ8+eBI9bLBZKliyJyWSiTZs26bbfCRMmsGrVqnTbnlG++OIL/P39jQ4DiP9cJneztU7Ppk2bePfdd6lXrx7z5s1jwoQJRoeUarb0OhAREbFF/+2nuLi4UKxYMVq0aMFnn31GaGio0SEaolGjRinqv6W24JnR3nrrLX766SeGDx/OwoULadmypdEhpcqVK1cYPXo0x48fNzoUEUkhk8VisRgdhEh25+/vj6+vLy4uLvj6+vLFF1/Ee3zHjh00btwYZ2dnfHx8WLt2bar3MXr0aMaMGcODb2lXV1c6dOiQoLASExNDVFQUzs7Oqf7lOC7W7du306hRIwCioqIwm804OzunOu6UqFy5MoUKFUpwppjZbCYyMhInJyfs7DLnN4/z58+zb9++eMt69+5N7dq1ef31163LXF1dadeuXabElBLDhg1j8uTJ3L17FycnJ6PDSRNbeh2IiIjYorg+59ixYyldujRRUVEEBASwY8cONm/ezGOPPcaaNWuoWrWqdZ3o6Giio6NxcXFJ9f7u3buHg4MDDg4OKV4no/uNidm8eTOBgYHW+4cPH+azzz7j/fff58knn7Qur1q1arzcGM3T0xMfHx8WLVpkdChpcuTIEZ5++mnmzZtHz5494z1mxOtARB4u5X/NReSRPffccyxfvpzPPvssXmdq8eLF1KxZkxs3bmRKHPb29tjb26fb9hwdHdNtW6lhZ2eXpg7toyhTpgxlypSJt6xv376UKVOGV199Ncn1oqOjMZvNhhXorl27Rq5cudJt/xaLhXv37pErV6502d6jMOJ1ICIiYstatWpFrVq1rPeHDx/Otm3baNOmDc8//zwnT560foantsj3oLR8/hrRb2zWrFm8+y4uLnz22Wc0a9bM+qN3YsLCwsiTJ08GR5e0a9eukT9//nTb3r1792zmR1Wjvj+ISPKM/+sgkoN07dqVmzdvsnnzZuuyyMhIVqxYwcsvv5ygfVJjrV28eBGTyZTspZcmk4mwsDDmz59vvYQi7he8xMYk9PLyok2bNmzatInq1avj4uJCxYoVWbly5UOPK7ExRcxmM59++ilVqlTBxcWFwoUL07JlS44cOWJtM2/ePJo0aYKHhwfOzs5UrFiRL7/8Mt52vLy8+P3339m5c6f1OOI6c0nlZ/ny5dSsWZNcuXJRqFAhXn31Vf75558EMbu6uvLPP//Qrl07XF1dKVy4MG+//TYxMTEPPebkxD0/U6ZMYfr06ZQtWxZnZ2f++OMPIiMjGTlyJDVr1iRfvnzkyZOHZ599lu3btye5ja+//tq6jaeffprDhw/HaxsQEICvry8lSpTA2dmZokWL8sILL1ifX5PJxLx58wgLC7PmMO61Ex0dzbhx46zb9/Ly4v333yciIiLB89CmTRt++uknatWqRa5cufjqq6+sz8GyZcsYM2YMxYsXJ2/evHTo0IHg4GAiIiIYMmQIHh4euLq64uvrm2Db2fV1ICIiYkuaNGnChx9+yKVLl+KdmfbfMQkrV65M48aNE6xvNpspXrw4HTp0sC777yW6oaGhDBkyBC8vL5ydnfHw8KBZs2YcO3bM2iaxfmNYWBhDhw6lZMmSODs7U758eaZMmcJ/L3ozmUwMHDiQVatWUblyZZydnalUqRIbN25Ma1qs4vLwxx9/8PLLL+Pu7k79+vUB+PXXX+nZsydlypTBxcUFT09PXnvtNW7evJnoNs6ePUvPnj3Jnz8/+fLlw9fXl/Dw8HhtN2/eTP369cmfPz+urq6UL1+e999/H7jfV7dYLMycOdPa94lz/vx5OnbsSIECBcidOzfPPPMM69ati7f9uP7RkiVLGDFiBMWLFyd37tyEhIRY+z+XL1+mTZs2uLq6Urx4cWbOnAnAb7/9RpMmTciTJw+lSpVi8eLF8bZ969Yt3n77bapUqYKrqytubm60atWKX375Jd7+n376aQB8fX0T9EFt9XUgktPpTEKRTOTl5UWdOnX47rvvaNWqFQAbNmwgODiYLl268Nlnn6XbvhYuXJjgMtiyZcsmu86ff/5J586d6du3Lz169GDevHl07NiRjRs3JvgF9mF69eqFv78/rVq1onfv3kRHR7N7924OHDhg/WX7yy+/pFKlSjz//PM4ODjw448/0r9/f8xmMwMGDABg+vTpDBo0CFdXVz744AMAihQpkuR+4y6zefrpp5k4cSKBgYF8+umn7N27l59//jner7ExMTG0aNECb29vpkyZwpYtW5g6dSply5alX79+qTrexMybN4979+7x+uuv4+zsTIECBQgJCeGbb76ha9eu9OnTh9DQUObMmUOLFi04dOhQgglFFi9eTGhoKG+88QYmk4lJkybx4osvcv78eesvsC+99BK///47gwYNwsvLi2vXrrF582YuX76Ml5cXCxcu5Ouvv+bQoUN88803ANStWxeIvVR6/vz5dOjQgaFDh3Lw4EEmTpzIyZMn+eGHH+LFcvr0abp27cobb7xBnz59KF++vPWxiRMnkitXLoYNG8bZs2eZMWMGjo6O2NnZERQUxOjRozlw4AD+/v6ULl2akSNHWtfN7q8DERERW9GtWzfef/99Nm3aRJ8+fRJt07lzZ0aPHk1AQACenp7W5Xv27OHKlSt06dIlye337duXFStWMHDgQCpWrMjNmzfZs2cPJ0+e5Kmnnkp0HYvFwvPPP8/27dvp1asX1atX56effuKdd97hn3/+4ZNPPonXfs+ePaxcuZL+/fuTN29ePvvsM1566SUuX75MwYIF05CV+Dp27Mjjjz/OhAkTrMWpzZs3c/78eXx9ffH09OT333/n66+/5vfff+fAgQMJhu/p1KkTpUuXZuLEiRw7doxvvvkGDw8PPv74YwB+//132rRpQ9WqVRk7dizOzs6cPXuWvXv3AtCgQQMWLlxIt27daNasGd27d7duOzAwkLp16xIeHs7g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fm7fvi3UrVtXCAgIEBITE4XffvtNMDExEebNmyfWISiVovkJDg4W6tatK2zbtk24fv26cPDgQaFp06bCoEGDxDoEpVHGPZ46UcZ9n7qo6L1PEXVf3VjR/GRkZAiNGjUSBgwYIFy6dEn4888/hebNmwsjRowQ6xCUStH8rF+/XtDR0RFWrlwpJCcnCydOnBCcnZ0FV1fXaotZrYuEgiAI3333ndC4cWNBT09PcHV1FU6fPi37Wbdu3YRhw4bJtf/xxx+FFi1aCHp6ekKbNm2E33//Xe7nUqlUmDVrlmBqairo6+sL7733npCYmFgdh1JpiuSiSZMmAoASj+DgYEEQBOHFixdCz549hbffflvQ1dUVmjRpIowcOVLl/8B9lSL5mDhxoqytqamp0Lt3b+Hs2bNy+9OUc0MQBOHq1asCAOHgwYMl9lWTz42jR4+Wet4XHf+wYcOEbt26lXiPo6OjoKenJ9ja2grr168vsd/y8quqFM1Ft27dym0vCILg4+MjmJubC3p6eoKlpaXg4+MjJCUlVe+BVYCiuVi0aJHQtGlTwcDAQGjYsKHg4eEhHDlypMR+NeG8EARBSE9PFwwNDYW1a9eWus+ael6UlgcActcATbvPoKqh6O/kV6l7kVAQFM/PqVOnBDc3N0FfX1+wtbUV5s+fL+Tn51dz1NVHkfzk5eUJ33zzjex3lpWVlTBu3Djh6dOn1R+4kinrHk9dKOO+T11U5Nx5lboXCSuSnytXrgienp6CoaGh0KhRIyEwMFDuS1Z1UpH8rFixQmjdurVgaGgomJubCx9//LFw9+7daotZIgga3peciIiIiIiIiIhIw6ntnIRERERERERERET0ZlgkJCIiIiIiIiIi0nAsEhIREREREREREWk4FgmJiIiIiIiIiIg0HIuEREREREREREREGo5FQiIiIiIiIiIiIg3HIiEREREREREREZGGY5GQiIiIiIiIiIhIw7FISEREREREREREpOFYJCQiIiIiIiIiItJwLBISERERERERERFpOBYJiYiIiIiIiIiINNz/ATEVKr8rafkEAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "jetTransient": { + "display_id": null + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rng_size = 20_000\n", + "x_mul = np.linspace(0.0, 2.0, 300)\n", + "x_div = np.linspace(0.3, 1.8, 300)\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(13, 4))\n", + "plot_hist_with_optional_pdf(\n", + " axes[0],\n", + " mul_d.sample(rng_size),\n", + " label=\"Samples: X * Y\",\n", + " pdf_distribution=mul_d,\n", + " x_grid=x_mul,\n", + ")\n", + "axes[0].set_title(\"Multiplication Transformation\")\n", + "\n", + "plot_hist_with_optional_pdf(\n", + " axes[1],\n", + " div_d.sample(rng_size),\n", + " label=\"Samples: X / Y\",\n", + " pdf_distribution=div_d,\n", + " x_grid=x_div,\n", + ")\n", + "axes[1].set_title(\"Division Transformation\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "681fde2f246f238d", + "metadata": {}, + "source": [ + "## 3) Finite Mixture\n", + "\n", + "A finite mixture combines multiple component distributions with validated non-negative weights that sum to one." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "63787691e4b1996e", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:17.942395175Z", + "start_time": "2026-03-28T19:39:17.878652414Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mixture transformation name: finite_mixture\n", + "Mixture support: ContinuousSupport(left=-inf, right=inf, left_closed=False, right_closed=False)\n", + "Mixture components: 3\n", + "Mixture weights: [0.35 0.5 0.15]\n", + "finite mixture mean= 0.445000 var= 0.985475\n" + ] + } + ], + "source": [ + "mixture_d = finite_mixture(\n", + " [\n", + " (0.35, normal_left),\n", + " (0.50, normal_right),\n", + " (0.15, exp_base),\n", + " ]\n", + ")\n", + "\n", + "print(\"Mixture transformation name:\", mixture_d.transformation_name)\n", + "print(\"Mixture support:\", mixture_d.support)\n", + "print(\"Mixture components:\", len(mixture_d.components))\n", + "print(\"Mixture weights:\", mixture_d.weights)\n", + "\n", + "show_moments(\"finite mixture\", mixture_d)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d79f61e1829d7958", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:18.426098049Z", + "start_time": "2026-03-28T19:39:17.945714781Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAj3VJREFUeJzs3Xd8FHX+x/HXlvQeSkIJhI7U0EVU0EOxixUriIqVs3CeJ2dBPBW98xC9Q7kDEaxgvfOnCCqKB4KiYGgSeggtCSG9Z3fm90fISkyAJCSZTfb9fMgj2dnvzHx2v5n1u5/5FptpmiYiIiIiIiIiIiLis+xWByAiIiIiIiIiIiLWUpJQRERERERERETExylJKCIiIiIiIiIi4uOUJBQREREREREREfFxShKKiIiIiIiIiIj4OCUJRUREREREREREfJyShCIiIiIiIiIiIj5OSUIREREREREREREfpyShiIiIiIiIiIiIj1OSUEQaRXJyMjabjQULFtRpf5vNxpNPPlmvMdW3phCjiIiISGNTO1BEpGlQklBE6sWCBQuw2WzV/nvkkUfq/XyrV6/mySefJDs7u16Pe+zrWLVqVZXnTdMkLi4Om83GJZdcUi/nXLJkidc2Kp988slKdRkcHEyvXr147LHHyM3N9ZT7bf0HBgbStm1bxowZw8svv0xeXt5Jj33svzlz5jTmyxQREZFToHZg3XlzO7BCYmIiN910E3FxcQQEBBAdHc3o0aN5/fXXcbvdnnLH1rvT6SQ6OppBgwZx//3388svv1Q5bkXyuLp/p59+emO+RBE5yml1ACLSvDz11FN06tSp0rY+ffrQsWNHioqK8PPzq9Nxi4qKcDp//chavXo106dP55ZbbiEyMvJUQq5WYGAg77zzDmeeeWal7d9++y379+8nICDgpDHW1JIlS5g9e7ZXNxBfffVVQkNDyc/P54svvuCZZ57h66+/5rvvvsNms3nKVdR/WVkZqamprFixggceeICZM2fyySef0K9fv+Me+1jDhg1r8NckIiIi9UvtwObXDpw3bx533XUXMTEx3HzzzXTr1o28vDyWL1/ObbfdxqFDh/jzn//sKX/eeecxfvx4TNMkJyeHDRs2sHDhQl555RWef/55pkyZUuUc119/PRdddFGlba1atWrw1yYiVSlJKCL16sILL2Tw4MHVPhcYGFjn457KvnVx0UUX8f777/Pyyy9XavC98847DBo0iIyMjCr7NHaMJ1NQUEBISEi9HOvqq6+mZcuWANx1111cddVVfPTRR3z//fcMHz7cU+639T916lS+/vprLrnkEi677DK2bt1KUFDQcY8tIiIiTZfagd6jPtqB33//PXfddRfDhw9nyZIlhIWFeZ574IEH+Omnn9i8eXOlfbp3785NN91Uadtzzz3HpZdeyh/+8Ad69uxZJSE4cODAKvuIiDU03FhEGkV1c9HccssthIaGcuDAAcaOHUtoaCitWrXioYceqjR0ASrP8/Lkk0/yxz/+EYBOnTp5hiUkJyd7yr/11lsMGjSIoKAgoqOjue6669i3b1+N473++us5cuQIX375pWdbaWkpH3zwATfccEO1+xwbY1FRET179qRnz54UFRV5ymRmZtKmTRvOOOMM3G43t9xyC7Nnz/bsX/EPYMWKFdhsNlasWFHj93LXrl1cdNFFhIWFceONNwJgGAazZs2id+/eBAYGEhMTw5133klWVlaN34/fOvfccwHYs2dPjco+/vjj7N27l7feeqvO5xQREZGmSe3Ack2tHTh9+nRsNhtvv/12pQRhhcGDB3PLLbec9DgtWrRg0aJFOJ1OnnnmmZOWFxHrKEkoIvUqJyeHjIyMSv9OxO12M2bMGFq0aMELL7zAyJEj+fvf/86///3v4+5z5ZVXcv311wPw4osv8uabb/Lmm296hiU888wzjB8/nm7dujFz5kweeOABli9fztlnn13juWvi4+MZPnw47777rmfb559/Tk5ODtddd91J9w8KCmLhwoXs3LmTRx991LP93nvvJScnhwULFuBwOLjzzjs577zzADyv480336xRjL/lcrkYM2YMrVu35oUXXuCqq64C4M477+SPf/wjI0aM4KWXXmLixIm8/fbbjBkzhrKysjqda9euXUB5o68mbr75ZgC++OKLKs9lZmZW+ns5leSliIiIWEftwHLNoR1YWFjoed86dOhQp5iO1aFDB0aOHMn3339faV7rinP99u+mrm1UETk1Gm4sIvVq9OjRVbaZpnnc8sXFxYwbN47HH38cKB/KOnDgQF577TXuvvvuavfp168fAwcO5N1332Xs2LHEx8d7ntu7dy/Tpk3j6aefrjQ/ypVXXsmAAQN45ZVXKm0/kRtuuIGpU6dSVFREUFAQb7/9NiNHjqRt27Y12n/YsGE8/PDDPP/881xxxRWkpaWxaNEiZs2aRffu3QEYPnw43bt358svvzzlYRYlJSVcc801zJgxw7Nt1apVzJs3j7fffrvSne9zzjmHCy64gPfff/+4d8SPlZmZCeCZk/CVV14hJiaGs846q0axtW/fnoiICE9y8Vg9evSo9Lhjx46VegOIiIhI06B24K+aejtw586dlJWV0bdv31OK61h9+vRh+fLlJCcnV5qnetq0aUybNq1S2W+++YZRo0bV27lFpGaUJBSRejV79mxPw6em7rrrrkqPzzrrrDrfRf3oo48wDINrr7220t3r2NhYunXrxjfffFPjxuG1117LAw88wKeffsoFF1zAp59+yssvv1yreJ588kk+/fRTJkyYQH5+PiNHjuS+++6r1TFq47cN6vfff5+IiAjOO++8Su/HoEGDCA0N5ZtvvqlRkvC3ibzevXuzcOFCgoODaxxbaGhotascf/jhh4SHh3se/3bOQhEREWka1A6srCm3Ayt6+1U3zLiuKhaq+2178I477uCaa66ptK1///71dl4RqTklCUWkXg0dOvS4E1ZXJzAwsMrqZVFRUXUecrpjxw5M06Rbt27VPl+bVfVatWrF6NGjeeeddygsLMTtdnP11VfXKh5/f3/mz5/PkCFDCAwM5PXXX6+0GnB9cjqdtG/fvtK2HTt2kJOTQ+vWravdJz09vUbHrkjk+fn50b59e7p06VLr+PLz86uN4+yzz9bCJSIiIs2A2oGVNeV2YMUN3Opu8NZVfn4+UDXx2K1bt2p7oYpI41OSUEQs5XA46vV4hmFgs9n4/PPPqz12xR3MmrrhhhuYNGkSqampXHjhhURGRtY6pmXLlgHlQ2p27NhBp06darTf8RqRv53Mu0JAQAB2e+WpZg3DoHXr1rz99tvV7vPbhvnxnGoib//+/eTk5NC1a9c6H0NERESaF7UDj8/qdmDXrl1xOp1s2rSpRvHWxObNm3E4HDV+D0Sk8SlJKCJN0vEaTl26dME0TTp16lTr4S7VueKKK7jzzjv5/vvvWbx4ca3337hxI0899RQTJ04kMTGR22+/nU2bNhEREeEpc7zXEhUVBVBlku29e/fW+PxdunThq6++YsSIEZYO460YNjRmzBjLYhAREZHmQe3AmjmVdmBwcDDnnnsuX3/9Nfv27SMuLq5W+/9WSkoK3377LcOHD6/XIcwiUr+0urGINEkhISFA1YbTlVdeicPhYPr06VUmyjZNkyNHjtTqPKGhobz66qs8+eSTXHrppbXat6ysjFtuuYW2bdvy0ksvsWDBAtLS0njwwQdr9Fo6duyIw+Hgf//7X6Xtr7zySo1juPbaa3G73fzlL3+p8pzL5arxKn+n4uuvv+Yvf/kLnTp14sYbb2zw84mIiEjzpnZgzZxqO3DatGmYpsnNN9/sGSp8rHXr1rFw4cKTxpGZmcn111+P2+2utNqziHgf9SQUkSZp0KBBADz66KNcd911+Pn5cemll9KlSxeefvpppk6dSnJyMmPHjiUsLIw9e/bw8ccfc8cdd/DQQw/V6lwTJkyoU4xPP/00iYmJLF++nLCwMPr168cTTzzBY489xtVXX81FF11U6bXcd999jBkzBofDwXXXXUdERATXXHMN//jHP7DZbHTp0oVPP/20xvMIAowcOZI777yTGTNmkJiYyPnnn4+fnx87duzg/fff56WXXqr1/Don8vnnn5OUlITL5SItLY2vv/6aL7/8ko4dO/LJJ58QGBhYb+cSERER36R2YM2cajvwjDPOYPbs2dxzzz307NmTm2++mW7dupGXl8eKFSv45JNPePrppyvts337dt566y1M0yQ3N5cNGzbw/vvvk5+fz8yZM7ngggvq8G6KSGNRklBEmqQhQ4bwl7/8hTlz5rB06VIMw2DPnj2EhITwyCOP0L17d1588UWmT58OQFxcHOeffz6XXXZZo8S3fv16nn32WSZPnsw555zj2f7II4/w3//+l0mTJrFlyxYiIyO58sor+f3vf8+iRYs8jarrrrsOgH/84x+UlZUxZ84cAgICuPbaa/nb3/5Gnz59ahzLnDlzGDRoEP/617/485//jNPpJD4+nptuuokRI0bU6+t+4okngPKJuqOjo+nbty+zZs1i4sSJGloiIiIi9ULtwMZrB955550MGTKEv//977zxxhscPnyY0NBQBg4cyOuvv85NN91UqfyXX37Jl19+id1uJzw8nE6dOjFhwgTuuOMOevXqVeO4RcQaNvO3/bBFRERERERERETEp2hOQhERERERERERER+nJKGIiIiIiIiIiIiPU5JQRERERERERETExylJKCIiIiIiIiIi4uOUJBQREREREREREfFxShKKiIiIiIiIiIj4OKfVATQ2wzA4ePAgYWFh2Gw2q8MRERERaRSmaZKXl0fbtm2x2333PrHagiIiIuJratoO9Lkk4cGDB4mLi7M6DBERERFL7Nu3j/bt21sdhmXUFhQRERFfdbJ2oM8lCcPCwoDyNyY8PLzBzmMYBllZWURFRfn03Xpvpjryfqoj76c68n6qI+/WmPWTm5tLXFycpy3kq9QWlAqqI++nOvJuqh/vpzryfo1VRzVtB/pckrBiWEl4eHiDNwxdLhfh4eG6GL2U6sj7qY68n+rI+6mOvJsV9ePrQ2zVFpQKqiPvpzrybqof76c68n6NXUcnawfqr0RERERERERERMTHKUkoIiIiIiIiIiLi45QkFBERERERERER8XE+NyehiIhIc2IYBqWlpSd8vqysjOLiYs1F44Xqs378/PxwOBz1FJm43W7KysrqvL+uPe9X0zrStSUiIr5CSUIREZEmqrS0lD179mAYxnHLmKbpWTXN1xes8Eb1XT+RkZHExsaqrk+BaZqkpqaSnZ19ysfRtefdalNHurZERMQXKEkoIiLSBJmmyaFDh3A4HMTFxR23F4xpmrhcLpxOp77ceqH6qh/TNCksLCQ9PR2ANm3a1FeIPqciQdi6dWuCg4PrXC+69rxfTepI15aIiPgSJQlFRESaIJfLRWFhIW3btiU4OPi45ZSo8G71WT9BQUEApKen07p1aw2PrAO32+1JELZo0eKUjqVrz/vVtI50bYmIiK/QBCkiIiJNkNvtBsDf39/iSMSbVCSMT2Uuvcb0v//9j0svvZS2bdtis9n4z3/+c9J9VqxYwcCBAwkICKBr164sWLCg3uKpeN9OlHgX39TUri0REZG6UJJQRESkCVMPJTlWU/t7KCgooH///syePbtG5ffs2cPFF1/MOeecQ2JiIg888AC33347y5Ytq9e4mtr7KA1PfxMiIuILNNxYRERERCxx4YUXcuGFF9a4/Jw5c+jUqRN///vfATjttNNYtWoVL774ImPGjGmoMEVERER8gpKEIiK1lJ5bzP9tPMT6lCx2pedjmCahAU4GdIjinB6tGdG1hXociNSTUaNGkZCQwKxZs6wORbzAmjVrGD16dKVtY8aM4YEHHjjuPiUlJZSUlHge5+bmAmAYRpWVwQ3DwDRNz7/6Up/Hqg/nnHMO/fv313V1jJPVUcXfRHV/N9JwKq5JvefeyZfqxzRNXIZJmdvA5TYpdRue38vcBqVuE9fRbWVuE8M0cRsVP8FtmhhG+TEMw/Q8dnvKgcttYJjg/s3zlfcrf97ExDTBNMEwTUzANMp/Gqb563bTpKi4BH///ZjYqt3POLqfeex+cLTcb48JUB6vaVb8/LVcxXtVXqriMZUec5zny7eZ1e7z6+PKTxz7yX2y85pm9fse+/l/sn3N3xT8bby/Vd3mqmVN2oT5sejOM6o/SD2p6XWqJKGISA3tTM/j719sZ9mW1KP/g6xsfUo2r63aQ//2ETxy4WkM73Jqk96LNEe33HILCxcu5M4772TOnDmVnrv33nt55ZVXmDBhgmeeuY8++gg/P79aHT87O7tGc9s1lPj4ePbu3QuUz2PWo0cPpk6dyjXXXAPAk08+yfTp0wFwOBxERkbSq1cvrrzySu6++24CAgI8xxo1ahTffvttlXOUlZXhdPpeMy41NZWYmJhK22JiYsjNzaWoqMizwMSxZsyY4Xm/j5WVlYXL5aq0raysDMMwcLlcVZ6ri4q5QxvabbfdxptvvsmkSZOqDN2+7777mDNnDjfffDOvvfYaAIsXL8bPz6/Gr/G2224jOzubDz/8sN5jr42ff/6Z559/nlWrVpGTk0NcXBxnn302U6ZMoXv37iQnJ9O9e3dP+dDQUOLi4hg5ciS///3v6datm+e5N954g9tvv73KOebMmcOtt95aZbvL5cIwDHJycigsLGyYFyhVGIZBXl4epmlit2umLG/jTfVjmiaFZQb5JS7yit3kl7rJL3GTX+I6+tNNUZmb4jKDYpdBics47u8lrqMJQMOkzF3+z1Vd41+knthMg8zMzAa9jvLy8mpUzvdalyIitVRY6uJvy7axcHWyJzk4qGMUvzutNafFhuPvtHM4r4Q1u47wyYaDbNifww3zvue+c7tx3++64bCrV6HIseLi4li0aBEvvviiJ6lTXFzMO++8Q4cOHSqVjY6OtiJE3G43Nputzo21p556ikmTJpGbm8vf//53xo0bR7t27TjjjPK7xL179+arr77C7XaTlpbGqlWreOaZZ3jzzTdZsWIFYWFhnmNNmjSJp556qtLxfTFBWFdTp05lypQpnse5ubnExcURFRVFeHh4pbLFxcVkZWXhdDrr7T1ujLqy2+3ExcXx3nvvMWvWrErX1aJFi+jQoQN2u90TS+vWrWt9/GP3r6tTua4+/fRTrr76asaMGcNbb71Fly5dSE9P5/333+epp55i0aJFnvi+/PJLevfuTWFhIZs2beLll19m8ODBfPLJJ/zud7/zvKbw8HA2b95c6XVFRERU+zqdTid2u52IiAgCAwPr+A5IbRmGgc1mIyoqyvIklFTVGPVjmiY5RWXszyriQHYR+7OKOJxXwpGCUjLyS8jIL/95JL+00RN5fg4bTrsdP4cNP4cdf6cdp92Gw27DabdhP/q7w3b0d9vRx3YbdpsNh51jfv9NWc/vVNrPYbNhq/hpo/wfNuy28rlTbYDdBtjKt2GalJQUExwUhN1ux2YDu6ecDcr/K9929DmOHsNmO+a4vylXcU6OOV7F+Sk/fSUVo6x++/xxt1N5Q9XnbdWXr3Tu6suc7Jy/HRFW01hPNpLseM8apkFJYQHR0dEN+jlX0/+Hq4UpInICm/bn8Pt315N8pLzXwHm9Ynjo/B70iA2rUnbsgHb88YIevLBsG4t+3MdLy3ew63A+L103QIlCkWMMHDiQXbt28dFHH3HjjTcC5T0GO3ToQKdOnSqVPXa4cVJSEgMHDmTevHnccMMNALz33ntMmDCBdevW8d5777Fw4ULg14baN998A5QPr8zKyiIyMhKAxMREBgwYwJ49e4iPj2fBggU88MADvPHGGzzyyCNs376dnTt30qZNGx599FHeffddsrOz6dOnD88//zyjRo064WsMCwsjNjaW2NhYZs+ezVtvvcX//d//eZKETqeT2NhYTNOkdevWDBgwgPPPP5/+/fvz/PPP8/TTT3uOFRwcTGxs7Km96c1EbGwsaWlplbalpaURHh5ebS9CgICAgEq9MytUJL5+u638y5DtlKeNOHb4UmNMQVFxXX388cee6+rjjz+udF1VxFHX66ri/Wrs66qwsJBbb72Viy66iI8//tizvXPnzpx++ulkZ2dXqrOWLVvSpk0bALp06cJll13G7373O26//XZ27dqFw+HwlI+NjcXpdJ78y93R8tX93UjD0vvu3eqrflxugz0ZBexIz2d7Wh470vPZlZ7P/qwi8ktq3rPbabcRHuRHWKCTsEAn4YEVv/sR4u8g0K/8X5C/g0Cnvfxnxbaj2wOcdvwc5f/8HXacFUlAhx0/56+JwaYwvZBhlPdQa+gElNRdeR1V3y6pTzU9tpKEIiLH8eG6/Uz9eBOlLoM2EYHMuLIvo3qcuPdFy9AAnruqH8M6R/PwBxv5dOMh/J12Xri6P3YlCqUBmaZJUVnVoY2maeJyuXAaDZeoCPJz1PrYt956K6+//ronmTF//nwmTpzIihUrjrtPz549eeGFF7jnnns488wzsdvt3HXXXTz//PP06tWLhx56iK1bt5Kbm8vrr78OlPdEXL16dY1iKiws5Pnnn2fevHm0aNGC1q1bM3nyZH755RcWLVpE27Zt+fjjj7ngggvYtGlTpaGLJ+J0OvHz86O0tPSE5Xr27MmFF17IRx99VClJKL8aPnw4S5YsqbTtyy+/ZPjw4Q1yvuNdVzXd91SuPV1Xv1q2bBkZGRk8/PDD1Z6jIkl5PHa7nfvvv58rrriCdevWMXTo0BrFLiIN52B2EetTskhMySZxXzabD+ZQXHb8OdNahgbQPiqIdlFBxIYH0jI0gBah/rQKDfD8HhXsT6CfvUkk70S8lZKEIiK/4TZMnvlsK/O/2wPA73q2Zua4BCKCaj4v2hUD2hPk5+Ted9bz0foDdIgO5oHR3U++o0gdFZW56fXEMkvO/ctTYwj2r12T4qabbmLq1Kmeufu+++47Fi1adMJkBsA999zDkiVLuOmmm/D392fIkCH8/ve/B8rnHwsKCqKkpKROPe/Kysp45ZVX6N+/PwApKSm8/vrrpKSk0LZtWwAeeughli5dyuuvv86zzz570mOWlpby97//nZycHM4999yTlu/ZsydffPFFpW2vvPIK8+bN8zy+8847Pav7NnX5+fns3LnT83jPnj0kJiYSHR1Nhw4dmDp1KgcOHOCNN94A4K677uKf//wnDz/8MLfeeitff/017733Hp999lmDxKfryjuuqx07dgDl10ddVeybnJzsSRLm5OQQFRXlKRMaGkpqamqdzyEix5df4mLVjgxW7TzMdzuPsCejoEqZEH8H3WLC6NY6lG4xoXRrHUaHFsG0iwwi0M9hQdQivkdJQhGRY5S43ExZvIHPNh0C4L5zu/LA6O516gV4QZ9YZlzZl4c/2MhLy3cwJD6aEV1b1nfIIk1Sq1atuPjii1mwYAGmaXLxxRfTsmXNro/58+fTvXt37HY7W7ZsqbceA/7+/vTr18/zeNOmTbjd7koLIUD5arktWpx4YaI//elPPPbYYxQXFxMaGspzzz3HxRdffNIYTNOs8npuvPFGHn30Uc/jk/Waakp++uknzjnnHM/jirkDKxavOXToECkpKZ7nO3XqxGeffcaDDz7ISy+9RPv27Zk3bx5jxoxp9Ni9UXO9rupjdeiKYxz7usLCwvjhhx88w401FE+kfuUUlvHV1jQ+33yI/+3IoNT1a09Bh93GaW3CSIiLJCEuioS4SDq3DNHIGxGLKUkoInJUfomLu95cx6qdGfg5bMy8NoFL+7c9pWNeOziOdclZLP5pH/cv+pkvHxxJVIh/PUUs8qsgPwe/PFU1UeIZ8liDObdO5dx1ceuttzJ58mSAKiuynsiGDRsoKCjAbrdz6NAhz9xjx1Pxxf/YRENZWVmVckFBQZXeo/z8fBwOB+vWrcPhqPwaQ0NDT3jOP/7xj9xyyy2EhoYSExNT4/d+69atVeZljIiIoGvXrjXav6kZNWrUCRNAFatc/3afn3/+uQGj+tXxrquaONVrT9fVryoSiklJSXUeWr5161aASteX3W6na9euDfr5KOJrDMNkze4jLPpxH8s2p1Lq/jUxGN8imJHdWzGia0tO79KC8MCaj9IRkcahJKGICHAkv4SJC35k4/4cgv0d/PvmwZzZrX56/U2/vDfrU7LYkZ7Pc58n8fzV/U6+k0gt2Wy2aocmmqaJy45Xfgm+4IILKC0txWaz1bgnWGZmJrfccguPPvoohw4d4sYbb2T9+vWeRSv8/f1xuyvPIdeqVSsADh065BlamJiYeNJzDRgwALfbTXp6OmeddVYtXln5wgm1TewlJSWxdOlSpk6dWqv9pOEc77qqCauuveZ4XZ1//vm0bNmSv/71r5UWLqmQnZ19wh62hmHw8ssv06lTJwYMGFCjc4pI7eSXuFi0NoU31uwlJbPQs717TCgX9mnDRX3b0D0m1OvaIiJSmZKEIuLz9mcVMv61tezOKCA6xJ/XbxlC/7jIejt+oJ+DGVf25eo5a1j80z6uHtyeIfHR9XZ8kabK4XB4evf8tkfR8dx1113ExcXx2GOPUVJSwoABA3jooYc8Pabi4+NZtmwZ27Zto0WLFp5eeHFxcTz55JM888wzbN++vUZz+nXv3p0bb7yR8ePH8/e//50BAwZw+PBhli9fTr9+/Wo0fPh4XC4XqampuN1u0tLSWLVqFc888wwJCQn88Y9/rPNxRZrjdRUSEsK8efO45ppruOyyy7jvvvvo2rUrGRkZvPfee6SkpLBo0SJP+SNHjpCamkphYSGbN29m1qxZrF27ls8++6zG74mI1Ex2YRkL123nje9TyCkq700cFuDk8gFtuW5IB/q0i7A4QhGpDU28ISI+bVtqHle/uobdGQW0jQjkvTuH12uCsMLg+GiuGxIHwOP/2YxhnPr8SiLNQXh4OOHh4TUq+8Ybb7BkyRLefPNNnE4nISEhvPXWW8ydO5fPP/8cgEmTJtGjRw8GDx5Mq1at+O677/Dz8+Pdd98lKSmJfv368fzzz9d49eDXX3+d8ePH84c//IEePXowduxYfvzxRzp06FDn1wywZcsW2rRpQ8eOHTnvvPN4//33mTp1KitXrjzpUGaRk2mO19Xll1/O6tWr8fPz44YbbqBnz55cf/315OTkVDnv6NGjadOmDX379uWRRx7htNNOY+PGjZXmvxSRU1NY6uLl5Tu4dG4i//hmFzlFZXRqGcKMK/vyw6O/4+mxfZUgFGmCbGZ9zATchOTm5hIREUFOTk6NG091YRgGmZmZREdHaxJkL6U68n7v/rCXl77azt+vTWBEt1b1fvzvdx9h0hs/kVfsomvrUN68bShtIoLq/TwVsgpKOftv35BX7OKl6xK4PKFdg52rseg6sk5xcTF79uyhU6dOBAYGHrdcY8xJKHVX3/Vzor+LxmoDebsTvQ81va5qQtee96tNHdXn34bUnNoZ3scwTD5cv58XvthGWm4JAH3ahnPPOV0Z0zsWhxYe8Sq6hrxfY9VRTduB+isREa/12qo9pOaVcuNra9l3zNwm9WHJpkOMf20tecUuBneM4oO7hjdoghAgKsSfO8/uDMCLX26n7JiJnEVEREREvNm21DyufHU1f/xgI2m5JbSPCuLZS7ry33vP4KK+bZQgFGkGlCQUEa8VE/Hrnfq73lpHfonrlI9pmibzVu7m3nfWU+o2GNM7hrduH0ZkcOOsODxxRCdahPiTfKSQD9btb5RzioiIiIjUVanLYOaX27nkHytJ3JdNWICTqRf25MsHzuL8ni3UW1qkGVGSUES8lp/j14+oLQdzuW3BjxSVuk+wx4kVlLj4/bs/8/RnWzFNuPn0jrxy4yAC/RpvEvOQACd3j+oCwL++3YVbcxOKiIiIiJdKzijg6jmreXn5DsrcJuf1iuHLKSO5c2QXAhqxDS0ijUNJQhHxWhVTpt58egfCApz8sCeTiQvWklVQWutjbTmYw+Wzv+PTjYdw2m1Mu7QXT13e25JhEdcP7UBEkB/JRwr58pe0Rj+/iIiIiMjJ/OfnA1z88ko27s8hIsiPf94wgH/fPIjYCM3LKdJceUWScPbs2cTHxxMYGMiwYcNYu3ZtjfZbtGgRNpuNsWPHNmyAImKJik52CXGRvD5xCCH+Dr7fnclls1exYV92jY6RV1zG80uTuOyf37EzPZ+Y8AAW3XE6E0d0smxoREiAk5tOL1/Bce7K3ZbEICIiIiJSHZfbYPr/beGBxYkUlLoZ2imaz+8/i0v6tdXQYpFmzvIk4eLFi5kyZQrTpk1j/fr19O/fnzFjxpCenn7C/ZKTk3nooYc466yzGilSEWlsxtGehDYbDI6P5sN7ziAuOoh9mUWMfeU7Hv5gA9tS86rdd39WIS99tYOz/voNr64oH9Z7Ud9YPv39WQyOj27Ml1GtCcPj8XfYWbc3i/UpWVaHI01YRY9bEShfIU9Ond5H+S39TYivyCooZfz8tbz+XTIA953blXcnnU7byIZd4E9EvIPT6gBmzpzJpEmTmDhxIgBz5szhs88+Y/78+TzyyCPV7uN2u7nxxhuZPn06K1euJDs7uxEjFpHGUtEet1F+x7JnbDif3Hsmf/n0Fz76+QDv/bSf937aT5dWIZzWJpywQD8KSlxsPZTLjvR8z3G6tArhTxf05PzesVa8jGq1Dg/ksoS2fLBuP299v5eBHaKsDkmaGD8/P2w2G4cPH6ZVq1bHvbNvmiYulwun06m7/16ovurHNE1KS0s5fPgwdrsdf//GWYypufH398dut3Pw4EFatWqFv79/netF1573q0kd6doSX7Ivs5AJr69l9+ECgv0dzLw2gQv6eE/7WUQanqVJwtLSUtatW8fUqVM92+x2O6NHj2bNmjXH3e+pp56idevW3HbbbaxcubIxQhURS5T3kDp22sCoEH9mjkvgxtM78K9vd/N1Ujq7Dhew63BBlb3P6NKCcUPiuLhvG5wOyztOV3HjsA58sG4/n208xLRLehMR7Gd1SNKEOBwO2rdvz/79+0lOTj5uOdM0MQwDu92uRIUXqu/6CQ4OpkOHDtjt3veZ1xTY7XY6derEoUOHOHjw4CkdS9ee96tNHenakuZu66FcJsxfS3peCW0jAnl94lB6xIZZHZaINDJLk4QZGRm43W5iYmIqbY+JiSEpKanafVatWsVrr71GYmJijc5RUlJCSUmJ53Fubi5QPmSgIYcNGIbhaXiId1Ideb9jV/79bT0NiItkzk0DySwoJXFfNjvT8ylxGQQ47XRtHUq/9hG0DA047v7eoF+7cHrGhpGUmseH6/dxyxnxVodUa7qOrBUcHEyXLl0oKys7bhnDMMjNzSU8PFxfbr1QfdaPw+Hw9Iiq7prUdVoz/v7+dOjQAZfLhdvtrvNxDMMgJyeHiIgIXXteqqZ1dOy1JdIcrdubxS3z15JX4qJHTBgLbx2qxUlEfJTlw41rIy8vj5tvvpm5c+fSsmXLGu0zY8YMpk+fXmV7VlYWLpervkP0MAyDvLw8TNNUw9BLqY68X+nRxEdhQT6ZmZnHLZfQ2klC68jf7FxAZmbV3oXe5rLe0SSl5vHWmmQu7RHW5L6A6DryfoZhUFRUhNPpVB15ocasn7y86udwlapsNht+fn74+dW9h7dhGBQWFhIYGKhrz0upjkRg3d5MJsz/kfwSF0Pjo5k7YTARQRrdIuKrLE0StmzZEofDQVpaWqXtaWlpxMZWnftg165dJCcnc+mll3q2VdwVdzqdbNu2jS5dulTaZ+rUqUyZMsXzODc3l7i4OKKioggPD6/Pl1OJYRjYbDaioqLU6PBSqiPv53CUf0SFh4URHW39YiMN4YYzwnj5f/vYfaSI/YUO+sdFWh1Sreg68n6qI+/WmPXjdDape8MiItLAfkrOZML8tRSUuhneuQXzbxlCkL/D6rBExEKWthb9/f0ZNGgQy5cvZ+zYsUB5Y3n58uVMnjy5SvmePXuyadOmStsee+wx8vLyeOmll4iLi6uyT0BAAAEBAVW22+32Bm+M22y2RjmP1J3qyLtVDDZ2OJpvHUWGBHB+r1g+2XCQ/244xICOTS8ZquvI+6mOvFtj1Y/qX0REKmw+kMPE13+koNTNGV1a8NoEJQhFxAuGG0+ZMoUJEyYwePBghg4dyqxZsygoKPCsdjx+/HjatWvHjBkzCAwMpE+fPpX2j4yMBKiyXUSaPsMsTxM2rQG4tXfFgHZ8suEg/7fhII9efBp+XrjIioiIiIg0D8kZBdzyevkchEPjo5UgFBEPy5OE48aN4/DhwzzxxBOkpqaSkJDA0qVLPYuZpKSk6M63iI86miOstLpxc3Rmt5a0CPHnSEEpq3ZmcE6P1laHJCIiIiLNUHpeMePnryUjv5TT2oQz75bBShCKiIflSUKAyZMnVzu8GGDFihUn3HfBggX1H5CIeIWKxY2b2mIeteXnsHNp/7YsWJ3Mx+sPKEkoIiIiIvWuuMzNpDfWkZJZSIfoYBbeOoTwQC1SIiK/Uhc9EfFa5tGuhPZmniQEuDyhLQBfbU2juMxtcTQiIiIi0pyYpslD729gw75sIoP9eOPWobQOC7Q6LBHxMkoSiojXMjxJQosDaQQJcZG0jQiksNTNqh0ZVocjIiIiIs3IS8t38OnGQzjtNl69cRDxLUOsDklEvJCShCLitXxluDGUv8bze8cCsHRLqsXRiIiIiEhzsXxrGrO+2gHAM1f0YXiXFhZHJCLeSklCEfFavtSTEOCCPuVJwq+2plHmNiyORkRERESaun2ZhUx5bwMAE4Z3ZNyQDhZHJCLeTElCEfFapg/1JAQYEh9NixB/sgvLWLsn0+pwRERERKQJK3G5ufed9eQUldE/LpJHL+5ldUgi4uWUJBQRr2X6WE9Ch93Geb1iAFi6WUOORURERKTunvlsKxv35xAR5MfsGwbg79TXfxE5MX1KiIjXqpiT0BdWN64w5uiQ4y9+ScWoeANERERERGrh040HeWPNXgBeHNef9lHBFkckIk2BkoQi4rUq5iT0oRwhZ3RpQViAk7TcEhL3Z1sdjoiIiIg0MYdyivjzR5sAuGdUF87tGWNxRCLSVChJKCJey/TBnoQBTgfn9GwNwBdb0iyORkRERESaEtM0efiDjeQWu+jXPoIHz+tudUgi0oQoSSgiXsvXVjeu8LvTypOEK7alWxyJiIiIiDQlb36/l5U7Mghw2pl5bQJ+Dn3lF5Ga0yeGiHgtX5yTEOCsbq2w2SApNY/UnGKrwxERERGRJmDX4XyeXbIVgKkX9qRr61CLIxKRpkZJQhHxWhU9CfGtHCHRIf70ax8JwLfb1ZtQRERERE7M5TaY8t4GissMRnRtwfjh8VaHJCJNkJKEIuL1fK0nIcCo7q0AWLHtsMWRiIiIiIi3W7A6mQ37sgkLdPK3q/tj97X5ekSkXihJKCJey1fnJAQY1aM8SbhqRwZlbsPiaERERETEW+3PKuTvX2wH4M8XnUbbyCCLIxKRpkpJQhHxWr46JyFAv/aRRAX7kVfiYv3eLKvDEREREREvZJomT/x3C0VlbobGRzNucJzVIYlIE6YkoYh4LeNoltDmg0lCh93G2RVDjrdryLGIiIiIVPXZpkN8nZSOv8POs1f20TBjETklShKKiNcyfXi4Mfw65FjzEoqIiIjIb+UUlvHkJ78AcM85XejaOsziiESkqVOSUES8li8PNwY4u1srbDbYeiiX9Nxiq8MRERERES/yty+SyMgvoUurEO4e1cXqcESkGVCSUES81tEcIb6ZIoQWoQH0bhsOwOpdRyyORkRERES8xZaDObzzQwoAT4/tS4DTYXFEItIcKEkoIl6rYnVjm6+ONwbO6NISgNW7MiyORERERES8gWmaTP/kFwwTLunXhuFdWlgdkog0E0oSiojX8vU5CQHOONro+27nEc/7ISIiIiK+6/82HmJtciaBfnb+fNFpVocjIs2IkoQi4rV8fU5CgKGdonHabRzILmJfZpHV4YiIiIiIhQpLXcxYshWAe0Z1pW1kkMURiUhzoiShiHgtQz0JCfZ3MqBDJADfacixiIiIiE97dcUuDuUU0z4qiDvO7mx1OCLSzChJKCJeq6Inoc2HexLCsfMSavESEREREV+1P6uQf/1vNwCPXdyLQD8tViIi9UtJQhHxWpqTsFzFvIRrdmVoXkIRERERHzXzi+2UugyGd27BmN4xVocjIs2QkoQi4rUq8mE2fDtLOKBDFIF+djLyS9melm91OCIi9Wr27NnEx8cTGBjIsGHDWLt27QnLz5o1ix49ehAUFERcXBwPPvggxcXFjRStiIg1thzM4ePEAwD8+aLTfH6kjYg0DCUJRcRraU7Ccv5OO0PiowH4bqfmJRSR5mPx4sVMmTKFadOmsX79evr378+YMWNIT0+vtvw777zDI488wrRp09i6dSuvvfYaixcv5s9//nMjRy4i0rie+zwJ04TL+relb/sIq8MRkWZKSUIR8Vqak/BXI7pqXkIRaX5mzpzJpEmTmDhxIr169WLOnDkEBwczf/78asuvXr2aESNGcMMNNxAfH8/555/P9ddff9LehyIiTdn/th9m5Y4M/Bw2/jimh9XhiEgz5rQ6ABGR6hw7956v9yQEGN65fF7CH5MzMQwTu94UEWniSktLWbduHVOnTvVss9vtjB49mjVr1lS7zxlnnMFbb73F2rVrGTp0KLt372bJkiXcfPPNxz1PSUkJJSUlnse5ubkAGIaBYRj19GqqMgwD0zQb9BxyalRH3k91BIZhMuPzrQDcfHpH2kUGes37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", + "text/plain": [ + "
" + ] + }, + "jetTransient": { + "display_id": null + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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U2nWCwsLszg6OlrGjx+fZvmBAwcsTk5Oty2/0+vYuXOn5cMPP7R4enpabt68abFYLJauXbta2rZta7FYLJby5ctbHnroobtmtFgsluHDh1scHBwsGzdutKxYscICWGbMmJFmnYEDB1rS+7e9fv16C2BZv359muV3+10OGzYszbqbNm2yAJbPP/88zfI1a9bctjz1td+63fSsXLnS+ju6m9TfW6qEhARL7dq1Le3atUuzHLC4urqmacu5c+daAEvJkiUt169fty4fPnz4be3eunVrC2CZOnWqdVl8fLwlMDDQUqJECUtCQoLFYkn/93b//fdb6tSpY4mLi7MuM5vNlubNm1uqVKliXVavXr3b2jsjhg8fbnF2drZERESkyebt7W3p37+/dVmRIkUsAwcOzPT2U9vs119/tVy+fNly5swZy7Jlyyy+vr4Wd3d3y9mzZy0Wy79/SytWrLjjturVq2fx8fG5bdt3ek+LSMGhHgsRMcSsWbP45Zdf0lzu5cUXX0xzu2XLlpw8eTJLz//NN99gNpvp1q1bmm9YS5YsSZUqVVi/fn2Gt9WtWzdiY2NZtWoV0dHRrFq16o7DoO5k9OjR1KpVi759+zJgwABat27Nyy+/nNmXlWEvvfRSmtsrVqygSJEidOjQIc3vIygoCA8PjzS/j379+mGxWOjXr99dnyN1svyqVavS9Hj8l7u7u/V6ZGQkUVFRtGzZMt1v+e+//34qVKhgvd2kSRMAnnzySTw9PW9b/t+/DycnJ1544QXrbRcXF1544QXCw8PZvXt3uvkiIiL43//+R7du3YiOjrb+bq5evUqnTp04fvw4586ds77mgwcPcvz48Tu+3vR0796dxMREvvnmG+uyn3/+mWvXrtG9e3frMm9vb37//XfOnz+fqe2nat++PcWLF8ff358ePXrg4eHBypUrKVOmTIa34eHhQXR09G3Ls/KeFhH7osnbImKIxo0bZ2qip5ubm3UIUyofH5/bxv9n1PHjx7FYLFSpUiXd+zNzVKrixYvTvn17li5dys2bN0lOTr5t4uu9uLi4sGDBAho1aoSbmxsLFy60zqHIaU5OTpQtWzbNsuPHjxMVFUWJEiXSfUx4eHimn6d169Y8+eSTjBkzhunTp9OmTRu6dOlCr1690hwta9WqVYwbN469e/cSHx9vXZ7e6y9Xrlya20WKFAHA398/3eX//fsoXbr0bRPVq1atCqTMSWnatOltz3nixAksFgtvv/02b7/9drqvNTw8nDJlyjB27Fgee+wxqlatSu3atencuTPPPPMMdevWTfdxqerVq0f16tVZvnw5zz77LJAyDKpYsWJpjsD03nvv0bdvX/z9/QkKCuLBBx+kT58+BAQE3HX7qWbNmkXVqlVxcnLCz8+PatWq4eCQue8Yb9y4kaaIS5XZ97SI2B8VFiJiExwdHXN0e2azGZPJxOrVq9PdtoeHR6a216tXL0JCQrh48SIPPPBAlg5tu3btWgDi4uI4fvz4bYfuvJM7FSD/ndieytXV9bYPk2azmRIlSvD555+n+5j/FnUZzfXVV1+xfft2fvjhB9auXUv//v2ZOnUq27dvx8PDg02bNvHoo4/SqlUrPvroI0qVKoWzszMLFy5k6dKlt23zTn8Hd1pusVgynfu/zGYzAEOHDrXOdfivypUrA9CqVSv++usvvvvuO37++Wfmz5/P9OnTmTNnDs8999xdn6d79+6MHz+eK1eu4Onpyffff0/Pnj3THG2sW7dutGzZkpUrV/Lzzz8zefJkJk2axDfffMMDDzxwz9eS3Q//iYmJHDt2TIeRFZF0qbAQEbt2pw/dlSpVwmKxULFiRes31tnx+OOP88ILL7B9+3aWL1+e6cfv37+fsWPHEhwczN69e3nuuec4cOCA9Zt3uPNr8fHxAbhtwvnff/+d4eevVKkSv/76Ky1atEgzNCknNG3alKZNmzJ+/HiWLl1K7969WbZsGc899xxff/01bm5urF27Nk0vxsKFC3M0Q6rz58/fdnjdY8eOAaQZYnWr1N4AZ2dn2rdvf8/nKFq0KMHBwQQHB3Pjxg1atWrF6NGjM1RYjBkzhq+//ho/Pz+uX7+e7gEASpUqxYABAxgwYADh4eE0aNCA8ePHZ6iwyK6vvvqK2NjYOxZYIlKwaY6FiNi11A+Q//3Q/cQTT+Do6MiYMWNu+1bbYrFw9erVTD2Ph4cHs2fPZvTo0TzyyCOZemxiYiL9+vWjdOnSvP/++yxatIhLly7x6quvZui1lC9fHkdHRzZu3Jhm+UcffZThDN26dSM5OZl33nnntvuSkpLSPGdGDzcbGRl52+82MDAQwDrkydHREZPJlKZ3JSwsjG+//TbD2TMjKSmJuXPnWm8nJCQwd+5cihcvTlBQULqPKVGiBG3atGHu3LlcuHDhtvtvPRv1f/9uPDw8qFy5cpohXndSo0YN6tSpw/Lly1m+fDmlSpWiVatW1vuTk5Nv+52XKFGC0qVLZ2j72bVv3z6GDBmCj48PAwcOzPXnExHbox4LEbFrqR8W33zzTXr06IGzszOPPPIIlSpVYty4cQwfPpywsDC6dOmCp6cnp06dYuXKlTz//PMMHTo0U8/Vt2/fLGVMnV+wbt06PD09qVu3LiNHjuStt97iqaee4sEHH0zzWl5++WU6deqEo6MjPXr0oEiRInTt2pWZM2diMpmoVKkSq1atytS8iNatW/PCCy8wYcIE9u7dS8eOHXF2dub48eOsWLGC999/3zpvZOXKlQQHB7Nw4cK7TuBevHgxH330EY8//jiVKlUiOjqaefPm4eXlZX1NDz30ENOmTaNz58706tWL8PBwZs2aReXKldm/f3+Wfp93U7p0aSZNmkRYWBhVq1Zl+fLl7N27l48//viu82pmzZrFfffdR506dQgJCSEgIIBLly6xbds2zp49y759+wCoWbMmbdq0ISgoiKJFi7Jr1y6++uorBg0alKF83bt3Z+TIkbi5ufHss8+mGbIWHR1N2bJleeqpp6hXrx4eHh78+uuv7Ny5k6lTp2bvF/MfmzZtIi4ujuTkZK5evcqWLVv4/vvvKVKkCCtXrqRkyZI5+nwiYh9UWIiIXWvUqBHvvPMOc+bMYc2aNZjNZk6dOkXhwoUZNmwYVatWZfr06YwZMwZImQTcsWNHHn300TzJt2fPHt59910GDRpE27ZtrcuHDRvGd999R0hICAcPHsTb25snnniCwYMHs2zZMj777DMsFot1qMzMmTNJTExkzpw5uLq60q1bNyZPnpypsfBz5swhKCiIuXPnMmLECJycnKhQoQJPP/00LVq0yPRra926NTt27GDZsmVcunSJIkWK0LhxYz7//HPr/JF27drxySefMHHiRIYMGULFihWtH/xzo7Dw8fFh8eLFDB48mHnz5uHn58eHH35ISEjIXR9Xs2ZNdu3axZgxY1i0aBFXr16lRIkS1K9fn5EjR1rXe/nll/n+++/5+eefiY+Pp3z58owbN856osZ76d69O2+99RY3b95MczQogEKFCjFgwAB+/vln61HNKleuzEcffXTbUb6y64MPPgBShn95e3tTo0YNxowZQ0hISJbm24hIwWCy5MTMNhERkXyuTZs2XLlyhT///NPoKCIidklzLEREREREJNtUWIiIiIiISLapsBARERERkWzTHAsREREREck29ViIiIiIiEi2qbAQEREREZFss4nzWJjNZs6fP4+npycmk8noOCIiIiIiBYLFYiE6OprSpUunOWlnemyisDh//jz+/v5GxxARERERKZDOnDlD2bJl77qOTRQWnp6eQMoL8vLyMiSD2WwmMjISHx+fe1Zrkv+pPe2L2tO+qD3ti9rTvqg97UtG2vP69ev4+/tbP4/fjU0UFqnDn7y8vAwtLJKSkvDy8tIbyQ6oPe2L2tO+qD3ti9rTvqg97Utm2jMj0xH0FyEiIiIiItmmwkJERERERLJNhYWIiIiIiGSbTcyxEBEREbFHZrOZhIQEo2NkmNlsJjExkbi4OM2xsANmsxmz2Zxj21NhISIiImKAhIQETp06laMf7HKbxWKxHklI5xazfantmZSURKlSpbLdpiosRERERPKYxWLhwoULODo64u/vbzPf/lssFpKSknByclJhYQfMZjM3btzg6tWrmEwmSpUqla3tqbAQERERyWNJSUncvHmT0qVLU6hQIaPjZJgKC/tisVhwdnbGwcGBy5cvU6JECRwdHbO8Pdsoj0VERETsSHJyMgAuLi4GJxHBWtwmJiZmazsqLEREREQMom/9JT/Iqb9DFRYiIiIiIpJtKixEREREJFe0adOGIUOGGB3DLlSoUIEZM2YYHeOuNHlbREREJJ+Yt/Fknj5fSKuATK0fHBzM4sWLef7555k7d26a+wYOHMhHH31E3759WbRoEQDffPMNzs7OGd5+v379uHbtGt9++22mckn+oB4LEREREckwf39/li9fTmxsrHVZXFwcS5cupVy5cmnWLVq0KJ6ennkdkeTkZJs6P4i9UGEhIpIH5m08edeLiIitCAwMxN/fn2+++ca67JtvvqFcuXLUr18/zbq3DoU6cuQIhQoVYunSpdb7v/zyS9zd3Tl06BCjR49m8eLFfPfdd5hMJkwmExs2bGDDhg2YTCauXbtmfdzevXsxmUyEhYUBsGjRIry9vfn++++pWbMmrq6unD59mvj4eIYOHUqZMmUoXLgwTZo0YcOGDXd8bRaLhdGjR1OuXDlcXV0pXbo0L7/8svX+JUuW0LBhQzw9PSlZsiS9evUiPDzcen9q1rVr11K/fn3c3d1p164d4eHhrF69mho1auDl5UWvXr24efNmmt/ToEGDGDRoEEWKFKFYsWK8/fbbWCyWO2a9du0azz33HMWLF8fLy4t27dqxb98+6/379u2jbdu2eHp64uXlRVBQELt27brj9nKCCgsRERERyZTg4GAWLlxovb1gwQKCg4Pv+pjq1aszZcoUBgwYwOnTpzl79iwvvvgikyZNombNmgwdOpRu3brRuXNnLly4wIULF2jevHmGM928eZNJkyYxf/58Dh48SIkSJRg0aBDbtm1j2bJl7N+/n65du9K5c2eOHz+e7ja+/vprpk+fzty5czl+/DjffvstderUsd6fmJjIO++8w759+/j2228JCwujX79+t21n9OjRfPjhh2zdupUzZ87QrVs3ZsyYwdKlS/nxxx/5+eefmTlzZprHLF68GCcnJ3bs2MH777/PtGnTmD9//h1fb9euXa0Fy+7du2nQoAH3338/ERERAPTu3ZuyZcuyc+dOdu/ezbBhwzI1LC0rNMdCRERERDLl6aefZsSIEfz9998AbNmyhWXLlt21NwBgwIAB/PTTTzz99NO4uLjQqFEjBg8eDICHhwfu7u7Ex8dTsmTJTGdKTEzko48+ol69egCcPn2ahQsXcvr0aUqXLg3A0KFDWbNmDQsXLuTdd9+9bRunT5+mZMmStG/fHmdnZ8qVK0fjxo2t9/fv3996PSAggA8++IBGjRpx48YNPDw8rPeNGzeOFi1aAPDss88yfPhw/vrrLwICUua0PPXUU6xfv5433njD+hh/f3+mT5+OyWSiWrVqHDhwgOnTpxMSEnJbzs2bN7Njxw7Cw8NxdXUFYMqUKXz77bd89dVXPP/885w+fZrXX3+d6tWrA1ClSpVM/04zS4WFiIiIiGRK8eLFeeihh1i0aBEWi4WHHnqIYsWKZeixCxYsoGrVqjg4OHDw4MEcO4eCi4sLdevWtd4+cOAAycnJVK1aNc168fHx+Pr6pruNrl27MmPGDAICAujcuTMPPvggjzzyCE5OKR+Zd+/ezejRo9m3bx+RkZHWeRynT5+mZs2a1u3cmsPPz49ChQpZi4rUZTt27Ejz3E2bNk3zu2jWrBlTp04lOTn5trNh79u3jxs3btz2OmJjY/nrr78ACA0N5bnnnmPJkiW0b9+erl27UqlSpTv89nKGCgsRERERybT+/fszaNAgAGbNmpXhx+3bt4+YmBgcHBy4cOECpUqVuuv6Dg4pI/dvnW+Q3hmi3d3d03wwv3HjBo6Ojuzevfu2D+a39i7cyt/fn6NHj/Lrr7/yyy+/MGDAACZPnsxvv/1GQkICnTp1olOnTnz++ecUL16c06dP06lTJxISEtJs59YhRyaT6bYhSCaTKVuTy2/cuEGpUqXS7SHy9vYGUoZj9erVix9//JHVq1czatQoli1bxuOPP57l570XFRYiIiIikmmdO3cmISEBk8lEp06dMvSYiIgI+vXrx5tvvsmFCxfo3bs3e/bswd3dHUjpdUhOTk7zmOLFiwNw4cIFfHx8gJTJ2/dSv359kpOTCQ8Pp2XLlhl+Xe7u7jzyyCM88sgjDBw4kOrVq3PgwAEsFgtXr15l4sSJ+Pv7A+ToZOjff/89ze3t27dTpUqV24oigAYNGnDx4kWcnJyoUKHCHbdZtWpVqlatyquvvkrPnj1ZuHBhrhYWmrwtIiIiIpnm6OjI4cOHOXToULofftPz4osv4u/vz1tvvcW0adNITk5m6NCh1vsrVKjA/v37OXr0KFeuXCExMZHKlSvj7+/P6NGjOX78OD/++CNTp06953NVrVqV3r1706dPH7755htOnTrFjh07mDBhAj/++GO6j1m0aBGffPIJf/75JydPnuSzzz7D3d2d8uXLU65cOVxcXJg5cyYnT57k+++/55133snYLysDTp8+TWhoKEePHuWLL75g5syZvPLKK+mu2759e5o1a0aXLl34+eefCQsLY+vWrbz55pvs2rWL2NhYBg0axIYNG/j777/ZsmULO3fupEaNGjmWNz0qLEREREQkS7y8vPDy8srQup9++ik//fQTS5YswcnJicKFC/PZZ58xb948Vq9eDUBISAjVqlWjYcOGFC9enC1btuDs7MwXX3zBkSNHqFu3LpMmTWLcuHEZes6FCxfSp08fXnvtNapVq0aXLl3YuXPnbefbSOXt7c28efNo0aIFdevW5ddff+WHH37A19eX4sWLs2jRIlasWEHNmjWZOHEiU6ZMydgvKgP69OlDbGwsjRs3ZuDAgbzyyis8//zz6a5rMpn46aefaNWqFcHBwVStWpUePXrw999/4+fnh6OjI1evXqVPnz5UrVqVbt268cADDzBmzJgcy5tuLsvdDpCbT1y/fp0iRYoQFRWV4T/enGY2m4mIiKBo0aLWsX5iu9Se9sUW2vNe56rI7Nlv7ZkttKdknNozfXFxcZw6dYqKFSvi5uZmdJwMs1gsJCUl4eTklGOTriXlPBaBgYHMmDEjT583tT2TkpIICwtL9+8xM5/D9Q4XEREREZFs0+RtEZE8ZDabuXL+NGf+OsL1iMs4ODri6e3LpWpP4OfnZ3Q8ERGRLFNhISKSB65cOMOmVcv5/ZfviLh0/rb757w9gHr16vH666/TvXt36zHTRUTE/t3rxIK2QkOhRERy0YULF3jxxRd5q3d7Vn82O92iItW+fft4+umnqV27Nrt3787DlCIiItmnr8RERHKBxWLhk08+YejQoURFRVmXV6wZSKN2D1OxRj2KlfLHbE7m8vnTFLpymLlz53Lp0iWOHj1qPePq4MGDDXwVIiIiGafCQkQkh0VERPDMM8/w008/WZfVbtKax559lfLV6ty2vk/xkoS06sGwYcOYPn06o0aNIjExkZdffpnIyEhGjhyZl/FFRESyRIWFiEgO2r17N08++SR///03AGXKlGHu3Lmc97z3SYnc3d0ZMWIE7dq144knnuDChQuMGjUKBwcH3nrrrdyOLiIiki2aYyEikkOWLFlCixYtrEXFE088wcGDB3nooYcytZ2mTZuyfv16SpUqBcDbb7/Nl19+meN5RUREcpIKCxGRbLJYLIwfP54+ffoQHx+Po6MjU6ZM4auvvqJIkSJZ2ma1atX43//+h7e3NwD9+/fn0KFDOZhaREQkZ6mwEBHJhqSkJF566SXrUCUfHx9+/fVXXnvttWyflbZ69ep8/vnnAMTExPDkk08SFxeX7cwiIiK5QYWFiEgWxcTE8MQTTzB37lwAypUrx5YtW2jTpk2mtzVv48l0L+c8qvNw35QjQx05coRx48bl5EsQEcmU4OBgXFxccHBwwMXFhcqVKzN27FiSkpLYsGEDJpMJk8mEg4MDRYoUoX79+vzf//0fFy5cSLOd0aNHW9e99fLrr78a9MokJ6iwEBHJgsuXL9OuXTt++OEHAOrVq8e2bduoUePek7Qz66E+g2jQoAEAkyZN4sCBAzn+HCIiGdWpUyfOnz/P8ePHee211xg9ejSTJ0+23n/06FHOnz/Pzp07eeONN/j111+pXbv2bf+7atWqxYULF9JcWrVqldcvR3KQjgolIpJJJ06c4IEHHuDEiRMAdOjQgYdfncyPJ+LgxMkcfz5HJyfmz59Po0aNSEpK4vnnn2fr1q3ZHmolIpIVLi4ulCxZEpPJxEsvvcTKlSv5/vvvadasGQAlSpTA29ubkiVLUrVqVR577DHq16/PSy+9xObNm63bcXJyomTJkka9DMkF6rEQEZt3+PBhnnnmGZYuXZrrz7V161aaNm1qLSr69OnDqlWrcC/smavPW79+fUJDQwHYvn07X331Va4+n4hIRrm7u5OQkHDX+1988UW2bNlCeHh4HiaTvKYeCxGxeR999BFr1qxhzZo1FClShEceeSTT25i38e49DSGtAvjyyy+tR34CePPNN3nnnXfyrOfgrbfeYuHChVy5coURI0bQpUsXnJ2d8+S5RST3DRkyhL179+b58wYGBjJjxoxMP85isbBu3TrWrl3L4MGD77pu9erVAQgLC6NEiRIAHDhwAA8PD+s6NWvWZMeOHZnOIfmHCgsRsXmpvQcAvXv3Zvv27dSsWTPHtm+xWJg0aRLDhg0DUrrv586dS//+/XPsOTLCy8uLt99+m1deeYUTJ04wb948BgwYkKcZRCT37N27l99++83oGPf0008/4enpSWJiImazmV69ejF69Gh27tx5x8dYLBaANF/EVKtWje+//95629XVNfdCS55QYSEiNi/12y+A6OhoOnfuzIYNGwgICMj2tm/eiGbJ5BHs3vATkPLh/uuvv6Z9+/bZ3nZWvPjii8yYMYNTp07xzjvv0L9/f9zc3AzJIiI5KzAw0Caet02bNsyePRtXV1dKly6Nk9O9P04ePnwYgAoVKliXpR5VSuyHCgsRsXlmsznN7TNnztCmTRvWr19PpUqVsrzdv4/9ycejB3P53GkAivqVZvDE+ZxyCbjn0Knc4uLiwujRo+nbty8XL17k008/5fnnnzcki4jkrKwMRzJCoUKFqFy5coaHgcbGxvLxxx/TqlUrihcvnsvpxEiavC0iNi+1sChRpjwPPP0SkFJc1GvQkCFTFmW6CEiMj+f7BTOYNKCrtaio3bQNb378HWUCquVs+Czo2bMn5cqVA2Dy5MkkJycbnEhE5F/h4eFcvHiR48ePs2zZMlq0aMGVK1eYPXu20dEkl2WpsJg1axYVKlTAzc2NJk2aZHiizbJlyzCZTHTp0iUrTysikq7UwsLk4EiX517joT4DAYi5fo0P/q8/yz4YS0RExL23k5zM9p+/ZXRwZ1YtnklSYgIOjo488cL/MWjCPDy9i+bq68goZ2dnXnvtNSBlfsk333xjcCIRkX9Vq1aN0qVLExQUxMSJE2nfvj1//vlnjs59k/wp00Ohli9fTmhoKHPmzKFJkybMmDGDTp06cfTo0TTjnP8rLCyMoUOH0rJly2wFFhH5r9TCwsEh5cytjz0bSqnylfl08ggS4mL539eLqfy/7wkODqZXr17UrVvXejSlhIQE/vzzT75fsIjtP3/LlQtnrNv1r1yTZ4aOp0KNuoa8rlv9t9fFoVo7ChfxISYqkv8bOY6uXbsalExECpKFCxeSlJSU7n1t2rSxTtK+l9GjRzN69OgcTCb5QaZ7LKZNm0ZISAjBwcHUrFmTOXPmUKhQIRYsWHDHxyQnJ9O7d2/GjBmTI5MpRURuZe2xMP37L61x+0cZMXclVQObABAZGcm0adNo2LAhbu6FKFaqLEVLlKKwhydBQUGsWjzTWlR4FS1Oj5dHMmLuynxRVKTH1b0Qbbs8DUDYkf13PRqLiIhIXshUYZGQkMDu3bvTHA3FwcGB9u3bs23btjs+buzYsZQoUYJnn30260lFRO7g36FQaf+lla5QhddmfM5L73xkLTAAzMlJXL14jsjLF0lK/PekTuWr1aHHK6N4d9kG2j3ZF8cMHOnESC0f6YGDoyOAxi6LiIjhMrXXvHLlCsnJyfj5+aVZ7ufnx5EjR9J9zObNm/nkk08ydcKX+Ph46wmoAK5fvw6kfHj479Ff8orZbMZisRj2/JKz1J725d8eCxP8pxveBNRv2ZH6LTty9eI5jv6xnfN/nyDqajhOzs4U8vDCv3JNAmoGUqJshX8fmMHufCP5FPOjXov2/LFxLV988QXvvfceRYvmj3kg2aH3p31Re6Yv9feSerFFtppb0pf6Pv3vezUz791c/TouOjqaZ555hnnz5lGsWLEMP27ChAmMGTPmtuWRkZF3HNeX28xmM9HR0VgsFhwcdDAtW6f2tC9xcXEAOJjAOSnmjuuVLOZNyQ6d77yhuzw2v2r/8JP8sXEtcXFxzJ49m5deesnoSNmm96d9UXumL/XkcklJSYZ9tskqHYnOviQnJ5OUlITZbCYqKoqbN2+muT86OjrD28pUYVGsWDEcHR25dOlSmuWXLl2iZMmSt63/119/ERYWxiOPPGJdllr1ODk5cfTo0XSPMT98+HBCQ0Ott69fv46/vz8+Pj54eXllJnKOMZvNmEwmfHx89I/RDqg97UvqyZlMDk4kOhU2OE3eqtyoLX7+AVw6c5LPP/+cESNGZPjY8vmV3p/2Re2Zvri4OCIjI3FycsrQCebyG1vMLHeWWvgXKVLktpOuZqatM/VX4eLiQlBQEOvWrbMeMtZsNrNu3ToGDRp02/rVq1fnwIEDaZa99dZbREdH8/777+Pv75/u87i6uqZ7WncHBwdD/ymZTCbDM0jOUXvajzRzLGz8Q3VmmUwm7nuoK1/PmcTRo0fZtWsXTZo0ufcD8zm9P+2L2vN2Dg4O1i8BbOnLgFuHP9lSbklfanumfgGQ3vs0M+/bTJeboaGh9O3bl4YNG9K4cWNmzJhBTEwMwcHBAPTp04cyZcowYcIE3NzcqF27dprHe3t7A9y2XEQkq6zjPwvoTq5Jxy6s/HgyZrOZxYsX20VhIWLvnJ2dMZlMXL58meLFi9vMh3SLxUJSUhJOTk42k1nuzGw2ExcXx9WrV3FwcMDFxSVb28t0YdG9e3cuX77MyJEjuXjxIoGBgaxZs8Y6ofv06dP6RkJE8lTqNy4OBXQn5+1bgo4dO7JmzRqWLVvG9OnT0+31FZH8w9HRkbJly3L27FnCwsKMjpNhqRN8b+1xEduV2p4eHh6ULl0625/hszRAbtCgQekOfQLYsGHDXR+7aNGirDyliMgd3elwswVJ3759WbNmDZGRkaxatYonn3zS6Egicg8eHh5UqVKFxMREo6NkWOoE3yJFiuiLZDuQenCF1HnU2aWZNyJi81J7LG49QV5Bc9W3Lu4ensTeiOad6XOIKF4/zf0hrXRyUpH8yNHRMUc+0OUVs9nMzZs3cXNzU2FhB1LbM6d6n/QXISI2798ei4LbLe/i6kaDVimH0j24YyM3b2T88IAiIiI5QYWFiNi8f0+QV7D/pTVs9xAASYkJ7Nv8i8FpRESkoCnYe2ERsQuphUVB75avXr8ZhYv4ALBrw08GpxERkYKmYO+FRcQuqMcihaOTEw1adQLg0M7NxERHGZxIREQKkoK9FxYRu6A5Fv9q9M9wqOSkRPZu0nAoERHJOyosRMTmqcfiX1XqNsbTxxeA3b+tNjiNiIgUJNoLi4jN03ks/uXo5ERgi/YAHNmzlbibMQYnEhGRgkJ7YRGxedbCAg2FAqj3T2GRlJDA4V2bDU4jIiIFhQoLEbF51hPkaY4FANWDmuPs6gbAvi3rDE4jIiIFhQoLEbF5mmORlourGzUb3QfA/m3rMScnG5xIREQKAu2FRcTm6TwWt6vXPGU41I2oCE4e+sPgNCIiUhBoLywiNu/fHgsNhUpVt3lb6+9Dw6FERCQvqLAQEZuno0LdzsunGAG16gOwd8uvBqcREZGCQHthEbF5mmORvtSjQ106fZKjR48anEZEROyd9sIiYvM0xyJ99Zrfb73+ww8/GJhEREQKAu2FRcTmaY5F+kqWr0TxMuUAWLNmjcFpRETE3qmwEBGbpzkW6TOZTNRq1AqATZs2EROjs3CLiEju0V5YRGye5ljcWer5LBISEvjtt98MTiMiIvZMe2ERsXnqsbizavWb4uDoBMDPP/9scBoREbFn2guLiM37t8fC4CD5kHthTyrVbgDA2rVrDU4jIiL2TIWFiNg8i8UCaCjUnaQOhzpy5AinT582OI2IiNgr7YVFxOZpKNTd1WrU0npdvRYiIpJbtBcWEZtnPY+FeizSVa5qbXx9fQEVFiIiknu0FxYRm/dvj4UmWaTHwcGBDh06ALBu3TqSkpIMTiQiIvZIhYWI2DwdbvbeOnXqBMC1a9fYuXOnwWlERMQeaS8sIjZPcyzurWPHjtbrOuysiIjkBu2FRcTm/dtjoaFQd1K6dGlq1KgBwPr16w1OIyIi9kiFhYjYPOvkbfVY3FXbtm0B2LZtG7GxsQanERERe6O9sIjYPM2xyJh27doBkJCQwLZt2wxOIyIi9kZ7YRGxeZpjkTGtW7e2XtdwKBERyWnaC4uIzdMci4wpVqwYdevWBeB///ufwWlERMTeqLAQEZtnsVgAFRYZkTrPYseOHdy4ccPgNCIiYk9UWIiIzdMci4xLnWeRlJTEli1bDE4jIiL2RHthEbF5mmORca1atbIePUvDoUREJCdpLywiNs1isWgoVCZ4e3tTv359QBO4RUQkZ6mwEBGbllpUgM5jkVGpw6F2795NVFSUwWlERMReOBkdQEQkO1KHQYHmWNzNvI0nrdfjilUHUn53b370JfVa3E9IqwCjoomIiJ3QXlhEbFqawsJBQ6Eyokrdhjg4OgJwfP9Og9OIiIi9UGEhIjZNPRaZ51bIA//KNQA4sX+XwWlERMReaC8sIjYtbY+F/qVlVOW6jQAIO3qA+LhYg9OIiIg90F5YRGzarYWFg3osMqzKP4WFOTmJU4f2GhtGRETsgvbCImLTNMciayrXCbJe13AoERHJCSosRMSmaY5F1nj5FMOvXMqRoI4fUGEhIiLZp72wiNi0W89joRPkZU6VOg0BOHnwD5KSkgxOIyIitk6FhYjYNE3ezrrKdVMKi/jYGPbu3WtsGBERsXnaC4uITUs7FEo9FpmROoEbYNOmTQYmERERe6DCQkRsmnossq5YKX+8i/kBKixERCT7tBcWEZumydtZZzKZrMOhNm/enGa+ioiISGZpLywiNi3NeSx0uNlMSx0OdfnyZY4ePWpwGhERsWUqLETEpqnHIntunWexefNmA5OIiIit015YRGya5lhkT+mKVSnk4QVonoWIiGSP9sIiYtPUY5E9Dg4OVPrnLNwqLEREJDu0FxYRm5a2x0JzLLIidTjUqVOnOHfunMFpRETEVqmwEBGblmbytnossqTKP0eGAtiyZYuBSURExJZpLywiNi3NIVJ1grwsKVelNs7OzgBs27bN4DQiImKrVFiIiE3THIvsc3Z1JSgoZZ7F1q1bDU4jIiK2SnthEbFpOo9FzmjWrBkAf/zxB3FxcQanERERW5SlwmLWrFlUqFABNzc3mjRpwo4dO+647jfffEPDhg3x9vamcOHCBAYGsmTJkiwHFhG5lXosckZqYZGYmMju3bsNTiMiIrYo03vh5cuXExoayqhRo9izZw/16tWjU6dOhIeHp7t+0aJFefPNN9m2bRv79+8nODiY4OBg1q5dm+3wIiI6j0XOSC0sQMOhREQkazK9F542bRohISEEBwdTs2ZN5syZQ6FChViwYEG667dp04bHH3+cGjVqUKlSJV555RXq1q2rM7yKSI5Qj0XOKFu2LP7+/oAmcIuISNY4ZWblhIQEdu/ezfDhw63LHBwcaN++fYZ2RBaLhf/9738cPXqUSZMm3XG9+Ph44uPjrbevX78OpHyAuPVDRF4ym81YLBbDnl9yltrTfiQlJVmvm0wmuPUoUZJhZrOZpk2bcubMGbZt20ZycnLK79OgLHp/2g+1p31Re9qXjLRnZto6U4XFlStXSE5Oxs/PL81yPz8/jhw5csfHRUVFUaZMGeLj43F0dOSjjz6iQ4cOd1x/woQJjBkz5rblkZGRaT5E5CWz2Ux0dDQWiwUHDbeweWpP+3Ht2jXrdWdLAs5JMcaFsWERERHUq1ePFStWcPHiRfbu3Uv58uUNyaL3p31Re9oXtad9yUh7RkdHZ3h7mSosssrT05O9e/dy48YN1q1bR2hoKAEBAbRp0ybd9YcPH05oaKj19vXr1/H398fHxwcvL6+8iHwbs9mMyWTCx8dHbyQ7oPa0Hx4eHtbrZid3Ep0KG5jGdhUtWpT777+ft956C4DDhw9Tv359Q7Lo/Wlf1J72Re1pXzLSnk5OGS8XMlVYFCtWDEdHRy5dupRm+aVLlyhZsuQdH+fg4EDlypUBCAwM5PDhw0yYMOGOhYWrqyuurq7pbsfIP2KTyWR4Bsk5ak/7Y3Jw0EnyssjBwYEGDRrg5uZGXFwc27dv5+mnnzYsj96f9kXtaV/UnvblXu2ZmXbO1F+Ei4sLQUFBrFu3zrrMbDazbt26NEcUuRez2ZxmDoWISFbpqFA5Y97GkyzefpayVWoD8MMvG5i38aT1IiIici+ZHgoVGhpK3759adiwIY0bN2bGjBnExMQQHBwMQJ8+fShTpgwTJkwAUuZLNGzYkEqVKhEfH89PP/3EkiVLmD17ds6+EhEpkHRUqJwVUKs+Jw7s4uxfR4iPvYmreyGjI4mIiI3IdGHRvXt3Ll++zMiRI7l48SKBgYGsWbPGOqH79OnTabpMYmJiGDBgAGfPnsXd3Z3q1avz2Wef0b1795x7FSJSYFluOQqUUUcxsieVaqXMqzAnJxN2ZD/V6jc1OJGIiNiKLE3eHjRoEIMGDUr3vg0bNqS5PW7cOMaNG5eVpxERuae0Q6FUWGRXQK0G1usnD/6hwkJERDJM4wZExKZpKFTOKuJbnGKlUk6U99fBPwxOIyIitkR7YRGxabcWFjpCSc4I+Gc41MmDf6QZaiYiInI32guLiE1Tj0XOq1Q7ZTjUjagIws+FGRtGRERshvbCImLTNMci56X2WAD89eceA5OIiIgtUWEhIjZNPRY5r2xAdVzc3IGU4VAiIiIZob2wiNg0zbHIeY5OTlSoXhdQYSEiIhmnvbCI2DT1WOSOSv8cdvbcyaPExkQbnEZERGyB9sIiYtM0xyJ3pM6zsFgshB3Zb3AaERGxBSosRMSmqccidwTUCrRe13AoERHJCO2FRcSmpe2x0L+0nOLp7UvxMuUAOHlor7FhRETEJmgvLCI27dYTuJnQUKicFFDznxPlHdqrE+WJiMg9qbAQEZumORa5J3WeRUxUJCdOnDA4jYiI5HcqLETEpmmORe4JqBlovb59+3bjgoiIiE3QXlhEbJrOY5F7ylSqjrOrGwDbtm0zOI2IiOR32guLiE1L22OhoVA5ycnJmQrV6gDqsRARkXtTYSEiNk1HhcpdqfMs9u/fT0xMjMFpREQkP9NeWERsmuZY5K7UeRbJycns2rXL2DAiIpKvaS8sIjZNPRa5q+I/PRageRYiInJ32guLiE1LM3lbcyxynLdvCXxLlgE0z0JERO5OhYWI2DT1WOS+iv8Mh9q+fbtOlCciInekvbCI2DTNsch9lf4ZDnXp0iXCwsKMDSMiIvmW9sIiYtNu/QZdh5vNHRVr/jvPQsOhRETkTlRYiIhNSzsUSoVFbihXpSaurq6AJnCLiMidqbAQEZumoVC5z8nZhQYNGgDqsRARkTvTXlhEbJomb+eNZs2aAfDHH38QGxtrcBoREcmPtBcWEZumHou80bRpUwCSkpLYs2ePwWlERCQ/0l5YRGxamvNYaI5FrkntsQDNsxARkfSpsBARm6Yei7xRtmxZypTRifJEROTOtBcWEZumORZ5J3U4lAoLERFJj/bCImLT0vZYaChUbkodDnXu3DnOnDljcBoREclvnIwOICKSHeqxyBvzNp4k3M3fenvcgu9o2PZB6+2QVgFGxBIRkXxEe2ERsWlpJm9rjkWuKle1No5OzgCcPPiHwWlERCS/0V5YRGyaeizyjourG/6VawBw8pAKCxERSUt7YRGxaRaLxXpdcyxyX0Ct+gCcPnaQxIR4g9OIiEh+osJCRGxaao+Fioq8EVAzEICkxATOnDhsbBgREclXVFiIiE37t7DQv7O8EFCrgfW65lmIiMittCcWEZtmLSx01u084VuyDF5FiwGaZyEiImmpsBARm6Yei7xlMpmo+M9wqFOH9hqaRURE8hftiUXEpqUWFg46IlSeqfTPcKirF89x7Wq4wWlERCS/0J5YRGyaJm/nvdQeC4BTmmchIiL/UGEhIjbt3zkW+neWVypUq4ODoyMAf6mwEBGRf2hPLCI2TXMs8p6reyHKBFQHNM9CRET+pT2xiNi0f+dYaChUXqr0z4nywo4eICkp0eA0IiKSH6iwEBGbph4LY6TOs0iMj+PcX0eMDSMiIvmC9sQiYtPCw/85KpEmb+ep1B4L0DwLERFJocJCRGzWV199xYoVKwAoX7m6wWkKluJlylO4iA+geRYiIpJChYWI2KSjR48SHBwMgLe3N31eecvgRAWLyWQi4J/hUDoDt4iIgAoLEbFBMTExPPnkk9y4cQOAxYsXU6J0WYNTFTwB/wyHunzu9L9D0kREpMBSYSEiNiU5OZnevXtz8OBBAN58800efvhhg1MVTAG3nCjv999/Ny6IiIjkCyosRMRmWCwWXn31Vb777jsAOnbsyJgxYwxOVXBVrFHPesbzbdu2GZxGRESM5mR0ABGRjJi38SS/fLmAFbNmAlAmoBoPDXmPBVv+BosFZ4PzFURuhTwoXbEq504eZfv27UbHERERg6nHQkRswvafv+Wrj94FwLuYHy9P+gT3wp4Gp5LUeRY7duwgKSnJ4DQiImIkFRYiku998cUXLJzwOhaLBVf3wgyeOB+fEqWMjiVAQM2UwiImJsY670VERAomFRYikq99+eWXPP3001jMZlzc3Hl50if4V6lpdCz5x60TuDXPQkSkYFNhISL51uLFi+nVqxdmsxlnVzcGT5xPlXqNjI4lt/ArF0AhDy8AzbMQESngVFiISL5jsVh477336NevH8nJybi5uTFowsdUq9/U6GjyHw4ODlSsWQ9QYSEiUtCpsBCRfCU5OZnQ0FDeeOMNIOWs2r/88gs1gloYnEzuJKBWAyDlbOgREREGpxEREaOosBCRfCMyMpKHH36YGTNmAFCmTBk2bdrEfffdZ2wwuSudKE9ERCCLhcWsWbOoUKECbm5uNGnShB07dtxx3Xnz5tGyZUt8fHzw8fGhffv2d11fRAqmgwcP0rhxY9asWQNAzZo12bp1K7Vr1zY4mdxLxRqB1uuawC0iUnBlurBYvnw5oaGhjBo1ij179lCvXj06depEeHh4uutv2LCBnj17sn79erZt24a/vz8dO3bk3Llz2Q4vIvZh5cqVNG3alBMnTgDw+OOPs337dsqVK2dwMsmIQp5e1KhRA9A8CxGRgizThcW0adMICQkhODiYmjVrMmfOHAoVKsSCBQvSXf/zzz9nwIABBAYGUr16debPn4/ZbGbdunXZDi8iti0mJoYXX3yRJ554ghs3bgAwduxYvvrqKzw9dfI7W9KsWTMgZSiU2Ww2OI2IiBjBKTMrJyQksHv3boYPH25d5uDgQPv27TPc/X3z5k0SExMpWrToHdeJj48nPj7eevv69esAmM1mw3ZYZrMZi8WiHaadUHsab/fu3Tz99NMcO3YMAC8vLxYvXsyjjz4KcHvbWCx33pjF8u9FDNG4cWMWLFjA9evXOXjwILVq1crytvT+tC9qT/ui9rQvGWnPzLR1pgqLK1eukJycjJ+fX5rlfn5+HDlyJEPbeOONNyhdujTt27e/4zoTJkxgzJgxty2PjIwkKSkpM5FzjNlsJjo6GovFgoOD5rzbOrWncZKTk5k5cyaTJk2yvp+bNm3KRx99hL+//x2PKuScFHPnjVosOCXHpVw3mXI6smRAwD9DoQDWrVtHqVJZPzO63p/2Re1pX9Se9iUj7RkdHZ3h7WWqsMiuiRMnsmzZMjZs2ICbm9sd1xs+fDihoaHW29evX8ff3x8fHx+8vLzyIuptzGYzJpMJHx8fvZHsgNoz732y6RRXLpxl4YTXOb5/JwAOjk481n8InXqEUK9e5bs+PtEp6s53/tNTkehUWIWFQZo1q4GnpyfR0dEcOHDgrr3S96L3p31Re9oXtad9yUh7OjllvFzIVGFRrFgxHB0duXTpUprlly5domTJknd97JQpU5g4cSK//vordevWveu6rq6uuLq63rbcwcHB0D9ik8lkeAbJOWrPvPX7r9/z+fSRxMWkzKXwKxfAc29No3y1OgB8sjns7hu4V8FgMv17kTzn7OxM48aNWbduHb///nu231d6f9oXtad9UXval3u1Z2baOVN/ES4uLgQFBaWZeJ06ETt14l563nvvPd555x3WrFlDw4YNM/OUImLjrl27Rq9evfhkXKi1qGj9aC/e+vg7a1Eh9iF1P3Do0CGiou7SwyQiInYp00OhQkND6du3Lw0bNqRx48bMmDGDmJgYgoODAejTpw9lypRhwoQJAEyaNImRI0eydOlSKlSowMWLFwHw8PDAw8MjB1+KiOQ3GzZsoE+fPpw5cwYAT++i9H1jEnWbtzM4meSGpk2bAmCxWNixYwcdOnQwOJGIiOSlTBcW3bt35/Lly4wcOZKLFy8SGBjImjVrrBO6T58+nabLZPbs2SQkJPDUU0+l2c6oUaMYPXp09tKLSL6UmJjIqFGjmDhxIpZ/5j/UadaWvv83Ea+ixQxOJ7kltbCAlBPlqbAQESlYsjR5e9CgQQwaNCjd+zZs2JDmdlhYWFaeQkRs1KlTp+jZsye///47AO7u7kydOhXHmh0xaf6DXfP19aVKlSocP35cJ8oTESmANOtGRHLEvI0nCRn1PjXr1LUWFWUr12DYnG9xqtVJRUUBkTrPYvv27TrOvYhIAaPCQkSyLSYmhkUT32D+2CHWCdrtnuzL8I++plSFux9GVuxL6nCoyMhIjh8/bnAaERHJS3l6HgsRsT8nT57k8ccfZ//+/QAULuJDvzcmUa/F/QYnEyPcOs9i+/btVKtWzcA0IiKSl1RYiEiWrV27lp49exIZGQlA1cAmPPvWNHyK3/28NmJ/5m08CUByUmFc3NxJiIvlk6/XkFCxJQAhrQKMjCciInlAQ6FEJNMsFguTJk3iwQcftBYVHXuE8OrUT1VUFHCOTk4E1AwE4MT+XcaGERGRPKUeCxHJlMTEREJCQli8eDGQctSnBQsWEF26scHJJL+oXLcRR/Zs48LfJ4i+FoGnd1GjI4mISB5Qj4WIZNj169d56KGHrEVFxYoV2bZtGz169DA4meQnVeo2tF4/cUC9FiIiBYUKCxHJkMnfbKN2UDN++eUXACpUr8vA6cvYEeVpHV8vAhBQsz4Ojikd4hoOJSJScKiwEJF7On78OBMHPMWZE4cAqNusHa/N+BwvH51FW27n6l6I8tVqA3Bs3w6D04iISF5RYSEid3X06FHatGlDxKXzALR8pAcvjZuNq3shg5NJfpY6HOrMiUPE3YwxOI2IiOQFFRYickeHDx+mTZs2nD+fUlQ81GcgT782DkcnHfdB7q5y3UYAmJOTOXnwD4PTiIhIXlBhISLpOnjwIG3atOHixYsAPBo8hMeeDcVkMhmcTGxB5dpB1uvH9+80MImIiOQVfe0oIrc5evQobdu25fLlywCMHz+e4vfpyE+ScR5FfChdsSrnTx1TYSEiUkCox0JE0jh79iwdO3a0FhWTJk1ixIgRBqcSW1Tln+FQpw7tJSEhweA0IiKS21RYiAgA8zaeZPoPu2ncsh2nT58GUoY/+TR9SoeTlSxJncCdmBDPrl067KyIiL1TYSEiAMTH3uTDYSFcCDsOQNvHn+GhvoMMTiW2LLXHAmDTpk0GJhERkbygwkJESEpKYs7IgZw8lHL0nkbtHqb7yyM1UVuyxadEKXxLlgVUWIiIFAQqLESEV199lYM7NgJQq3FLgkdMxsFB/x4k+6rWS+m12LJlC2az2eA0IiKSm/TJQaSAmz17Nh9++CEA/pVr8sKYWTg5uxicSuxF6vksrl27xp9//mlwGhERyU0qLEQKsF9//ZXBgwcD4FW0GAMnzMWtUGGDU4k9uXWexW+//WZgEhERyW0qLEQKqGPHjtG1a1eSk5NxdXVlwPi5FC1R2uhYYmf8/CviVbQYABs2bDA2jIiI5CoVFiIFUHR0NI8++ijXrl0DYOHChQTUDDQ0k9gnk8lEtcCmQEphoXkWIiL2S4WFSAFjsVh49tlnOXr0KABvvfUWPXv2NDiV2LPqDZoBEBERwf79+w1OIyIiuUWFhUgB88EHH7BixQoAOnfuzJgxYwxOJPau2j+FBcD69esNTCIiIrlJhYVIAbJ161aGDh0KQLly5fjss890WFnJdcVLl8Pf3x+A//3vfwanERGR3KJPFCIFRHh4ON26dSMpKQlnZ2dWrFiBr6+v0bGkADCZTLRr1w6AjRs3kpSUZHAiERHJDSosRAoAs9nMM888w7lz5wCYMWMGjRs3NjiVFCRt27YF4Pr16+zZs8fgNCIikhtUWIgUANOnT+fnn38GoGfPnrz00ksGJ5KCJrWwAM2zEBGxVyosROzcnj17GD58OAABAQHMnTsXk8lkcCopaMqVK0elSpUAzbMQEbFXTkYHEJHcExMTQ8+ePUlMTMTB0Ynu/zeFZX9cBi4bHU0KoHbt2vHXX3+xefNmEhIScHFxMTqSiIjkIPVYiNixIUOGcOzYMQAe6z+EijXqGZxICrLU4VA3b95kx44dBqcREZGcpsJCxE59/fXXzJ8/H4A2bdrQqefzBieSgu7WeRYaDiUiYn9UWIjYoUuXLvHCCy8A4OPjw5IlS3BwdDQ4lRR0JUuWpGbNmoAmcIuI2CPNsRCxMxaLheeff56rV68CMGfOHMqWLQsnTxqcTAqyeRtT/v5KVAvi0KFDbN6ylVm/HMLF1Q2AkFYBRsYTEZEcoB4LETuzZMkSvv/+ewC6d+9Ot27dDE4k8q/qDZoBkJSYwF9/7jY4jYiI5CQVFiJ25OzZs7z88ssA+Pn5MWvWLIMTiaRVLbCJ9XDHh3dtMTiNiIjkJBUWInbCYrHw3HPPERUVBcDHH3+Mr6+vwalE0irs5U356nUBOLhjk8FpREQkJ6mwELET8+bNY+3atQD07duXRx991OBEIumr1eg+AM6cOMT1iCsGpxERkZyiwkLEDoSFhfHaa68BULZsWWbMmGFsIJG7qNmolfX6oV2bDUwiIiI5SYWFiI2zWCyEhIRw48YNAD755BO8vb2NDSVyFxVr1sOtkAcAh3ZqOJSIiL1QYSFi4xYvXsyvv/4KwHPPPUfHjh0NTiRyd05OztajQx3atRmz2WxwIhERyQk6j4WIDbt06RKhoaEAFPEtQe0uA63nCxDJz2o2asnezb9wPeIK504ehTaVjY4kIiLZpB4LERs2ePBgIiMjAej16hgKeXoZnEgkY2o1bmm9fnDHRgOTiIhITlFhIWKjvvvuO1asWAHAU089Rf2WGgIltqN46XIUL1MO0ARuERF7ocJCxAZFRUUxYMAAAHx8fJg5c6bBiUQyr9Y/R4c6sX8XMTExBqcREZHsUmEhYoP+7//+j/PnzwMwdepUSpYsaXAikcyr+c9wqKTEBNavX29wGhERyS4VFiI25rfffuPjjz8GoH379vTr18/YQCJZVL1+M5ycXQD48ccfDU4jIiLZpcJCxIbExsYSEhICQKFChZg7dy4mk8ngVCJZ41aoMFUDmwCwatUqLBaLwYlERCQ7VFiI2JCxY8dy/PhxAN555x0CAgIMTiSSPXWbtQXg7Nmz7N+/3+A0IiKSHSosRGzE3r17mTx5MgCNGjXilVdeMTiRSPbV+aewgJReCxERsV0qLERsQHJyMiEhISQnJ+Pk5MT8+fNxdHQ0OpZIthUvXY5SFaoAKixERGydzrwtYgM+/PBDdu3aBUCH7iH8fs2D33WGbbETdZu15ULYcX7//XfCw8MpVqyY0ZFERCQL1GMhks+dPn2aN998E4DiZcrxUN9BBicSyVl1m7UDwGKxsHr1aoPTiIhIVqmwEMnHLBYLAwcOtJ487OnXxuHi6mZwKpGcFVCrPj4+PoCGQ4mI2DIVFiL52Ndff239oNWs0+PUCGphcCKRnOfo5ETnzp0BWLt2LQkJCQYnEhGRrFBhIZJPXbt2jcGDBwPg6+vLUwNGGJxIJPc8/PDDAERHR7NhwwZjw4iISJaosBDJp4YNG8bFixcBmD59Op7eRQ1OJJJ7HnzwQZydnQFYuXKlwWlERCQrVFiI5EObN29m7ty5ALRv356nn37a4EQiucvb25v27dsD8O2335KcnGxwIhERyawsFRazZs2iQoUKuLm50aRJE3bs2HHHdQ8ePMiTTz5JhQoVMJlMzJgxI6tZRezavI0nmbfxJB/9ephuTwcD4OziSpv+I5i/6ZTB6URy3xNPPAFAeHj4XfcrIiKSP2W6sFi+fDmhoaGMGjWKPXv2UK9ePTp16kR4eHi669+8eZOAgAAmTpxIyZIlsx1YxN6t/eJjLvx9AoCH+71MiTLlDU4kkvvmbTzJ9RL1MDmk7JZmLlrOJ5tOWQtuERHJ/zJdWEybNo2QkBCCg4OpWbMmc+bMoVChQixYsCDd9Rs1asTkyZPp0aMHrq6u2Q4sYs8unj7JT0s+AqBMQDU6dH/W4EQiecfT25cqdRsBsHvTOiwWi8GJREQkMzJ15u2EhAR2797N8OHDrcscHBxo374927Zty7FQ8fHxxMfHW29fv34dALPZjNlszrHnyQyz2YzFYjHs+SVn5cf2NCcns2TKCJISEzCZTDwzdDxOjk6gD1f3ZrH8exGb1qBVJ47t/Z2Iyxf5++gBKlSvC5Cv3quSOfnx/61kndrTvmSkPTPT1pkqLK5cuUJycjJ+fn5plvv5+XHkyJHMbOquJkyYwJgxY25bHhkZSVJSUo49T2aYzWaio6OxWCw4OGjOu63Lj+35+4+fc3zfTgDaPdqdalWrQFKMwalshMWCU3JcynWTydgski2Nmt3Hsg9Sru/dsIoqlSsBEBERYWAqyY78+P9Wsk7taV8y0p7R0dEZ3l6mCou8Mnz4cEJDQ623r1+/jr+/Pz4+Pnh5eRmSyWw2YzKZ8PHx0RvJDuS39rx06RLLP54BgHcxPx59/g0SnQobG8qW/NNTkehUWIWFjfMsFUDFGvU4dXgfuzb/j8deGI7JZKJoUR1u2Vblt/+3kj1qT/uSkfZ0csp4uZCpwqJYsWI4Ojpy6dKlNMsvXbqUoxOzXV1d052P4eDgYOgfsclkMjyD5Jz81J6vvfYaN6OjAOg5ZDTuHsYU0DbNZPr3IjatfqtOnDq8j/CzYZw9eRT/yjXyxftUsi4//b+V7FN72pd7tWdm2jlTfxEuLi4EBQWxbt066zKz2cy6deto1qxZZjYlIv9YvXo1X3zxBQD1W3akfsuOBicSMVajtg9Zr//+y3cGJhERkczIdKkZGhrKvHnzWLx4MYcPH+all14iJiaG4OCU4+736dMnzeTuhIQE9u7dy969e0lISODcuXPs3buXEydO5NyrELFRN27c4KWXXgLArZAHPV4ZZXAiEeP5lixDldr1Adi57gdNEhURsRGZnmPRvXt3Ll++zMiRI7l48SKBgYGsWbPGOqH79OnTabpMzp8/T/369a23p0yZwpQpU2jdujUbNmzI/isQsWHDhw/n77//BuDx54fiU1znehEBaNruQY7/+QeRly9y4sAuaFPZ6EgiInIPJosNHCj8+vXrFClShKioKEMnb0dERFC0aFGNKbQD+aE9N2/eTMuWLQFo2bIlvccu0N9WVlksOCfFaPK2vbBYiLt6liHd2mNOTqLVoz357bulRqeSLMoP/28l56g97UtG2jMzn8P1FyFigNjYWJ59NuXkd25ubsyfP1//oEVu4VnEh1qNUgrv3RtWk5CQYHAiERG5F32SETHA6NGjOXbsGABjx46latWqBicSyX8at38EgJjr1/j5558NTiMiIveiwkIkj+3atYspU6YA0LBhQ1599VWDE4nkT/Wa34+LmzsAS5dqKJSISH6nwkIkDyUkJNC/f3/MZjPOzs4sWLAgUyeeESlI3AoVJrBFewC+/fZboqKiDE4kIiJ3o8JCJA9NmDCBAwcOAPDmm29Sp04dgxOJ5G/NOj8BpMxLWrZsmcFpRETkbvRVqUgeGb1oNePGjQegTEA1StzXnXkbTxqcSiR/qxHUAp8SpYgMv8D8+fN54YUXjI4kIiJ3oB4LkTyQmJjIoon/R3JSIiYHB/q+MREnZxejY4nkew6OjrR4sCuQMj9p3759BicSEZE7UWEhkgfGjRvH30f/BKBTjxAqVK9rcCIR29Higacw/XN+kk8++cTgNCIicicqLERy2Y4dOxg//t8hUI8Ev2JwIhHb4luyDB06dADgs88+Iy4uzuBEIiKSHhUWIrno5s2b9OnTh+TkZBydnOn/5lScXVyNjiVic1JPKBkZGcnKlSsNTiMiIulRYSGSi4YPH87Ro0cBeLT/EPwr1zA4kYhteuyxx/D19QVg3rx5BqcREZH0qLAQySXr1q3jgw8+AKB58+Z06hFicCIR2+Xq6krfvn0BWL9+PX/++afBiURE5L9UWIjkgmvXrtGvXz8AChcuzKeffoqDo6OxoURs3IABA6yTuGfOnGlwGhER+S8VFiI5zGKxMGDAAM6ePQvA1KlTqVSpksGpRGxfpUqVePjhhwFYsmQJERERBicSEZFb6QR5Ijkk9WR3W1d/xRdffAFA7SatMVVvrxPhiWRT6nuoctuu8MMPxMbGEjJiEp17pZwwL6RVgJHxREQE9ViI5KiLp0+ydMZoALyKFqPf8PesQzdEJPuqN2hO6YpVAFi/cgnJSUkGJxIRkVQqLERySGJCPPPGvEJCXCwA/UdMxcunmMGpROyLyWSi3RMpk7gjwy+wd/MvBicSEZFUKixEcsg3c9/jzIlDAHTu9QI1G91ncCIR+9SkYxcKeRYB4Jfln2CxWAxOJCIioDkWIhl2t3kSe7f8yrqvFgFQoUY9Hn321TxKJVLwuLq50/rRXqz+fDYnD/3B0T+2QWsdIEFExGjqsRDJpvBzf7Pw3aEAuBXy4Lm3p+Pk5GxwKhH71r5bMC5u7gD8+Oksg9OIiAiosBDJloT4OOaMHEjsjWgA+g6bRIky5Q1OJWL/PL19af1oLwCO/rGdLVu2GJxIRERUWIhkkcViYem0kZw9cRiADt2fJah1Z4NTiRQcHbo/i5OLCwDjx483OI2IiKiwEMmizT9+ydY1XwNQpV5jnnj+/wxOJFKweBfz474HuwGwevVqdu/ebXAiEZGCTYWFSBacOryPL94fDYBX0eKEjHofRycdC0Ekr3Xq+TwOjinvvVGjRhmcRkSkYFNhIZJJkeEXmDXiBZISEnBwdOSF0TPx9i1hdCyRAsm3ZBlaPPgUAD/++CMbNmwwNpCISAGmwkIkE+Jjb/LhiBe4HnEZgB6DR1KlXiODU4kUbI/0e5lChQoB8Prrr2M2mw1OJCJSMGnshkgGmc1mFk54nTPHDwLQpsvTtHn8aYNTiYh3MT/aPtWfHz/9kF27dvHC6A9o3P7RNOuEtAowKJ2ISMGhHguRDPph4fvs+W0NANWDmtNt8FsGJxKRVJ16huDp4wvAynlTSUyINziRiEjBo8JCJAMWLFjAj59+CECJshV4YcyHOgmeSD7iVsiDR/q9AsDVi2f539eLDU4kIlLwqLAQuYdVq1bx/PPPA1DIswiDJsyjsGcRg1OJyH/d93A3/MqlDHn6YdEHXL14zuBEIiIFiwoLkbvYtm0b3bp1Izk5GWcXVwZPnEfJchqrLZIfOTk50/vVMQAkxMXyxftjsFgsBqcSESk4VFiI3MHhw4d5+OGHiY2NxdHRkedHz6RS7SCjY4nIXVRv0JxmnR4HYP/Wdezd9LPBiURECg4VFiLpOHnyJB07diQiIgKAuXPnUq/F/QanEpGMeGrAcAp7eQPwxQdjibt5w9hAIiIFhAoLkf8ICwujbdu2nD17FoBx48bx7LPPGpxKRDLK09uXJ18cBsC1yxf58sPxBicSESkYVFiI/GPexpNM/HITDZvdx+nTpwHo3PtFit/Xg3kbTxqcTkQyo8WDT1GtflMANv/4JV9//bXBiURE7J8KC5F/XL10nqlDeluPJNOxRwiPhwzFZDIZnExEMstkMtF/xBQK/XMEt5CQEGsvpIiI5A4VFiLA8ePHmfJyT65cOANAh27P8uSLb6ioELFhPiVK8czr7wIQGRlJnz59SE5ONjiViIj9UmEhBd6ePXto0aIFVy+mfJt5/1P9eGrAcBUVInYgqHVn7nuoGwDr169n1KhRBicSEbFfKiykQFu/fj1t2rTh8uXLADzcdzDdBr2lokLEjnQb9BbVq1cHYPz48SxbtszgRCIi9kmFhRRY3333HQ8++CDR0dGYTCZ6vDKKR/sPUVEhYmfcChXm+++/x9vbG4Dg4GB27dplbCgRETukwkIKHLPZzJgxY3juuedISEjA2dmZpUuX0u6JPkZHE5FcUqVKFb788kscHByIi4vjsccesx79TUREcoYKCylQbty4QdeuXRk7diwAXl5erFq1ih49ehicTERy07yNJwlzrUTXgSMAOH/+PI2at2Lyyu06nLSISA5RYSEFxqlTp2jRogXffPMNAAEBAWzbto2OHTsanExE8kq7J/txf9dgAMLP/c300D5EX4swOJWIiH1QYSEFwvLlywkMDGT//v0AdOjQgbVr11ondIpIwWAymeg28E1aPpLSS3kh7DgzXutLeHi4wclERGyfCguxazExMTz33HP06NGD69evAxAaGsqqVausEzlFpGAxmUz0Dn2HJh26AHDmxCFatGjBX3/9ZWwwEREb52R0AJHcsnPnTvr06cORI0cAKOzlTd9hk6jeoj2Lt53BOSmGRKco0FGgRAocBwcH+g2bhIOjA9vWfMOJEydo3rw5P/30E0FBQUbHExGxSeqxELsTExPDa6+9RtOmTa1FRevWrRm54EcCW7Q3OJ2I5BeOTk70G/YeD/R+CYDw8HBatmzJp59+anAyERHbpMJC7Movv/xCnTp1mDZtGmazGWdnZ8aNG8e6devwKV7S6Hgiks+YTCYef34oM2fOxGQyERsbS9++fQkJCSE2NtboeCIiNkWFhdiF48eP88QTT9CxY0dOnToFQLNmzdi7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CAoKYt26ddbCwmw2s27dOgYNGmRsOMk0i8XC4MGDWblyJRs2bFBRYePuv/9+Dhw4kGZZcHAw1atX54033lBRYWNatGhx2+Gfjx07Rvny5Q1KJNlx8+bN2z7rODo6Yjabs7VdFRZZUK5cuTS3PTw8AKhUqRJly5Y1IpJkw7lz52jTpg3ly5dnypQpXL582XpfyZIlDUwmGREaGkrfvn1p2LAhjRs3ZsaMGcTExBAcHGx0NMmkgQMHsnTpUr777js8PT25ePEiAEWKFMHd3d3gdJJZnp6et82PKVy4ML6+vpo3Y4NeffVVmjdvzrvvvku3bt3YsWMHH3/8MR9//LHR0SQLHnnkEcaPH0+5cuWsRzadNm0a/fv3z9Z2VVhIgffLL79w4sQJTpw4cVthqJGC+V/37t25fPkyI0eO5OLFiwQGBrJmzZrbJnRL/jd79mwA2rRpk2b5woUL6devX94HEhGrRo0asXLlSoYPH87YsWOpWLEiM2bMyPZkXzHGzJkzefvttxkwYADh4eGULl2aF154gZEjR2Zru5pjISIiIiIi2aaB5CIiIiIikm0qLEREREREJNtUWIiIiIiISLapsBARERERkWxTYSEiIiIiItmmwkJERERERLJNhYWIiIiIiGSbCgsREREREck2FRYiIiIiIpJtKixERERERCTbVFiIiIiIiEi2qbAQEREREZFs+3/ZdEurpBQatAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "jetTransient": { + "display_id": null + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x_mix = np.linspace(-4.0, 6.0, 600)\n", + "mix_pdf = _method_values(mixture_d, CharacteristicName.PDF, x_mix)\n", + "mix_cdf = _method_values(mixture_d, CharacteristicName.CDF, x_mix)\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(13, 4))\n", + "plot_curve(axes[0], x_mix, mix_pdf, label=\"Mixture PDF\", title=\"Finite Mixture PDF\")\n", + "plot_curve(axes[1], x_mix, mix_cdf, label=\"Mixture CDF\", title=\"Finite Mixture CDF\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "plot_hist_with_optional_pdf(\n", + " ax,\n", + " mixture_d.sample(30_000),\n", + " label=\"Mixture samples\",\n", + " pdf_distribution=mixture_d,\n", + " x_grid=x_mix,\n", + ")\n", + "ax.set_title(\"Finite Mixture: samples vs PDF\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "274ef21285b5f440", + "metadata": {}, + "source": [ + "## 4) Approximation of Transformed Characteristics\n", + "\n", + "`DerivedDistribution.approximate(...)` materializes selected characteristics with interpolation/spline methods.\n", + "The resulting distribution keeps transformation metadata and exposes approximated methods." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7f9cc047e0e8ca74", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:18.496258939Z", + "start_time": "2026-03-28T19:39:18.448132115Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Approximated transformation name: approximation\n", + "Loop analytical flag (PDF): False\n" + ] + } + ], + "source": [ + "approximated_affine = affine_pos.approximate(\n", + " methods={\n", + " CharacteristicName.PDF: PDFLinearInterpolationApproximation(\n", + " n_grid=513,\n", + " lower_limit=-4.0,\n", + " upper_limit=4.0,\n", + " ),\n", + " CharacteristicName.CDF: CDFMonotoneSplineApproximation(\n", + " n_grid=513,\n", + " lower_limit_prob=1e-6,\n", + " upper_limit_prob=1e-6,\n", + " ),\n", + " CharacteristicName.PPF: PPFMonotoneSplineApproximation(\n", + " n_grid=513,\n", + " lower_limit=0.0,\n", + " upper_limit=1.0,\n", + " ),\n", + " }\n", + ")\n", + "\n", + "print(\"Approximated transformation name:\", approximated_affine.transformation_name)\n", + "print(\n", + " \"Loop analytical flag (PDF):\",\n", + " approximated_affine.loop_is_analytical(\n", + " CharacteristicName.PDF,\n", + " \"PySATL_default_analytical_computation\",\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "57a8d037d67cf2b1", + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-28T19:39:18.840017455Z", + "start_time": "2026-03-28T19:39:18.498795524Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Max |PDF error|: 4.07083947989717e-06\n", + "Max |CDF error|: 6.537550822915961e-08\n", + "Max |PPF error|: inf\n" + ] + }, + { + "data": { + "image/png": "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rkU9g2vylL5BPYNrU88gFemv3td69OhxwfvCZ599ZjZr1sz09fU1W7VqZX733XcVnNh1lWZbNmzY8KLvWxMnTqz44C6qtP82/9ewYcPMQYMGlX9IN1HabblixQozPj7etNvtZuPGjc3nn3/eLCwsrODUrqs027OgoMB85plnzCZNmph+fn5meHi4ed9995kZGRkVH9zF/F0/OmzYMLNHjx4XPCc6Otr09fU1GzduXG69nmGaOo5GRERERERERERERERcn+ud9FdEREREREREREREROQiNNQQERERERERERERERG3oKGGiIiIiIiIiIiIiIi4BQ01RERERERERERERETELWioISIiIiIiIiIiIiIibkFDDRERERERERERERERcQsaaoiIiIiIiIiIiIiIiFvQUENERERERERERERERNyChhoiIiIiIiIiIiIiIuIWNNQQERERERERERERERG3oKGGiIiIiIiIiIiIiIi4BQ01RMRlpaWlERYWxgsvvFC8bMWKFfj6+rJ06VILk4mIiIgrUc8gIiIipZGTk8Ptt9+Ov78/derU4ZVXXqFnz548+OCDVkcTkRLQUENEXFZwcDAffPABzzzzDOvWrSM7O5vbbruNMWPGcNlll1kdT0RERFyEegYREREpjUceeYSff/6ZBQsWsHjxYhITE9mwYYPVsUSkhAzTNE2rQ4iI/JXRo0fz448/Ehsby9atW1m7di12u93qWCIiIuJi1DOIiIjI3zlz5gy1atXi3//+N0OGDAEgPT2d+vXrc/fddzNjxgxrA4rI39JQQ0Rc3tmzZ2ndujWHDx9m/fr1tGnTxupIIiIi4oLUM4iIiMjf2bx5M9HR0Rw8eJAGDRoUL2/fvj09evTQUEPEDej0UyLi8pKSkjh27BgOh4Pk5GSr44iIiIiLUs8gIiIiIuL5NNQQEZeWn5/Prbfeyo033sjkyZO56667OHHihNWxRERExMWoZxAREZGSaNKkCT4+Pqxevbp4WUZGBnv27LEwlYiUhrfVAURE/sqTTz5JZmYmr7/+Ov7+/ixcuJA77riDb7/91upoIiIi4kLUM4iIiEhJ+Pv7c+edd/LII49Qq1YtQkJCePLJJ7HZtO+3iLvQb6uIuKzExERmzJjBxx9/TEBAADabjY8//phffvmFd955x+p4IiIi4iLUM4iIiEhpvPzyy3Tr1o2BAwfSp08funbtSkxMjNWxRKSEdKFwERERERERERERqdR69uxJdHS0LhQu4gZ0pIaIiIiIiIiIiIiIiLgFDTVERERERERERERERMQt6PRTIiIiIiIiIiIiIiLiFnSkhoiIiIiIiIiIiIiIuAUNNURERERERERERERExC1oqCEiIiIiIiIiIiIiIm5BQw0REREREREREREREXELGmqIiIiIiIiIiIiIiIhb0FBDRERERERERERERETcgoYaIiIiIiIiIiIiIiLiFjTUEBERERERERERERERt6ChhoiIiIiIiIiIiIiIuIX/Bx1xjB6X3EmaAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "jetTransient": { + "display_id": null + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x_cmp = np.linspace(-4.0, 4.0, 600)\n", + "q_cmp = np.linspace(0.0, 1.0, 600)\n", + "\n", + "exact_pdf = _method_values(affine_pos, CharacteristicName.PDF, x_cmp)\n", + "approx_pdf = _method_values(approximated_affine, CharacteristicName.PDF, x_cmp)\n", + "exact_cdf = _method_values(affine_pos, CharacteristicName.CDF, x_cmp)\n", + "approx_cdf = _method_values(approximated_affine, CharacteristicName.CDF, x_cmp)\n", + "exact_ppf = _method_values(affine_pos, CharacteristicName.PPF, q_cmp)\n", + "approx_ppf = _method_values(approximated_affine, CharacteristicName.PPF, q_cmp)\n", + "\n", + "print(\"Max |PDF error|:\", float(np.max(np.abs(exact_pdf - approx_pdf))))\n", + "print(\"Max |CDF error|:\", float(np.max(np.abs(exact_cdf - approx_cdf))))\n", + "print(\"Max |PPF error|:\", float(np.max(np.abs(exact_ppf - approx_ppf))))\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(16, 4))\n", + "plot_curve(axes[0], x_cmp, exact_pdf, label=\"Exact\", title=\"PDF: exact vs approx\")\n", + "axes[0].plot(x_cmp, approx_pdf, linestyle=\"--\", label=\"Approx\")\n", + "axes[0].legend()\n", + "\n", + "plot_curve(axes[1], x_cmp, exact_cdf, label=\"Exact\", title=\"CDF: exact vs approx\")\n", + "axes[1].plot(x_cmp, approx_cdf, linestyle=\"--\", label=\"Approx\")\n", + "axes[1].legend()\n", + "\n", + "plot_curve(axes[2], q_cmp, exact_ppf, label=\"Exact\", title=\"PPF: exact vs approx\", xlabel=\"q\")\n", + "axes[2].plot(q_cmp, approx_ppf, linestyle=\"--\", label=\"Approx\")\n", + "axes[2].legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "19a33b9a0f6d7aab", + "metadata": {}, + "source": [ + "## 5) Quick Summary\n", + "\n", + "- `affine`, `binary`, and `finite_mixture` create `DerivedDistribution` objects.\n", + "- Operator overloads route to the same transformation primitives.\n", + "- Sampling is available through `distribution.sample(...)` for transformed objects.\n", + "- Approximation utilities can materialize selected characteristics while preserving a consistent interface." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 4d798c140da91fa3443d598b550785d058535951 Mon Sep 17 00:00:00 2001 From: LeonidElkin Date: Sat, 28 Mar 2026 22:46:52 +0300 Subject: [PATCH 4/4] chore(README): update README to match the addition of the transformations module --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 130a057..8354b96 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![CI][status-shield]][status-url] [![MIT License][license-shield]][license-url] -**PySATL Core** is the computational core of the PySATL project, providing abstractions and infrastructure for probability distributions, parametric families, characteristic-based computations, and sampling. +**PySATL Core** is the computational core of the PySATL project, providing abstractions and infrastructure for probability distributions, parametric families, characteristic-based computations, transformations, and sampling. The library is designed as a **foundational kernel** rather than a ready-to-use end-user package. Its primary goals are explicit probabilistic structure, extensibility, and suitability as a basis for further stochastic and statistical tooling. @@ -26,6 +26,9 @@ The library is designed as a **foundational kernel** rather than a ready-to-use - A global **family registry** for configuring, querying, and extending available distribution families. - **Characteristic computation graph** (`CharacteristicRegistry`) that allows computing arbitrary characteristics by specifying only a minimal analytical subset. +- **Transformations module** for derived distributions: + affine transformations (`aX + b`), binary operations (`X ± Y`, `X * Y`, `X / Y`), + finite weighted mixtures, and characteristic-level approximations. - Distribution objects exposing common probabilistic operations (sampling, analytical and fitted computations). - Clear separation between *distribution definitions*, *parametrizations*, @@ -110,6 +113,16 @@ This example uses a **predefined family** and **predefined parametrizations**. PySATL Core also supports defining custom families, parametrizations, and characteristic graphs. +For transformation workflows, see `examples/transformations_overview.ipynb`. + +--- + +## 📓 Notebooks + +- `examples/overview.ipynb` — base walkthrough for families, parametrizations, and characteristic queries. +- `examples/transformations_overview.ipynb` — affine, binary, finite-mixture, and approximation workflows. +- `examples/example_sampling_methods.ipynb` — sampling backends and UNURAN-oriented scenarios. + --- ## 📖 Documentation @@ -143,7 +156,7 @@ poetry run pre-commit run --all-files ## 🗺 Roadmap -- **Transformations module** for mixtures and distribution transformations. +- Extension of the transformations module (functional transforms, performance improvements). - Extension of characteristic graphs. - Stabilization of APIs and **publishing PySATL Core as an installable package**.