QiliSDK
Abstract
Features Introduced a real expression-tree AST rooted at the new qilisdk.core.Expression base class, replacing the previous flattened-polynomial model. Building expressions is unchanged: 2 * BinaryVariable("b") + 1 and every other arithmetic spelling still works exactly as before, the resulting object is just an Expression instead of a Term. Symbolic values (gate angles, Hamiltonian coefficients, schedule coefficients, model objectives/constraints) are now uniformly typed Expression, built from Add/Mul/Pow/Constant and the unary Function nodes Sin/Cos/Exp/Log/Tan/Sqrt/Abs. The new tree adds symbolic differentiation (Expression.derivative), distribution (Expression.expand), substitution (Expression.substitute), operand listing (Expression.to_list), arbitrary unary functions, and non-integer/symbolic powers, while preserving order-independent structural equality (x + y == y + x). Variable now defaults to Domain.INTEGER, so the domain only has to be passed when you want a different one. It also fixes a wrong-answer bug in the old model, where a product of two identical function terms collapsed into a sum: Sin(x) * Sin(x) evaluated as 2 * sin(x) and is now sin(x) ** 2. (PR #226) A new digital simulation method has been added to QiliSim, based on stabilizer states. This allow efficient simulation of circuits with small numbers of T gates. For example, to sample from a 1000-qubit GHZ state in around 10s on a laptop: from time import time from qilisdk.digital import Circuit, H, CNOT from qilisdk.backends import QiliSim, DigitalMethod from qilisdk.readout import Readout from qilisdk.functionals import DigitalPropagation # Settings nqubits = 1000 nshots = 10000 # Circuit creating a GHZ state c = Circuit(nqubits) c.add(H(0)) for i in range(1, nqubits): c.add(CNOT(i - 1, i)) # Set everything up digital = DigitalPropagation(circuit=c) backend_stab = QiliSim(digital_simulation_method=DigitalMethod.stabilizer()) readout = Readout().with_sampling(nshots) # Run it t0 = time() results_stab = backend_stab.execute(digital, readout) t1 = time() samples_stab = results_stab.get_samples() print(samples_stab) print(f"Execution time: {t1 - t0} seconds") It can also do more general circuits, however either a performance cost (scaling as 2^{number of T gates}) or an accuracy cost (if truncation is enabled via setting max_states to something non-zero). (PR #239) Support for time-dependent Lindblad noise has been added, supported in both QiliSim and QutipBackend: from qilisdk.noise import LindbladGenerator from qilisdk.core import QTensor import numpy as np J1 = QTensor(np.array([[0, 1], [0, 0]])) T = 100 lindblad_noise = LindbladGenerator(jump_operators=[J1], rates=[lambda t: 0.005 * (1 + np.sin(t / T))]) By extension, QutipBackend now also supports general noise for analog evolution (but for now still not for digital). (PR #245) GPU support has been added to the Variational Annealing method in QiliSim. No extra dependencies have been added, it tries to load the CUDA libraries as they would be installed via the QiliSDK CUDA optional install, and defaults to CPU if that fails. Usage is via a flag in the ExecutionConfig: from qilisdk.backends import QiliSim, AnalogMethod, ExecutionConfig backend = QiliSim( analog_simulation_method=AnalogMethod.variational_annealing(), execution_config=ExecutionConfig(gpu=True), ) For a 60-qubit test case (an X to ZZ chain, second-order ansatz, defaults nshots), the time taken goes from 108s (8-core CPU), to 18s (8-core CPU + RTX 4060 GPU). (PR #252) QiliSim statevector performance has been improved for both digital and analog simulation: For a 27 qubit random circuit of 1000 gates, sampling time is down from 44s -> 16s (2.75x speedup). For a 25 qubit X to ZZ annealing task, time to reach 99% of the solution is down from 122s -> 79s (1.54x speedup). This was done by analyzing the assembly via perf, then improving the memory accesses and ensuring that we correctly use SIMD instructions. We also implement gate-fusion, where nearby gates that act on similar sections of the statevector are grouped and applied together to improve memory access. To control this, there are two new arguments to the DigitalMethod.statevector() method: fuse_gates and max_fused_qubits. fuse_gates is a boolean toggle to decide whether to use gate fusion (defaults to on), whilst max_fused_qubits decides how many qubits we should try to fuse at once, defaulting to 4, which has been benchmarked and seems to be optimum (based on modern CPU cache sizes). Performance can be pushed if the user compiles specific to their machine with the new compile flag -Ccmake.define.build_native=ON, which for newer CPUs can offer some slight benefit. If the user cares more about performance than precision, they can also compile using single-precision floats rather than doubles with the new flag -Ccmake.define.single_precision=ON. This is fine for sampling (since finite sampling is inherently noisy) but can give non-physical values for expectation values or state tomography. On my laptop with both flags the above random circuit takes 14s instead, so a ~10% speedup. (PR #255) Two new classical solvers have been added: ScipySolver and ScipSolver. SCIP is one of the leading mixed-integer programming solvers which doesn't need a license (compared to something like Gurobi). It can be installed via an optional extra: uv pip install qilisdk[scip] Usage is the same as with the BruteForceSolver: from qilisdk.core import Model from qilisdk.utils.classical_solvers import BruteForceSolver, ScipySolver, ScipSolver model = Model.random_ising(10) # solver = BruteForceSolver() # solver = ScipSolver() solver = ScipySolver(method="nelder-mead") solution = solver.solve(model) print(solution) For a random Ising model with 13 qubits: Solver Time Solution ------------------------------------------------------------------ Brute force 7.4171 s -5.1688 Scipy (Nelder-Mead) 0.6702 s -4.4559 Scipy (Powell) 0.7337 s -4.4559 Scipy (TNC) 0.1107 s -2.8488 Scipy (COBYLA) 0.1357 s -4.4323 Scipy (differential_evolution) 4.3912 s -5.1688 Scipy (basinhopping) 1.4425 s -3.3048 SCIP 0.4231 s -5.1688 QiliSim (statevector) 1.4903 s -5.0092 QiliSim (VA, order=1) 1.0353 s -4.5232 QiliSim (VA, order=2) 2.3634 s -5.1386 For a random Ising model with 30 qubits: Solver Time Solution ------------------------------------------------------------------ SCIP 2.3561 s -28.1468 QiliSim (VA, order=1) 3.4792 s -28.0689 QiliSim (VA, order=2) 32.7094 s -28.1075 ``` ([PR #259](https://github.com/qilimanjaro-tech/qilisdk/pull/259)) Previously QTensor used a row-sparse matrix format, which was better some quantum objects (e.g. gates) but not so good for things like statevectors. Now, the C++ side of QTensor detects the best sparsity pattern based on the initialization, allowing faster initialization and manipulation. The before and after, comparing versus numpy for some 25-qubit state initializatons: # Task | Numpy | QTensor Before | QTensor After # --------|---------|----------------|---------------- # zero | 0.00003 | 0.37700 | 0.00001 # uniform | 0.09614 | 1.19820 | 0.08174 # ket | 0.00002 | 0.38340 | 0.00001 # bra | 0.00001 | 0.00002 | 0.00000 ``` ([PR #265](https://github.com/qilimanjaro-tech/qilisdk/pull/265)) Updated SpeQtrum job submission to display maintenance messages from PublicAPI and fixed the execution type when submitting a quantum reservoir payload. (PR #270) When executing a job, there is now a new field that can be returned: the time taken for execution. For now this is only used for local simulation jobs, but later support may be added for real quantum hardware execution time. Example: from qilisdk.digital import Circuit, H, CNOT from qilisdk.backends import QiliSim from qilisdk.functionals import DigitalPropagation from qilisdk.readout import Readout c = Circuit(5) c.add(H(0)) c.add(CNOT(0, 1)) func = DigitalPropagation(c) readout = Readout().with_sampling(nshots=100) backend = QiliSim() res = backend.execute(func, readout) print(res) ``` ([PR #276](https://github.com/qilimanjaro-tech/qilisdk/pull/276)) More logging has been added to QiliSDK, using the loguru library as before. Different logging levels are now used throughout the library (TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL) and can be enabled either via changed the logging_config.yaml or via the configure function as follows: from qilisdk.logging import configure_logging configure_logging(level="DEBUG") (PR #278) The normalization of the state in all simulation methods in QiliSim can now be disabled: from qilisdk.backends import QiliSim, ExecutionConfig backend = QiliSim(execution_config=ExecutionConfig(normalize_state=False)) (PR #280) QiliSim performance has been generally increased via a variety of micro-optimizations and improvements to gate fusion. The Arnoldi step now does matrix-free operations by default, increasing performance, although the integrators are still faster. Reservoirs have benefited especially from these performance improvements. Approximate performance improvements: digital statevector simulation: ~1.5x analog integration: ~1.2x reservoirs: ~10x (PR #283) Some generators of common machine-learning datasets have been added to QiliSDK. These are useful when working with quantum reservoirs. They all have a generate method that allows generating samples as follows: from qilisdk.ml.datasets import MackeyGlass inputs, targets = MackeyGlas
Publication details
- Journal / source
- Zenodo (CERN European Organization for Nuclear Research)
- Publication date
- 9 September 2026
- Language
- EN