diff --git a/src/Qiskit.jl b/src/Qiskit.jl index e18952b..562fca9 100644 --- a/src/Qiskit.jl +++ b/src/Qiskit.jl @@ -24,11 +24,13 @@ include("c_circuit.jl") include("c_target.jl") include("c_transpile.jl") include("c_observable.jl") +include("c_param.jl") end # module C import .C: LibQiskit +include("parameter.jl") include("circuit.jl") include("target.jl") include("transpile.jl") diff --git a/src/c_circuit.jl b/src/c_circuit.jl index 33856fd..c8d4a8d 100644 --- a/src/c_circuit.jl +++ b/src/c_circuit.jl @@ -108,6 +108,22 @@ function qk_circuit_gate(qc::Ref{QkCircuit}, gate::QkGate, qubits::AbstractVecto nothing end +function qk_circuit_parameterized_gate(qc::Ref{QkCircuit}, gate::QkGate, qubits::AbstractVector{<:Integer}, params::AbstractVector{Ptr{QkParam}}; offset::Int = 1)::Nothing + check_not_null(qc) + if length(qubits) != qk_gate_num_qubits(gate) + throw(ArgumentError("Unexpected number of qubits for gate.")) + end + if length(params) != qk_gate_num_params(gate) + throw(ArgumentError("Unexpected number of parameters for gate.")) + end + if !checkindex(Bool, range(offset, length=qk_circuit_num_qubits(qc)), qubits) + throw(ArgumentError("Invalid qubit index")) + end + qubits0 = Vector{UInt32}(qubits .- offset) + check_exit_code(LibQiskit.qk_circuit_parameterized_gate(qc, gate, qubits0, params)) + nothing +end + function qk_circuit_measure(qc::Ref{QkCircuit}, qubit::Integer, clbit::Integer; offset::Int = 1)::Nothing check_not_null(qc) if !checkindex(Bool, range(offset, length=qk_circuit_num_qubits(qc)), qubit) @@ -187,7 +203,7 @@ end export QkGate, QkCircuit, QkDelayUnit, QkParam export qk_circuit_free, qk_circuit_num_qubits, qk_circuit_num_clbits, qk_circuit_num_instructions, qk_circuit_get_instruction, qk_circuit_count_ops -export qk_circuit_gate, qk_circuit_measure, qk_circuit_reset, qk_circuit_barrier, qk_circuit_unitary, qk_circuit_delay +export qk_circuit_gate, qk_circuit_parameterized_gate, qk_circuit_measure, qk_circuit_reset, qk_circuit_barrier, qk_circuit_unitary, qk_circuit_delay # Export enum instances for e in (QkGate, QkDelayUnit) diff --git a/src/c_param.jl b/src/c_param.jl new file mode 100644 index 0000000..401d94b --- /dev/null +++ b/src/c_param.jl @@ -0,0 +1,165 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import .LibQiskit: QkParam + +function qk_param_free(param::Ptr{QkParam})::Nothing + LibQiskit.qk_param_free(param) + nothing +end + +function qk_param_new_symbol(name::String)::Ptr{QkParam} + ptr = LibQiskit.qk_param_new_symbol(name) + if ptr == C_NULL + throw(ErrorException("Failed to create parameter symbol.")) + end + ptr +end + +function qk_param_from_double(value::Float64)::Ptr{QkParam} + ptr = LibQiskit.qk_param_from_double(value) + if ptr == C_NULL + throw(ErrorException("Failed to create parameter from double.")) + end + ptr +end + +function qk_param_from_complex(value::ComplexF64)::Ptr{QkParam} + ptr = LibQiskit.qk_param_from_complex(LibQiskit.QkComplex64(real(value), imag(value))) + if ptr == C_NULL + throw(ErrorException("Failed to create parameter from complex.")) + end + ptr +end + +function qk_param_zero()::Ptr{QkParam} + ptr = LibQiskit.qk_param_zero() + if ptr == C_NULL + throw(ErrorException("Failed to create zero parameter.")) + end + ptr +end + +function qk_param_copy(param::Ptr{QkParam})::Ptr{QkParam} + ptr = LibQiskit.qk_param_copy(param) + if ptr == C_NULL + throw(ErrorException("Failed to copy parameter.")) + end + ptr +end + +function qk_param_str(param::Ptr{QkParam})::String + cstr = LibQiskit.qk_param_str(param) + s = unsafe_string(cstr) + LibQiskit.qk_str_free(cstr) + s +end + +function qk_param_equal(a::Ptr{QkParam}, b::Ptr{QkParam})::Bool + LibQiskit.qk_param_equal(a, b) +end + +function qk_param_as_real(param::Ptr{QkParam})::Float64 + LibQiskit.qk_param_as_real(param) +end + +function qk_param_add(out::Ptr{QkParam}, a::Ptr{QkParam}, b::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_add(out, a, b)) + nothing +end + +function qk_param_sub(out::Ptr{QkParam}, a::Ptr{QkParam}, b::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_sub(out, a, b)) + nothing +end + +function qk_param_mul(out::Ptr{QkParam}, a::Ptr{QkParam}, b::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_mul(out, a, b)) + nothing +end + +function qk_param_div(out::Ptr{QkParam}, num::Ptr{QkParam}, den::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_div(out, num, den)) + nothing +end + +function qk_param_pow(out::Ptr{QkParam}, base::Ptr{QkParam}, pow::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_pow(out, base, pow)) + nothing +end + +function qk_param_sin(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_sin(out, src)) + nothing +end + +function qk_param_cos(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_cos(out, src)) + nothing +end + +function qk_param_tan(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_tan(out, src)) + nothing +end + +function qk_param_asin(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_asin(out, src)) + nothing +end + +function qk_param_acos(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_acos(out, src)) + nothing +end + +function qk_param_atan(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_atan(out, src)) + nothing +end + +function qk_param_log(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_log(out, src)) + nothing +end + +function qk_param_exp(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_exp(out, src)) + nothing +end + +function qk_param_abs(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_abs(out, src)) + nothing +end + +function qk_param_sign(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_sign(out, src)) + nothing +end + +function qk_param_neg(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_neg(out, src)) + nothing +end + +function qk_param_conjugate(out::Ptr{QkParam}, src::Ptr{QkParam})::Nothing + check_exit_code(LibQiskit.qk_param_conjugate(out, src)) + nothing +end + +export QkParam +export qk_param_free, qk_param_new_symbol, qk_param_from_double, qk_param_from_complex +export qk_param_zero, qk_param_copy, qk_param_str, qk_param_equal, qk_param_as_real +export qk_param_add, qk_param_sub, qk_param_mul, qk_param_div, qk_param_pow +export qk_param_sin, qk_param_cos, qk_param_tan, qk_param_asin, qk_param_acos, qk_param_atan +export qk_param_log, qk_param_exp, qk_param_abs, qk_param_sign, qk_param_neg, qk_param_conjugate diff --git a/src/circuit.jl b/src/circuit.jl index 689a47d..d156072 100644 --- a/src/circuit.jl +++ b/src/circuit.jl @@ -11,7 +11,7 @@ # that they have been altered from the originals. import .C: qk_circuit_free, qk_circuit_num_qubits, qk_circuit_num_clbits, qk_circuit_num_instructions, qk_circuit_get_instruction, qk_circuit_count_ops, QkCircuit, QkGate, CircuitInstruction, QkDelayUnit, QkDelayUnit_S, QkDelayUnit_MS, QkDelayUnit_US, QkDelayUnit_NS, QkDelayUnit_PS -import .C: qk_circuit_gate, qk_circuit_measure, qk_circuit_reset, qk_circuit_barrier, qk_circuit_unitary, qk_circuit_delay, check_not_null +import .C: qk_circuit_gate, qk_circuit_parameterized_gate, qk_circuit_measure, qk_circuit_reset, qk_circuit_barrier, qk_circuit_unitary, qk_circuit_delay, check_not_null using .C """ @@ -105,9 +105,17 @@ function _apply_gate(qc::QuantumCircuit, gate, num_qubits::Int, num_params::Int, if length(args) != num_qubits + num_params throw(ArgumentError("Unexpected number of arguments for gate")) end - params = collect(Float64, args[1:num_params]) + param_args = args[1:num_params] qubits = collect(Int32, args[num_params+1:end]) - qk_circuit_gate(qc, gate, qubits, params) + if all(p -> isa(p, Real), param_args) + params = collect(Float64, param_args) + qk_circuit_gate(qc, gate, qubits, params) + elseif all(p -> isa(p, Parameter), param_args) + param_ptrs = Ptr{QkParam}[p.ptr for p in param_args] + qk_circuit_parameterized_gate(qc, gate, qubits, param_ptrs) + else + throw(ArgumentError("Gate parameters must be all real numbers or all Parameter objects")) + end return nothing end @@ -422,6 +430,9 @@ end qk_circuit_gate(qc::QuantumCircuit, args...)::Nothing = qk_circuit_gate(qc.ptr, args...; offset=qc.offset) +qk_circuit_parameterized_gate(qc::QuantumCircuit, args...)::Nothing = + qk_circuit_parameterized_gate(qc.ptr, args...; offset=qc.offset) + qk_circuit_measure(qc::QuantumCircuit, qubit::Integer, clbit::Integer)::Nothing = qk_circuit_measure(qc.ptr, qubit, clbit; offset=qc.offset) diff --git a/src/parameter.jl b/src/parameter.jl new file mode 100644 index 0000000..d3d13ff --- /dev/null +++ b/src/parameter.jl @@ -0,0 +1,171 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import .C: QkParam, LibQiskit +using .C + +""" + Parameter + +Represents a circuit parameter which may hold real, complex, or symbolic values. + +# Constructors + +- `Parameter(name::String)` — create a symbolic parameter with the given name. +- `Parameter(value::Real)` — create a parameter holding a real number. +- `Parameter(value::Complex)` — create a parameter holding a complex number. + +# Examples + +```julia +theta = Parameter(\"θ\") +phi = Parameter(\"φ\") +expr = 2 * theta + sin(phi) +qc.rz(theta, 1) +``` +""" +mutable struct Parameter + ptr::Ptr{QkParam} + + function Parameter(name::String) + ptr = qk_param_new_symbol(name) + p = new(ptr) + finalizer(qk_param_free, p) + p + end + + function Parameter(value::Real) + ptr = qk_param_from_double(Float64(value)) + p = new(ptr) + finalizer(qk_param_free, p) + p + end + + function Parameter(value::Complex) + ptr = qk_param_from_complex(ComplexF64(value)) + p = new(ptr) + finalizer(qk_param_free, p) + p + end + + function Parameter(ptr::Ptr{QkParam}) + p = new(ptr) + finalizer(qk_param_free, p) + p + end +end + +function qk_param_free(p::Parameter)::Nothing + if p.ptr != C_NULL + LibQiskit.qk_param_free(p.ptr) + p.ptr = C_NULL + end + nothing +end + +function Base.copy(p::Parameter)::Parameter + Parameter(qk_param_copy(p.ptr)) +end + +function Base.string(p::Parameter)::String + qk_param_str(p.ptr) +end + +function Base.show(io::IO, p::Parameter) + if p.ptr == C_NULL + print(io, "Parameter(NULL)") + else + print(io, "Parameter($(string(p)))") + end +end + +function Base.:(==)(a::Parameter, b::Parameter)::Bool + qk_param_equal(a.ptr, b.ptr) +end + +# Binop helpers +_param_binop(a::Parameter, b::Parameter, op) = (out = qk_param_zero(); op(out, a.ptr, b.ptr); Parameter(out)) +_param_binop(a::Parameter, b::Real, op) = _param_binop(a, Parameter(b), op) +_param_binop(a::Real, b::Parameter, op) = _param_binop(Parameter(a), b, op) + +function Base.:+(a::Parameter, b::Parameter) + _param_binop(a, b, qk_param_add) +end +Base.:+(a::Parameter, b::Real) = _param_binop(a, b, qk_param_add) +Base.:+(a::Real, b::Parameter) = _param_binop(a, b, qk_param_add) + +function Base.:-(a::Parameter, b::Parameter) + _param_binop(a, b, qk_param_sub) +end +Base.:-(a::Parameter, b::Real) = _param_binop(a, b, qk_param_sub) +Base.:-(a::Real, b::Parameter) = _param_binop(a, b, qk_param_sub) + +function Base.:*(a::Parameter, b::Parameter) + _param_binop(a, b, qk_param_mul) +end +Base.:*(a::Parameter, b::Real) = _param_binop(a, b, qk_param_mul) +Base.:*(a::Real, b::Parameter) = _param_binop(a, b, qk_param_mul) + +function Base.:/(a::Parameter, b::Parameter) + _param_binop(a, b, qk_param_div) +end +Base.:/(a::Parameter, b::Real) = _param_binop(a, b, qk_param_div) +Base.:/(a::Real, b::Parameter) = _param_binop(a, b, qk_param_div) + +function Base.:^(a::Parameter, b::Parameter) + _param_binop(a, b, qk_param_pow) +end +Base.:^(a::Parameter, b::Real) = _param_binop(a, b, qk_param_pow) +Base.:^(a::Real, b::Parameter) = _param_binop(a, b, qk_param_pow) + +# Unary helpers +_param_unop(a::Parameter, op) = (out = qk_param_zero(); op(out, a.ptr); Parameter(out)) + +function Base.:-(a::Parameter) + _param_unop(a, qk_param_neg) +end + +function Base.conj(a::Parameter) + _param_unop(a, qk_param_conjugate) +end + +function Base.abs(a::Parameter) + _param_unop(a, qk_param_abs) +end + +function Base.sign(a::Parameter) + _param_unop(a, qk_param_sign) +end + +# Trig functions +Base.sin(a::Parameter) = _param_unop(a, qk_param_sin) +Base.cos(a::Parameter) = _param_unop(a, qk_param_cos) +Base.tan(a::Parameter) = _param_unop(a, qk_param_tan) +Base.asin(a::Parameter) = _param_unop(a, qk_param_asin) +Base.acos(a::Parameter) = _param_unop(a, qk_param_acos) +Base.atan(a::Parameter) = _param_unop(a, qk_param_atan) + +# Transcendental functions +Base.log(a::Parameter) = _param_unop(a, qk_param_log) +Base.exp(a::Parameter) = _param_unop(a, qk_param_exp) + +""" + as_real(p::Parameter) -> Float64 + +Attempt to cast the parameter as a `Float64`. Returns `NaN` if the parameter +contains unbound symbolic variables. +""" +function as_real(p::Parameter)::Float64 + qk_param_as_real(p.ptr) +end + +export Parameter, as_real diff --git a/test/runtests.jl b/test/runtests.jl index eee8acf..37e3039 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -25,6 +25,7 @@ using Unitful include("test_target.jl") include("test_transpiler.jl") include("test_observable.jl") + include("test_parameter.jl") # The wrapper types (`QuantumCircuit`, `Target`, `TargetEntry`, # `SparseObservable`, transpile layouts) register finalizers that call the diff --git a/test/test_parameter.jl b/test/test_parameter.jl new file mode 100644 index 0000000..1c36caf --- /dev/null +++ b/test/test_parameter.jl @@ -0,0 +1,167 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +@testset "Parameter" begin + @testset "Construction" begin + p = Parameter("theta") + @test string(p) == "theta" + @test isa(p, Parameter) + + p2 = Parameter(3.14) + @test as_real(p2) ≈ 3.14 + + p3 = Parameter(2.5 + 1.0im) + @test isa(p3, Parameter) + + p4 = Parameter(42) + @test as_real(p4) ≈ 42.0 + end + + @testset "Copy and equality" begin + a = Parameter("a") + b = copy(a) + @test a == b + + c = Parameter("c") + @test a != c + end + + @testset "Arithmetic" begin + a = Parameter("a") + b = Parameter("b") + + c = a + b + @test isa(c, Parameter) + + c = a - b + @test isa(c, Parameter) + + c = a * b + @test isa(c, Parameter) + + c = a / b + @test isa(c, Parameter) + + c = a^b + @test isa(c, Parameter) + + c = -a + @test isa(c, Parameter) + end + + @testset "Arithmetic with reals" begin + a = Parameter("a") + + c = a + 2.0 + @test isa(c, Parameter) + + c = 2.0 + a + @test isa(c, Parameter) + + c = a - 1.5 + @test isa(c, Parameter) + + c = 1.5 - a + @test isa(c, Parameter) + + c = a * 3.0 + @test isa(c, Parameter) + + c = 3.0 * a + @test isa(c, Parameter) + + c = a / 2.0 + @test isa(c, Parameter) + + c = 2.0 / a + @test isa(c, Parameter) + + c = a^2 + @test isa(c, Parameter) + + c = 2^a + @test isa(c, Parameter) + end + + @testset "Unary functions" begin + a = Parameter("a") + + @test isa(sin(a), Parameter) + @test isa(cos(a), Parameter) + @test isa(tan(a), Parameter) + @test isa(asin(a), Parameter) + @test isa(acos(a), Parameter) + @test isa(atan(a), Parameter) + @test isa(log(a), Parameter) + @test isa(exp(a), Parameter) + @test isa(abs(a), Parameter) + @test isa(sign(a), Parameter) + @test isa(conj(a), Parameter) + end + + @testset "Numeric evaluation" begin + p = Parameter(2.5) + @test as_real(p) ≈ 2.5 + + p2 = Parameter(1.0 + 2.0im) + @test as_real(p2) ≈ 1.0 + end + + @testset "Show" begin + p = Parameter("theta") + io = IOBuffer() + show(io, p) + @test String(take!(io)) == "Parameter(theta)" + + p2 = Parameter(3.14) + io = IOBuffer() + show(io, p2) + @test String(take!(io)) == "Parameter(3.14)" + end + + @testset "Parameterized circuit gates" begin + qc = QuantumCircuit(2, 0) + theta = Parameter("theta") + + qc.rz(theta, 1) + @test qc.num_instructions == 1 + @test qc.data[1].name == "rz" + # Symbolic params return NaN via as_real + @test isnan(qc.data[1].params[1]) + + qc.rx(theta, 2) + @test qc.num_instructions == 2 + @test qc.data[2].name == "rx" + + qc_copy = copy(qc) + @test qc_copy.num_instructions == 2 + end + + @testset "Parameterized !-suffixed functions" begin + qc = QuantumCircuit(2, 0) + theta = Parameter("theta") + + rz!(qc, theta, 1) + @test qc.num_instructions == 1 + @test qc.data[1].name == "rz" + end + + @testset "Expression as gate parameter" begin + qc = QuantumCircuit(2, 0) + theta = Parameter("theta") + expr = 2 * theta + 1.0 + + qc.rz(expr, 1) + @test qc.num_instructions == 1 + @test qc.data[1].name == "rz" + end +end