"""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]
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.")