Codi font per a qilisdk.backends.backend_config

# Copyright 2026 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

import secrets
from abc import ABC, abstractmethod
from typing import Any, Literal

import psutil
from pydantic import BaseModel, Field, field_validator
from pydantic import ConfigDict as PydanticConfigDict

# ----------------------------
# Base config interface
# ----------------------------

[documents] ConfigValue = bool | int | float | str
[documents] SolverConfigDict = dict[str, ConfigValue]
[documents] class BaseSimulatorConfig(BaseModel, ABC): """Abstract base class for all QiliSim configuration sections.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Disallow positional arguments to keep configuration explicit. Raises: TypeError: If positional arguments are provided. """ if args: cls = self.__class__ field_names = ", ".join(cls.model_fields.keys()) raise TypeError( f"{cls.__name__} does not accept positional arguments. " f"Use keyword arguments instead. " f"Valid fields: [{field_names}].\n" f"Received positional arguments: {args!r}." ) super().__init__(**kwargs) @abstractmethod
[documents] def get_config(self) -> SolverConfigDict: """Serialize the configuration to the flat dictionary consumed by the C++ backend."""
# ---------------------------- # Analog (time evolution) config # ----------------------------
[documents] class MonteCarloConfig(BaseSimulatorConfig): """Configuration for Monte Carlo trajectory sampling in open-system simulations. Args: trajectories (int): Number of Monte Carlo trajectories to simulate when Monte Carlo mode is enabled. Defaults to ``100``. """
[documents] trajectories: int = Field( default=100, gt=0, description="Number of Monte Carlo trajectories to simulate when Monte Carlo mode is enabled.", )
[documents] def get_config(self) -> SolverConfigDict: """Return Monte Carlo settings in backend-compatible key names.""" return {"num_monte_carlo_trajectories": self.trajectories}
[documents] class AnalogMethod(BaseSimulatorConfig): """Configuration for analog time-evolution method selection and its hyperparameters. Preferred constructors: - :meth:`integrator` for integrate-based evolution. - :meth:`arnoldi` for Krylov/Arnoldi-based evolution. - :meth:`direct` for direct evolution. Args: evolution_method (str): Analog time-evolution method to use: ``"direct"``, ``"arnoldi"``, ``"integrate_rk4"``, ``"integrate_rk45_matrix_free"``, ``"integrate_rk4_matrix_free"``, or ``"variational_exponential"``. Defaults to ``"integrate_rk4_matrix_free"``. arnoldi_dim (int): Dimension of the Arnoldi Krylov subspace used when ``evolution_method="arnoldi"``. Defaults to ``10``. num_arnoldi_substeps (int): Number of integration substeps per schedule step for the Arnoldi method. Defaults to ``1``. """
[documents] evolution_method: Literal[ "direct", "arnoldi", "integrate_rk4", "integrate_rk45_matrix_free", "integrate_rk4_matrix_free", "variational_exponential", ] = Field( default="integrate_rk4_matrix_free", description="Analog time-evolution method to use: 'direct', 'arnoldi', 'integrate_rk4', 'integrate_rk45_matrix_free', 'integrate_rk4_matrix_free', or 'variational_exponential'.", )
[documents] arnoldi_dim: int = Field( default=10, gt=0, description="Dimension of the Arnoldi Krylov subspace used when `evolution_method='arnoldi'`.", )
[documents] num_arnoldi_substeps: int = Field( default=1, gt=0, description="Number of integration substeps per schedule step when using the Arnoldi method.", )
[documents] adaptive_tol: float = Field( default=1e-2, gt=0, description="Tolerance for the adaptive integrator method when `evolution_method='integrate_rk45_matrix_free'`.", )
[documents] variational_shots: int = Field( default=100, gt=0, description="Number of shots to use when estimating expectation values for the variational optimization when `evolution_method='variational_exponential'`.", )
[documents] variational_warmups: int = Field( default=10, ge=0, description="Number of warmup iterations to perform before collecting samples for the variational optimization when `evolution_method='variational_exponential'`.", )
[documents] variational_order: int = Field( default=2, gt=0, description="Order of the polynomial expansion used in the variational ansatz when `evolution_method='variational_exponential'`.", )
[documents] def get_config(self) -> SolverConfigDict: """Return a complete analog solver configuration for the C++ backend.""" d: SolverConfigDict = { "evolution_method": self.evolution_method, "arnoldi_dim": self.arnoldi_dim, "num_arnoldi_substeps": self.num_arnoldi_substeps, "adaptive_tol": self.adaptive_tol, "variational_shots": self.variational_shots, "variational_warmups": self.variational_warmups, "variational_order": self.variational_order, } return d
@classmethod
[documents] def integrator(cls, *, matrix_free: bool = True) -> AnalogMethod: """Build an ``integrate`` analog method configuration. Args: matrix_free (bool): Whether to use the matrix-free implementation for the Integrate method. Defaults to ``False``. Returns: AnalogMethod: Configured integrate-method analog configuration. """ evolution_method = "integrate_rk4_matrix_free" if matrix_free else "integrate_rk4" return cls(evolution_method=evolution_method)
@classmethod
[documents] def variational_annealing(cls, *, order: int = 2, shots: int = 100, warmups: int = 10) -> AnalogMethod: """ Anneal a variational ansatz rather than the full state. Based on this paper: https://arxiv.org/pdf/2403.05147 Args: order (int): Order of the polynomial expansion used in the variational ansatz. shots (int): Number of samples to use when estimating expectation values for the variational optimization. warmups (int): Number of warmup iterations to perform before collecting samples for the variational optimization. Returns: AnalogMethod: Configured variational-method analog configuration. """ return cls( evolution_method="variational_exponential", variational_order=order, variational_shots=shots, variational_warmups=warmups, )
@classmethod
[documents] def adaptive_integrator(cls, *, tol: float = 1e-2) -> AnalogMethod: """Build an ``adaptive_integrate`` analog method configuration. This uses a Dormand-Prince Runge-Kutta 4/5 method with adaptive step size control. It automatically adjusts the integration timestep to maintain a local error estimate below the specified tolerance, which can improve efficiency for problems with varying timescales. Args: tol (float): Tolerance for the adaptive algorithm. Defaults to ``1e-2``. This relates to the allowed fidelity error between the RK4 and RK5 estimates. Returns: AnalogMethod: Configured integrate-method analog configuration. """ return cls(evolution_method="integrate_rk45_matrix_free", adaptive_tol=tol)
@classmethod
[documents] def arnoldi( cls, *, num_substeps: int = 1, dim: int = 10, ) -> AnalogMethod: """Build an ``arnoldi`` analog method configuration. Args: num_substeps (int): Number of integration substeps per schedule step when using the Arnoldi method. Defaults to ``1``. dim (int): Dimension of the Arnoldi Krylov subspace. Defaults to ``10``. Returns: AnalogMethod: Configured arnoldi-method analog configuration. """ return cls( evolution_method="arnoldi", arnoldi_dim=dim, num_arnoldi_substeps=num_substeps, )
@classmethod
[documents] def direct(cls) -> AnalogMethod: """Build a ``direct`` analog method configuration. Returns: AnalogMethod: Configured direct-method analog configuration. """ return cls(evolution_method="direct")
# ---------------------------- # Execution config # ----------------------------
[documents] class ExecutionConfig(BaseSimulatorConfig): """Configuration for execution-level controls (threading and randomness). Args: num_threads (int): Number of CPU threads used for simulation. If set to ``0``, all available cores are selected. Defaults to ``0``. seed (int | None): Random seed used by the simulator. If ``None``, a random seed is generated. Defaults to ``None``. monte_carlo (MonteCarloConfig | None): Monte Carlo configuration. If ``None``, Monte Carlo is disabled and deterministic evolution is used. Defaults to ``None``. """
[documents] model_config = PydanticConfigDict(validate_default=True)
[documents] num_threads: int = Field( default=0, ge=0, description=("Number of CPU threads used for simulation. If set to 0, all available cores are selected."), ) # 0 means "use all cores"
[documents] seed: int | None = Field( default=None, ge=0, description="Random seed used by the simulator. If `None`, a random seed is generated.", )
# None means Monte-Carlo disabled
[documents] monte_carlo: MonteCarloConfig | None = Field( default=None, description=( "Monte Carlo configuration. If `None`, Monte Carlo is disabled and deterministic evolution is used." ), )
[documents] measurement_collapse: bool = Field( default=False, description=( "Whether to apply state collapse immediately after measurements. If `False`, measurements are recorded but the state is not collapsed." ), )
[documents] def get_config(self) -> SolverConfigDict: """Return execution settings with resolved defaults.""" d: dict[str, float | str] = { "num_threads": self.num_threads, "seed": int(self.seed) if self.seed is not None else 0, # defensive "monte_carlo": self.monte_carlo is not None, "measurement_collapse": self.measurement_collapse, } if self.monte_carlo is not None: d.update(self.monte_carlo.get_config()) else: d.update({"num_monte_carlo_trajectories": 100}) return d
@field_validator("num_threads", mode="after") @classmethod def _validate_num_threads(cls, num_threads: int) -> int: if num_threads <= 0: return psutil.cpu_count(logical=False) or 1 return num_threads @field_validator("seed", mode="after") @classmethod def _validate_seed(cls, seed: int | None) -> int: if seed is None: return secrets.randbelow(2**15) return seed
# ---------------------------- # Digital config # ----------------------------
[documents] class DigitalMethod(BaseSimulatorConfig): """Configuration for digital-circuit simulation options. Preferred constructors: - :meth:`statevector` for standard state-vector simulation settings. Args: max_cache_size (int): Maximum number of cached gate representations used by the digital simulator. Defaults to ``1000``. normalize_after_each_gate (bool): Whether to normalize the statevector after each gate application to mitigate numerical errors at the cost of increased runtime. Defaults to ``True``. combine_single_qubit_gates (bool): Whether to combine consecutive single-qubit gates into a single operation to reduce overhead at the cost of increased memory usage. Defaults to ``True``. matrix_free (bool): Whether to use the matrix-free implementation for statevector simulation. Defaults to ``True``. """
[documents] max_cache_size: int = Field( default=1000, ge=0, description="Maximum number of cached gate representations used by the digital simulator.", )
[documents] normalize_after_each_gate: bool = Field( default=False, description="Whether to normalize the statevector after each gate application to mitigate numerical errors at the cost of increased runtime.", )
[documents] combine_single_qubit_gates: bool = Field( default=True, description="Whether to combine consecutive single-qubit gates into a single operation to reduce overhead at the cost of increased memory usage.", )
[documents] matrix_free: bool = Field( default=True, description="Whether to use the matrix-free implementation for statevector simulation.", )
[documents] def get_config(self) -> SolverConfigDict: """Return digital simulation settings in backend-compatible key names.""" return { "max_cache_size": self.max_cache_size, "sampling_method": "statevector_matrix_free" if self.matrix_free else "statevector", "normalize_after_each_gate": self.normalize_after_each_gate, "combine_single_qubit_gates": self.combine_single_qubit_gates, }
@classmethod
[documents] def statevector( cls, *, max_cache_size: int = 1000, normalize_after_each_gate: bool = False, matrix_free: bool = True, combine_single_qubit_gates: bool = True, ) -> DigitalMethod: """Build the standard statevector simulation configuration. Args: max_cache_size (int): Maximum number of cached gate representations used by the digital simulator. Defaults to ``1000``. normalize_after_each_gate (bool): Whether to normalize the statevector after each gate application to mitigate numerical errors at the cost of increased runtime. Defaults to ``False``. matrix_free (bool): Whether to use the matrix-free implementation for statevector simulation. Defaults to ``True``. combine_single_qubit_gates (bool): Whether to combine consecutive single-qubit gates into a single operation. Defaults to ``True``. Returns: DigitalMethod: Configured statevector digital configuration. """ return cls( max_cache_size=max_cache_size, normalize_after_each_gate=normalize_after_each_gate, combine_single_qubit_gates=combine_single_qubit_gates, matrix_free=matrix_free, )