From ce376f2ed3ea7a24f19914826c6bea73f4308ff3 Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Mon, 28 Oct 2024 13:06:00 -0400 Subject: [PATCH] docs: Add more information about providing pragma parsing and gate sets --- docs/make.jl | 3 +++ docs/src/gates.md | 35 +++++++++++++++++++++++++ docs/src/index.md | 20 +-------------- docs/src/internals.md | 9 +++++++ docs/src/pragmas.md | 59 +++++++++++++++++++++++++++++++++++++++++++ src/Quasar.jl | 51 ++++++++++++++++++++++++++++++++----- 6 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 docs/src/gates.md create mode 100644 docs/src/internals.md create mode 100644 docs/src/pragmas.md diff --git a/docs/make.jl b/docs/make.jl index 5b8bf31..ab47817 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,6 +14,9 @@ makedocs(; ), pages=[ "Home" => "index.md", + "Internals" => "internals.md", + "Supplying built-in gates" => "gates.md", + "Handling pragmas" => "pragmas.md", ], ) diff --git a/docs/src/gates.md b/docs/src/gates.md new file mode 100644 index 0000000..908cba8 --- /dev/null +++ b/docs/src/gates.md @@ -0,0 +1,35 @@ +# [Defining your own gate sets](@id custom_gates) + +OpenQASM3 has [two built-in gates](https://openqasm.com/language/gates.html#built-in-gates) -- `U` and `gphase`. Combined with [gate modifiers](https://openqasm.com/language/gates.html#quantum-gate-modifiers), these two are enough to define a universal gate set for quantum computation. + +OpenQASM3 also offers a [standard library of gates](https://openqasm.com/language/standard_library.html), all defined in terms of the two built-in gates, in `stdgates.inc`. `Quasar.jl` provides a copy of `stdgates.inc` in its `test/` directory, which you can import with: + +``` +include "stdgates.inc"; +``` + +However, many quantum computers and simulators have different built-in gates, so-called "native gates", which they can implement without reference to `gphase` or `U`. +You can provide your own built-in gate set to `Quasar.jl` to ensure that the output circuit instructions are in your "gate dialect". + +In `Quasar.jl`, you can create a **function** which takes no arguments and returns a `Dict{String, BuiltinGateDefinition}`. The keys are the names of your built-in gates (e.g. `"msaa"`) and a `BuiltinGateDefinition` has the following fields: + +- `name::String` -- the name of the gate +- `arguments::Vector{String}` -- the names of the gate's arguments, if any (can be empty) +- `qubit_targets::Vector{String}` -- the names of the gate's target(s). +- `body::CircuitInstruction` -- the **single** `CircuitInstruction` which defines the gate + +So, for example, if the Pauli X gate `x` and the X rotation gate `Rx` are built-in gates, you could create the function: + +```julia +my_builtin_gates() = Dict( + "x"=>BuiltinGateDefinition("x", String[], ["a"], (type="x", arguments=InstructionArgument[], targets=[0], controls=Pair{Int,Int}[], exponent=1.0)), + "rx"=>BuiltinGateDefinition("rx", ["θ"], ["a"], (type="rx", arguments=InstructionArgument[θ], targets=[0], controls=Pair{Int,Int}[], exponent=1.0)), +) +``` + +You can provide this definition to `Quasar.jl` by updating the reference `builtin_gates`: + +```julia +using Quasar +Quasar.builtin_gates[] = my_builtin_gates +``` diff --git a/docs/src/index.md b/docs/src/index.md index 90709d2..20a32c4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -21,7 +21,7 @@ Then, it walks (visits) the AST to evaluate all loops, conditionals, gate calls, - Function definitions and calls - Casting classical types - Timing statements `barrier` and `delay` -- Pragmas (see below) +- Pragmas What is *not* yet supported: - `angle` types @@ -74,21 +74,3 @@ Quasar.builtin_gates[] = my_builtin_generator ``` A generator function is used here in order to allow visitors to overwrite builtin functions in certain scopes without corrupting the reference definition. If you're writing a package which uses Quasar, the `Quasar.builtin_gates[] = my_builtin_generator` should be placed in your main module's `__init__` function. - -## Working with `pragma`s - -[OpenQASM `pragma`s](https://openqasm.com/language/directives.html#pragmas) allow specific platforms to provide custom instructions. -`Quasar.jl` provides a bare `parse_pragma` function you can override and extend to support parsing your pragma constructs. This will -teach `Quasar.jl` how to incorporate your `pragma` instructions into the AST. You will also need to write a `visit_pragma` function -for the AST walking step. You may want to use `pragma`s to support custom circuit operations (like noise channels) or results. - -Your `parse_pragma` should take four arguments: -- `tokens` -- some vector of tokens generated by the tokenizer which represents the `pragma` line, with the leading `#pragma` removed -- `stack` -- the current stack of `QasmExpressions`, used for error reporting -- `start` -- the location in the QASM source string of the start of the `#pragma` line, used for error reporting -- `qasm` -- the QASM source string, used for error reporting - -Your `visit_pragma` should take two arguments: -- `visitor` -- any subtype of `AbstractVisitor`, e.g. a `QasmProgramVisitor`, `QasmForLoopVisitor`, etc. -- `expr` -- the `QasmExpression` containing your full `pragma` invocation, generated by the above `parse_pragma` function - diff --git a/docs/src/internals.md b/docs/src/internals.md new file mode 100644 index 0000000..5b5a419 --- /dev/null +++ b/docs/src/internals.md @@ -0,0 +1,9 @@ +# `Quasar.jl` internals + +Here we document some of the internal types useful for developers and those extending `Quasar.jl` with custom gate sets and pragmas. + +```@docs +Quasar.InstructionArgument +Quasar.CircuitInstruction +Quasar.CircuitResult +``` diff --git a/docs/src/pragmas.md b/docs/src/pragmas.md new file mode 100644 index 0000000..21f3466 --- /dev/null +++ b/docs/src/pragmas.md @@ -0,0 +1,59 @@ +# Handling pragmas + +OpenQASM3 allows you to define [pragmas](https://openqasm.com/language/directives.html#pragmas), which can be used to +support operations like results besides measurements (e.g. expectation values), noise channels, or compiler directives. + +Since pragmas are by nature specific to the application, `Quasar.jl` doesn't provide support for parsing or visiting them. +However, you can implement support for this in order to handle your specific pragmas. This will +teach `Quasar.jl` how to incorporate your `pragma` instructions into the AST. You may want to use `pragma`s to support custom circuit operations (like noise channels) or result types (like expectation values). + +You'll need to implement two functions: `parse_pragma` and `visit_pragma`. + +## Parsing pragmas to `QasmExpression`s + +`parse_pragma` should accept four arguments: + +- `tokens` -- the list of tokens generated from tokenizing the raw OpenQASM 3.0 string +- `stack` -- the current AST stack (used for error reporting) +- `start` -- the start location for the beginning of this pragma (used for error reporting) +- `qasm` -- the raw QASM string (used for error reporting) + +The `tokens` is a list of 3-tuples: the first item is an `Int64` indicating the location in the `qasm` string of the *start* of the token, the second is an `Int32` indicating the *length* of the token, and the third is a `Token` enum indicating the type of the token (e.g., a `semicolon` or an `identifier`). + +The list of tokens provided to `parse_pragma` **does not** contain the leading `pragma` token, and contains **only** the tokens until the next newline. You're responsible for then walking through the line and generating appropriate `QasmExpression`s you'll visit later in `visit_pragma`. Your `parse_pragma` function should return a `QasmExpression` that has `:pragma` as its head. A skeleton of the function might be: + +```julia +function my_parse_pragma(tokens, stack, start, qasm) + pragma_expr = QasmExpression(:pragma) + while !isempty(tokens) + inner_expr = # do some lexing in here + push!(pragma_expr, inner_expr) + end + return pragma_expr +end +``` + +If the submitted QASM has an invalid pragma, you can throw a `Quasar.QasmParseError` like so: + +```julia +your_error_message = "Oh no! A sample error message!" +throw(Quasar.QasmParseError(your_error_message, stack, start, qasm)) +``` + +## Visiting the parsed pragmas + +Once you've generated the subtree(s) for your pragma(s), you can then visit them to generate your final instructions and results (if any) for your QASM file. When `Quasar.jl`'s visitor encounters a `QasmExpression` with a head of `:pragma`, it will call `visit_pragma[](v::AbstractVisitor, expr::QasmExpression)`. You can then dispatch on the type of pragma, if you support multiple. You can then push [`CircuitInstruction`](@ref Quasar.CircuitInstruction)s and [`CircuitResult`](@ref Quasar.CircuitResult)s to the visitor `v`. + +## Loading your functions into `Quasar.jl` + +Similar to [custom gate sets](@ref custom_gates), you can +let `Quasar.jl` know about these at module initialization time: + +```julia +function __init__() + Quasar.parse_pragma[] = my_parse_pragma + Quasar.vist_pragma[] = my_visit_pragma +end +``` + + diff --git a/src/Quasar.jl b/src/Quasar.jl index c5e23ae..c780c2a 100644 --- a/src/Quasar.jl +++ b/src/Quasar.jl @@ -109,15 +109,20 @@ const qasm_tokens = [ :forbidden_keyword => re"cal|defcal|extern", ] -const dt_type = Ref{DataType}() +const dt_type = Ref{DataType}() const builtin_gates = Ref{Function}() +const visit_pragma = Ref{Function}() +const parse_pragma = Ref{Function}() + +function basic_parse_pragma end +function basic_visit_pragma end function __init__() dt_type[] = Nanosecond - builtin_gates[] = basic_builtin_gates + builtin_gates[] = basic_builtin_gates + visit_pragma[] = basic_visit_pragma + parse_pragma[] = basic_parse_pragma end -function parse_pragma end -function visit_pragma end @eval @enum Token error $(first.(qasm_tokens)...) make_tokenizer((error, @@ -791,7 +796,7 @@ function parse_qasm(clean_tokens::Vector{Tuple{Int64, Int32, Token}}, qasm::Stri closing = findfirst(triplet->triplet[end] == newline, clean_tokens) isnothing(closing) && throw(QasmParseError("missing final newline for #pragma", stack, start, qasm)) pragma_tokens = splice!(clean_tokens, 1:closing-1) - push!(stack, parse_pragma(pragma_tokens, stack, start, qasm)) + push!(stack, parse_pragma[](pragma_tokens, stack, start, qasm)) elseif token == include_token closing = findfirst(triplet->triplet[end] == semicolon, clean_tokens) isnothing(closing) && throw(QasmParseError("missing final semicolon for include", stack, start, qasm)) @@ -960,8 +965,42 @@ struct Qubit size::Int end +""" + InstructionArgument + +Union type of `Symbol`, `Dates.Period`, `Real`, `Matrix{ComplexF64}` which +represents a possible argument to a [`CircuitInstruction`](@ref). +""" const InstructionArgument = Union{Symbol, Dates.Period, Real, Matrix{ComplexF64}} + +""" + CircuitInstruction + +A `NamedTuple` representing a circuit instruction (a gate, a noise operation, a timing operation). Fields are: + +- `type::String` - the name of the operation (e.g. `rx`) +- `arguments::Vector{InstructionArgument}` - arguments, if any, to the operation (e.g. `π` for an angled gate) +- `targets::Vector{Int}` - qubit targets for the operation, **including control qubits, if any** +- `controls::Vector{Pair{Int, Int}}` - control qubits and bit-values for the operation, so that a `ctrl @ x 0, 2;` could have `controls = [0=>1]` and `negctrl @ x 0, 2;` could have `controls = [0=>0]`. +- `exponent::Float64` - exponent to which the operation is raised, if any + +`CircuitInstruction`s can be used with a package like [`StructTypes.jl`](https://github.com/JuliaData/StructTypes.jl) to build the +actual types of your package from these `NamedTuple`s. +""" const CircuitInstruction = @NamedTuple begin type::String; arguments::Vector{InstructionArgument}; targets::Vector{Int}; controls::Vector{Pair{Int, Int}}; exponent::Float64 end +""" + CircuitResult + +A `NamedTuple` representing a circuit result (e.g. an expectation value). Fields are: + +- `type::Symbol` - the name of the result type (e.g. `:variance`) +- `operator::Vector{Union{String, Matrix{ComplexF64}}}` - the operator to measure, if applicable +- `targets::Vector{Int}` - qubit targets for the result +- `states::Vector{String}` - vector of bitstrings to measure the amplitudes of, if the result type is an `:amplitude` + +`CircuitResult`s can be used with a package like [`StructTypes.jl`](https://github.com/JuliaData/StructTypes.jl) to build the +actual result objects of your package from these `NamedTuple`s. +""" const CircuitResult = @NamedTuple begin type::Symbol; operator::Vector{Union{String, Matrix{ComplexF64}}}; targets::Vector{Int}; states::Vector{String}; end abstract type AbstractGateDefinition end @@ -1711,7 +1750,7 @@ function (v::AbstractVisitor)(program_expr::QasmExpression) full_function_def = FunctionDefinition(function_name, function_arguments, function_body, function_return_type) v.function_defs[function_name] = full_function_def elseif head(program_expr) == :pragma - visit_pragma(v, program_expr) + visit_pragma[](v, program_expr) elseif head(program_expr) ∈ (:integer_literal, :float_literal, :string_literal, :complex_literal, :irrational_literal, :boolean_literal, :duration_literal) return program_expr.args[1] elseif head(program_expr) == :array_literal