|
15 | 15 | import inspect |
16 | 16 | from collections.abc import Mapping |
17 | 17 | from functools import partial |
18 | | -from typing import TYPE_CHECKING, Any, cast, dataclass_transform |
| 18 | +from typing import TYPE_CHECKING, Any, cast, dataclass_transform, overload |
19 | 19 |
|
20 | 20 | import numpy as np |
21 | 21 |
|
@@ -451,4 +451,224 @@ def score(self, parameters: Parametrization, x: NumericArray) -> NumericArray: |
451 | 451 | base_grad = self._base_score(base_params, x_arr) |
452 | 452 | return parameters.gradient_transform(base_grad) |
453 | 453 |
|
| 454 | + def view( |
| 455 | + self, |
| 456 | + *, |
| 457 | + parametrization_name: str | None = None, |
| 458 | + **fixed_params: Any, |
| 459 | + ) -> PartialParametricFamily: |
| 460 | + """ |
| 461 | + Create a view of this family with partially fixed parameters. |
| 462 | +
|
| 463 | + Parameters |
| 464 | + ---------- |
| 465 | + parametrization_name : str, optional |
| 466 | + Name of the parametrization in which the fixed parameters are given. |
| 467 | + If not provided, the base parametrization of the family is used. |
| 468 | + **fixed_params : Any |
| 469 | + Parameter names and values to fix. |
| 470 | +
|
| 471 | + Returns |
| 472 | + ------- |
| 473 | + PartialParametricFamily |
| 474 | + A view that behaves like the original family but with the specified |
| 475 | + parameters fixed. |
| 476 | +
|
| 477 | + Examples |
| 478 | + -------- |
| 479 | + >>> uniform = ParametricFamilyRegister.get("uniform") |
| 480 | + >>> uniform_lower0 = uniform.view(lower_bound=0) |
| 481 | + >>> dist = uniform_lower0.distribution(upper_bound=1) # Uniform(0,1) |
| 482 | + """ |
| 483 | + return PartialParametricFamily( |
| 484 | + base_family=self, |
| 485 | + fixed_params=fixed_params, |
| 486 | + parametrization_name=parametrization_name, |
| 487 | + ) |
| 488 | + |
| 489 | + __call__ = distribution |
| 490 | + |
| 491 | + |
| 492 | +class PartialParametricFamily(ParametricFamily): |
| 493 | + """ |
| 494 | + View on a parametric family with partially fixed parameters. |
| 495 | +
|
| 496 | + This class represents a parametric family where some parameters have been |
| 497 | + fixed to specific values. It inherits all behaviour from `ParametricFamily` |
| 498 | + but restricts the available parametrization to the one in which parameters |
| 499 | + are fixed. All analytical characteristics are preserved via delegation to |
| 500 | + the base parametrization of the original family. |
| 501 | +
|
| 502 | + Parameters |
| 503 | + ---------- |
| 504 | + base_family : ParametricFamily |
| 505 | + The original parametric family. |
| 506 | + fixed_params : dict[str, Any] |
| 507 | + Dictionary of fixed parameter names and their values. |
| 508 | + parametrization_name : str, optional |
| 509 | + Name of the parametrization in which the fixed parameters are specified. |
| 510 | + If not provided, the base parametrization of the family is used. |
| 511 | +
|
| 512 | + Raises |
| 513 | + ------ |
| 514 | + ValueError |
| 515 | + If all parameters of the chosen parametrization are fixed (use `.distribution()` directly), |
| 516 | + or if any fixed parameter name is unknown for that parametrization, |
| 517 | + or if the parametrization name is not registered in the family. |
| 518 | + """ |
| 519 | + |
| 520 | + def __init__( |
| 521 | + self, |
| 522 | + base_family: ParametricFamily, |
| 523 | + fixed_params: dict[str, Any], |
| 524 | + parametrization_name: str | None = None, |
| 525 | + ) -> None: |
| 526 | + self._fixed_in_param = parametrization_name or base_family.base_parametrization_name |
| 527 | + param_class = base_family.get_parametrization(self._fixed_in_param) |
| 528 | + |
| 529 | + # Check completeness: if all parameters are fixed, raise an error |
| 530 | + required_fields = set(getattr(param_class, "__dataclass_fields__", {}).keys()) |
| 531 | + if required_fields.issubset(fixed_params): |
| 532 | + raise ValueError( |
| 533 | + f"All parameters of parametrization '{self._fixed_in_param}' are already fixed. " |
| 534 | + "Use `.distribution()` directly to create a distribution instance." |
| 535 | + ) |
| 536 | + |
| 537 | + # Keep only characteristics that have an implementation for fixed_in |
| 538 | + filtered_chars = {} |
| 539 | + for char_name, char_map in base_family.distr_characteristics.items(): |
| 540 | + if self._fixed_in_param in char_map: |
| 541 | + filtered_chars[char_name] = {self._fixed_in_param: char_map[self._fixed_in_param]} |
| 542 | + |
| 543 | + super().__init__( |
| 544 | + name=base_family._name, |
| 545 | + distr_type=base_family._distr_type, |
| 546 | + distr_parametrizations=[self._fixed_in_param], |
| 547 | + distr_characteristics=filtered_chars, |
| 548 | + support_by_parametrization=base_family._support_resolver, |
| 549 | + base_score=base_family._base_score, |
| 550 | + ) |
| 551 | + |
| 552 | + # Register the parametrization (needed for parent methods) |
| 553 | + self.register_parametrization(self._fixed_in_param, param_class) |
| 554 | + |
| 555 | + self._base_family = base_family |
| 556 | + self._param_class = param_class |
| 557 | + self._fixed_params = fixed_params.copy() |
| 558 | + |
| 559 | + # Validate that fixed parameters exist |
| 560 | + unknown = set(self._fixed_params) - required_fields |
| 561 | + if unknown: |
| 562 | + raise ValueError( |
| 563 | + f"Unknown parameters for parametrization '{self._fixed_in_param}': {unknown}" |
| 564 | + ) |
| 565 | + |
| 566 | + @property |
| 567 | + def base(self) -> type[Parametrization]: |
| 568 | + """Return the only parametrization class available in this view.""" |
| 569 | + return self._param_class |
| 570 | + |
| 571 | + @property |
| 572 | + def parametrizations(self) -> dict[str, type[Parametrization]]: |
| 573 | + """Return a dictionary containing only the fixed parametrization.""" |
| 574 | + return {self._fixed_in_param: self._param_class} |
| 575 | + |
| 576 | + @overload |
| 577 | + def get_parametrization(self) -> type[Parametrization]: ... |
| 578 | + |
| 579 | + @overload |
| 580 | + def get_parametrization(self, name: ParametrizationName) -> type[Parametrization]: ... |
| 581 | + |
| 582 | + def get_parametrization(self, name: ParametrizationName | None = None) -> type[Parametrization]: |
| 583 | + if name is None: |
| 584 | + return self._param_class |
| 585 | + if name != self._fixed_in_param: |
| 586 | + raise KeyError( |
| 587 | + f"Parametrization '{name}' is not available in this view. " |
| 588 | + f"Only '{self._fixed_in_param}' is available." |
| 589 | + ) |
| 590 | + return self._param_class |
| 591 | + |
| 592 | + def _build_analytical_computations( |
| 593 | + self, parameters: Parametrization |
| 594 | + ) -> dict[GenericCharacteristicName, dict[LabelName, AnalyticalComputation[Any, Any]]]: |
| 595 | + """ |
| 596 | + Build analytical computations using the parent family's plan. |
| 597 | + Characteristics not defined in the fixed parametrization are computed |
| 598 | + via the base parametrization of the original family. |
| 599 | + """ |
| 600 | + # Use the parent family's plan (full, not filtered) |
| 601 | + base_plan = self._base_family._analytical_plan.get(parameters.name, {}) |
| 602 | + result: dict[ |
| 603 | + GenericCharacteristicName, |
| 604 | + dict[LabelName, AnalyticalComputation[Any, Any]], |
| 605 | + ] = {} |
| 606 | + base_params: Parametrization | None = None |
| 607 | + |
| 608 | + for characteristic, provider_name in base_plan.items(): |
| 609 | + if provider_name == self._fixed_in_param: |
| 610 | + params_obj = parameters |
| 611 | + else: |
| 612 | + if base_params is None: |
| 613 | + base_params = self._base_family.to_base(parameters) |
| 614 | + params_obj = base_params |
| 615 | + |
| 616 | + labeled_providers = self._base_family.distr_characteristics[characteristic][ |
| 617 | + provider_name |
| 618 | + ] |
| 619 | + result[characteristic] = { |
| 620 | + label_name: AnalyticalComputation( |
| 621 | + target=characteristic, |
| 622 | + func=self._bind_parametrization(func_factory, params_obj), |
| 623 | + ) |
| 624 | + for label_name, func_factory in labeled_providers.items() |
| 625 | + } |
| 626 | + |
| 627 | + return result |
| 628 | + |
| 629 | + def is_complete(self) -> bool: |
| 630 | + """Check whether all parameters of the fixed parametrization are already fixed.""" |
| 631 | + required_fields = set(getattr(self._param_class, "__dataclass_fields__", {}).keys()) |
| 632 | + return required_fields.issubset(self._fixed_params) |
| 633 | + |
| 634 | + def distribution( |
| 635 | + self, |
| 636 | + parametrization_name: str | None = None, |
| 637 | + sampling_strategy: SamplingStrategy | None = None, |
| 638 | + computation_strategy: ComputationStrategy | None = None, |
| 639 | + **kwargs: Any, |
| 640 | + ) -> ParametricFamilyDistribution: |
| 641 | + target = parametrization_name or self._fixed_in_param |
| 642 | + if target != self._fixed_in_param: |
| 643 | + raise ValueError( |
| 644 | + f"Only parametrization '{self._fixed_in_param}' is available in this view. " |
| 645 | + "Please omit 'parametrization_name' or use the fixed one." |
| 646 | + ) |
| 647 | + for key, fixed_val in self._fixed_params.items(): |
| 648 | + if key in kwargs and kwargs[key] != fixed_val: |
| 649 | + raise ValueError( |
| 650 | + f"Parameter '{key}' is fixed to {fixed_val}, but got {kwargs[key]}" |
| 651 | + ) |
| 652 | + all_params = {**self._fixed_params, **kwargs} |
| 653 | + return super().distribution( |
| 654 | + parametrization_name=target, |
| 655 | + sampling_strategy=sampling_strategy, |
| 656 | + computation_strategy=computation_strategy, |
| 657 | + **all_params, |
| 658 | + ) |
| 659 | + |
| 660 | + def view( |
| 661 | + self, |
| 662 | + *, |
| 663 | + parametrization_name: str | None = None, |
| 664 | + **additional_params: Any, |
| 665 | + ) -> PartialParametricFamily: |
| 666 | + if parametrization_name is not None and parametrization_name != self._fixed_in_param: |
| 667 | + raise ValueError( |
| 668 | + f"Cannot change parametrization. Current fixed parametrization is " |
| 669 | + f"'{self._fixed_in_param}'. Use the same or omit the argument." |
| 670 | + ) |
| 671 | + new_fixed = {**self._fixed_params, **additional_params} |
| 672 | + return PartialParametricFamily(self._base_family, new_fixed, self._fixed_in_param) |
| 673 | + |
454 | 674 | __call__ = distribution |
0 commit comments