"""Declared-parameter metadata, synthesized ``__init__`` signatures, and validation.
:func:`parameter` is the field-declaration surface concrete
:class:`~quchip.declarative.models.DeviceModel`,
:class:`~quchip.declarative.models.CouplingModel`, and
:class:`~quchip.declarative.envelope_shape.EnvelopeShape` subclasses use;
this module resolves those declarations into synthesized constructors and
runs their sign constraints, both at construction and on post-construction
writes.
"""
from __future__ import annotations
import inspect
from dataclasses import dataclass
from typing import Any, TypeAlias
from quchip.utils.jax_utils import maybe_concrete_scalar
Scalar: TypeAlias = Any
Modulation: TypeAlias = Any
_MISSING = object()
[docs]
@dataclass(frozen=True)
class Parameter:
"""Metadata for a declarative model parameter field.
The metadata is intentionally lightweight: it records validation and
serialization intent while leaving the runtime value fully traceable.
Sign constraints (``positive`` / ``nonnegative``) are enforced only on
concrete scalars, so traced values flow through unchecked.
"""
default: Any = _MISSING
positive: bool = False
nonnegative: bool = False
serialize: bool = True
unit: str | None = None
@property
def has_default(self) -> bool:
"""Return whether this field has a declared default value."""
return self.default is not _MISSING
[docs]
def parameter(
*,
default: Any = _MISSING,
positive: bool = False,
nonnegative: bool = False,
serialize: bool = True,
unit: str | None = None,
) -> Any:
"""Declare a traceable scalar or modulation 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. When omitted the field is required in the
synthesized ``__init__``.
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 (e.g. ``"GHz"``).
Examples
--------
>>> from quchip.declarative import DeviceModel, parameter, Scalar
>>> class Oscillator(DeviceModel):
... freq: Scalar = parameter(positive=True, unit="GHz")
... def local_hamiltonian(self, op):
... return self.freq * op.n
>>> Oscillator(freq=5.0, levels=3).freq
5.0
"""
return Parameter(
default=default, positive=positive, nonnegative=nonnegative,
serialize=serialize, unit=unit,
)
[docs]
def serializable_value(value: Any) -> Any:
"""Prefer a concrete scalar for serialization while preserving tracers."""
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 a field's declared sign constraint on concrete scalars only.
Shared by construction (:func:`resolve_declared_params`) and
post-construction writes (``DeviceModel._validate_param_write``) so the
two paths cannot drift. Traced values flow through unchecked;
``None`` means "unset" and always passes.
"""
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}")
def _check_declaration_order(param_fields: dict[str, Parameter], owner: type | None) -> None:
"""Raise if a required declared field follows an optional one.
``inspect.Signature`` enforces the same ordering rule a ``def`` header
does (every parameter after the first one carrying a default must also
carry one) and raises a bare ``ValueError`` from deep inside
:mod:`inspect` when it does not hold — naming neither the declaring
class nor the two offending fields. This walks *param_fields* first so
the failure instead names the class and both fields, with the two
available remedies.
"""
last_optional: str | None = None
for name, spec in param_fields.items():
if spec.has_default:
last_optional = name
continue
if last_optional is not None:
owner_name = owner.__name__ if owner is not None else "<unknown class>"
raise TypeError(
f"{owner_name} declares required parameter {name!r} after optional parameter "
f"{last_optional!r}. Reorder the declarations so required fields precede optional "
f"ones, or give {name!r} a default."
)
[docs]
def build_declared_signature(
param_fields: dict[str, Parameter],
trailing: tuple[inspect.Parameter, ...] = (),
*,
owner: type | None = None,
) -> inspect.Signature:
"""Build a synthesized ``__init__`` signature from declared param fields.
Declared parameters become positional-or-keyword arguments in
declaration order, carrying their declared defaults. *trailing* appends
extra (typically keyword-only) structural parameters — e.g. ``levels``,
``label`` and the noise kwargs for a :class:`DeviceModel`. *owner* names
the class whose declarations are being synthesized, for the ordering
error raised by :func:`_check_declaration_order`.
"""
_check_declaration_order(param_fields, owner)
params = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)]
for name, spec in param_fields.items():
if spec.has_default:
params.append(inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=spec.default))
else:
params.append(inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD))
params.extend(trailing)
return inspect.Signature(params)
[docs]
def resolve_declared_params(cls: type, params: dict[str, Any]) -> dict[str, Any]:
"""Resolve declared parameters into a {name: value} dict.
Walks ``parameter_fields(cls)``: for each declared field, 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 per declared field. Raises
``TypeError`` if a required field is missing or if *params* still
contains unrecognized keys after the loop.
"""
fields = parameter_fields(cls)
values: dict[str, Any] = {}
for name, spec in fields.items():
if name in params:
value = params.pop(name)
elif spec.has_default:
value = spec.default
else:
raise TypeError(f"Missing required parameter {name!r}")
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 parameter_fields(cls: type) -> dict[str, Parameter]:
"""Resolve declarative parameter fields for *cls*, walking the MRO.
A field is included iff 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 (e.g. ``freq: Scalar = 5.0``)
silently drops the field — by design, so subclasses can elide a
parent's parameter when they want a concrete override.
"""
fields: dict[str, Parameter] = {}
for base in reversed(cls.__mro__):
annotations = getattr(base, "__annotations__", {})
for name in annotations:
value = getattr(cls, name, None)
if isinstance(value, Parameter):
fields[name] = value
return fields