Codi font per a qilisdk.core.parameterizable

# Copyright 2025 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

from abc import ABC
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable

    from qilisdk.core.comparison import Comparison
    from qilisdk.core.variables import BaseVariable, Parameter

    from .types import RealNumber


[documents] class Parameterizable(ABC): """Mixin for objects that expose tunable parameters and constraints. Subclasses can compose parameter interfaces by yielding child :class:`Parameterizable` instances from :meth:`_iter_parameter_children`. """ def __init__(self) -> None: super().__init__() self._parameters: dict[str, Parameter] = {} self._parameter_constraints: list[Comparison] = [] self._prefix = "" # Private Methods def _iter_parameter_children(self) -> Iterable[Parameterizable]: # ruff: ignore[no-self-use] """Yield child objects that compose this object's parameter interface. Override this method in subclasses that expose nested :class:`Parameterizable` objects. Returns: Iterable[Parameterizable]: Child objects that contribute parameters to this instance. """ return () def _iter_parameter_items(self) -> Iterable[tuple[str, Parameter]]: """Yield ``(label, parameter)`` items from this object and its children.""" local_params = self._parameters or {} yield from local_params.items() for child in self._iter_parameter_children(): yield from child._iter_parameter_items() # ruff: ignore[private-member-access] def _add_parameter(self, label: str, parameter: Parameter) -> None: """Add a parameter under the current prefix. Args: label (str): Parameter label before prefixing. parameter (Parameter): Parameter instance to register. """ self._parameters[self._prefix + label] = parameter def _add_parameter_from(self, parameter_label: str, other: Parameterizable, new_label: str | None = None) -> None: """Add a parameter from another :class:`Parameterizable`. Args: parameter_label (str): Label of the source parameter in ``other``. other (Parameterizable): Object to pull the parameter from. new_label (str | None, optional): Optional label to use in this object. """ if new_label: self._parameters[self._prefix + new_label] = other._parameters[parameter_label] else: self._parameters[self._prefix + parameter_label] = other._parameters[parameter_label] @staticmethod def _query_parameter_original_name(parameterizable: Parameterizable, label: str) -> str: """Return the underlying parameter label stored on the :class:`Parameter`. Args: parameterizable (Parameterizable): Object containing the parameter. label (str): Parameter label used in ``parameterizable``. Returns: str: Original parameter label. """ return parameterizable._parameters[label].label def _update_parameters(self, parameters: dict[str, Parameter]) -> None: """Update local parameters with the provided mapping. Args: parameters (dict[str, Parameter]): Parameters to merge into this object. """ self._parameters.update(parameters) def _link_parameters(self, other: Parameterizable) -> None: """Link all parameters from another object into this one. Parameters are shared by reference, so updates in this object affect the same underlying :class:`Parameter` instances. Args: other (Parameterizable): Object to copy parameter references from. """ for label, p in other._parameters.items(): self._add_parameter(label, p) def _filtered_parameter_map( self, where: Callable[[Parameter], bool] | None = None, ) -> dict[str, Parameter]: """Return parameters, optionally filtered by a predicate. Args: where (Callable[[Parameter], bool] | None, optional): Predicate applied to each parameter. Returns: dict[str, Parameter]: Filtered parameter mapping. """ if where is None: return dict(self._iter_parameter_items()) return {label: param for label, param in self._iter_parameter_items() if where(param)} def _pop(self, label: str) -> Parameter: """Remove parameter from the Parameterizable interface. Args: label (str): the label of the parameter as specified in the Parameterizable object. Raises: ValueError: If the label is not a valid label in the current Parameterizable object or any of its children. Returns: Parameter: the removed parameter. """ if label in self._parameters: return self._parameters.pop(label) for child in self._iter_parameter_children(): if label in child.get_parameter_names(): return child._pop(label) # ruff: ignore[private-member-access] raise ValueError(f"Parameter {label} is not defined in the current object or any of its children.") # Public Methods @property
[documents] def nparameters(self) -> int: """Number of tunable parameters defined by the object.""" return len(dict(self._iter_parameter_items()))
[documents] def set_prefix( self, prefix: str, where: Callable[[Parameter], bool] | None = None, ) -> None: """Set a prefix on parameter labels. Args: prefix (str): Prefix to prepend to selected parameter labels. where (Callable[[Parameter], bool] | None): Optional predicate selecting local parameters. Notes: The ``where`` predicate is applied to local parameters only. Child parameterizable objects always receive the same prefix operation recursively. """ logger.trace("[Parameterizable] Setting parameter prefix to {}", prefix) old_keys: list[str] = [label for label, param in self._parameters.items() if where is None or where(param)] for name in old_keys: if not name.startswith(prefix): _name = name.removeprefix(self._prefix) if self._prefix and name.startswith(self._prefix) else name self._parameters[prefix + _name] = self._parameters.pop(name) for child in self._iter_parameter_children(): child.set_prefix(prefix) self._prefix = prefix
[documents] def get_prefix(self) -> str: """Return the currently configured parameter prefix for this object.""" return self._prefix
[documents] def add_parameter_constraint(self, constraint: Comparison) -> None: """Add a constraint on a single or a set of parameters Args: constraint (Comparison): The comparison term to specify the constraint. Only Parameter objects are allowed in the constraint. Raises: ValueError: If Generic Variables are present in the constraint. """ if not (constraint.lhs.is_parameterized() and constraint.rhs.is_parameterized()): raise ValueError( "The constraint should only contain parameters and having generic variables is not allowed." ) logger.debug("[Parameterizable] Adding parameter constraint {}", constraint) self._parameter_constraints.append(constraint)
[documents] def get_parameter_values( self, where: Callable[[Parameter], bool] | None = None, ) -> list[float]: """Return the current numerical values of the parameters. Args: where (Callable[[Parameter], bool] | None): Optional predicate over ``Parameter`` objects. """ return list(self.get_parameters(where=where).values())
[documents] def get_parameter_names( self, where: Callable[[Parameter], bool] | None = None, ) -> list[str]: """Return the ordered list of parameter labels. Args: where (Callable[[Parameter], bool] | None): Optional predicate over ``Parameter`` objects. """ return list(self.get_parameters(where=where).keys())
[documents] def get_parameters( self, where: Callable[[Parameter], bool] | None = None, ) -> dict[str, RealNumber]: """Return a mapping from parameter labels to their current numerical values. Args: where (Callable[[Parameter], bool] | None): Optional predicate over ``Parameter`` objects. """ return {label: param.value for label, param in self._filtered_parameter_map(where=where).items()}
[documents] def set_parameter_values( self, values: list[float], where: Callable[[Parameter], bool] | None = None, ) -> None: """ Update all parameter values at once. Args: values (list[float]): New parameter values ordered consistently with ``get_parameter_names()``. where (Callable[[Parameter], bool] | None): Optional predicate over ``Parameter`` objects. Raises: ValueError: If ``values`` does not match the number of parameters selected by ``where``. """ logger.opt(lazy=True).trace("[Parameterizable] Setting parameter values {}", lambda: values) param_names = self.get_parameter_names(where=where) if len(values) != len(param_names): raise ValueError(f"Provided {len(values)} but this object has {len(param_names)} parameters.") value_dict = {param_names[i]: values[i] for i in range(len(values))} self.set_parameters(value_dict)
[documents] def set_parameters(self, parameters: dict[str, float]) -> None: """ Update a subset of parameters by label. Args: parameters (dict[str, float]): Mapping from parameter labels to updated numeric values. Raises: ValueError: If an unknown parameter label is provided or constraints are violated. """ logger.opt(lazy=True).trace("[Parameterizable] Setting parameters by label {}", lambda: parameters) available_parameters = self._filtered_parameter_map() if not self.check_constraints(parameters): raise ValueError( f"New assignation of the parameters breaks the parameter constraints: \n{self.get_constraints()}" ) for label, param in parameters.items(): if label not in available_parameters: raise ValueError(f"Parameter {label} is not defined for this object.") available_parameters[label].set_value(param)
[documents] def get_parameter_bounds( self, where: Callable[[Parameter], bool] | None = None, ) -> dict[str, tuple[float, float]]: """Return the ``(lower, upper)`` bounds associated with each parameter. Args: where (Callable[[Parameter], bool] | None): Optional predicate over ``Parameter`` objects. """ return {label: param.bounds for label, param in self._filtered_parameter_map(where=where).items()}
[documents] def set_parameter_bounds(self, ranges: dict[str, tuple[float, float]]) -> None: """ Update the allowable ranges for the specified parameters. Args: ranges (dict[str, tuple[float, float]]): Mapping from parameter label to ``(lower, upper)`` bounds. Raises: ValueError: If an unknown parameter label is provided. """ logger.opt(lazy=True).trace("[Parameterizable] Setting bounds for parameters {}", lambda: ranges) available_parameters = self._filtered_parameter_map() for label, bound in ranges.items(): if label not in available_parameters: raise ValueError( f"The provided parameter label {label} is not defined in the list of parameters in this object." ) available_parameters[label].set_bounds(bound[0], bound[1])
[documents] def get_constraints(self) -> list[Comparison]: """Get all constraints on the parameters. Returns: list[Comparison]: Comparison terms defined locally and by child parameterizable objects. """ constraints = list((self._parameter_constraints or [])) for child in self._iter_parameter_children(): constraints.extend(child.get_constraints()) return constraints
[documents] def check_constraints(self, parameters: dict[str, float]) -> bool: """Validate that proposed parameter updates satisfy all constraints. Args: parameters (dict[str, float]): Candidate updates keyed by parameter label. Returns: bool: True if every constraint evaluates to True for the provided values. Raises: ValueError: If an unknown parameter label is provided. """ available_parameters = self._filtered_parameter_map() evaluate_dict: dict[BaseVariable, float] = {} for label, value in parameters.items(): if label not in available_parameters: raise ValueError(f"Parameter {label} is not defined for this object.") evaluate_dict[available_parameters[label]] = value constraints = self.get_constraints() valid = all(con.evaluate(evaluate_dict) for con in constraints) return valid