Source code for quchip.interop.base
"""Library-agnostic registry and dispatch for third-party model mappings.
A :class:`ModelMapping` subclass authors a one-directional or bidirectional
conversion between a third-party circuit-QED object and a quchip device.
Subclasses register themselves at class-definition time, keyed by the
third-party type they import from and/or the ``(library, quchip type)`` pair
they export to. Nothing in this module depends on any specific third-party
library; concrete mappings (e.g. for scqubits) live in sibling modules that
import both sides.
"""
from __future__ import annotations
from typing import Any, ClassVar
_IMPORT_REGISTRY: dict[str, type["ModelMapping"]] = {}
_EXPORT_REGISTRY: dict[tuple[str, type], type["ModelMapping"]] = {}
_AUTHORING_SKELETON = """
class MyMapping(ModelMapping):
source = "some_library.SomeClass" # import support
target = MyQuchipDevice # export support
def import_model(self, obj, **opts): ...
def export_model(self, device, **opts): ...
""".strip()
[docs]
def source_key(tp: type) -> str:
"""Return the registry key for third-party type *tp*.
The key is the type's top-level module name joined with its qualified
name, e.g. ``"scqubits.Transmon"``. Only the top-level module is used so
that a mapping registered against a package root matches classes
re-exported from submodules.
"""
return f"{tp.__module__.split('.')[0]}.{tp.__qualname__}"
[docs]
class ModelMapping:
"""Base class for a single third-party <-> quchip device conversion.
Attributes
----------
source : str or None
Registry key (see :func:`source_key`) of the third-party type this
mapping imports from. ``None`` means the mapping supports export
only.
target : type or None
The quchip device type this mapping exports to. Required whenever
:meth:`export_model` is overridden; ``None`` means import-only.
library : str or None
Name of the third-party library this mapping exports for. Defaults
to ``source.split(".")[0]`` when ``source`` is set; export-only
mappings must set it explicitly.
Subclassing registers the mapping automatically:
* setting ``source`` registers it for :func:`import_object` under that
key; a second subclass reusing the same ``source`` raises
:class:`TypeError` at class-definition time.
* overriding :meth:`export_model` registers it for :func:`export_object`
under ``(library, target)``; overriding without setting ``target``
raises :class:`TypeError`, overriding without a resolvable ``library``
(no ``source`` to default it from) raises :class:`TypeError`, and a
second subclass reusing the same ``(library, target)`` pair raises
:class:`TypeError` naming both classes.
The abstract base itself (``source is None`` and no ``export_model``
override) registers nothing.
"""
source: ClassVar[str | None] = None
target: ClassVar[type | None] = None
library: ClassVar[str | None] = None
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
if cls.library is None and cls.source is not None:
cls.library = cls.source.split(".")[0]
# Validate both registrations before mutating either registry: a
# class that fails export validation (missing target/library, or a
# duplicate export key) must not leave a ghost import-registry entry
# behind just because the import-side check ran first.
if cls.source is not None:
existing_import = _IMPORT_REGISTRY.get(cls.source)
if existing_import is not None:
raise TypeError(
f"Duplicate ModelMapping.source {cls.source!r}: "
f"already registered by {existing_import.__qualname__}, cannot register {cls.__qualname__}."
)
export_key: tuple[str, type] | None = None
if "export_model" in cls.__dict__:
if cls.target is None:
raise TypeError(
f"{cls.__qualname__} overrides export_model but does not set 'target'. "
"Export mappings must declare the quchip device type they produce."
)
if cls.library is None:
raise TypeError(
f"{cls.__qualname__} overrides export_model but has no 'library' to register under "
"(no 'source' to default it from). Set 'library' explicitly on the mapping."
)
export_key = (cls.library, cls.target)
existing_export = _EXPORT_REGISTRY.get(export_key)
if existing_export is not None:
raise TypeError(
f"Duplicate ModelMapping export target {export_key!r}: "
f"already registered by {existing_export.__qualname__}, cannot register {cls.__qualname__}."
)
if cls.source is not None:
_IMPORT_REGISTRY[cls.source] = cls
if export_key is not None:
_EXPORT_REGISTRY[export_key] = cls
[docs]
def import_model(self, obj: Any, **opts: Any) -> Any:
"""Convert third-party object *obj* into a quchip device.
Override to support import. The base implementation raises
:class:`NotImplementedError`.
"""
raise NotImplementedError(f"{type(self).__qualname__} does not support import.")
[docs]
def export_model(self, device: Any, **opts: Any) -> Any:
"""Convert quchip *device* into a third-party object.
Override to support export; overriding requires setting ``target``.
The base implementation raises :class:`NotImplementedError`.
"""
raise NotImplementedError(f"{type(self).__qualname__} does not support export.")
[docs]
def import_object(obj: Any, **opts: Any) -> Any:
"""Import third-party *obj* into a quchip device via a registered mapping.
Walks ``type(obj).__mro__`` and dispatches to the first
:class:`ModelMapping` registered under that class's :func:`source_key`.
Raises
------
LookupError
No mapping is registered for ``type(obj)`` or any of its base
classes. The message names the missing source key and shows the
skeleton for authoring a new :class:`ModelMapping`.
"""
for klass in type(obj).__mro__:
mapping_cls = _IMPORT_REGISTRY.get(source_key(klass))
if mapping_cls is not None:
return mapping_cls().import_model(obj, **opts)
raise LookupError(
f"No ModelMapping registered for {source_key(type(obj))!r} (or any base class). "
f"Author one:\n{_AUTHORING_SKELETON}"
)
[docs]
def export_object(device: Any, library: str, **opts: Any) -> Any:
"""Export quchip *device* to a third-party object of *library*.
Walks ``type(device).__mro__`` and dispatches to the first
:class:`ModelMapping` registered under ``(library, that class)``.
Raises
------
LookupError
No mapping is registered for ``(library, type(device))`` or any base
class. The message lists the device types *library* can export.
"""
for klass in type(device).__mro__:
mapping_cls = _EXPORT_REGISTRY.get((library, klass))
if mapping_cls is not None:
return mapping_cls().export_model(device, **opts)
exportable = sorted(tp.__qualname__ for (lib, tp) in _EXPORT_REGISTRY if lib == library)
raise LookupError(
f"No ModelMapping registered to export {type(device).__qualname__!r} to library {library!r}. "
f"Exportable types for {library!r}: {exportable}"
)
[docs]
def registered_mappings() -> dict[str, type[ModelMapping]]:
"""Return a copy of the import registry, keyed by source key.
For introspection and tests; mutating the returned dict does not affect
registration state.
"""
return dict(_IMPORT_REGISTRY)