Skip to content

Commit

Permalink
Merge pull request #6 from kshyatt-aws/ksh/docs
Browse files Browse the repository at this point in the history
docs: Add more information about providing pragma parsing and gate sets
  • Loading branch information
kshyatt-aws authored Oct 29, 2024
2 parents faf8911 + ce376f2 commit 6724dd5
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 25 deletions.
3 changes: 3 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ makedocs(;
),
pages=[
"Home" => "index.md",
"Internals" => "internals.md",
"Supplying built-in gates" => "gates.md",
"Handling pragmas" => "pragmas.md",
],
)

Expand Down
35 changes: 35 additions & 0 deletions docs/src/gates.md
Original file line number Diff line number Diff line change
@@ -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
```
20 changes: 1 addition & 19 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

9 changes: 9 additions & 0 deletions docs/src/internals.md
Original file line number Diff line number Diff line change
@@ -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
```
59 changes: 59 additions & 0 deletions docs/src/pragmas.md
Original file line number Diff line number Diff line change
@@ -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
```


51 changes: 45 additions & 6 deletions src/Quasar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -806,7 +811,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))
Expand Down Expand Up @@ -975,8 +980,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
Expand Down Expand Up @@ -1726,7 +1765,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
Expand Down

0 comments on commit 6724dd5

Please sign in to comment.