diff --git a/quantinuum_schemas/__init__.py b/quantinuum_schemas/__init__.py index d41ba76..315c2aa 100644 --- a/quantinuum_schemas/__init__.py +++ b/quantinuum_schemas/__init__.py @@ -11,11 +11,14 @@ QuantinuumConfig, QulacsConfig, SelenePlusConfig, + HeliosConfig, + HeliosEmulatorConfig, ) from quantinuum_schemas.models.emulator_config import ( ClassicalReplaySimulator, CoinflipSimulator, DepolarizingErrorModel, + HeliosCustomErrorModel, HeliosRuntime, MatrixProductStateSimulator, NoErrorModel, @@ -24,6 +27,10 @@ StabilizerSimulator, StatevectorSimulator, ) +from quantinuum_schemas.models.quantinuum_systems_noise import ( + HeliosErrorParams, + UserErrorParams, +) __all__ = [ "AerConfig", @@ -37,6 +44,8 @@ "SeleneConfig", "SelenePlusConfig", "SimpleRuntime", + "HeliosConfig", + "HeliosEmulatorConfig", "HeliosRuntime", "NoErrorModel", "DepolarizingErrorModel", @@ -46,4 +55,7 @@ "CoinflipSimulator", "MatrixProductStateSimulator", "ClassicalReplaySimulator", + "HeliosCustomErrorModel", + "HeliosErrorParams", + "UserErrorParams", ] diff --git a/quantinuum_schemas/models/backend_config.py b/quantinuum_schemas/models/backend_config.py index 8a16df8..09553fa 100644 --- a/quantinuum_schemas/models/backend_config.py +++ b/quantinuum_schemas/models/backend_config.py @@ -5,7 +5,7 @@ as our backend credential classes handle those. """ -# pylint: disable=too-many-lines +# pylint: disable=too-many-lines,no-member import abc from typing import Any, Dict, Literal, Optional, Type, TypeVar, Union @@ -18,6 +18,7 @@ ClassicalReplaySimulator, CoinflipSimulator, DepolarizingErrorModel, + HeliosCustomErrorModel, HeliosRuntime, MatrixProductStateSimulator, NoErrorModel, @@ -32,6 +33,8 @@ ST = TypeVar("ST", bound="BaseModel") +KNOWN_NEXUS_HELIOS_EMULATORS = ["Helios-1E-lite"] + class BaseBackendConfig(BaseModel, abc.ABC): """Base class for all the backend configs. @@ -161,8 +164,8 @@ def check_local_remote_parameters_are_consistent( # pylint: disable=no-self-arg return values -class QuantinuumCompilerOptions(BaseModel): - """Class for Quantinuum Compiler Options. +class QuantinuumOptions(BaseModel): + """Class for Quantinuum additional options. Intentionally allows extra unknown flags to be defined. """ @@ -173,10 +176,10 @@ class QuantinuumCompilerOptions(BaseModel): def check_field_values_are_supported_types( # pylint: disable=no-self-argument, cls, values: Dict[str, Any] ) -> Dict[str, Any]: - """Check that compiler option values are supported types.""" + """Check that option values are supported types.""" for key in values: assert isinstance(values[key], (str, int, bool, float, list)), ( - "Compiler options must be str, bool int, float or a list of floats" + "Options must be str, bool int, float or a list of floats" ) if isinstance(values[key], list): for x in values[key]: @@ -184,6 +187,14 @@ def check_field_values_are_supported_types( # pylint: disable=no-self-argument, return values +# Alias via inheritance for backwards compatibility +class QuantinuumCompilerOptions(QuantinuumOptions): + """Class for Quantinuum compiler options. + + Intentionally allows extra unknown flags to be defined. + """ + + class QuantinuumConfig(BaseBackendConfig): """Runs circuits on Quantinuum's quantum devices and simulators. @@ -280,7 +291,7 @@ class BaseEmulatorConfig(BaseModel): n_qubits: The maximum number of qubits to simulate. """ - n_qubits: int = Field(ge=1) + n_qubits: int | None = None @model_validator(mode="after") def prevent_direct_instantiation(self) -> Self: @@ -338,9 +349,118 @@ class SelenePlusConfig(BaseEmulatorConfig, BaseBackendConfig): | ClassicalReplaySimulator ) = Field(default_factory=StatevectorSimulator) runtime: SimpleRuntime | HeliosRuntime = Field(default_factory=HeliosRuntime) - error_model: NoErrorModel | DepolarizingErrorModel | QSystemErrorModel = Field( - default_factory=QSystemErrorModel - ) + error_model: ( + NoErrorModel + | DepolarizingErrorModel + | QSystemErrorModel + | HeliosCustomErrorModel + ) = Field(default_factory=QSystemErrorModel) + + @model_validator(mode="after") + def validate_runtime_and_error_model(self) -> Self: + """Validate that the runtime and error model are compatible.""" + if isinstance(self.error_model, (QSystemErrorModel, HeliosCustomErrorModel)): + if not isinstance(self.runtime, HeliosRuntime): + raise ValueError( + f"error_model of type: {self.error_model.__class__.__name__} " + "can only be used with runtime of type: HeliosRuntime" + ) + if isinstance(self.error_model, HeliosCustomErrorModel): + if isinstance(self.simulator, StabilizerSimulator): + if self.error_model.error_params.coherent_dephasing is False: + raise ValueError( + "HeliosErrorModel with StabilizerSimulator must have " + "coherent_dephasing set to True" + ) + else: + if self.error_model.error_params.coherent_dephasing is True: + raise ValueError( + "HeliosErrorModel with non-StabilizerSimulator must have " + "coherent_dephasing set to False" + ) + + return self + + +class HeliosEmulatorConfig(BaseEmulatorConfig): + """Configuration for Helios emulator systems.""" + + n_qubits: int | None = None + + simulator: ( + StatevectorSimulator + | StabilizerSimulator + | MatrixProductStateSimulator + | CoinflipSimulator + | ClassicalReplaySimulator + ) = Field(default_factory=StatevectorSimulator) + error_model: ( + NoErrorModel + | DepolarizingErrorModel + | QSystemErrorModel + | HeliosCustomErrorModel + ) = Field(default_factory=QSystemErrorModel) + runtime: HeliosRuntime = Field(default_factory=HeliosRuntime) + + +class HeliosConfig(BaseBackendConfig): + """Configuration for Helios generation QPUs, emulators and checkers.""" + + type: Literal["HeliosConfig"] = "HeliosConfig" + + system_name: str = "Helios-1" + emulator_config: HeliosEmulatorConfig | None = None + + max_cost: int | None = None + + attempt_batching: bool = False + max_batch_cost: int = 2000 + + options: QuantinuumOptions | None = None + + @model_validator(mode="after") + def check_valid_config(self) -> Self: + """Perform simple configuration validation.""" + + if self.max_cost is None: + if ( + not self.system_name.endswith("SC") + and self.system_name not in KNOWN_NEXUS_HELIOS_EMULATORS + ): + raise ValueError(f"max_cost must be set for {self.system_name}.") + + if self.emulator_config is not None: + if self.attempt_batching: + raise ValueError("Batching not available for emulators.") + if self.system_name in KNOWN_NEXUS_HELIOS_EMULATORS: + if self.max_cost: + raise ValueError( + f"max_cost not currently supported for {self.system_name}" + ) + if self.system_name not in KNOWN_NEXUS_HELIOS_EMULATORS: + if self.emulator_config.simulator.type == "ClassicalReplaySimulator": + raise ValueError( + f"ClassicalReplaySimulator is only available for " + f"emulators in: {KNOWN_NEXUS_HELIOS_EMULATORS}" + ) + if self.emulator_config.error_model.type == "DepolarizingErrorModel": + raise ValueError( + f"DepolarizingErrorModel is only available for " + f"emulators in: {KNOWN_NEXUS_HELIOS_EMULATORS}" + ) + if self.emulator_config.runtime.seed is not None: + raise ValueError( + f"runtime.seed will be ignored for {self.system_name}" + ) + if self.emulator_config.simulator.seed is not None: + raise ValueError( + f"simulator.seed will be ignored for {self.system_name}" + ) + if self.emulator_config.error_model.seed is not None: + raise ValueError( + f"error_model.seed will be ignored for {self.system_name}" + ) + return self BackendConfig = Annotated[ @@ -355,6 +475,7 @@ class SelenePlusConfig(BaseEmulatorConfig, BaseBackendConfig): QulacsConfig, SeleneConfig, SelenePlusConfig, + HeliosConfig, ], Field(discriminator="type"), ] diff --git a/quantinuum_schemas/models/emulator_config.py b/quantinuum_schemas/models/emulator_config.py index 8d93b89..2631144 100644 --- a/quantinuum_schemas/models/emulator_config.py +++ b/quantinuum_schemas/models/emulator_config.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, Field, model_validator from typing_extensions import Self +from quantinuum_schemas.models.quantinuum_systems_noise import HeliosErrorParams + class SimpleRuntime(BaseModel): """A 'simple' runtime for the Selene emulator. @@ -70,7 +72,8 @@ class DepolarizingErrorModel(BaseModel): class QSystemErrorModel(BaseModel): - """Model for simulating error for a specific QSystem via Selene. + """Preconfigured Error Model for simulating error for a specific QSystem via Selene. + Will use a preconfiguration of the error model as specified by the name parameter. Args: seed: Random seed for the error model. @@ -83,6 +86,20 @@ class QSystemErrorModel(BaseModel): name: str = "alpha" +class HeliosCustomErrorModel(BaseModel): + """Configurable Error Model for simulating error for the Helios system via Selene. + + Args: + seed: Random seed for the error model. + error_params: Parameters for the Helios error model. + """ + + type: Literal["HeliosCustomErrorModel"] = "HeliosCustomErrorModel" + + seed: int | None = Field(default=None) + error_params: HeliosErrorParams = Field(default_factory=HeliosErrorParams) + + class StatevectorSimulator(BaseModel): """Statevector simulator built on a QuEST backend. @@ -149,6 +166,8 @@ def check_valid_config(self) -> Self: raise ValueError("CPU backend does not support chi > 256.") if self.chi and self.truncation_fidelity: raise ValueError("Cannot set both chi and truncation_fidelity.") + if self.backend != "auto": + raise ValueError("Only backend='auto' is supported at this time.") return self diff --git a/quantinuum_schemas/models/quantinuum_systems_noise.py b/quantinuum_schemas/models/quantinuum_systems_noise.py index a96eea6..26d1207 100644 --- a/quantinuum_schemas/models/quantinuum_systems_noise.py +++ b/quantinuum_schemas/models/quantinuum_systems_noise.py @@ -1,10 +1,224 @@ """Validation classes for Quantinuum Systems noise models.""" from typing import Optional, Tuple, Union +from typing_extensions import Self + +from pydantic import AliasChoices, Field, model_validator from .base import BaseModel +_KEYS_1Q_PAULI = {"X", "Y", "Z"} +_KEYS_1Q_EMISSION = _KEYS_1Q_PAULI | {"L"} +_KEYS_2Q_PAULI = { + "IX", + "IY", + "IZ", + "XI", + "XX", + "XY", + "XZ", + "YI", + "YX", + "YY", + "YZ", + "ZI", + "ZX", + "ZY", + "ZZ", +} +_KEYS_2Q_EMISSION = _KEYS_2Q_PAULI | { + "IL", + "XL", + "YL", + "ZL", + "LI", + "LX", + "LY", + "LZ", + "LL", +} + + +def _validate_distribution( + name: str, distribution: dict[str, float], keys: set[str] +) -> None: + """ + Validate that the distribution keys are in the allowed set and that + all values are between 0 and 1. + """ + for k, v in distribution.items(): + assert k in keys, ( + f"{name} keys must be a subset of {keys}, " + f"but an invalid entry was provided with key '{k}'" + ) + assert 0 <= v <= 1, ( + f"{name} values must be between 0 and 1, but {k}={v} was provided" + ) + if distribution: # If non-empty + sum_values = sum(distribution.values()) + assert abs(1 - sum_values) < 1e-9, ( + f"{name} values must sum to 1 +/- 1e-9, but the provided values sum to {sum_values}" + ) + + +class HeliosErrorParams(BaseModel): + """ + Error model configuration for emulation of Quantinuum's Helios System. + + parameters: + p_init: Probability of error during preparation. Alias: p_prep. + p_meas_0: Probability of flipping 0 to 1 during measurement. + p_meas_1: Probability of flipping 1 to 0 during measurement. + p1: Probability of error after single-qubit gates. + p2: Probability of error after two-qubit gates. + p1_emission_ratio: Emission ratio for single-qubit gates. + p2_emission_ratio: The proportion of two-qubit errors that are emission faults. + p1_pauli_model: The pauli model for single-qubit gates, e.g. + `{"X": 0.1,"Y": 0.2,"Z": 0.3}`. + p1_emission_model: The emission model for single-qubit gates, e.g. + `{"X": 0.1,"Y": 0.2,"Z": 0.3}`. + p2_pauli_model: The pauli model for two-qubit gates, e.g. + `{"XX": 0.2, "YZ": 0.3, "ZI": 0.4}`. + p2_emission_model: The emission model for two-qubit gates, e.g. + `{"XX": 0.2, "YZ": 0.3, "ZI": 0.4}`. + p_prep_leak_ratio: Preparation leakage ratio. + p1_seepage_prob: Probability of a leaked qubit being seeped (released from leakage) for + single-qubit. + p2_seepage_prob: Probability of a leaked qubit being seeped (released from leakage) for + two-qubit. + scale: Overall scaling factor. + memory_scale: Memory scaling factor. + init_scale: Initial scaling factor. Alias: prep_scale. + meas_scale: Measurement scaling factor. + p1_scale: Single-qubit gate scaling factor. + p2_scale: Two-qubit gate scaling factor. + emission_scale: Emission scaling factor. + przz_a: Scaling parameters for RZZ gate error rate - coefficient a. + przz_b: Scaling parameters for RZZ gate error rate - coefficient b. + przz_c: Scaling parameters for RZZ gate error rate - coefficient c. + przz_d: Scaling parameters for RZZ gate error rate - coefficient d. + przz_power: Power parameter for RZZ error scaling. + p_crosstalk_meas: Probability of crosstalk during measurement operations. + Alias: p_meas_crosstalk. + p_crosstalk_init: Probability of crosstalk during initialization operations. + Alias: p_prep_crosstalk. + noiseless_gates: List of gates to be treated as noiseless. + coherent_dephasing: Whether to include coherent dephasing. + coherent_to_incoherent_factor: Coherent to incoherent conversion factor. + leak2depolar: Replace leakage with general noise. + p_meas_crosstalk_scale: Measurement crosstalk rescale factor. + p_prep_crosstalk_scale: Preparation crosstalk rescale factor. + crosstalk_per_gate: Whether to apply crosstalk on a per-gate basis. + linear_dephasing_rate: Linear rate for idle noise. + Alias: p_idle_linear_rate. + quadratic_dephasing_rate: Quadratic rate for idle noise. + Alias: p_idle_quadratic_rate. + p2_idle: Stochastic idle noise after each two-qubit gate. + p_idle_linear_model: Pauli model for linear idle noise in a comma-delimited format. + """ + + p_init: float = Field( + default=0.0, + ge=0.0, + le=1.0, + validation_alias=AliasChoices("p_init", "p_prep"), + serialization_alias="p_prep", + ) + p_meas_0: float = Field(default=0.0, ge=0.0, le=1.0) + p_meas_1: float = Field(default=0.0, ge=0.0, le=1.0) + p1: float = Field(default=0.0, ge=0.0, le=1.0) + p2: float = Field(default=0.0, ge=0.0, le=1.0) + p1_emission_ratio: float = Field(default=0.0, ge=0.0, le=1.0) + p2_emission_ratio: float = Field(default=0.0, ge=0.0, le=1.0) + p1_pauli_model: dict[str, float] = Field(default_factory=dict) + p1_emission_model: dict[str, float] = Field(default_factory=dict) + p2_pauli_model: dict[str, float] = Field(default_factory=dict) + p2_emission_model: dict[str, float] = Field(default_factory=dict) + p_prep_leak_ratio: float = Field(default=0.0, ge=0.0, le=1.0) + p1_seepage_prob: float = Field(default=0.0, ge=0.0, le=1.0) + p2_seepage_prob: float = Field(default=0.0, ge=0.0, le=1.0) + scale: float = Field(default=1.0, ge=0.0) + memory_scale: float = Field(default=1.0, ge=0.0) + init_scale: float = Field( + default=1.0, + ge=0.0, + validation_alias=AliasChoices("init_scale", "prep_scale"), + serialization_alias="prep_scale", + ) + meas_scale: float = Field(default=1.0, ge=0.0) + p1_scale: float = Field(default=1.0, ge=0.0) + p2_scale: float = Field(default=1.0, ge=0.0) + emission_scale: float = Field(default=1.0, ge=0.0) + przz_a: float | None = None + przz_b: float | None = None + przz_c: float | None = None + przz_d: float | None = None + przz_power: float = 1.0 + p_crosstalk_meas: float = Field( + default=0.0, + ge=0.0, + le=1.0, + validation_alias=AliasChoices("p_crosstalk_meas", "p_meas_crosstalk"), + serialization_alias="p_meas_crosstalk", + ) + p_crosstalk_init: float = Field( + default=0.0, + ge=0.0, + le=1.0, + validation_alias=AliasChoices("p_crosstalk_init", "p_prep_crosstalk"), + serialization_alias="p_prep_crosstalk", + ) + noiseless_gates: list[str] = Field(default_factory=list) + coherent_dephasing: bool = True + coherent_to_incoherent_factor: float = 1.5 + leak2depolar: bool = False + p_meas_crosstalk_scale: float = 1.0 + p_prep_crosstalk_scale: float = 1.0 + crosstalk_per_gate: bool | None = None + linear_dephasing_rate: float = Field( + default=0.0, + ge=0.0, + validation_alias=AliasChoices("linear_dephasing_rate", "p_idle_linear_rate"), + serialization_alias="p_idle_linear_rate", + ) + quadratic_dephasing_rate: float = Field( + default=0.0, + ge=0.0, + validation_alias=AliasChoices( + "quadratic_dephasing_rate", "p_idle_quadratic_rate" + ), + serialization_alias="p_idle_quadratic_rate", + ) + p2_idle: float = Field(default=0.0, ge=0.0) + p_idle_linear_model: dict[str, float] = Field(default_factory=dict) + + @model_validator(mode="after") + def check_valid_config(self) -> Self: + """Validate the error model configuration.""" + _validate_distribution("p1_pauli_model", self.p1_pauli_model, _KEYS_1Q_PAULI) + _validate_distribution( + "p1_emission_model", self.p1_emission_model, _KEYS_1Q_EMISSION + ) + _validate_distribution( + "p_idle_linear_model", self.p_idle_linear_model, _KEYS_1Q_EMISSION + ) + _validate_distribution("p2_pauli_model", self.p2_pauli_model, _KEYS_2Q_PAULI) + _validate_distribution( + "p2_emission_model", self.p2_emission_model, _KEYS_2Q_EMISSION + ) + + przz_params = [self.przz_a, self.przz_b, self.przz_c, self.przz_d] + if not ( + all(x is None for x in przz_params) + or all(x is not None for x in przz_params) + ): + raise ValueError( + "When setting przz_x, you must either set the four of them, or none." + ) + return self + + class UserErrorParams(BaseModel): """User provided error values that override machine values for emulation of Quantinuum Systems hardware. diff --git a/tests/models/test_backend_config.py b/tests/models/test_backend_config.py index f1142d9..3f56a72 100644 --- a/tests/models/test_backend_config.py +++ b/tests/models/test_backend_config.py @@ -107,6 +107,16 @@ def test_selene_plus_config_roundtrip( """Test roundtrip of SelenePlusConfig, importantly the ability to discriminate the error model and the runtime.""" + if error_model_class is QSystemErrorModel and runtime_class is not HeliosRuntime: + with pytest.raises(ValidationError): + SelenePlusConfig( + runtime=runtime_class(), + simulator=simulator_class(), + error_model=error_model_class(), + n_qubits=4, + ) + return + config = SelenePlusConfig( runtime=runtime_class(), simulator=simulator_class(), diff --git a/uv.lock b/uv.lock index 16da358..176c6e7 100644 --- a/uv.lock +++ b/uv.lock @@ -833,7 +833,7 @@ wheels = [ [[package]] name = "quantinuum-schemas" -version = "6.0.0" +version = "7.0.0" source = { editable = "." } dependencies = [ { name = "orjson" }, @@ -868,7 +868,7 @@ dev = [ { name = "pylint-pytest", specifier = ">=1.1.8" }, { name = "pyright", specifier = ">=1.1.302" }, { name = "pytest", specifier = ">=8.0.0,<9.0.0" }, - { name = "pytest-cov", specifier = ">=5.0.0,<6.0.0" }, + { name = "pytest-cov", specifier = ">=5.0.0,<7.0.0" }, { name = "ruff", specifier = ">0.11" }, ]