Skip to content

Commit bfe1cbd

Browse files
committed
feat(families): added ability for partial fixing parameters in parametric families
1 parent 937137c commit bfe1cbd

2 files changed

Lines changed: 422 additions & 1 deletion

File tree

src/pysatl_core/families/parametric_family.py

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import inspect
1616
from collections.abc import Mapping
1717
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
1919

2020
import numpy as np
2121

@@ -451,4 +451,224 @@ def score(self, parameters: Parametrization, x: NumericArray) -> NumericArray:
451451
base_grad = self._base_score(base_params, x_arr)
452452
return parameters.gradient_transform(base_grad)
453453

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+
454674
__call__ = distribution

0 commit comments

Comments
 (0)