0.2.0
Features
-
Added quantum reservoir computing support through the new
QuantumReservoir,ReservoirLayer, andReservoirInputAPIs. Backends can now execute reservoir workflows with per-layer inputs, optional pre/post encoding stages, and optional intermediate/final state output.Example:
from qilisdk.analog import Schedule, Z from qilisdk.backends import QiliSim from qilisdk.core import ket from qilisdk.digital import Circuit, RX from qilisdk.functionals import QuantumReservoir, ReservoirInput, ReservoirLayer from qilisdk.readout import Readout theta = ReservoirInput("res_in", 0.0) pre = Circuit(1) pre.add(RX(0, theta=theta)) layer = ReservoirLayer( evolution_dynamics=Schedule( hamiltonians={"h": Z(0)}, coefficients={"h": {(0.0, 1.0): 1.0}}, dt=0.1, ), input_encoding=pre, ) reservoir = QuantumReservoir( initial_state=ket(0), reservoir_layer=layer, input_per_layer=[{"res_in": 0.1}, {"res_in": 0.4}], ) result = QiliSim().execute(reservoir, Readout().with_expectation([Z(0)])) print(result.get_expectation_values())
(PR #143)
-
Noise models are now supported in QiliSim. Usage is the same as with CudaBackend:
from qilisdk.digital import Circuit, X from qilisdk.noise import NoiseModel, BitFlip from qilisdk.functionals import DigitalPropagation from qilisdk.backends import QiliSim from qilisdk.readout import Readout circuit = Circuit(nqubits=1) circuit.add(X(0)) noise_model = NoiseModel() noise_model.add(BitFlip(probability=0.5)) backend = QiliSim(noise_model=noise_model) result = backend.execute(DigitalPropagation(circuit), Readout().with_sampling(100))
(PR #146)
-
CUDA 13 is now officially supported alongside CUDA 12, and the optional dependency system has been refactored to support these mutually exclusive CUDA variants in a clean and explicit way. The internal optional-import mechanism now supports "ANY-of" dependency groups, allowing the
cudafeature to be satisfied by either the CUDA 12 or CUDA 13 distribution. If neither is installed, CUDA symbols (e.g.,CudaBackend) resolve to informative stubs that raise anOptionalDependencyErrorwith clear installation instructions indicating the valid extras.Two new mutually exclusive extras are introduced:
cuda12→ installs the CUDA 12 backend stackcuda13→ installs the CUDA 13 backend stack
For convenience, aggregated extras are also provided:
all-cu12→ installs all optional features plus CUDA 12all-cu13→ installs all optional features plus CUDA 13
For backwards compatibility, the legacy
cudaandallspecifiers are temporarily preserved and default to CUDA 12:cuda→ aliases tocuda12all→ equivalent toall-cu12
This ensures existing environments and CI pipelines continue to work unchanged while users transition to explicit CUDA version selection. Conflicts between incompatible extras (e.g.,
cuda12vscuda13,all-cu12vsall-cu13) are enforced viauvconfiguration to prevent invalid combinations.User installation examples:
-
Core only (no CUDA):
pip install qilisdk -
CUDA 12 (explicit):
pip install "qilisdk[cuda12]"
or
uv sync --extra cuda12 -
CUDA 13 (explicit):
pip install "qilisdk[cuda13]"
or
uv sync --extra cuda13 -
Full stack with CUDA 12:
pip install "qilisdk[all-cu12]"
or
uv sync --extra all-cu12 -
Full stack with CUDA 13:
pip install "qilisdk[all-cu13]"
or
uv sync --extra all-cu13 -
Backwards-compatible installs (defaulting to CUDA 12):
pip install "qilisdk[cuda]"
pip install "qilisdk[all]"
Developer installation examples:
-
Development + CUDA 12:
uv sync --group dev --extra cuda12
or
pip install -e ".[dev,cuda12]" -
Development + CUDA 13:
uv sync --group dev --extra cuda13
or
pip install -e ".[dev,cuda13]" -
Development without CUDA (CPU-only CI):
uv sync --group dev
(PR #154)
-
Added a composable digital circuit transpilation framework centered around
CircuitTranspilerand concrete implemetations ofCircuitTranspilerPass. The new transpiler is intentionally thin: it executes an ordered list of non-mutating passes, resets shared state for everytranspile()call, and returns aCircuitTranspilerResultthat exposes the transpiled circuit throughcircuit, the per-pass outputs throughintermediate_results, the final user-facing logical-to-physical mapping throughlayout, and any pass-level diagnostics throughmetrics.The default transpiler has two modes. Without topology information,
CircuitTranspiler.default()builds a local-rewrite pipeline made ofDecomposeMultiControlledGatesPass,CancelIdentityPairsPass,DecomposeToCanonicalBasisPass, andFuseSingleQubitGatesPass. This path is meant for basis normalization and cheap circuit cleanup without introducing device-specific constraints. When a topology is provided, the default pipeline becomes hardware-aware: it performs the same decomposition and simplification steps first, then applies layout and routing (SabreLayoutPassplusSabreSwapPasswhen no mapping is given, orCustomLayoutPasswhen a user mapping is supplied), and finally runs canonical decomposition and single-qubit fusion again so any routing artefacts are pushed back into the requested gate basis.DecomposeMultiControlledGatesPasshandles the structural lowering of multi-controlled single-qubit operations before any basis work is attempted. Gates such asX(2).controlled(0, 1)are recursively rewritten into equivalent sequences containing only single-control operations, square roots of the target gate, their adjoints, and auxiliary controlled Pauli-X operations. Single-control gates are left unchanged, while controlled versions of multi-qubit payloads are rejected explicitly, which keeps later passes focused on 1Q/2Q synthesis only.CancelIdentityPairsPassperforms fixed-point cancellation of gates whose product is the identity up to global phase, even when those gates are separated by operations on disjoint qubits. It removes explicitIgates immediately, recognizes self-inverse primitives such asH,X,Y,Z,CNOT,CZ, andSWAP, cancels parametric inverses such asRX(theta)withRX(-theta)andU3(theta, phi, gamma)withU3(-theta, -gamma, -phi), understandsAdjoint(...)wrappers and controlled inverse pairs, and falls back to matrix-based matching for custom gates that expose a unitary. Measurements and gates without a matrix act as barriers on the qubits they touch, so the pass stays conservative around non-unitary or opaque operations.DecomposeToCanonicalBasisPassis responsible for basis normalization. It maps supported one-qubit gates into either aU3basis or anRX/RY/RZbasis, depending onsingle_qubit_basis, and rewrites two-qubit structure into eitherCNOT-based orCZ-based entangling skeletons, depending ontwo_qubit_basis. In practice this meansH,X,Y,Z,U1,U2, arbitrary one-qubitBasicGateinstances,CNOT,CZ,SWAP, single-control controlled one-qubit gates,Adjoint(...), and one-qubitExponential(...)gates can all be rewritten into a circuit containing only the chosen 1Q basis, the chosen 2Q entangler, and measurements. Multi-controlled gates are deliberately out of scope here and are expected to be lowered byDecomposeMultiControlledGatesPassfirst.FuseSingleQubitGatesPassthen compresses maximal adjacent runs of one-qubit unitary gates on each wire into a basis-respecting sequence. InSingleQubitGateBasis.U3mode, each fused run is emitted as a singleU3. InSingleQubitGateBasis.RxRyRzmode, the pass emits compact axis-aligned rotations when the fused unitary is recognizable as pureRX,RY, orRZ, and otherwise falls back to a canonicalRZ-RY-RZrealization. Multi-qubit gates, measurements, and any unsupported or non-unitary operations flush pending fusion on the affected qubits, so gate order and circuit semantics are preserved.The topology-aware passes cover both automatic placement and user-directed placement.
SabreLayoutPassis a layout-only pass that chooses a logical-to-physical mapping with a SABRE-style heuristic using a front layer, a bounded look-ahead set, randomized trials, and a light decay penalty to avoid thrashing. It retargets the entire circuit to physical qubits, can enlarge the returned circuit to the device size, and records the selected initial layout in the shared context.SabreSwapPassstarts from either an explicitinitial_layout, the layout already present in the context, or a default identity-style placement, and then inserts SWAPs so every emitted two-qubit gate acts on an edge of the topology while preserving the original gate order. It stores the routed final layout inresult.layoutand exposesswap_countthroughresult.metrics.CustomLayoutPassis the deterministic alternative for users who already know where logical qubits should live: it validates that the mapping covers every logical qubit exactly once, is injective, and only references physical qubits in the topology; it then retargets the circuit to the requested physical qubits and, when necessary, inserts shortest-path SWAPs and immediately undoes them so later operations remain pinned to the user-requested mapping.Changing the target basis is now a one-line configuration change on the default transpiler. The example below requests an
RX/RY/RZone-qubit basis andCZas the only two-qubit entangler, so the final circuit contains only those basis gates plus measurements:from qilisdk.digital import CNOT, H, Circuit from qilisdk.digital.circuit_transpiler import CircuitTranspiler from qilisdk.digital.circuit_transpiler_passes import SingleQubitGateBasis, TwoQubitGateBasis circuit = Circuit(2) circuit.add(H(0)) circuit.add(CNOT(0, 1)) transpiler = CircuitTranspiler.default( single_qubit_basis=SingleQubitGateBasis.RxRyRz, two_qubit_basis=TwoQubitGateBasis.CZ, ) result = transpiler.transpile(circuit) assert all(gate.name in {"RX", "RY", "RZ", "CZ"} for gate in result.circuit.gates)
Supplying a topology activates layout and routing automatically. The topology can be provided either as a
rustworkx.PyGraphor as a simple edge list, and the transpiler will return both the routed circuit and routing diagnostics:from qilisdk.digital import CZ, Circuit from qilisdk.digital.circuit_transpiler import CircuitTranspiler circuit = Circuit(3) circuit.add(CZ(0, 2)) transpiler = CircuitTranspiler.default(topology=[(0, 1), (1, 2)]) result = transpiler.transpile(circuit) print(result.layout) # final logical -> physical mapping after routing print(result.metrics["swap_count"])
If you want to pin logical qubits to specific physical locations, pass
qubit_mappingtogether with the topology. In this mode the transpiler usesCustomLayoutPass, keeps the reported final layout equal to the user mapping, and routes non-adjacent two-qubit interactions with temporary SWAPs under the hood:from qilisdk.digital import CZ, RX, Circuit from qilisdk.digital.circuit_transpiler import CircuitTranspiler circuit = Circuit(2) circuit.add(RX(0, theta=0.5)) circuit.add(CZ(0, 1)) transpiler = CircuitTranspiler.default( topology=[(0, 1), (1, 2)], qubit_mapping={0: 2, 1: 0}, ) result = transpiler.transpile(circuit) assert result.layout == {0: 2, 1: 0} assert result.circuit.nqubits == 3
The pipeline is also fully customizable, which makes it easy to build a transpiler that only performs the stages you want and then inspect the circuit produced after each stage. In the example below, the transpiler lowers multi-controlled gates, removes trivial inverse pairs, canonicalizes everything to a
CZ+U3basis, and finally fuses one-qubit runs; the returnedintermediate_resultspreserve the per-pass circuit snapshots in execution order:from qilisdk.digital import Controlled, X, Circuit from qilisdk.digital.circuit_transpiler import CircuitTranspiler from qilisdk.digital.circuit_transpiler_passes import ( CancelIdentityPairsPass, DecomposeMultiControlledGatesPass, DecomposeToCanonicalBasisPass, FuseSingleQubitGatesPass, SingleQubitGateBasis, TwoQubitGateBasis, ) circuit = Circuit(3) circuit.add(Controlled(0, 1, basic_gate=X(2))) transpiler = CircuitTranspiler( pipeline=[ DecomposeMultiControlledGatesPass(), CancelIdentityPairsPass(), DecomposeToCanonicalBasisPass( single_qubit_basis=SingleQubitGateBasis.U3, two_qubit_basis=TwoQubitGateBasis.CZ, ), FuseSingleQubitGatesPass(single_qubit_basis=SingleQubitGateBasis.U3), ] ) result = transpiler.transpile(circuit) print([step.name for step in result.intermediate_results]) print(result.circuit)
(PR #164)
-
Refactored the QiliSim backend configuration API around typed Pydantic models and aligned docs/tests with the new usage.
Main changes:
- Introduced structured QiliSim configuration models:
AnalogMethod(withintegrator,arnoldi, anddirectbuilders)DigitalMethod(withstate_vectorbuilder)ExecutionConfig(threading and RNG seed controls)MonteCarloConfig(trajectory control for Monte Carlo mode)
- Updated
QiliSiminitialization to accept:analog_simulation_methoddigital_simulation_methodexecution_config
instead of many flat constructor parameters.
- Added
QiliSim.get_config()and keptsolver_paramsas a backward-compatible alias. - Fixed solver configuration wiring so execution paths use the refactored internal config dictionary.
- Expanded docstrings for new and affected methods/classes, including:
- per-field
Field(description=...)metadata for Pydantic model fields - top-level model parameter documentation for better IDE hover help
- classmethod argument documentation aligned with field descriptions.
- per-field
- Updated backend documentation (
docs/fundamentals/backends.rst) to describe the new configuration objects and provide an updated configuration example. - Updated and expanded QiliSim unit/integration tests to validate:
- new configuration builders and validation behavior
- new backend construction patterns using
ExecutionConfig/AnalogMethod/DigitalMethod/MonteCarloConfig - compatibility of shared backend integration suites with the new API.
(PR #165)
- Introduced structured QiliSim configuration models:
-
Matrix-free methods have been added to QiliSim. This is now the default simulation method for circuit sampling, so without changing your workflow you should notice performance and memory improvements. (PR #167)
-
QTensor has been moved to C++, giving increased performance.
The interface remains almost exactly the same, the only change being that now
QTensor * QTensordoes element-wise multiplication, for matrix multiplication please change to usingQTensor @ QTensor. (PR #168) -
Added an about() method to QiliSDK, as in Qililab, to output useful information about the installation to aid in debugging. (PR #170)
-
Almost all QiliSDK objects can now be printed (i.e. print(thing)) to show details about the object. (PR #171)
-
Restructured the functionals and results system with a decoupled, type-safe readout architecture.
Breaking Changes
Samplingrenamed toDigitalPropagation: The functional now takes only acircuitargument. Shot count is specified via readout.TimeEvolutionrenamed toAnalogEvolution: The functional now takesschedule,initial_state, andstore_intermediate_results. Observables and shot count are specified via readout.SamplingResultandTimeEvolutionResultremoved: All primitive functionals now return a unifiedFunctionalResult.backend.execute()now requires areadoutparameter: AReadoutspecification is passed at execution time instead of being bundled into the functional.
New Readout System
A new
qilisdk.readoutmodule provides theReadoutbuilder and three readout types.
Build a specification by chainingwith_*methods and pass it tobackend.execute():from qilisdk.analog import Z from qilisdk.readout import Readout from qilisdk.backends import QiliSim from qilisdk.functionals import DigitalPropagation from qilisdk.digital import Circuit backend = QiliSim() functional = DigitalPropagation(Circuit(2)) result = backend.execute(functional, readout=Readout().with_sampling(nshots=1000)) result = backend.execute(functional, readout=Readout().with_expectation(observables=[Z(0)])) result = backend.execute(functional, readout=Readout().with_state_tomography()) # Multiple readout types can be combined in a single execution result = backend.execute( functional, readout=Readout().with_sampling(nshots=500).with_expectation(observables=[Z(0)]), )
The three readout types are:
Readout().with_sampling(nshots): sample in the computational basis and collect bitstring counts.Readout().with_expectation(observables, nshots=0): compute⟨ψ|O|ψ⟩for each observable (nshots=0uses the exact state-vector inner product).Readout().with_state_tomography(): return the full quantum state vector.
Type-Safe Results
Readoutcarries generic type parametersS,E,Tthat track which readout slots are populated.
backend.execute()propagates these to the returnedFunctionalResult, so the type checker knows
which result fields are present without runtime guards:from qilisdk.analog import Z from qilisdk.readout import Readout # result.sampling is SamplingReadoutResult — type checker knows this, no Optional guard needed result = backend.execute(functional, readout=Readout().with_sampling(nshots=1000)) top2 = result.sampling.get_probabilities(n=2) # result.expectation is ExpectationReadoutResult — type checker knows this result = backend.execute(functional, readout=Readout().with_expectation(observables=[Z(0)])) evs = result.expectation.expectation_values
Migration Guide
Before (digital circuit sampling):
from qilisdk.functionals import Sampling sampling = Sampling(circuit=circuit, nshots=500) result = backend.execute(sampling) print(result.samples)
After:
from qilisdk.functionals import DigitalPropagation from qilisdk.readout import Readout functional = DigitalPropagation(circuit) result = backend.execute(functional, readout=Readout().with_sampling(nshots=500)) print(result.get_samples())
Before (analog time evolution):
from qilisdk.functionals import TimeEvolution te = TimeEvolution(schedule=schedule, initial_state=psi0, observables=[Z(0)], nshots=100) result = backend.execute(te) print(result.expected_values)
After:
from qilisdk.analog import Z from qilisdk.functionals import AnalogEvolution from qilisdk.readout import Readout evolution = AnalogEvolution(schedule=schedule, initial_state=psi0) result = backend.execute(evolution, readout=Readout().with_expectation(observables=[Z(0)]).with_state_tomography()) print(result.expectation_values)
Before (variational program):
vp = VariationalProgram(functional=Sampling(ansatz), optimizer=opt, cost_function=cost_fn) result = backend.execute(vp)
After:
from qilisdk.readout import Readout vp = VariationalProgram(functional=DigitalPropagation(ansatz), optimizer=opt, cost_function=cost_fn) result = backend.execute(vp, readout=Readout().with_sampling(nshots=1000))
Result Properties
The unified
FunctionalResultprovides convenience shortcuts:get_samples: shot counts (with_sampling)get_probabilities: measurement probabilities (with_samplingorwith_state_tomography)get_state: full quantum state (with_state_tomography)get_expectation_values: list of expectation values (with_expectation)intermediate_samples,intermediate_probabilities,intermediate_states,intermediate_expectation_values: per-step lists whenstore_intermediate_results=True
Typed forwarding properties (
result.sampling,result.expectation,result.state_tomography) return the raw result objects and are preferred for production code.Other Changes
- Added a
Readoutdocumentation page covering the builder pattern, all three readout types, result access, intermediate results, and a complete end-to-end example. - Added QiliSim C++ backend support for
DigitalPropagation,AnalogEvolution, andQuantumReservoir. - Added CUDA and QuTiP backend support for the new readout system.
- Updated SpeQtrum cloud submission (
SpeQtrum.submit) to acceptReadout. - Updated all documentation to reflect the new structure.
(PR #175)
-
Gate.matrixis now a cached property instead of a stored instance attribute. This considerably reduces serialization size by excluding the matrix from YAML output. (PR #177) -
Added a method to plot a single-qubit QTensor state on the Bloch sphere:
from qilisdk.core import QTensor state = QTensor.ket(0) state.draw()(PR #179)
-
QiliSim now supports mid-circuit measurements. Additionally, partial samples are now returned as "11_0", where a "_" indicates that the qubit wasn't measured. Usage:
from qilisdk.digital import Circuit, X, M from qilisdk.backends import QiliSim from qilisdk.readout import Readout from qilisdk.functionals import DigitalPropagation c = Circuit(2) c.add(X(0)) c.add(M(0)) c.add(X(0)) c.add(M(0)) c.add(M(1)) results = QiliSim().execute(DigitalPropagation(c), Readout().with_sampling(1000)) print(results)
(PR #185)
-
Added a new QTensor constructor to allow users to easily construct a uniform superposition state of a certain number of qubits:
from qilisdk.core import QTensor state = QTensor.uniform(2)
(PR #186)
-
QiliSim's default analog simulator - integrator - has been optimized and now offers around 4x performance for the same number of steps.
A new analog simulation method has also been added: adaptive_integrator, capable of speeding up when the problem is easy and slowing down when precision is needed. It does this using a Dormand-Prince-style RK45 method. For some cases this can offer a speedup, but the main advantage is that the user doesn't have to choose their dt carefully. Usage is as follows:
from qilisdk.backends import QiliSim, AnalogMethod backend = QiliSim(analog_simulation_method=AnalogMethod.adaptive_integrator(tol=0.01),)
(PR #188)
-
Several init methods to simplify the construction of basic Schedules have been added:
from qilisdk.analog import Schedule, X, Z H1 = X(0) H2 = Z(0) T = 10 dt = 0.1 schedule = Schedule.linear(H1, H2, T, dt) schedule = Schedule.quadratic(H1, H2, T, dt) schedule = Schedule.polynomial(H1, H2, T, dt, 5) schedule = Schedule.sinusoidal(H1, H2, T, dt)
(PR #190)
-
You can now generate a plot of the eigenspectrum of a Schedule for small (i.e. less than 7 qubits) systems.
This can also be combined with simulation data to visualize how the state evolves.
Usage is as follows:from qilisdk.backends import QiliSim, AnalogMethod from qilisdk.readout import Readout from qilisdk.functionals import AnalogEvolution from qilisdk.analog import Schedule, X, Z from qilisdk.core import QTensor, Interpolation nqubits = 5 T = 10.0 dt = 0.1 Hx = -sum(X(i) for i in range(nqubits)) Hz = sum(Z(i)*Z((i+1) % nqubits) for i in range(nqubits)) schedule = Schedule( hamiltonians={"driver": Hx, "problem": Hz}, coefficients={ "driver": {(0.0, T): lambda t: 1 - t / T}, "problem": {(0.0, T): lambda t: t / T}, }, dt=dt, interpolation=Interpolation.LINEAR, ) backend = QiliSim(analog_simulation_method=AnalogMethod.integrator()) evolution = AnalogEvolution(schedule=schedule, initial_state=QTensor.uniform(nqubits), store_intermediate_results=True) res = backend.execute(evolution, Readout().with_state_tomography()) schedule.draw_eigenvalues(levels=200, intermediate_states=res.get_intermediate_states(), show_overlaps=True)
which generates the following, where the percentages are the overlap with the evolved state versus each of the eigenstates:
(PR #191)
-
Model.to_qubo()now automatically linearizes pseudo-Boolean objectives and constraints of degree greater than two. High-degree monomials are reduced pairwise to quadratic form by introducing fresh auxiliary binary variablesw = a * benforced via the Rosenberg penaltyP(a, b, w) = a * b - 2 * a * w - 2 * b * w + 3 * wwhich is quadratic, non-negative, and zero iff
w = a * b. Shared sub-products (e.g.x*y*zandx*y*wboth reusingx*y) share a single auxiliary and a single penalty.Two new kwargs control the behavior:
linearize(defaultTrue): toggles the reduction. Setlinearize=Falseto keep the previous strict behavior where exporting a model with terms of degree 3+ raisesValueError.linearization_lagrange_multiplier(default100): the Lagrange multiplier applied to each Rosenberg penalty constraint.
Usage:
from qilisdk.core import BinaryVariable, EQ, Model, ObjectiveSense x, y, z = BinaryVariable("x"), BinaryVariable("y"), BinaryVariable("z") model = Model("cubic") model.set_objective(x * y * z, sense=ObjectiveSense.MAXIMIZE) model.add_constraint("forbid_triple", EQ(x * y * z, 0), lagrange_multiplier=10) qubo = model.to_qubo(linearization_lagrange_multiplier=50) ham = qubo.to_hamiltonian()
(PR #195)
-
Added QIR (Quantum Intermediate Representation) Base-Profile import and export, bridging
qilisdk.digital.Circuitand Microsoft's pyqir library. The new entry points live inqilisdk.utils.qir:to_qir(circuit, *, name="circuit")— serialize aCircuitto QIR textual LLVM IR.to_qir_file(circuit, filename, *, name=None)— write a.ll(textual) or.bc(bitcode) file; dispatched on extension.from_qir(qir_text)— parse QIR textual LLVM IR back into aCircuit.from_qir_file(filename)— read a.llor.bcfile.
pyqiris an optional dependency; install withpip install qilisdk[qir].Example:
from qilisdk.digital import CNOT, Circuit, H, M from qilisdk.utils.qir import from_qir, to_qir circuit = Circuit(2) circuit.add(H(0)) circuit.add(CNOT(0, 1)) circuit.add(M(0, 1)) qir_text = to_qir(circuit, name="bell") reparsed = from_qir(qir_text)
Supported gates map to the standard
__quantum__qis__*__bodyintrinsics:X,Y,Z,H,S,T,Adjoint(S),Adjoint(T),RX,RY,RZ,CNOT,CZ,SWAP, andM. Identity is emitted as a no-op. Gates outside this set (U1/U2/U3, three-qubit unitaries, arbitraryControlled/Exponentialwrappers) must be decomposed before export. Rotation angles are exported as their currently-resolved numeric values — rebind anyParametervalues to concrete numbers before callingto_qir.OpenQASM 2 and 3 support has been moved to an optional dependency group and now lives under the
qilisdk.utils.openqasmpackage. Install withpip install qilisdk[openqasm]; the public entry points (to_qasm2,from_qasm2,to_qasm2_file,from_qasm2_file,to_qasm3,from_qasm3,to_qasm3_file,from_qasm3_file) are unchanged but now imported fromqilisdk.utils.openqasmrather thanqilisdk.utils.openqasm2/qilisdk.utils.openqasm3:# Before from qilisdk.utils.openqasm2 import to_qasm2, from_qasm2 from qilisdk.utils.openqasm3 import to_qasm3, from_qasm3 # After from qilisdk.utils.openqasm import to_qasm2, from_qasm2, to_qasm3, from_qasm3
(PR #209)
Bugfixes
- Fixed a bug in the cuda backend with time evolution that was erasing information from the hamiltonian. (PR #152)
- Fixed issues with empty elements in cuda hamiltonian that caused issues during simulation. (PR #155)
- Fixed multiple backend issues in analog time evolution: CUDA now supports
QTensorobservables (via Hamiltonian conversion), CUDA Lindblad jump-operator dimension handling was corrected for global/per-qubit noise, and QiliSim now correctly applies Lindblad rates (jump_operators_with_rates) and expands global single-qubit jump operators per qubit so dephasing strength affects results as expected. (PR #157) - Fixed YAML serialization for
@yaml.register_classobjects so transient hash cache fields (_hash_cacheand_hash_chache) are always dumped asnull(None) without mutating the in-memory object state. (PR #158) - Reworked custom hashing to a deterministic
hashlib.blake2b-basedqilisdk.utils.hashing.hash(...)interface (no Python builtin hash dependency in the hashing pipeline), migrated Variable/Term/ComparisonTerm/QTensor/Hamiltonian/Pauli hashing to it, and added consistency tests for hashing behavior (stability, equality-consistency, and order-insensitive structures). (PR #159) - Fixed OpenQASM 2.0 import parsing by replacing parameter parsing with AST-based expression evaluation (supporting
pi, arithmetic, and standard functions likesin/cos/sqrt), handling inline//comments, and adding stricter gate-parameter parsing with clear errors for invalid/deeply nested expressions; expanded unit tests to cover these cases and full supported gate round-trips. (PR #162) - Fix numpy version constraints for macOS to avoid MKL resolution issue (PR #163)
- Fixed a bug when building the docs. Versions of sphinx >= 9 cause issues with the newest (0.2.4) version of sphinx-multiversion, so now we limit the version of sphinx to be less than 9. (PR #182)
- Build issues have been fixed for Mac, the "compile from source" guide in the docs now works as expected. (PR #193)
- Fixed
ReservoirLayer.set_parametersandReservoirLayer.set_parameter_boundsso they dispatch through each sub-component (input encoding, evolution dynamics, output encoding) instead of relying on the inheritedParameterizableimplementation. The previous behavior updatedParametervalues directly and bypassed the encoding gates' setters, leaving their cached matrices stale after a parameter update on the layer. Added tests covering both the matrix-cache invalidation on encoding gates and the propagation of bounds to each child component. (PR #197) - Fixed
Circuit.set_parameters,Circuit.set_parameter_bounds, andCircuit.set_prefixso they correctly handle a singleParametershared across multiple gates. PreviouslyCircuit._parameters_linkstored a single(label, gate)tuple per parameter, so only the last-added gate received the update; earlier gates kept their cached matrices stale after the underlyingParameterwas mutated. The link is now a list of every(label, gate)pair sharing a parameter, and all of them are updated. Added regression tests covering shared parameters acrossset_parameters,set_parameter_bounds, and theset_prefix+set_parametersflow. (PR #199) - Fixed
.envfile loading inQiliSDKSettingsso that only variables prefixed withQILISDK_are read, preventing unrelated environment variables in the user's.envfrom causing validation errors. (PR #201) - Fixed build issues when trying to compile QiliSDK from source on Windows using VSCode. (PR #202)
Improved Documentation
- Fixed a bug in the documentation whereby titles could be hidden by a highlight after searching. (PR #160)
- Added a "Tutorials" section to the documentation, covering some quantum basics as well as a number of useful examples.
The rest of the documentation has also been proof-read and adjusted. (PR #183) - The docs have been further refactored, splitting the larger pages into smaller ones. (PR #194)
- Updated the QIR feature table and added missing Spanish and Catalan translations for the QIR and OpenQASM documentation. (PR #214)
- Expanded the Backends documentation: full QiliSim page with simulation-method descriptions, per-backend functional-support tables (with partial-support markers for QuantumReservoir on CUDA and Qutip), a sampling-methods table for the CUDA backend, noise-model notes, and unified section structure across the QiliSim, CUDA, and Qutip pages. (PR #216)