Source code for quchip.declarative.parameters

"""Declared-parameter metadata, synthesized ``__init__`` signatures, and validation.

Concrete :class:`~quchip.declarative.models.DeviceModel`,
:class:`~quchip.declarative.models.CouplingModel`, and
:class:`~quchip.control.envelopes.Envelope` subclasses use :func:`parameter` to
declare fields. This module resolves those declarations into synthesized
constructors and enforces their sign constraints at construction and on later
writes.
"""

from __future__ import annotations

import inspect
from abc import ABCMeta
from dataclasses import dataclass
from typing import Any, Callable, TypeAlias, TypeVar, dataclass_transform

from quchip.utils.jax_utils import maybe_concrete_scalar
from quchip.utils.values import copy_value

Scalar: TypeAlias = Any


class _Unbound:
    """Marker for a declared parameter that has no current numerical value."""

    def __repr__(self) -> str:
        return "unbound"

    def __deepcopy__(self, memo: dict[int, Any]) -> _Unbound:
        return self


UNBOUND = _Unbound()
_DEFAULT_OMITTED = object()


[docs] @dataclass(frozen=True) class Parameter: """Metadata for a declared parameter's validation and serialization. Sign constraints apply to concrete scalars. Traced values pass unchecked. Parameters ---------- default : Any, default ``UNBOUND`` Constructor default. ``UNBOUND`` makes the field required. positive, nonnegative : bool, default False Reject concrete values outside the selected sign constraint. serialize : bool, default True Include the field in serialized model data. unit, symbol : str or None Display metadata for units and mathematical notation. noise, kw_only, required : bool Mark noise fields, force keyword-only construction, or require a value. """ default: Any = UNBOUND positive: bool = False nonnegative: bool = False serialize: bool = True unit: str | None = None symbol: str | None = None noise: bool = False kw_only: bool = False required: bool = False
[docs] @dataclass(frozen=True) class Setting: """Metadata for a serialized, non-traceable structural model choice. Parameters ---------- default : Any, default ``UNBOUND`` Structural default. ``UNBOUND`` makes the setting required. serialize : bool, default True Include the setting in serialized model data. kw_only : bool, default True Must stay true because settings are structural choices. """ default: Any = UNBOUND serialize: bool = True kw_only: bool = True @property def required(self) -> bool: return self.default is UNBOUND
@dataclass(frozen=True) class _ConstructorField: """Runtime marker removed after static constructor inference."""
[docs] def constructor_field( *, default: Any = UNBOUND, kw_only: bool = False, runtime: Any = UNBOUND, ) -> Any: """Describe a synthesized constructor argument to static type checkers.""" _ = (default, kw_only) return _ConstructorField() if runtime is UNBOUND else runtime
[docs] def parameter( *, default: Any = _DEFAULT_OMITTED, positive: bool = False, nonnegative: bool = False, serialize: bool = True, unit: str | None = None, symbol: str | None = None, noise: bool = False, kw_only: bool = False, ) -> Any: """Declare a traceable numerical parameter on a model class. ``unit`` is display metadata for human-readable surfaces such as :meth:`Chip.describe`. The package-wide units contract (GHz, ns, mK) still governs the value itself. ``None`` means dimensionless or unknown. Returns a :class:`Parameter` field descriptor that :func:`parameter_fields` collects at class-definition time. Parameters ---------- default : Any, optional Declared default value. If omitted, the constructor argument is required. Use ``default=UNBOUND`` to defer its value until numerical materialization. positive : bool, optional Reject concrete values ``<= 0``. Traced values pass unchecked. nonnegative : bool, optional Reject concrete values ``< 0``. Traced values pass unchecked. serialize : bool, optional Include the field in :meth:`to_dict` output. unit : str or None, optional Display-only unit label (for example ``"GHz"``). symbol : str or None, optional Mathematical symbol for displaying authored physics. If omitted, the display uses the field name. noise : bool, optional If set, :meth:`Chip.set_noise` can configure this field while its current value is unset. kw_only : bool, optional Place the declared field after the ``*`` in synthesized constructors. Examples -------- >>> from quchip.declarative import DeviceModel, parameter, Scalar >>> class Oscillator(DeviceModel): ... freq: Scalar = parameter(positive=True, unit="GHz") ... def local_hamiltonian(self, op, p): ... return p.freq * op.n >>> Oscillator(freq=5.0, levels=3).freq 5.0 """ required = default is _DEFAULT_OMITTED return Parameter( default=UNBOUND if required else default, positive=positive, nonnegative=nonnegative, serialize=serialize, unit=unit, symbol=symbol, noise=noise, kw_only=kw_only, required=required, )
[docs] def setting(*, default: Any = UNBOUND, serialize: bool = True, kw_only: bool = True) -> Any: """Declare serialized structural configuration on a model class. Parameters ---------- default : Any, default ``UNBOUND`` Structural default. If omitted, the setting is required. serialize : bool, default True Include the setting in serialized data. kw_only : bool, default True Must stay true for structural settings. """ if not kw_only: raise ValueError("Declarative settings are keyword-only.") return Setting(default=default, serialize=serialize, kw_only=kw_only)
[docs] @dataclass_transform( field_specifiers=(Parameter, parameter, Setting, setting, constructor_field), ) class DeclarativeMeta(ABCMeta): """Expose synthesized declarative constructors without runtime fields.""" def __new__( mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any, ) -> type: namespace = dict(namespace) for field_name, value in tuple(namespace.items()): if isinstance(value, _ConstructorField): namespace.pop(field_name) return super().__new__(mcls, name, bases, namespace, **kwargs)
[docs] @dataclass_transform( field_specifiers=(Parameter, parameter, Setting, setting, constructor_field), kw_only_default=True, ) class KeywordOnlyDeclarativeMeta(DeclarativeMeta): """Expose keyword-only synthesized constructors for control declarations."""
[docs] def serializable_value(value: Any) -> Any: """Prefer a concrete scalar for serialization, but keep tracers.""" if value is UNBOUND: return None concrete = maybe_concrete_scalar(value) return concrete if concrete is not None else value
[docs] def validate_sign(name: str, spec: Parameter, value: Any) -> None: """Enforce declared sign constraints on concrete scalars. Tracers and ``None`` (unset) pass unchecked. """ if value is UNBOUND: return concrete = maybe_concrete_scalar(value) if spec.positive and concrete is not None and concrete <= 0: raise ValueError(f"{name} must be positive, got {value}") if spec.nonnegative and concrete is not None and concrete < 0: raise ValueError(f"{name} must be non-negative, got {value}")
[docs] def build_declared_signature( param_fields: dict[str, Parameter], trailing: tuple[inspect.Parameter, ...] = (), ) -> inspect.Signature: """Build a synthesized ``__init__`` signature from declared param fields. Declared parameters become optional positional-or-keyword arguments in declaration order. *trailing* appends structural keyword-only parameters such as ``levels`` and ``label``. """ params = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)] for name, spec in param_fields.items(): if spec.kw_only: continue default = inspect.Parameter.empty if spec.required else spec.default params.append(inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=default)) for name, spec in param_fields.items(): if not spec.kw_only: continue default = inspect.Parameter.empty if spec.required else spec.default params.append(inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY, default=default)) params.extend(trailing) return inspect.Signature(params)
def _synthesize_init( cls: type, signature: inspect.Signature, initializer: Callable[..., None], *, doc: str | None = None, normalize_kwargs: Callable[[dict[str, Any]], dict[str, Any]] | None = None, ) -> Any: """Bind a declared signature before delegating to its owning initializer.""" def __init__(self: Any, *args: Any, **kwargs: Any) -> None: if normalize_kwargs is not None: kwargs = normalize_kwargs(kwargs) bound = signature.bind(self, *args, **kwargs) bound.apply_defaults() initializer(**bound.arguments) __init__.__signature__ = signature # type: ignore[attr-defined] __init__.__qualname__ = f"{cls.__qualname__}.__init__" __init__.__module__ = initializer.__module__ __init__.__doc__ = doc return __init__
[docs] def resolve_declared_params( cls: type, params: dict[str, Any], *, fields: dict[str, Parameter] | None = None, ) -> dict[str, Any]: """Resolve declared parameters into a {name: value} dict. Walks ``parameter_fields(cls)``. For each declared field, it pops the matching kwarg from *params* (or uses its default), runs the concrete-only positivity check, and collects the result. Returns a dict of validated parameter values, one entry for each declared field. Raises ``TypeError`` if a required field is missing or if *params* still contains unrecognized keys after the loop. """ resolved_fields = parameter_fields(cls) if fields is None else fields values: dict[str, Any] = {} for name, spec in resolved_fields.items(): if name not in params and spec.required: raise TypeError(f"Missing required parameter: {name}") value = _resolve_declared_value(name, spec, params) validate_sign(name, spec, value) values[name] = value if params: unknown = ", ".join(sorted(params)) raise TypeError(f"Unexpected parameter(s): {unknown}") return values
[docs] def resolve_declared_settings(cls: type, values: dict[str, Any]) -> dict[str, Any]: """Pop declared structural settings from *values* and apply defaults.""" return { name: _resolve_declared_value(name, spec, values) for name, spec in setting_fields(cls).items() }
def _resolve_declared_value(name: str, spec: Parameter | Setting, values: dict[str, Any]) -> Any: """Resolve one field, giving each instance its own mutable default.""" value = values.pop(name, spec.default) if isinstance(spec, Setting) and value is UNBOUND: raise TypeError(f"Missing required setting: {name}") return copy_value(value) if value is spec.default else value _Field = TypeVar("_Field", Parameter, Setting) def _declared_fields(cls: type, field_type: type[_Field]) -> dict[str, _Field]: """Collect one declarative field type in base-first definition order.""" fields: dict[str, _Field] = {} for base in reversed(cls.__mro__): for name in getattr(base, "__annotations__", {}): value = getattr(cls, name, None) if isinstance(value, field_type): fields[name] = value return fields
[docs] def parameter_fields(cls: type) -> dict[str, Parameter]: """Resolve declarative parameter fields for *cls* through the MRO. A field is included if and only if some class in the MRO annotates the name and the resolved class attribute (``getattr(cls, name)``) is a :class:`Parameter` instance. A subclass that shadows an inherited ``Parameter`` with a concrete value (for example ``freq: Scalar = 5.0``) silently drops the field. By design, this lets subclasses remove a parent's parameter when they want a concrete override. """ return _declared_fields(cls, Parameter)
[docs] def setting_fields(cls: type) -> dict[str, Setting]: """Resolve structural setting fields for *cls* through the MRO.""" return _declared_fields(cls, Setting)
[docs] def authored_component_values(component: Any) -> dict[str, Any]: """Declared numerical and structural values used for copying and cache keys.""" names = dict.fromkeys(( *component.parameter_values(), *parameter_fields(type(component)), *setting_fields(type(component)), *getattr(type(component), "structural_setting_names", ()), )) return {name: getattr(component, name) for name in names}
[docs] def copy_authored_fields(source: Any, target: Any) -> None: """Give a derived component independent writable authored values.""" from quchip.utils.values import copy_value for name, value in copy_value(authored_component_values(source)).items(): object.__setattr__(target, name, value)
[docs] def component_fingerprint(component: Any, *, traced: bool = False) -> Any: """Track authored buffer edits and ordinary versioned assignments. ``traced=True`` keys traced values by identity. See :func:`~quchip.utils.values.scoped_entry`. """ from quchip.utils.values import value_fingerprint try: values = value_fingerprint(authored_component_values(component), traced=traced) except (ValueError, RecursionError): # A fresh key prevents reuse when the payload cannot be inspected. values = object() return type(component), component.label, component.state_version, values
[docs] def validate_declared_fields(cls: type) -> None: """Validate cross-cutting constraints on a class's declared fields.""" for name, spec in parameter_fields(cls).items(): if spec.noise and not spec.serialize: raise TypeError(f"Noise parameter {name!r} must be serializable.")