"""Physical Markovian input-output channels attached to a chip."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
import jax
import numpy as np
from quchip.declarative.dissipation import CollapseChannel, normalize_dissipation
from quchip.utils.jax_utils import maybe_concrete_scalar
from quchip.utils.labeling import auto_label, resolve_label
if TYPE_CHECKING:
from quchip.chip.chip import Chip
from quchip.chip.port_network import ComponentPort, FieldTerminal, PortNetwork, SLHComponent
[docs]
class Port:
"""One accessible Markovian channel with a dimensionless coupling operator.
``rate``, ``external_quality_factor``, and ``phase`` can be traced or swept
between solves but remain constant within a solve. Model shaped emission
with an explicit buffer or coupler device holding a static ``Port`` and a
modulated Hamiltonian coupling.
Parameters
----------
target : object or sequence of object
Device target(s), or labels resolved when attached to a chip. Multiple
targets require an explicit joint ``operator``.
rate : float or array-like or None, default=None
External coupling rate in 1/ns. Mutually exclusive with
``external_quality_factor``.
external_quality_factor : float or array-like or None, default=None
Dimensionless external quality factor for one target, giving the rate
``2*pi*freq/Q`` in 1/ns.
operator : object or str or None, default=None
Dimensionless coupling operator. ``None`` uses the target's lowering operator.
phase : float or array-like, default=0.0
Reference-plane phase in radians.
label : str or None, default=None
Stable channel label, generated if omitted.
"""
_type_prefix = "port"
_parameter_names = ("rate", "external_quality_factor", "phase")
def __init__(
self,
target: Any | Sequence[Any],
*,
rate: Any = None,
external_quality_factor: Any = None,
operator: Any = None,
phase: Any = 0.0,
label: str | None = None,
) -> None:
if isinstance(target, Sequence) and not isinstance(target, (str, bytes)):
targets = tuple(target)
else:
targets = (target,)
if not targets:
raise ValueError("Port requires at least one target device.")
if (rate is None) == (external_quality_factor is None):
raise ValueError("Port requires exactly one of rate or external_quality_factor.")
if external_quality_factor is not None and len(targets) != 1:
raise ValueError("external_quality_factor is defined only for a single target device.")
if operator is None and len(targets) != 1:
raise ValueError("A multi-device Port requires an explicit coupling operator.")
self._targets = targets
self.rate = rate
self.external_quality_factor = external_quality_factor
self.operator = operator
self.phase = phase
self.label = label if label is not None else auto_label(self._type_prefix)
self._network: PortNetwork | None = None
self._network_component: SLHComponent | None = None
# Removed modes whose kept response the Markov field of a transformed port lacks.
self._omitted_responses: tuple[str, ...] = ()
def _bind_network(self, network: "PortNetwork", component: "SLHComponent") -> None:
"""Bind terminal access to the one network that owns this port."""
if self._network is not None and self._network is not network:
raise ValueError(
f"Port {self.label!r} already belongs to another PortNetwork; copy it first."
)
self._network = network
self._network_component = component
@property
def input(self) -> "FieldTerminal":
"""Return this port's field input terminal."""
if self._network_component is None:
raise AttributeError("A Port has terminals only after it belongs to a PortNetwork.")
return self._network_component.input
@property
def output(self) -> "FieldTerminal":
"""Return this port's field output terminal."""
if self._network_component is None:
raise AttributeError("A Port has terminals only after it belongs to a PortNetwork.")
return self._network_component.output
@property
def side(self) -> "ComponentPort":
"""Return this port's physical side, pairing its input and output terminals."""
if self._network_component is None:
raise AttributeError("A Port has terminals only after it belongs to a PortNetwork.")
return self._network_component.port("field")
@staticmethod
def _validate_positive(name: str, value: Any) -> None:
concrete = maybe_concrete_scalar(value)
if concrete is not None and concrete <= 0:
raise ValueError(f"{name} must be positive, got {value}")
def __setattr__(self, name: str, value: Any) -> None:
if name in ("rate", "external_quality_factor"):
self._validate_positive(name, value)
other_name = "external_quality_factor" if name == "rate" else "rate"
if hasattr(self, other_name):
other = getattr(self, other_name)
if (value is None) == (other is None):
raise ValueError("Port requires exactly one of rate or external_quality_factor.")
super().__setattr__(name, value)
[docs]
def resolve_targets(self, chip: "Chip") -> tuple[str, ...]:
"""Return target labels after checking that they belong to a chip.
Parameters
----------
chip : Chip
Chip against which labels are resolved.
"""
labels = tuple(resolve_label(target) for target in self._targets)
unknown = [label for label in labels if label not in chip.device_map]
if unknown:
raise ValueError(f"Port {self.label!r} targets unknown device(s) {unknown}.")
if len(set(labels)) != len(labels):
raise ValueError(f"Port {self.label!r} repeats a target device.")
return labels
[docs]
def rate_value(self, chip: "Chip") -> Any:
"""Return the external coupling rate in 1/ns.
Parameters
----------
chip : Chip
Chip that supplies the target frequency for a quality-factor rate.
"""
if self.rate is not None:
return self.rate
label = self.resolve_targets(chip)[0]
target = chip[label]
if not hasattr(target, "freq"):
raise TypeError("external_quality_factor requires a target with a freq parameter.")
return 2.0 * np.pi * target.freq / self.external_quality_factor
def _authored_operator(self, chip: "Chip") -> Any:
labels = self.resolve_targets(chip)
if self.operator is not None and not isinstance(self.operator, str):
return self.operator
if isinstance(self.operator, str) and len(labels) != 1:
raise ValueError("A named Port operator requires exactly one target.")
from quchip.backend import get_default_backend
# The target's own operator is a constant of its authored space; keep
# it concrete inside jax.jit so its excitation change stays known.
with jax.ensure_compile_time_eval(), get_default_backend().eager_operators():
if self.operator is None:
return chip[labels[0]].lowering_operator()
return chip[labels[0]].local_operator(self.operator)
def _collapse_channels_with_paths(
self,
chip: "Chip",
) -> tuple[tuple[CollapseChannel, tuple[str, ...]], ...]:
labels = self.resolve_targets(chip)
normalized = normalize_dissipation(
(CollapseChannel(self._authored_operator(chip), self.rate_value(chip), "external_coupling"),),
labels=labels,
dims=tuple(chip[label].local_space().dimension for label in labels),
owner=self,
scope=f"port.{self.label}",
)
rate_paths: tuple[str, ...]
if self.rate is not None:
rate_paths = (f"port.{self.label}.rate",)
else:
rate_paths = (
f"{labels[0]}.freq",
f"port.{self.label}.external_quality_factor",
)
return tuple((channel, tuple(dict.fromkeys((*paths, *rate_paths)))) for channel, paths in normalized)
[docs]
def parameter_values(self) -> dict[str, Any]:
"""Return active sweepable port values."""
return {
name: value
for name in self._parameter_names
if (value := getattr(self, name)) is not None
}
[docs]
def set_parameter_value(self, name: str, value: Any) -> None:
"""Set one port parameter on an isolated chip copy.
Parameters
----------
name : {"rate", "external_quality_factor", "phase"}
Local parameter name.
value : Any
Replacement value in 1/ns, dimensionless units, or radians.
"""
if name not in self._parameter_names:
raise KeyError(name)
setattr(self, name, value)
[docs]
def copy(self) -> "Port":
"""Return an independent port that keeps label-based targets."""
operator = self.operator.copy() if isinstance(self.operator, np.ndarray) else self.operator
result = Port(
tuple(resolve_label(target) for target in self._targets),
rate=self.rate,
external_quality_factor=self.external_quality_factor,
operator=operator,
phase=self.phase,
label=self.label,
)
result._omitted_responses = self._omitted_responses
return result
[docs]
def physics_notes(self) -> list[str]:
"""Return the input-output convention owned by this port."""
return [
"This accessible Markovian channel uses "
"L = exp(i phase) sqrt(rate) A and b_out = b_in + L; "
"the same L sets damping, coherent input coupling, and reported output."
]
[docs]
def to_dict(self) -> dict[str, Any]:
"""Serialize port targets and scalar coupling data."""
operator: Any = self.operator
if operator is not None and not isinstance(operator, str):
if hasattr(operator, "matrix"):
operator = operator.matrix()
elif hasattr(operator, "to_jax"):
operator = operator.to_jax()
elif hasattr(operator, "full"):
operator = operator.full()
array = np.asarray(operator, dtype=complex)
operator = {
"kind": "dense",
"real": array.real.tolist(),
"imag": array.imag.tolist(),
}
elif isinstance(operator, str):
operator = {"kind": "named", "name": operator}
omitted = {"omitted_responses": list(self._omitted_responses)} if self._omitted_responses else {}
return {
"targets": [resolve_label(target) for target in self._targets],
"rate": self.rate,
"external_quality_factor": self.external_quality_factor,
"operator": operator,
"phase": self.phase,
"label": self.label,
**omitted,
}
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Port":
"""Reconstruct a port from label-based serialized data.
Parameters
----------
data : dict[str, Any]
Payload produced by :meth:`to_dict`.
"""
targets = data["targets"]
target: Any = targets[0] if len(targets) == 1 else targets
operator = data.get("operator")
if isinstance(operator, dict):
if operator.get("kind") == "named":
operator = operator["name"]
elif operator.get("kind") == "dense":
operator = np.asarray(operator["real"]) + 1j * np.asarray(operator["imag"])
else:
raise TypeError(f"Unknown serialized Port operator kind: {operator.get('kind')!r}")
port = cls(
target,
rate=data.get("rate"),
external_quality_factor=data.get("external_quality_factor"),
operator=operator,
phase=data.get("phase", 0.0),
label=data.get("label"),
)
omitted = data.get("omitted_responses", ())
if any(not isinstance(label, str) or not label for label in omitted):
raise ValueError("Omitted port responses must be nonempty labels.")
port._omitted_responses = tuple(sorted(set(omitted)))
return port